001package org.hl7.fhir.r4.elementmodel;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.OutputStream;
035import java.io.OutputStreamWriter;
036import java.math.BigDecimal;
037import java.util.HashSet;
038import java.util.IdentityHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.Map.Entry;
042import java.util.Set;
043
044import org.hl7.fhir.exceptions.FHIRException;
045import org.hl7.fhir.exceptions.FHIRFormatError;
046import org.hl7.fhir.r4.conformance.ProfileUtilities;
047import org.hl7.fhir.r4.context.IWorkerContext;
048import org.hl7.fhir.r4.elementmodel.Element.SpecialElement;
049import org.hl7.fhir.r4.formats.IParser.OutputStyle;
050import org.hl7.fhir.r4.formats.JsonCreator;
051import org.hl7.fhir.r4.formats.JsonCreatorCanonical;
052import org.hl7.fhir.r4.formats.JsonCreatorGson;
053import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
054import org.hl7.fhir.r4.model.StructureDefinition;
055import org.hl7.fhir.utilities.StringPair;
056import org.hl7.fhir.utilities.FileUtilities;
057import org.hl7.fhir.utilities.Utilities;
058import org.hl7.fhir.utilities.json.JsonTrackingParser;
059import org.hl7.fhir.utilities.json.JsonTrackingParser.LocationData;
060import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
061import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
062import org.hl7.fhir.utilities.xhtml.XhtmlParser;
063
064import com.google.gson.JsonArray;
065import com.google.gson.JsonElement;
066import com.google.gson.JsonNull;
067import com.google.gson.JsonObject;
068import com.google.gson.JsonPrimitive;
069
070@Deprecated
071public class JsonParser extends ParserBase {
072
073  private JsonCreator json;
074  private Map<JsonElement, LocationData> map;
075
076  public JsonParser(IWorkerContext context) {
077    super(context);
078  }
079
080  public Element parse(String source, String type) throws Exception {
081    JsonObject obj = (JsonObject) new com.google.gson.JsonParser().parse(source);
082    String path = "/" + type;
083    StructureDefinition sd = getDefinition(-1, -1, type);
084    if (sd == null)
085      return null;
086
087    Element result = new Element(type, new Property(context, sd.getSnapshot().getElement().get(0), sd));
088    checkObject(obj, path);
089    result.setType(type);
090    parseChildren(path, obj, result, true);
091    result.numberChildren();
092    return result;
093  }
094
095  @Override
096  public Element parse(InputStream stream) throws IOException, FHIRException {
097    // if we're parsing at this point, then we're going to use the custom parser
098    map = new IdentityHashMap<JsonElement, LocationData>();
099    String source = FileUtilities.streamToString(stream);
100    if (policy == ValidationPolicy.EVERYTHING) {
101      JsonObject obj = null;
102      try {
103        obj = JsonTrackingParser.parse(source, map);
104      } catch (Exception e) {
105        logError(-1, -1, "(document)", IssueType.INVALID, "Error parsing JSON: " + e.getMessage(), IssueSeverity.FATAL);
106        return null;
107      }
108      assert (map.containsKey(obj));
109      return parse(obj);
110    } else {
111      JsonObject obj = JsonTrackingParser.parse(source, null); // (JsonObject) new
112                                                               // com.google.gson.JsonParser().parse(source);
113//                      assert (map.containsKey(obj));
114      return parse(obj);
115    }
116  }
117
118  public Element parse(JsonObject object, Map<JsonElement, LocationData> map) throws FHIRException {
119    this.map = map;
120    return parse(object);
121  }
122
123  public Element parse(JsonObject object) throws FHIRException {
124    JsonElement rt = object.get("resourceType");
125    if (rt == null) {
126      logError(line(object), col(object), "$", IssueType.INVALID, "Unable to find resourceType property",
127          IssueSeverity.FATAL);
128      return null;
129    } else {
130      String name = rt.getAsString();
131      String path = "/" + name;
132
133      StructureDefinition sd = getDefinition(line(object), col(object), name);
134      if (sd == null)
135        return null;
136
137      Element result = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd));
138      checkObject(object, path);
139      result.markLocation(line(object), col(object));
140      result.setType(name);
141      parseChildren(path, object, result, true);
142      result.numberChildren();
143      return result;
144    }
145  }
146
147  private void checkObject(JsonObject object, String path) throws FHIRFormatError {
148    if (policy == ValidationPolicy.EVERYTHING) {
149      boolean found = false;
150      for (Entry<String, JsonElement> e : object.entrySet()) {
151        // if (!e.getKey().equals("fhir_comments")) {
152        found = true;
153        break;
154        // }
155      }
156      if (!found)
157        logError(line(object), col(object), path, IssueType.INVALID, "Object must have some content",
158            IssueSeverity.ERROR);
159    }
160  }
161
162  private void parseChildren(String path, JsonObject object, Element context, boolean hasResourceType)
163      throws FHIRException {
164    reapComments(object, context);
165    List<Property> properties = context.getProperty().getChildProperties(context.getName(), null);
166    Set<String> processed = new HashSet<String>();
167    if (hasResourceType)
168      processed.add("resourceType");
169    processed.add("fhir_comments");
170
171    // note that we do not trouble ourselves to maintain the wire format order here
172    // - we don't even know what it was anyway
173    // first pass: process the properties
174    for (Property property : properties) {
175      parseChildItem(path, object, context, processed, property);
176    }
177
178    // second pass: check for things not processed
179    if (policy != ValidationPolicy.NONE) {
180      for (Entry<String, JsonElement> e : object.entrySet()) {
181        if (!processed.contains(e.getKey())) {
182          logError(line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE,
183              "Unrecognised property '@" + e.getKey() + "'", IssueSeverity.ERROR);
184        }
185      }
186    }
187  }
188
189  public void parseChildItem(String path, JsonObject object, Element context, Set<String> processed,
190      Property property) {
191    if (property.isChoice() || property.getDefinition().getPath().endsWith("data[x]")) {
192      for (TypeRefComponent type : property.getDefinition().getType()) {
193        String eName = property.getName().substring(0, property.getName().length() - 3)
194            + Utilities.capitalize(type.getWorkingCode());
195        if (!isPrimitive(type.getWorkingCode()) && object.has(eName)) {
196          parseChildComplex(path, object, context, processed, property, eName);
197          break;
198        } else if (isPrimitive(type.getWorkingCode()) && (object.has(eName) || object.has("_" + eName))) {
199          parseChildPrimitive(object, context, processed, property, path, eName);
200          break;
201        }
202      }
203    } else if (property.isPrimitive(property.getType(null))) {
204      parseChildPrimitive(object, context, processed, property, path, property.getName());
205    } else if (object.has(property.getName())) {
206      parseChildComplex(path, object, context, processed, property, property.getName());
207    }
208  }
209
210  private void parseChildComplex(String path, JsonObject object, Element context, Set<String> processed,
211      Property property, String name) throws FHIRException {
212    processed.add(name);
213    String npath = path + "/" + property.getName();
214    JsonElement e = object.get(name);
215    if (property.isList() && (e instanceof JsonArray)) {
216      JsonArray arr = (JsonArray) e;
217      for (JsonElement am : arr) {
218        parseChildComplexInstance(npath, object, context, property, name, am);
219      }
220    } else {
221      if (property.isList()) {
222        logError(line(e), col(e), npath, IssueType.INVALID, "This property must be an Array, not " + describeType(e),
223            IssueSeverity.ERROR);
224      }
225      parseChildComplexInstance(npath, object, context, property, name, e);
226    }
227  }
228
229  private String describeType(JsonElement e) {
230    if (e.isJsonArray())
231      return "an Array";
232    if (e.isJsonObject())
233      return "an Object";
234    if (e.isJsonPrimitive())
235      return "a primitive property";
236    if (e.isJsonNull())
237      return "a Null";
238    return null;
239  }
240
241  private void parseChildComplexInstance(String npath, JsonObject object, Element context, Property property,
242      String name, JsonElement e) throws FHIRException {
243    if (e instanceof JsonObject) {
244      JsonObject child = (JsonObject) e;
245      Element n = new Element(name, property).markLocation(line(child), col(child));
246      checkObject(child, npath);
247      context.getChildren().add(n);
248      if (property.isResource())
249        parseResource(npath, child, n, property);
250      else
251        parseChildren(npath, child, n, false);
252    } else
253      logError(
254          line(e), col(e), npath, IssueType.INVALID, "This property must be "
255              + (property.isList() ? "an Array" : "an Object") + ", not a " + e.getClass().getName(),
256          IssueSeverity.ERROR);
257  }
258
259  private void parseChildPrimitive(JsonObject object, Element context, Set<String> processed, Property property,
260      String path, String name) throws FHIRException {
261    String npath = path + "/" + property.getName();
262    processed.add(name);
263    processed.add("_" + name);
264    JsonElement main = object.has(name) ? object.get(name) : null;
265    JsonElement fork = object.has("_" + name) ? object.get("_" + name) : null;
266    if (main != null || fork != null) {
267      if (property.isList() && ((main == null) || (main instanceof JsonArray))
268          && ((fork == null) || (fork instanceof JsonArray))) {
269        JsonArray arr1 = (JsonArray) main;
270        JsonArray arr2 = (JsonArray) fork;
271        for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) {
272          JsonElement m = arrI(arr1, i);
273          JsonElement f = arrI(arr2, i);
274          parseChildPrimitiveInstance(context, property, name, npath, m, f);
275        }
276      } else
277        parseChildPrimitiveInstance(context, property, name, npath, main, fork);
278    }
279  }
280
281  private JsonElement arrI(JsonArray arr, int i) {
282    return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i);
283  }
284
285  private int arrC(JsonArray arr) {
286    return arr == null ? 0 : arr.size();
287  }
288
289  private void parseChildPrimitiveInstance(Element context, Property property, String name, String npath,
290      JsonElement main, JsonElement fork) throws FHIRException {
291    if (main != null && !(main instanceof JsonPrimitive))
292      logError(line(main), col(main), npath, IssueType.INVALID,
293          "This property must be an simple value, not a " + main.getClass().getName(), IssueSeverity.ERROR);
294    else if (fork != null && !(fork instanceof JsonObject))
295      logError(line(fork), col(fork), npath, IssueType.INVALID,
296          "This property must be an object, not a " + fork.getClass().getName(), IssueSeverity.ERROR);
297    else {
298      Element n = new Element(name, property).markLocation(line(main != null ? main : fork),
299          col(main != null ? main : fork));
300      context.getChildren().add(n);
301      if (main != null) {
302        JsonPrimitive p = (JsonPrimitive) main;
303        n.setValue(p.getAsString());
304        if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) {
305          try {
306            XhtmlParser xp = new XhtmlParser();
307            n.setXhtml(xp.parse(n.getValue(), null).getDocumentElement());
308            if (policy == ValidationPolicy.EVERYTHING) {
309              for (StringPair s : xp.getValidationIssues()) {
310                logError(line(main), col(main), npath, IssueType.INVALID, s.getName() + " " + s.getValue(),
311                    IssueSeverity.ERROR);
312              }
313            }
314          } catch (Exception e) {
315            logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing XHTML: " + e.getMessage(),
316                IssueSeverity.ERROR);
317          }
318        }
319        if (policy == ValidationPolicy.EVERYTHING) {
320          // now we cross-check the primitive format against the stated type
321          if (Utilities.existsInList(n.getType(), "boolean")) {
322            if (!p.isBoolean())
323              logError(line(main), col(main), npath, IssueType.INVALID,
324                  "Error parsing JSON: the primitive value must be a boolean", IssueSeverity.ERROR);
325          } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) {
326            if (!p.isNumber())
327              logError(line(main), col(main), npath, IssueType.INVALID,
328                  "Error parsing JSON: the primitive value must be a number", IssueSeverity.ERROR);
329          } else if (!p.isString())
330            logError(line(main), col(main), npath, IssueType.INVALID,
331                "Error parsing JSON: the primitive value must be a string", IssueSeverity.ERROR);
332        }
333      }
334      if (fork != null) {
335        JsonObject child = (JsonObject) fork;
336        checkObject(child, npath);
337        parseChildren(npath, child, n, false);
338      }
339    }
340  }
341
342  private void parseResource(String npath, JsonObject res, Element parent, Property elementProperty)
343      throws FHIRException {
344    JsonElement rt = res.get("resourceType");
345    if (rt == null) {
346      logError(line(res), col(res), npath, IssueType.INVALID, "Unable to find resourceType property",
347          IssueSeverity.FATAL);
348    } else {
349      String name = rt.getAsString();
350      StructureDefinition sd = context.fetchResource(StructureDefinition.class,
351          ProfileUtilities.sdNs(name, context.getOverrideVersionNs()));
352      if (sd == null)
353        throw new FHIRFormatError(
354            "Contained resource does not appear to be a FHIR resource (unknown name '" + name + "')");
355      parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd),
356          SpecialElement.fromProperty(parent.getProperty()), elementProperty);
357      parent.setType(name);
358      parseChildren(npath, res, parent, true);
359    }
360  }
361
362  private void reapComments(JsonObject object, Element context) {
363    if (object.has("fhir_comments")) {
364      JsonArray arr = object.getAsJsonArray("fhir_comments");
365      for (JsonElement e : arr) {
366        context.getComments().add(e.getAsString());
367      }
368    }
369  }
370
371  private int line(JsonElement e) {
372    if (map == null || !map.containsKey(e))
373      return -1;
374    else
375      return map.get(e).getLine();
376  }
377
378  private int col(JsonElement e) {
379    if (map == null || !map.containsKey(e))
380      return -1;
381    else
382      return map.get(e).getCol();
383  }
384
385  protected void prop(String name, String value, String link) throws IOException {
386    json.link(link);
387    if (name != null)
388      json.name(name);
389    json.value(value);
390  }
391
392  protected void open(String name, String link) throws IOException {
393    json.link(link);
394    if (name != null)
395      json.name(name);
396    json.beginObject();
397  }
398
399  protected void close() throws IOException {
400    json.endObject();
401  }
402
403  protected void openArray(String name, String link) throws IOException {
404    json.link(link);
405    if (name != null)
406      json.name(name);
407    json.beginArray();
408  }
409
410  protected void closeArray() throws IOException {
411    json.endArray();
412  }
413
414  @Override
415  public void compose(Element e, OutputStream stream, OutputStyle style, String identity)
416      throws FHIRException, IOException {
417    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
418    if (style == OutputStyle.CANONICAL)
419      json = new JsonCreatorCanonical(osw);
420    else
421      json = new JsonCreatorGson(osw);
422    json.setIndent(style == OutputStyle.PRETTY ? "  " : "");
423    json.beginObject();
424    prop("resourceType", e.getType(), null);
425    Set<String> done = new HashSet<String>();
426    for (Element child : e.getChildren()) {
427      compose(e.getName(), e, done, child);
428    }
429    json.endObject();
430    json.finish();
431    osw.flush();
432  }
433
434  public void compose(Element e, JsonCreator json) throws Exception {
435    this.json = json;
436    json.beginObject();
437
438    prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty()));
439    Set<String> done = new HashSet<String>();
440    for (Element child : e.getChildren()) {
441      compose(e.getName(), e, done, child);
442    }
443    json.endObject();
444    json.finish();
445  }
446
447  private void compose(String path, Element e, Set<String> done, Element child) throws IOException {
448    boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList();
449    if (!isList) {// for specials, ignore the cardinality of the stated type
450      compose(path, child);
451    } else if (!done.contains(child.getName())) {
452      done.add(child.getName());
453      List<Element> list = e.getChildrenByName(child.getName());
454      composeList(path, list);
455    }
456  }
457
458  private void composeList(String path, List<Element> list) throws IOException {
459    // there will be at least one element
460    String name = list.get(0).getName();
461    boolean complex = true;
462    if (list.get(0).isPrimitive()) {
463      boolean prim = false;
464      complex = false;
465      for (Element item : list) {
466        if (item.hasValue())
467          prim = true;
468        if (item.hasChildren())
469          complex = true;
470      }
471      if (prim) {
472        openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
473        for (Element item : list) {
474          if (item.hasValue())
475            primitiveValue(null, item);
476          else
477            json.nullValue();
478        }
479        closeArray();
480      }
481      name = "_" + name;
482    }
483    if (complex) {
484      openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
485      for (Element item : list) {
486        if (item.hasChildren()) {
487          open(null, null);
488          if (item.getProperty().isResource()) {
489            prop("resourceType", item.getType(),
490                linkResolver == null ? null : linkResolver.resolveType(item.getType()));
491          }
492          Set<String> done = new HashSet<String>();
493          for (Element child : item.getChildren()) {
494            compose(path + "." + name + "[]", item, done, child);
495          }
496          close();
497        } else
498          json.nullValue();
499      }
500      closeArray();
501    }
502  }
503
504  private void primitiveValue(String name, Element item) throws IOException {
505    if (name != null) {
506      if (linkResolver != null)
507        json.link(linkResolver.resolveProperty(item.getProperty()));
508      json.name(name);
509    }
510    String type = item.getType();
511    if (Utilities.existsInList(type, "boolean"))
512      json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false));
513    else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt"))
514      json.value(new Integer(item.getValue()));
515    else if (Utilities.existsInList(type, "decimal"))
516      try {
517        json.value(new BigDecimal(item.getValue()));
518      } catch (Exception e) {
519        throw new NumberFormatException("error writing number '" + item.getValue() + "' to JSON");
520      }
521    else
522      json.value(item.getValue());
523  }
524
525  private void compose(String path, Element element) throws IOException {
526    String name = element.getName();
527    if (element.isPrimitive() || isPrimitive(element.getType())) {
528      if (element.hasValue())
529        primitiveValue(name, element);
530      name = "_" + name;
531    }
532    if (element.hasChildren()) {
533      open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
534      if (element.getProperty().isResource()) {
535        prop("resourceType", element.getType(),
536            linkResolver == null ? null : linkResolver.resolveType(element.getType()));
537      }
538      Set<String> done = new HashSet<String>();
539      for (Element child : element.getChildren()) {
540        compose(path + "." + element.getName(), element, done, child);
541      }
542      close();
543    }
544  }
545
546}