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