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