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