
001package org.hl7.fhir.r5.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.ArrayList; 040import java.util.HashMap; 041import java.util.HashSet; 042import java.util.List; 043import java.util.Map; 044import java.util.Map.Entry; 045import java.util.Set; 046 047import org.hl7.fhir.exceptions.FHIRException; 048import org.hl7.fhir.exceptions.FHIRFormatError; 049import org.hl7.fhir.r5.conformance.profile.ProfileUtilities; 050import org.hl7.fhir.r5.context.ContextUtilities; 051import org.hl7.fhir.r5.context.IWorkerContext; 052import org.hl7.fhir.r5.elementmodel.Element.SpecialElement; 053import org.hl7.fhir.r5.formats.IParser.OutputStyle; 054import org.hl7.fhir.r5.formats.JsonCreator; 055import org.hl7.fhir.r5.formats.JsonCreatorCanonical; 056import org.hl7.fhir.r5.formats.JsonCreatorDirect; 057import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; 058import org.hl7.fhir.r5.model.StructureDefinition; 059import org.hl7.fhir.r5.utils.FHIRPathEngine; 060import org.hl7.fhir.utilities.CommaSeparatedStringBuilder; 061import org.hl7.fhir.utilities.StringPair; 062import org.hl7.fhir.utilities.TextFile; 063import org.hl7.fhir.utilities.Utilities; 064import org.hl7.fhir.utilities.VersionUtilities; 065import org.hl7.fhir.utilities.i18n.I18nConstants; 066import org.hl7.fhir.utilities.json.model.JsonArray; 067import org.hl7.fhir.utilities.json.model.JsonComment; 068import org.hl7.fhir.utilities.json.model.JsonElement; 069import org.hl7.fhir.utilities.json.model.JsonNull; 070import org.hl7.fhir.utilities.json.model.JsonObject; 071import org.hl7.fhir.utilities.json.model.JsonPrimitive; 072import org.hl7.fhir.utilities.json.model.JsonProperty; 073import org.hl7.fhir.utilities.validation.ValidationMessage; 074import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 075import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 076import org.hl7.fhir.utilities.xhtml.XhtmlParser; 077 078 079public class JsonParser extends ParserBase { 080 081 private JsonCreator json; 082 private boolean allowComments; 083 084 private ProfileUtilities profileUtilities; 085 private Element baseElement; 086 087 public JsonParser(IWorkerContext context, ProfileUtilities utilities) { 088 super(context); 089 090 this.profileUtilities = utilities; 091 } 092 093 public JsonParser(IWorkerContext context) { 094 super(context); 095 096 this.profileUtilities = new ProfileUtilities(this.context, null, null, new FHIRPathEngine(context)); 097 } 098 099 public Element parse(String source, String type) throws Exception { 100 JsonObject obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 101 String path = "/"+type; 102 StructureDefinition sd = getDefinition(-1, -1, type); 103 if (sd == null) 104 return null; 105 106 Element result = new Element(type, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities)); 107 result.setPath(type); 108 checkObject(obj, result, path); 109 result.setType(type); 110 parseChildren(path, obj, result, true); 111 result.numberChildren(); 112 return result; 113 } 114 115 116 @Override 117 public List<NamedElement> parse(InputStream stream) throws IOException, FHIRException { 118 // if we're parsing at this point, then we're going to use the custom parser 119 List<NamedElement> res = new ArrayList<>(); 120 String source = TextFile.streamToString(stream); 121 JsonObject obj = null; 122 if (policy == ValidationPolicy.EVERYTHING) { 123 try { 124 obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 125 } catch (Exception e) { 126 logError(ValidationMessage.NO_RULE_DATE, -1, -1,context.formatMessage(I18nConstants.DOCUMENT), IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_, e.getMessage()), IssueSeverity.FATAL); 127 return null; 128 } 129 } else { 130 obj = org.hl7.fhir.utilities.json.parser.JsonParser.parseObject(source, true, true); 131 } 132 Element e = parse(obj); 133 if (e != null) { 134 res.add(new NamedElement(null, e)); 135 } 136 return res; 137 } 138 139 public Element parse(JsonObject object) throws FHIRException { 140 StructureDefinition sd = getLogical(); 141 String name; 142 String path; 143 if (sd == null) { 144 JsonElement rt = object.get("resourceType"); 145 if (rt == null) { 146 logError(ValidationMessage.NO_RULE_DATE, line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL); 147 return null; 148 } else if (!rt.isJsonString()) { 149 logError("2022-11-26", line(object), col(object), "$", IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL); 150 return null; 151 } else { 152 name = rt.asString(); 153 154 sd = getDefinition(line(object), col(object), name); 155 if (sd == null) { 156 return null; 157 } 158 } 159 path = name; 160 } else { 161 name = sd.getType(); 162 path = sd.getTypeTail(); 163 } 164 baseElement = new Element(name, new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities)); 165 checkObject(object, baseElement, path); 166 baseElement.markLocation(line(object), col(object)); 167 baseElement.setType(name); 168 baseElement.setPath(baseElement.fhirTypeRoot()); 169 parseChildren(path, object, baseElement, true); 170 baseElement.numberChildren(); 171 return baseElement; 172 } 173 174 private void checkObject(JsonObject object, Element b, String path) { 175 checkComments(object, b, path); 176 if (policy == ValidationPolicy.EVERYTHING) { 177 if (object.getProperties().size() == 0) { 178 logError(ValidationMessage.NO_RULE_DATE, line(object), col(object), path, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_MUST_HAVE_SOME_CONTENT), IssueSeverity.ERROR); 179 } 180 } 181 } 182 183 private void checkComments(JsonElement element, Element b, String path) throws FHIRFormatError { 184 if (element != null && element.hasComments()) { 185 if (allowComments) { 186 for (JsonComment c : element.getComments()) { 187 b.getComments().add(c.getContent()); 188 } 189 } else { 190 for (JsonComment c : element.getComments()) { 191 logError("2022-11-26", c.getStart().getLine(), c.getStart().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMENTS_NOT_ALLOWED), IssueSeverity.ERROR); 192 } 193 } 194 } 195 } 196 197 private void parseChildren(String path, JsonObject object, Element element, boolean hasResourceType) throws FHIRException { 198 reapComments(object, element); 199 List<Property> properties = element.getProperty().getChildProperties(element.getName(), null); 200 Set<String> processed = new HashSet<String>(); 201 if (hasResourceType) { 202 processed.add("resourceType"); 203 } 204 Map<String, JsonProperty> recognisedChildren = new HashMap<>(); 205 Set<String> unique = new HashSet<>(); 206 for (JsonProperty p : object.getProperties()) { 207 if (p.isUnquotedName()) { 208 logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_NO_QUOTES, p.getName()), IssueSeverity.ERROR); 209 } 210 if (p.isNoComma()) { 211 logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_MISSING), IssueSeverity.ERROR); 212 } 213 if (unique.contains(p.getName())) { 214 logError("2022-11-26", line(p.getValue()), col(p.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY, p.getName()), IssueSeverity.ERROR); 215 } else { 216 unique.add(p.getName()); 217 recognisedChildren.put(p.getName(), p); 218 } 219 } 220 221 // note that we do not trouble ourselves to maintain the wire format order here - we don't even know what it was anyway 222 // first pass: process the properties 223 for (Property property : properties) { 224 parseChildItem(path, recognisedChildren, element, processed, property); 225 } 226 227 // second pass: check for things not processed 228 if (policy != ValidationPolicy.NONE) { 229 for (Entry<String, JsonProperty> e : recognisedChildren.entrySet()) { 230 if (!processed.contains(e.getKey())) { 231 StructureDefinition sd = element.getProperty().isLogical() ? new ContextUtilities(context).fetchByJsonName(e.getKey()) : null; 232 if (sd != null) { 233 Property property = new Property(context, sd.getSnapshot().getElementFirstRep(), sd, element.getProperty().getUtils()); 234 parseChildItem(path, recognisedChildren, element, null, property); 235 } else if ("fhir_comments".equals(e.getKey()) && (VersionUtilities.isR2BVer(context.getVersion()) || VersionUtilities.isR2Ver(context.getVersion()))) { 236 if (!e.getValue().getValue().isJsonArray()) { 237 logError("2022-12-17", line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, e.getValue().getValue().type().toName()), IssueSeverity.ERROR); 238 } else { 239 for (JsonElement c : e.getValue().getValue().asJsonArray()) { 240 if (!c.isJsonString()) { 241 logError("2022-12-17", line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.ILLEGAL_COMMENT_TYPE, c.type().toName()), IssueSeverity.ERROR); 242 } else { 243 element.getComments().add(c.asString()); 244 } 245 } 246 } 247 } else { 248 logError(ValidationMessage.NO_RULE_DATE, line(e.getValue().getValue()), col(e.getValue().getValue()), path, IssueType.STRUCTURE, context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_, e.getKey()), IssueSeverity.ERROR); 249 } 250 } 251 } 252 } 253 if (object.isExtraComma()) { 254 logError("2022-11-26", object.getEnd().getLine(), object.getEnd().getCol(), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR); 255 } 256 257 } 258 259 public void parseChildItem(String path, Map<String, JsonProperty> children, Element context, Set<String> processed, Property property) { 260 if (property.isChoice() || property.getDefinition().getPath().endsWith("data[x]")) { 261 if (property.isJsonPrimitiveChoice()) { 262 if (children.containsKey(property.getJsonName())) { 263 JsonElement je = children.get(property.getJsonName()).getValue(); 264 if (processed != null) processed.add(property.getJsonName()); 265 String type = getTypeFromJsonType(je); 266 if (type == null) { 267 logError(ValidationMessage.NO_RULE_DATE, line(je), col(je), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE, describeType(je), property.getName(), property.typeSummary()), IssueSeverity.ERROR); 268 } else if (property.hasType(type)) { 269 Property np = new Property(property.getContext(), property.getDefinition(), property.getStructure(), property.getUtils(), type); 270 parseChildPrimitive(children, context, processed, np, path, property.getName(), false); 271 } else { 272 logError(ValidationMessage.NO_RULE_DATE, line(je), col(je), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE_WRONG, describeType(je), property.getName(), type, property.typeSummary()), IssueSeverity.ERROR); 273 } 274 } 275 } else { 276 for (TypeRefComponent type : property.getDefinition().getType()) { 277 String eName = property.getJsonName().substring(0, property.getName().length()-3) + Utilities.capitalize(type.getWorkingCode()); 278 if (!isPrimitive(type.getWorkingCode()) && children.containsKey(eName)) { 279 parseChildComplex(path, children, context, processed, property, eName, false); 280 break; 281 } else if (isPrimitive(type.getWorkingCode()) && (children.containsKey(eName) || children.containsKey("_"+eName))) { 282 parseChildPrimitive(children, context, processed, property, path, eName, false); 283 break; 284 } 285 } 286 } 287 } else if (property.isPrimitive(property.getType(null))) { 288 parseChildPrimitive(children, context, processed, property, path, property.getJsonName(), property.hasJsonName()); 289 } else if (children.containsKey(property.getJsonName())) { 290 parseChildComplex(path, children, context, processed, property, property.getJsonName(), property.hasJsonName()); 291 } 292 } 293 294 private String getTypeFromJsonType(JsonElement je) { 295 if (je.isJsonPrimitive()) { 296 JsonPrimitive p = je.asJsonPrimitive(); 297 if (p.isJsonString()) { 298 return "string"; 299 } else if (p.isJsonBoolean()) { 300 return "boolean"; 301 } else { 302 String s = p.asString(); 303 if (Utilities.isInteger(s)) { 304 return "integer"; 305 } else { 306 return "decimal"; 307 } 308 } 309 } else { 310 return null; 311 } 312 } 313 314 private void parseChildComplex(String path, Map<String, JsonProperty> children, Element element, Set<String> processed, Property property, String name, boolean isJsonName) throws FHIRException { 315 if (processed != null) { 316 processed.add(name); 317 } 318 String npath = path+"."+property.getName(); 319 String fpath = element.getPath()+"."+property.getName(); 320 JsonProperty p = children.get(name); 321 JsonElement e = p == null ? null : p.getValue(); 322 if (property.isList() && !property.isJsonKeyArray() && (e instanceof JsonArray)) { 323 JsonArray arr = (JsonArray) e; 324 if (arr.isExtraComma()) { 325 logError("2022-11-26", arr.getEnd().getLine(), arr.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR); 326 } 327 if (arr.size() == 0) { 328 if (property.canBeEmpty()) { 329 // nothing 330 } else { 331 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ARRAY_CANNOT_BE_EMPTY), IssueSeverity.ERROR); 332 } 333 } 334 int c = 0; 335 for (JsonElement am : arr) { 336 parseChildComplexInstance(npath+"["+c+"]", fpath+"["+c+"]", element, property, name, am, c == 0 ? arr : null, path); 337 c++; 338 } 339 } else if (property.isJsonKeyArray()) { 340 String code = property.getJsonKeyProperty(); 341 List<Property> properties = property.getChildProperties(element.getName(), null); 342 if (properties.size() != 2) { 343 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_CHILD_COUNT, propNames(properties)), IssueSeverity.ERROR); 344 } else { 345 Property propK = properties.get(0); 346 Property propV = properties.get(1); 347 if (!propK.getName().equals(code)) { 348 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_PROP_NAME, propNames(properties)), IssueSeverity.ERROR); 349 } else if (!propK.isPrimitive()) { 350 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_PROP_TYPE, propNames(properties), propK.typeSummary()), IssueSeverity.ERROR); 351 } else if (propV.isList()) { 352 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_NO_LIST, propV.getName()), IssueSeverity.ERROR); 353 } else if (propV.isChoice() && propV.getName().endsWith("[x]")) { 354 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.OBJECT_CANNOT_BE_KEYED_ARRAY_NO_CHOICE, propV.getName()), IssueSeverity.ERROR); 355 } else if (!(e instanceof JsonObject)) { 356 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(e)), IssueSeverity.ERROR); 357 } else { 358 JsonObject o = (JsonObject) e; 359 if (o.isExtraComma()) { 360 logError("2022-11-26", o.getEnd().getLine(), o.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR); 361 } 362 363 int i = 0; 364 Set<String> names = new HashSet<>(); 365 for (JsonProperty pv : o.getProperties()) { 366 if (names.contains(pv.getName())) { 367 logError("2022-11-26", line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.DUPLICATE_JSON_PROPERTY_KEY, pv.getName()), IssueSeverity.ERROR); 368 } else { 369 names.add(pv.getName()); 370 } 371 // create an array entry 372 String npathArr = path+"."+property.getName()+"["+i+"]"; 373 String fpathArr = element.getPath()+"."+property.getName()+"["+i+"]"; 374 375 Element n = new Element(name, property).markLocation(line(pv.getValue()), col(pv.getValue())); 376 n.setPath(fpath); 377 element.getChildren().add(n); 378 // handle the key 379 String fpathKey = fpathArr+"."+propK.getName(); 380 Element nKey = new Element(code, propK).markLocation(line(pv.getValue()), col(pv.getValue())); 381 checkComments(pv.getValue(), n, fpathArr); 382 nKey.setPath(fpathKey); 383 n.getChildren().add(nKey); 384 nKey.setValue(pv.getName()); 385 386 387 boolean ok = true; 388 Property pvl = propV; 389 if (propV.isJsonPrimitiveChoice()) { 390 ok = false; 391 String type = getTypeFromJsonType(pv.getValue()); 392 if (type == null) { 393 logError(ValidationMessage.NO_RULE_DATE, line(pv.getValue()), col(pv.getValue()), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE, describeType(pv.getValue()), propV.getName(), propV.typeSummary()), IssueSeverity.ERROR); 394 } else if (propV.hasType(type)) { 395 pvl = new Property(propV.getContext(), propV.getDefinition(), propV.getStructure(), propV.getUtils(), type); 396 ok = true; 397 } else { 398 logError(ValidationMessage.NO_RULE_DATE, line(pv.getValue()), col(pv.getValue()), path, IssueType.STRUCTURE, this.context.formatMessage(I18nConstants.UNRECOGNISED_PROPERTY_TYPE_WRONG, describeType(pv.getValue()), propV.getName(), type, propV.typeSummary()), IssueSeverity.ERROR); 399 } 400 } 401 if (ok) { 402 // handle the value 403 String npathV = npathArr+"."+pvl.getName(); 404 String fpathV = fpathArr+"."+pvl.getName(); 405 if (propV.isPrimitive(pvl.getType(null))) { 406 parseChildPrimitiveInstance(n, pvl, pvl.getName(), false, npathV, fpathV, pv.getValue(), null); 407 } else if (pv.getValue() instanceof JsonObject || pv.getValue() instanceof JsonNull) { 408 parseChildComplexInstance(npathV, fpathV, n, pvl, pvl.getName(), pv.getValue(), null, null); 409 } else { 410 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(pv.getValue())), IssueSeverity.ERROR); 411 } 412 } 413 i++; 414 } 415 } 416 } 417 } else { 418 if (property.isList()) { 419 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(e), name, path), IssueSeverity.ERROR); 420 } 421 parseChildComplexInstance(npath, fpath, element, property, name, e, null, null); 422 } 423 } 424 425 private Object propNames(List<Property> properties) { 426 CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder(); 427 for (Property p: properties) { 428 b.append(p.getName()); 429 } 430 return b.toString(); 431 } 432 433 private void parseChildComplexInstance(String npath, String fpath, Element element, Property property, String name, JsonElement e, JsonElement commentContext, String commentPath) throws FHIRException { 434 if (property.hasTypeSpecifier()) { 435 FHIRPathEngine fpe = new FHIRPathEngine(context); 436 String type = null; 437 String cond = null; 438 for (StringPair sp : property.getTypeSpecifiers()) { 439 if (fpe.evaluateToBoolean(null, baseElement, baseElement, element, fpe.parse(sp.getName()))) { 440 type = sp.getValue(); 441 cond = sp.getName(); 442 break; 443 } 444 } 445 if (type != null) { 446 StructureDefinition sd = context.fetchResource(StructureDefinition.class, type); 447 if (sd == null) { 448 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ILLEGAL_TYPE, type, cond), IssueSeverity.ERROR); 449 } else { 450 if (sd.getAbstract()) { 451 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_ABSTRACT_TYPE, type, cond), IssueSeverity.ERROR); 452 } 453 property = property.cloneToType(sd); 454 } 455 } else { 456 StructureDefinition sd = context.fetchTypeDefinition(property.getType()); 457 if (sd == null) { 458 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ILLEGAL_TYPE, property.getType()), IssueSeverity.ERROR); 459 } else if (sd.getAbstract()) { 460 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.TYPE_SPECIFIER_NM_ABSTRACT_TYPE, property.getType()), IssueSeverity.ERROR); 461 } 462 } 463 } 464 if (e instanceof JsonObject) { 465 JsonObject child = (JsonObject) e; 466 Element n = new Element(name, property).markLocation(line(child), col(child)); 467 n.setPath(fpath); 468 checkComments(commentContext, n, commentPath); 469 checkObject(child, n, npath); 470 element.getChildren().add(n); 471 if (property.isResource()) { 472 parseResource(npath, child, n, property); 473 } else { 474 parseChildren(npath, child, n, false); 475 } 476 } else if (property.isNullable() && e instanceof JsonNull) { 477 // we create an element marked as a null element so we know something was present 478 JsonNull child = (JsonNull) e; 479 Element n = new Element(name, property).markLocation(line(child), col(child)); 480 checkComments(commentContext, n, commentPath); 481 checkComments(child, n, fpath); 482 n.setPath(fpath); 483 element.getChildren().add(n); 484 n.setNull(true); 485 // nothing to do, it's ok, but we treat it like it doesn't exist 486 } else { 487 logError(ValidationMessage.NO_RULE_DATE, line(e), col(e), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE__NOT_, (property.isList() ? "an Array" : "an Object"), describe(e), name, npath), IssueSeverity.ERROR); 488 } 489 } 490 491 private String describe(JsonElement e) { 492 if (e instanceof JsonArray) { 493 return "an Array"; 494 } 495 if (e instanceof JsonObject) { 496 return "an Object"; 497 } 498 if (e instanceof JsonNull) { 499 return "a Null"; 500 } 501 if (e instanceof JsonPrimitive) { 502 return "a Primitive property"; 503 } 504 return null; 505 } 506 507 private String describeType(JsonElement e) { 508 return e.type().toName(); 509 } 510 511 private void parseChildPrimitive(Map<String, JsonProperty> children, Element element, Set<String> processed, Property property, String path, String name, boolean isJsonName) throws FHIRException { 512 String npath = path+"."+property.getName(); 513 String fpath = element.getPath()+"."+property.getName(); 514 processed.add(name); 515 processed.add("_"+name); 516 JsonProperty main = children.containsKey(name) ? children.get(name) : null; 517 JsonProperty fork = children.containsKey("_"+name) ? children.get("_"+name) : null; 518 if (main != null && main.getValue().isJsonString() && main.isUnquotedValue()) { 519 logError("2022-11-26", line(main.getValue()), col(main.getValue()), path, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_VALUE_NO_QUOTES, main.getName(), main.getValue().asString()), IssueSeverity.ERROR); 520 } 521 if (main != null || fork != null) { 522 if (property.isList()) { 523 boolean ok = true; 524 if (!(main == null || main.getValue() instanceof JsonArray)) { 525 logError(ValidationMessage.NO_RULE_DATE, line(main.getValue()), col(main.getValue()), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(main.getValue()), name, path), IssueSeverity.ERROR); 526 ok = false; 527 } 528 if (!(fork == null || fork.getValue() instanceof JsonArray)) { 529 logError(ValidationMessage.NO_RULE_DATE, line(fork.getValue()), col(fork.getValue()), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_BASE_PROPERTY_MUST_BE_AN_ARRAY_NOT_, describe(main.getValue()), name, path), IssueSeverity.ERROR); 530 ok = false; 531 } 532 if (ok) { 533 JsonArray arr1 = (JsonArray) (main == null ? null : main.getValue()); 534 JsonArray arr2 = (JsonArray) (fork == null ? null : fork.getValue()); 535 if (arr1 != null && arr1.isExtraComma()) { 536 logError("2022-11-26", arr1.getEnd().getLine(), arr1.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR); 537 } 538 if (arr2 != null && arr2.isExtraComma()) { 539 logError("2022-11-26", arr2.getEnd().getLine(), arr2.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Array"), IssueSeverity.ERROR); 540 } 541 542 for (int i = 0; i < Math.max(arrC(arr1), arrC(arr2)); i++) { 543 JsonElement m = arrI(arr1, i); 544 JsonElement f = arrI(arr2, i); 545 if (m != null && m.isJsonString() && arr1.isUnquoted(i)) { 546 logError("2022-11-26", line(m), col(m), path+"."+name+"["+i+"]", IssueType.INVALID, context.formatMessage(I18nConstants.JSON_PROPERTY_VALUE_NO_QUOTES, "item", m.asString()), IssueSeverity.ERROR); 547 } 548 parseChildPrimitiveInstance(element, property, name, isJsonName, npath, fpath, m, f); 549 } 550 } 551 } else { 552 parseChildPrimitiveInstance(element, property, name, isJsonName, npath, fpath, main == null ? null : main.getValue(), fork == null ? null : fork.getValue()); 553 } 554 } 555 } 556 557 private JsonElement arrI(JsonArray arr, int i) { 558 return arr == null || i >= arr.size() || arr.get(i) instanceof JsonNull ? null : arr.get(i); 559 } 560 561 private int arrC(JsonArray arr) { 562 return arr == null ? 0 : arr.size(); 563 } 564 565 private void parseChildPrimitiveInstance(Element element, Property property, String name, boolean isJsonName, String npath, String fpath, JsonElement main, JsonElement fork) throws FHIRException { 566 if (main != null && !(main.isJsonBoolean() || main.isJsonNumber() || main.isJsonString())) { 567 logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage( 568 I18nConstants.THIS_PROPERTY_MUST_BE_AN_SIMPLE_VALUE_NOT_, describe(main), name, npath), IssueSeverity.ERROR); 569 } else if (fork != null && !(fork instanceof JsonObject)) { 570 logError(ValidationMessage.NO_RULE_DATE, line(fork), col(fork), npath, IssueType.INVALID, context.formatMessage(I18nConstants.THIS_PROPERTY_MUST_BE_AN_OBJECT_NOT_, describe(fork), name, npath), IssueSeverity.ERROR); 571 } else { 572 Element n = new Element(isJsonName ? property.getName() : name, property).markLocation(line(main != null ? main : fork), col(main != null ? main : fork)); 573 if (main != null) { 574 checkComments(main, n, npath); 575 } 576 if (fork != null) { 577 checkComments(fork, n, npath); 578 } 579 n.setPath(fpath); 580 element.getChildren().add(n); 581 if (main != null) { 582 JsonPrimitive p = (JsonPrimitive) main; 583 n.setValue(property.hasImpliedPrefix() ? property.getImpliedPrefix()+p.asString() : p.asString()); 584 if (!n.getProperty().isChoice() && n.getType().equals("xhtml")) { 585 try { 586 XhtmlParser xhtml = new XhtmlParser(); 587 n.setXhtml(xhtml.setXmlMode(true).parse(n.getValue(), null).getDocumentElement()); 588 if (policy == ValidationPolicy.EVERYTHING) { 589 for (StringPair s : xhtml.getValidationIssues()) { 590 logError("2022-11-17", line(main), col(main), npath, IssueType.INVALID, context.formatMessage(s.getName(), s.getValue()), IssueSeverity.ERROR); 591 } 592 } 593 } catch (Exception e) { 594 logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_XHTML_, e.getMessage()), IssueSeverity.ERROR); 595 } 596 } 597 if (policy == ValidationPolicy.EVERYTHING) { 598 // now we cross-check the primitive format against the stated type 599 if (Utilities.existsInList(n.getType(), "boolean")) { 600 if (!p.isJsonBoolean()) { 601 logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_BOOLEAN), IssueSeverity.ERROR); 602 } 603 } else if (Utilities.existsInList(n.getType(), "integer", "unsignedInt", "positiveInt", "decimal")) { 604 if (!p.isJsonNumber()) 605 logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_NUMBER), IssueSeverity.ERROR); 606 } else if (!p.isJsonString()) { 607 logError(ValidationMessage.NO_RULE_DATE, line(main), col(main), npath, IssueType.INVALID, context.formatMessage(I18nConstants.ERROR_PARSING_JSON_THE_PRIMITIVE_VALUE_MUST_BE_A_STRING), IssueSeverity.ERROR); 608 } 609 } 610 } 611 if (fork != null) { 612 JsonObject child = (JsonObject) fork; 613 checkObject(child, n, npath); 614 parseChildren(npath, child, n, false); 615 } 616 } 617 } 618 619 620 private void parseResource(String npath, JsonObject res, Element parent, Property elementProperty) throws FHIRException { 621 JsonElement rt = res.get("resourceType"); 622 if (rt == null) { 623 logError(ValidationMessage.NO_RULE_DATE, line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.UNABLE_TO_FIND_RESOURCETYPE_PROPERTY), IssueSeverity.FATAL); 624 } else if (!rt.isJsonString()) { 625 logError("2022-11-26", line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.RESOURCETYPE_PROPERTY_WRONG_TYPE, rt.type().toName()), IssueSeverity.FATAL); 626 } else { 627 String name = rt.asString(); 628 StructureDefinition sd = context.fetchResource(StructureDefinition.class, ProfileUtilities.sdNs(name, null)); 629 if (sd == null) { 630 logError(ValidationMessage.NO_RULE_DATE, line(res), col(res), npath, IssueType.INVALID, context.formatMessage(I18nConstants.CONTAINED_RESOURCE_DOES_NOT_APPEAR_TO_BE_A_FHIR_RESOURCE_UNKNOWN_NAME_, name), IssueSeverity.FATAL); 631 } else { 632 parent.updateProperty(new Property(context, sd.getSnapshot().getElement().get(0), sd, this.profileUtilities), SpecialElement.fromProperty(parent.getProperty()), elementProperty); 633 parent.setType(name); 634 parseChildren(npath, res, parent, true); 635 } 636 } 637 if (res.isExtraComma()) { 638 logError("2022-11-26", res.getEnd().getLine(), res.getEnd().getCol(), npath, IssueType.INVALID, context.formatMessage(I18nConstants.JSON_COMMA_EXTRA, "Object"), IssueSeverity.ERROR); 639 } 640 641 } 642 643 private void reapComments(JsonObject object, Element context) { 644 // todo 645 } 646 647 private int line(JsonElement e) { 648 return e.getStart().getLine(); 649 } 650 651 private int col(JsonElement e) { 652 return e.getEnd().getCol(); 653 } 654 655 656 protected void prop(String name, String value, String link) throws IOException { 657 json.link(link); 658 if (name != null) 659 json.name(name); 660 json.value(value); 661 } 662 663 protected void open(String name, String link) throws IOException { 664 json.link(link); 665 if (name != null) 666 json.name(name); 667 json.beginObject(); 668 } 669 670 protected void close() throws IOException { 671 json.endObject(); 672 } 673 674 protected void openArray(String name, String link) throws IOException { 675 json.link(link); 676 if (name != null) 677 json.name(name); 678 json.beginArray(); 679 } 680 681 protected void closeArray() throws IOException { 682 json.endArray(); 683 } 684 685 686 @Override 687 public void compose(Element e, OutputStream stream, OutputStyle style, String identity) throws FHIRException, IOException { 688 if (e.getPath() == null) { 689 e.populatePaths(null); 690 } 691 692 OutputStreamWriter osw = new OutputStreamWriter(stream, "UTF-8"); 693 if (style == OutputStyle.CANONICAL) { 694 json = new JsonCreatorCanonical(osw); 695 } else if (style == OutputStyle.PRETTY) { 696 json = new JsonCreatorDirect(osw, true, allowComments); 697 } else { 698 json = new JsonCreatorDirect(osw, false, allowComments); 699 } 700 checkComposeComments(e); 701 json.beginObject(); 702 prop("resourceType", e.getType(), null); 703 Set<String> done = new HashSet<String>(); 704 for (Element child : e.getChildren()) { 705 compose(e.getName(), e, done, child); 706 } 707 json.endObject(); 708 json.finish(); 709 osw.flush(); 710 } 711 712 private void checkComposeComments(Element e) { 713 for (String s : e.getComments()) { 714 json.comment(s); 715 } 716 } 717 718 public void compose(Element e, JsonCreator json) throws Exception { 719 if (e.getPath() == null) { 720 e.populatePaths(null); 721 } 722 723 this.json = json; 724 checkComposeComments(e); 725 json.beginObject(); 726 727 prop("resourceType", e.getType(), linkResolver == null ? null : linkResolver.resolveProperty(e.getProperty())); 728 Set<String> done = new HashSet<String>(); 729 for (Element child : e.getChildren()) { 730 compose(e.getName(), e, done, child); 731 } 732 json.endObject(); 733 json.finish(); 734 } 735 736 private void compose(String path, Element e, Set<String> done, Element child) throws IOException { 737 checkComposeComments(child); 738 if (wantCompose(path, child)) { 739 boolean isList = child.hasElementProperty() ? child.getElementProperty().isList() : child.getProperty().isList(); 740 if (!isList) {// for specials, ignore the cardinality of the stated type 741 compose(path, child); 742 } else if (!done.contains(child.getName())) { 743 done.add(child.getName()); 744 List<Element> list = e.getChildrenByName(child.getName()); 745 composeList(path, list); 746 } 747 } 748 } 749 750 751 private void composeList(String path, List<Element> list) throws IOException { 752 // there will be at least one element 753 String name = list.get(0).getName(); 754 boolean complex = true; 755 if (list.get(0).isPrimitive()) { 756 boolean prim = false; 757 complex = false; 758 for (Element item : list) { 759 if (item.hasValue()) 760 prim = true; 761 if (item.hasChildren()) 762 complex = true; 763 } 764 if (prim) { 765 openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty())); 766 for (Element item : list) { 767 if (item.hasValue()) 768 primitiveValue(null, item); 769 else 770 json.nullValue(); 771 } 772 closeArray(); 773 } 774 name = "_"+name; 775 } 776 if (complex) { 777 openArray(name, linkResolver == null ? null : linkResolver.resolveProperty(list.get(0).getProperty())); 778 for (Element item : list) { 779 if (item.hasChildren()) { 780 open(null,null); 781 if (item.getProperty().isResource()) { 782 prop("resourceType", item.getType(), linkResolver == null ? null : linkResolver.resolveType(item.getType())); 783 } 784 Set<String> done = new HashSet<String>(); 785 for (Element child : item.getChildren()) { 786 compose(path+"."+name+"[]", item, done, child); 787 } 788 close(); 789 } else 790 json.nullValue(); 791 } 792 closeArray(); 793 } 794 } 795 796 private void primitiveValue(String name, Element item) throws IOException { 797 if (name != null) { 798 if (linkResolver != null) 799 json.link(linkResolver.resolveProperty(item.getProperty())); 800 json.name(name); 801 } 802 String type = item.getType(); 803 if (Utilities.existsInList(type, "boolean")) 804 json.value(item.getValue().trim().equals("true") ? new Boolean(true) : new Boolean(false)); 805 else if (Utilities.existsInList(type, "integer", "unsignedInt", "positiveInt")) 806 json.value(new Integer(item.getValue())); 807 else if (Utilities.existsInList(type, "decimal")) 808 try { 809 json.value(new BigDecimal(item.getValue())); 810 } catch (Exception e) { 811 throw new NumberFormatException(context.formatMessage(I18nConstants.ERROR_WRITING_NUMBER__TO_JSON, item.getValue())); 812 } 813 else 814 json.value(item.getValue()); 815 } 816 817 private void compose(String path, Element element) throws IOException { 818 String name = element.getName(); 819 if (element.isPrimitive() || isPrimitive(element.getType())) { 820 if (element.hasValue()) 821 primitiveValue(name, element); 822 name = "_"+name; 823 if (element.getType().equals("xhtml")) 824 json.anchor("end-xhtml"); 825 } 826 if (element.hasChildren()) { 827 open(name, linkResolver == null ? null : linkResolver.resolveProperty(element.getProperty())); 828 if (element.getProperty().isResource()) { 829 prop("resourceType", element.getType(), linkResolver == null ? null : linkResolver.resolveType(element.getType())); 830 } 831 Set<String> done = new HashSet<String>(); 832 for (Element child : element.getChildren()) { 833 compose(path+"."+element.getName(), element, done, child); 834 } 835 close(); 836 } 837 } 838 839 public boolean isAllowComments() { 840 return allowComments; 841 } 842 843 public JsonParser setAllowComments(boolean allowComments) { 844 this.allowComments = allowComments; 845 return this; 846 } 847 848 849}