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