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