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