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