
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.FileUtilities; 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 072@Deprecated 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, FHIRFormatError, DefinitionException { 100 // if we're parsing at this point, then we're going to use the custom parser 101 map = new HashMap<JsonElement, LocationData>(); 102 String source = FileUtilities.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 = (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 FHIRFormatError, DefinitionException { 121 this.map = map; 122 return parse(object); 123 } 124 125 public Element parse(JsonObject object) throws FHIRFormatError, DefinitionException { 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 DefinitionException, FHIRFormatError { 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 if (property.isChoice()) { 174 for (TypeRefComponent type : property.getDefinition().getType()) { 175 String eName = property.getName().substring(0, property.getName().length()-3) + Utilities.capitalize(type.getCode()); 176 if (!isPrimitive(type.getCode()) && object.has(eName)) { 177 parseChildComplex(path, object, context, processed, property, eName); 178 break; 179 } else if (isPrimitive(type.getCode()) && (object.has(eName) || object.has("_"+eName))) { 180 parseChildPrimitive(object, context, processed, property, path, eName); 181 break; 182 } 183 } 184 } else if (property.isPrimitive(property.getType(null))) { 185 parseChildPrimitive(object, context, processed, property, path, property.getName()); 186 } else if (object.has(property.getName())) { 187 parseChildComplex(path, object, context, processed, property, property.getName()); 188 } 189 } 190 191 // second pass: check for things not processed 192 if (policy != ValidationPolicy.NONE) { 193 for (Entry<String, JsonElement> e : object.entrySet()) { 194 if (!processed.contains(e.getKey())) { 195 logError(line(e.getValue()), col(e.getValue()), path, IssueType.STRUCTURE, "Unrecognised property '@"+e.getKey()+"'", IssueSeverity.ERROR); 196 } 197 } 198 } 199 } 200 201 private void parseChildComplex(String path, JsonObject object, Element context, Set<String> processed, Property property, String name) throws FHIRFormatError, DefinitionException { 202 processed.add(name); 203 String npath = path+"/"+property.getName(); 204 JsonElement e = object.get(name); 205 if (property.isList() && (e instanceof JsonArray)) { 206 JsonArray arr = (JsonArray) e; 207 for (JsonElement am : arr) { 208 parseChildComplexInstance(npath, object, context, property, name, am); 209 } 210 } else { 211 parseChildComplexInstance(npath, object, context, property, name, e); 212 } 213 } 214 215 private void parseChildComplexInstance(String npath, JsonObject object, Element context, Property property, String name, JsonElement e) throws FHIRFormatError, DefinitionException { 216 if (e instanceof JsonObject) { 217 JsonObject child = (JsonObject) e; 218 Element n = new Element(name, property).markLocation(line(child), col(child)); 219 checkObject(child, npath); 220 context.getChildren().add(n); 221 if (property.isResource()) 222 parseResource(npath, child, n, property); 223 else 224 parseChildren(npath, child, n, false); 225 } else 226 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); 227 } 228 229 private void parseChildPrimitive(JsonObject object, Element context, Set<String> processed, Property property, String path, String name) throws FHIRFormatError, DefinitionException { 230 String npath = path+"/"+property.getName(); 231 processed.add(name); 232 processed.add("_"+name); 233 JsonElement main = object.has(name) ? object.get(name) : null; 234 JsonElement fork = object.has("_"+name) ? object.get("_"+name) : null; 235 if (main != null || fork != null) { 236 if (property.isList() && ((main == null) || (main instanceof JsonArray)) &&((fork == null) || (fork instanceof JsonArray)) ) { 237 JsonArray arr1 = (JsonArray) main; 238 JsonArray arr2 = (JsonArray) fork; 239 for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) { 240 JsonElement m = arrI(arr1, i); 241 JsonElement f = arrI(arr2, i); 242 parseChildPrimitiveInstance(context, property, name, npath, m, f); 243 } 244 } else 245 parseChildPrimitiveInstance(context, property, name, npath, main, fork); 246 } 247 } 248 249 private JsonElement arrI(JsonArray arr, int i) { 250 return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i); 251 } 252 253 private int arrC(JsonArray arr) { 254 return arr == null ? 0 : arr.size(); 255 } 256 257 private void parseChildPrimitiveInstance(Element context, Property property, String name, String npath, 258 JsonElement main, JsonElement fork) throws FHIRFormatError, DefinitionException { 259 if (main != null && !(main instanceof JsonPrimitive)) 260 logError(line(main), col(main), npath, IssueType.INVALID, "This property must be an simple value, not a "+main.getClass().getName(), IssueSeverity.ERROR); 261 else if (fork != null && !(fork instanceof JsonObject)) 262 logError(line(fork), col(fork), npath, IssueType.INVALID, "This property must be an object, not a "+fork.getClass().getName(), IssueSeverity.ERROR); 263 else { 264 Element n = new Element(name, property).markLocation(line(main != null ? main : fork), col(main != null ? main : fork)); 265 context.getChildren().add(n); 266 if (main != null) { 267 JsonPrimitive p = (JsonPrimitive) main; 268 n.setValue(p.getAsString()); 269 if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) { 270 try { 271 XhtmlParser xp = new XhtmlParser(); 272 n.setXhtml(xp.parse(n.getValue(), null).getDocumentElement()); 273 if (policy == ValidationPolicy.EVERYTHING) { 274 for (StringPair s : xp.getValidationIssues()) { 275 logError(line(main), col(main), npath, IssueType.INVALID, s.getName()+ " "+ s.getValue(), IssueSeverity.ERROR); 276 } 277 } 278 } catch (Exception e) { 279 logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing XHTML: "+e.getMessage(), IssueSeverity.ERROR); 280 } 281 } 282 if (policy == ValidationPolicy.EVERYTHING) { 283 // now we cross-check the primitive format against the stated type 284 if (Utilities.existsInList(n.getType(), "boolean")) { 285 if (!p.isBoolean()) 286 logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing JSON: the primitive value must be a boolean", IssueSeverity.ERROR); 287 } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) { 288 if (!p.isNumber()) 289 logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing JSON: the primitive value must be a number", IssueSeverity.ERROR); 290 } else if (!p.isString()) 291 logError(line(main), col(main), npath, IssueType.INVALID, "Error parsing JSON: the primitive value must be a string", IssueSeverity.ERROR); 292 } 293 } 294 if (fork != null) { 295 JsonObject child = (JsonObject) fork; 296 checkObject(child, npath); 297 parseChildren(npath, child, n, false); 298 } 299 } 300 } 301 302 303 private void parseResource(String npath, JsonObject res, Element parent, Property elementProperty) throws DefinitionException, FHIRFormatError { 304 JsonElement rt = res.get("resourceType"); 305 if (rt == null) { 306 logError(line(res), col(res), npath, IssueType.INVALID, "Unable to find resourceType property", IssueSeverity.FATAL); 307 } else { 308 String name = rt.getAsString(); 309 StructureDefinition sd = context.fetchTypeDefinition(name); 310 if (sd == null) 311 throw new FHIRFormatError("Contained resource does not appear to be a FHIR resource (unknown name '"+name+"')"); 312 parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd), SpecialElement.fromProperty(parent.getProperty()), elementProperty); 313 parent.setType(name); 314 parseChildren(npath, res, parent, true); 315 } 316 } 317 318 private void reapComments(JsonObject object, Element context) { 319 if (object.has("fhir_comments")) { 320 JsonArray arr = object.getAsJsonArray("fhir_comments"); 321 for (JsonElement e : arr) { 322 context.getComments().add(e.getAsString()); 323 } 324 } 325 } 326 327 private int line(JsonElement e) { 328 if (map == null|| !map.containsKey(e)) 329 return -1; 330 else 331 return map.get(e).getLine(); 332 } 333 334 private int col(JsonElement e) { 335 if (map == null|| !map.containsKey(e)) 336 return -1; 337 else 338 return map.get(e).getCol(); 339 } 340 341 342 protected void prop(String name, String value, String link) throws IOException { 343 json.link(link); 344 if (name != null) 345 json.name(name); 346 json.value(value); 347 } 348 349 protected void open(String name, String link) throws IOException { 350 json.link(link); 351 if (name != null) 352 json.name(name); 353 json.beginObject(); 354 } 355 356 protected void close() throws IOException { 357 json.endObject(); 358 } 359 360 protected void openArray(String name, String link) throws IOException { 361 json.link(link); 362 if (name != null) 363 json.name(name); 364 json.beginArray(); 365 } 366 367 protected void closeArray() throws IOException { 368 json.endArray(); 369 } 370 371 372 @Override 373 public void compose(Element e, OutputStream stream, OutputStyle style, String identity) throws FHIRException, IOException { 374 OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8"); 375 if (style == OutputStyle.CANONICAL) 376 json = new JsonCreatorCanonical(osw); 377 else 378 json = new JsonCreatorGson(osw); 379 json.setIndent(style == OutputStyle.PRETTY ? " " : ""); 380 json.beginObject(); 381 prop("resourceType", e.getType(), null); 382 Set<String> done = new HashSet<String>(); 383 for (Element child : e.getChildren()) { 384 compose(e.getName(), e, done, child); 385 } 386 json.endObject(); 387 json.finish(); 388 osw.flush(); 389 } 390 391 public void compose(Element e, JsonCreator json) throws Exception { 392 this.json = json; 393 json.beginObject(); 394 395 prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty())); 396 Set<String> done = new HashSet<String>(); 397 for (Element child : e.getChildren()) { 398 compose(e.getName(), e, done, child); 399 } 400 json.endObject(); 401 json.finish(); 402 } 403 404 private void compose(String path, Element e, Set<String> done, Element child) throws IOException { 405 boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList(); 406 if (!isList) {// for specials, ignore the cardinality of the stated type 407 compose(path, child); 408 } else if (!done.contains(child.getName())) { 409 done.add(child.getName()); 410 List<Element> list = e.getChildrenByName(child.getName()); 411 composeList(path, list); 412 } 413 } 414 415 private void composeList(String path, List<Element> list) throws IOException { 416 // there will be at least one element 417 String name = list.get(0).getName(); 418 boolean complex = true; 419 if (list.get(0).isPrimitive()) { 420 boolean prim = false; 421 complex = false; 422 for (Element item : list) { 423 if (item.hasValue()) 424 prim = true; 425 if (item.hasChildren()) 426 complex = true; 427 } 428 if (prim) { 429 openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty())); 430 for (Element item : list) { 431 if (item.hasValue()) 432 primitiveValue(null, item); 433 else 434 json.nullValue(); 435 } 436 closeArray(); 437 } 438 name = "_"+name; 439 } 440 if (complex) { 441 openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty())); 442 for (Element item : list) { 443 if (item.hasChildren()) { 444 open(null,null); 445 if (item.getProperty().isResource()) { 446 prop("resourceType", item.getType(), linkResolver == null ? null : linkResolver.resolveType(item.getType())); 447 } 448 Set<String> done = new HashSet<String>(); 449 for (Element child : item.getChildren()) { 450 compose(path+"."+name+"[]", item, done, child); 451 } 452 close(); 453 } else 454 json.nullValue(); 455 } 456 closeArray(); 457 } 458 } 459 460 private void primitiveValue(String name, Element item) throws IOException { 461 if (name != null) { 462 if (linkResolver != null) 463 json.link(linkResolver.resolveProperty(item.getProperty())); 464 json.name(name); 465 } 466 String type = item.getType(); 467 if (Utilities.existsInList(type, "boolean")) 468 json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false)); 469 else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt")) 470 json.value(new Integer(item.getValue())); 471 else if (Utilities.existsInList(type, "decimal")) 472 json.value(new BigDecimal(item.getValue())); 473 else 474 json.value(item.getValue()); 475 } 476 477 private void compose(String path, Element element) throws IOException { 478 String name = element.getName(); 479 if (element.isPrimitive() || isPrimitive(element.getType())) { 480 if (element.hasValue()) 481 primitiveValue(name, element); 482 name = "_"+name; 483 } 484 if (element.hasChildren()) { 485 open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty())); 486 if (element.getProperty().isResource()) { 487 prop("resourceType", element.getType(), linkResolver == null ? null : linkResolver.resolveType(element.getType())); 488 } 489 Set<String> done = new HashSet<String>(); 490 for (Element child : element.getChildren()) { 491 compose(path+"."+element.getName(), element, done, child); 492 } 493 close(); 494 } 495 } 496 497}