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