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