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