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