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.TextFile;
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
070public class JsonParser extends ParserBase {
071
072  private JsonCreator json;
073  private Map<JsonElement, LocationData> map;
074
075  public JsonParser(IWorkerContext context) {
076    super(context);
077  }
078
079  public Element parse(String source, String type) throws Exception {
080    JsonObject obj = (JsonObject) new com.google.gson.JsonParser().parse(source);
081    String path = "/" + type;
082    StructureDefinition sd = getDefinition(-1, -1, type);
083    if (sd == null)
084      return null;
085
086    Element result = new Element(type, new Property(context, sd.getSnapshot().getElement().get(0), sd));
087    checkObject(obj, path);
088    result.setType(type);
089    parseChildren(path, obj, result, true);
090    result.numberChildren();
091    return result;
092  }
093
094  @Override
095  public Element parse(InputStream stream) throws IOException, FHIRException {
096    // if we're parsing at this point, then we're going to use the custom parser
097    map = new IdentityHashMap<JsonElement, LocationData>();
098    String source = TextFile.streamToString(stream);
099    if (policy == ValidationPolicy.EVERYTHING) {
100      JsonObject obj = null;
101      try {
102        obj = JsonTrackingParser.parse(source, map);
103      } catch (Exception e) {
104        logError(-1, -1, "(document)", IssueType.INVALID, "Error parsing JSON: " + e.getMessage(), IssueSeverity.FATAL);
105        return null;
106      }
107      assert (map.containsKey(obj));
108      return parse(obj);
109    } else {
110      JsonObject obj = JsonTrackingParser.parse(source, null); // (JsonObject) new
111                                                               // com.google.gson.JsonParser().parse(source);
112//                      assert (map.containsKey(obj));
113      return parse(obj);
114    }
115  }
116
117  public Element parse(JsonObject object, Map<JsonElement, LocationData> map) throws FHIRException {
118    this.map = map;
119    return parse(object);
120  }
121
122  public Element parse(JsonObject object) throws FHIRException {
123    JsonElement rt = object.get("resourceType");
124    if (rt == null) {
125      logError(line(object), col(object), "$", IssueType.INVALID, "Unable to find resourceType property",
126          IssueSeverity.FATAL);
127      return null;
128    } else {
129      String name = rt.getAsString();
130      String path = "/" + name;
131
132      StructureDefinition sd = getDefinition(line(object), col(object), name);
133      if (sd == null)
134        return null;
135
136      Element result = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd));
137      checkObject(object, path);
138      result.markLocation(line(object), col(object));
139      result.setType(name);
140      parseChildren(path, object, result, true);
141      result.numberChildren();
142      return result;
143    }
144  }
145
146  private void checkObject(JsonObject object, String path) throws FHIRFormatError {
147    if (policy == ValidationPolicy.EVERYTHING) {
148      boolean found = false;
149      for (Entry<String, JsonElement> e : object.entrySet()) {
150        // if (!e.getKey().equals("fhir_comments")) {
151        found = true;
152        break;
153        // }
154      }
155      if (!found)
156        logError(line(object), col(object), path, IssueType.INVALID, "Object must have some content",
157            IssueSeverity.ERROR);
158    }
159  }
160
161  private void parseChildren(String path, JsonObject object, Element context, boolean hasResourceType)
162      throws FHIRException {
163    reapComments(object, context);
164    List<Property> properties = context.getProperty().getChildProperties(context.getName(), null);
165    Set<String> processed = new HashSet<String>();
166    if (hasResourceType)
167      processed.add("resourceType");
168    processed.add("fhir_comments");
169
170    // note that we do not trouble ourselves to maintain the wire format order here
171    // - we don't even know what it was anyway
172    // first pass: process the properties
173    for (Property property : properties) {
174      parseChildItem(path, object, context, processed, property);
175    }
176
177    // second pass: check for things not processed
178    if (policy != ValidationPolicy.NONE) {
179      for (Entry<String, JsonElement> e : object.entrySet()) {
180        if (!processed.contains(e.getKey())) {
181          logError(line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE,
182              "Unrecognised property '@" + e.getKey() + "'", IssueSeverity.ERROR);
183        }
184      }
185    }
186  }
187
188  public void parseChildItem(String path, JsonObject object, Element context, Set<String> processed,
189      Property property) {
190    if (property.isChoice() || property.getDefinition().getPath().endsWith("data[x]")) {
191      for (TypeRefComponent type : property.getDefinition().getType()) {
192        String eName = property.getName().substring(0, property.getName().length() - 3)
193            + Utilities.capitalize(type.getWorkingCode());
194        if (!isPrimitive(type.getWorkingCode()) && object.has(eName)) {
195          parseChildComplex(path, object, context, processed, property, eName);
196          break;
197        } else if (isPrimitive(type.getWorkingCode()) && (object.has(eName) || object.has("_" + eName))) {
198          parseChildPrimitive(object, context, processed, property, path, eName);
199          break;
200        }
201      }
202    } else if (property.isPrimitive(property.getType(null))) {
203      parseChildPrimitive(object, context, processed, property, path, property.getName());
204    } else if (object.has(property.getName())) {
205      parseChildComplex(path, object, context, processed, property, property.getName());
206    }
207  }
208
209  private void parseChildComplex(String path, JsonObject object, Element context, Set<String> processed,
210      Property property, String name) throws FHIRException {
211    processed.add(name);
212    String npath = path + "/" + property.getName();
213    JsonElement e = object.get(name);
214    if (property.isList() && (e instanceof JsonArray)) {
215      JsonArray arr = (JsonArray) e;
216      for (JsonElement am : arr) {
217        parseChildComplexInstance(npath, object, context, property, name, am);
218      }
219    } else {
220      if (property.isList()) {
221        logError(line(e), col(e), npath, IssueType.INVALID, "This property must be an Array, not " + describeType(e),
222            IssueSeverity.ERROR);
223      }
224      parseChildComplexInstance(npath, object, context, property, name, e);
225    }
226  }
227
228  private String describeType(JsonElement e) {
229    if (e.isJsonArray())
230      return "an Array";
231    if (e.isJsonObject())
232      return "an Object";
233    if (e.isJsonPrimitive())
234      return "a primitive property";
235    if (e.isJsonNull())
236      return "a Null";
237    return null;
238  }
239
240  private void parseChildComplexInstance(String npath, JsonObject object, Element context, Property property,
241      String name, JsonElement e) throws FHIRException {
242    if (e instanceof JsonObject) {
243      JsonObject child = (JsonObject) e;
244      Element n = new Element(name, property).markLocation(line(child), col(child));
245      checkObject(child, npath);
246      context.getChildren().add(n);
247      if (property.isResource())
248        parseResource(npath, child, n, property);
249      else
250        parseChildren(npath, child, n, false);
251    } else
252      logError(
253          line(e), col(e), npath, IssueType.INVALID, "This property must be "
254              + (property.isList() ? "an Array" : "an Object") + ", not a " + e.getClass().getName(),
255          IssueSeverity.ERROR);
256  }
257
258  private void parseChildPrimitive(JsonObject object, Element context, Set<String> processed, Property property,
259      String path, String name) throws FHIRException {
260    String npath = path + "/" + property.getName();
261    processed.add(name);
262    processed.add("_" + name);
263    JsonElement main = object.has(name) ? object.get(name) : null;
264    JsonElement fork = object.has("_" + name) ? object.get("_" + name) : null;
265    if (main != null || fork != null) {
266      if (property.isList() && ((main == null) || (main instanceof JsonArray))
267          && ((fork == null) || (fork instanceof JsonArray))) {
268        JsonArray arr1 = (JsonArray) main;
269        JsonArray arr2 = (JsonArray) fork;
270        for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) {
271          JsonElement m = arrI(arr1, i);
272          JsonElement f = arrI(arr2, i);
273          parseChildPrimitiveInstance(context, property, name, npath, m, f);
274        }
275      } else
276        parseChildPrimitiveInstance(context, property, name, npath, main, fork);
277    }
278  }
279
280  private JsonElement arrI(JsonArray arr, int i) {
281    return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i);
282  }
283
284  private int arrC(JsonArray arr) {
285    return arr == null ? 0 : arr.size();
286  }
287
288  private void parseChildPrimitiveInstance(Element context, Property property, String name, String npath,
289      JsonElement main, JsonElement fork) throws FHIRException {
290    if (main != null && !(main instanceof JsonPrimitive))
291      logError(line(main), col(main), npath, IssueType.INVALID,
292          "This property must be an simple value, not a " + main.getClass().getName(), IssueSeverity.ERROR);
293    else if (fork != null && !(fork instanceof JsonObject))
294      logError(line(fork), col(fork), npath, IssueType.INVALID,
295          "This property must be an object, not a " + fork.getClass().getName(), IssueSeverity.ERROR);
296    else {
297      Element n = new Element(name, property).markLocation(line(main != null ? main : fork),
298          col(main != null ? main : fork));
299      context.getChildren().add(n);
300      if (main != null) {
301        JsonPrimitive p = (JsonPrimitive) main;
302        n.setValue(p.getAsString());
303        if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) {
304          try {
305            XhtmlParser xp = new XhtmlParser();
306            n.setXhtml(xp.parse(n.getValue(), null).getDocumentElement());
307            if (policy == ValidationPolicy.EVERYTHING) {
308              for (StringPair s : xp.getValidationIssues()) {
309                logError(line(main), col(main), npath, IssueType.INVALID, s.getName() + " " + s.getValue(),
310                    IssueSeverity.ERROR);
311              }
312            }
313          } catch (Exception e) {
314            logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing XHTML: " + e.getMessage(),
315                IssueSeverity.ERROR);
316          }
317        }
318        if (policy == ValidationPolicy.EVERYTHING) {
319          // now we cross-check the primitive format against the stated type
320          if (Utilities.existsInList(n.getType(), "boolean")) {
321            if (!p.isBoolean())
322              logError(line(main), col(main), npath, IssueType.INVALID,
323                  "Error parsing JSON: the primitive value must be a boolean", IssueSeverity.ERROR);
324          } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) {
325            if (!p.isNumber())
326              logError(line(main), col(main), npath, IssueType.INVALID,
327                  "Error parsing JSON: the primitive value must be a number", IssueSeverity.ERROR);
328          } else if (!p.isString())
329            logError(line(main), col(main), npath, IssueType.INVALID,
330                "Error parsing JSON: the primitive value must be a string", IssueSeverity.ERROR);
331        }
332      }
333      if (fork != null) {
334        JsonObject child = (JsonObject) fork;
335        checkObject(child, npath);
336        parseChildren(npath, child, n, false);
337      }
338    }
339  }
340
341  private void parseResource(String npath, JsonObject res, Element parent, Property elementProperty)
342      throws FHIRException {
343    JsonElement rt = res.get("resourceType");
344    if (rt == null) {
345      logError(line(res), col(res), npath, IssueType.INVALID, "Unable to find resourceType property",
346          IssueSeverity.FATAL);
347    } else {
348      String name = rt.getAsString();
349      StructureDefinition sd = context.fetchResource(StructureDefinition.class,
350          ProfileUtilities.sdNs(name, context.getOverrideVersionNs()));
351      if (sd == null)
352        throw new FHIRFormatError(
353            "Contained resource does not appear to be a FHIR resource (unknown name '" + name + "')");
354      parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd),
355          SpecialElement.fromProperty(parent.getProperty()), elementProperty);
356      parent.setType(name);
357      parseChildren(npath, res, parent, true);
358    }
359  }
360
361  private void reapComments(JsonObject object, Element context) {
362    if (object.has("fhir_comments")) {
363      JsonArray arr = object.getAsJsonArray("fhir_comments");
364      for (JsonElement e : arr) {
365        context.getComments().add(e.getAsString());
366      }
367    }
368  }
369
370  private int line(JsonElement e) {
371    if (map == null || !map.containsKey(e))
372      return -1;
373    else
374      return map.get(e).getLine();
375  }
376
377  private int col(JsonElement e) {
378    if (map == null || !map.containsKey(e))
379      return -1;
380    else
381      return map.get(e).getCol();
382  }
383
384  protected void prop(String name, String value, String link) throws IOException {
385    json.link(link);
386    if (name != null)
387      json.name(name);
388    json.value(value);
389  }
390
391  protected void open(String name, String link) throws IOException {
392    json.link(link);
393    if (name != null)
394      json.name(name);
395    json.beginObject();
396  }
397
398  protected void close() throws IOException {
399    json.endObject();
400  }
401
402  protected void openArray(String name, String link) throws IOException {
403    json.link(link);
404    if (name != null)
405      json.name(name);
406    json.beginArray();
407  }
408
409  protected void closeArray() throws IOException {
410    json.endArray();
411  }
412
413  @Override
414  public void compose(Element e, OutputStream stream, OutputStyle style, String identity)
415      throws FHIRException, IOException {
416    OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8");
417    if (style == OutputStyle.CANONICAL)
418      json = new JsonCreatorCanonical(osw);
419    else
420      json = new JsonCreatorGson(osw);
421    json.setIndent(style == OutputStyle.PRETTY ? "  " : "");
422    json.beginObject();
423    prop("resourceType", e.getType(), null);
424    Set<String> done = new HashSet<String>();
425    for (Element child : e.getChildren()) {
426      compose(e.getName(), e, done, child);
427    }
428    json.endObject();
429    json.finish();
430    osw.flush();
431  }
432
433  public void compose(Element e, JsonCreator json) throws Exception {
434    this.json = json;
435    json.beginObject();
436
437    prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty()));
438    Set<String> done = new HashSet<String>();
439    for (Element child : e.getChildren()) {
440      compose(e.getName(), e, done, child);
441    }
442    json.endObject();
443    json.finish();
444  }
445
446  private void compose(String path, Element e, Set<String> done, Element child) throws IOException {
447    boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList();
448    if (!isList) {// for specials, ignore the cardinality of the stated type
449      compose(path, child);
450    } else if (!done.contains(child.getName())) {
451      done.add(child.getName());
452      List<Element> list = e.getChildrenByName(child.getName());
453      composeList(path, list);
454    }
455  }
456
457  private void composeList(String path, List<Element> list) throws IOException {
458    // there will be at least one element
459    String name = list.get(0).getName();
460    boolean complex = true;
461    if (list.get(0).isPrimitive()) {
462      boolean prim = false;
463      complex = false;
464      for (Element item : list) {
465        if (item.hasValue())
466          prim = true;
467        if (item.hasChildren())
468          complex = true;
469      }
470      if (prim) {
471        openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
472        for (Element item : list) {
473          if (item.hasValue())
474            primitiveValue(null, item);
475          else
476            json.nullValue();
477        }
478        closeArray();
479      }
480      name = "_" + name;
481    }
482    if (complex) {
483      openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty()));
484      for (Element item : list) {
485        if (item.hasChildren()) {
486          open(null, null);
487          if (item.getProperty().isResource()) {
488            prop("resourceType", item.getType(),
489                linkResolver == null ? null : linkResolver.resolveType(item.getType()));
490          }
491          Set<String> done = new HashSet<String>();
492          for (Element child : item.getChildren()) {
493            compose(path + "." + name + "[]", item, done, child);
494          }
495          close();
496        } else
497          json.nullValue();
498      }
499      closeArray();
500    }
501  }
502
503  private void primitiveValue(String name, Element item) throws IOException {
504    if (name != null) {
505      if (linkResolver != null)
506        json.link(linkResolver.resolveProperty(item.getProperty()));
507      json.name(name);
508    }
509    String type = item.getType();
510    if (Utilities.existsInList(type, "boolean"))
511      json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false));
512    else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt"))
513      json.value(new Integer(item.getValue()));
514    else if (Utilities.existsInList(type, "decimal"))
515      try {
516        json.value(new BigDecimal(item.getValue()));
517      } catch (Exception e) {
518        throw new NumberFormatException("error writing number '" + item.getValue() + "' to JSON");
519      }
520    else
521      json.value(item.getValue());
522  }
523
524  private void compose(String path, Element element) throws IOException {
525    String name = element.getName();
526    if (element.isPrimitive() || isPrimitive(element.getType())) {
527      if (element.hasValue())
528        primitiveValue(name, element);
529      name = "_" + name;
530    }
531    if (element.hasChildren()) {
532      open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty()));
533      if (element.getProperty().isResource()) {
534        prop("resourceType", element.getType(),
535            linkResolver == null ? null : linkResolver.resolveType(element.getType()));
536      }
537      Set<String> done = new HashSet<String>();
538      for (Element child : element.getChildren()) {
539        compose(path + "." + element.getName(), element, done, child);
540      }
541      close();
542    }
543  }
544
545}