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