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