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