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}