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