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