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