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