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