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