001package org.hl7.fhir.validation.instance.type; 002 003import java.io.ByteArrayOutputStream; 004import java.io.IOException; 005import java.util.ArrayList; 006import java.util.Comparator; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Set; 010 011import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50; 012import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50; 013import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50; 014import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; 015import org.hl7.fhir.exceptions.FHIRException; 016import org.hl7.fhir.r5.conformance.ProfileUtilities; 017import org.hl7.fhir.r5.context.IWorkerContext; 018import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult; 019import org.hl7.fhir.r5.elementmodel.Element; 020import org.hl7.fhir.r5.elementmodel.Manager; 021import org.hl7.fhir.r5.elementmodel.Manager.FhirFormat; 022import org.hl7.fhir.r5.formats.IParser.OutputStyle; 023import org.hl7.fhir.r5.model.Coding; 024import org.hl7.fhir.r5.model.ElementDefinition; 025import org.hl7.fhir.r5.model.ExpressionNode; 026import org.hl7.fhir.r5.model.Extension; 027import org.hl7.fhir.r5.model.Resource; 028import org.hl7.fhir.r5.model.StructureDefinition; 029import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; 030import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; 031import org.hl7.fhir.r5.model.ValueSet; 032import org.hl7.fhir.r5.utils.FHIRPathEngine; 033import org.hl7.fhir.r5.utils.ToolingExtensions; 034import org.hl7.fhir.r5.utils.XVerExtensionManager; 035import org.hl7.fhir.utilities.Utilities; 036import org.hl7.fhir.utilities.VersionUtilities; 037import org.hl7.fhir.utilities.i18n.I18nConstants; 038import org.hl7.fhir.utilities.validation.ValidationMessage; 039import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 040import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 041import org.hl7.fhir.utilities.validation.ValidationOptions; 042import org.hl7.fhir.validation.BaseValidator; 043import org.hl7.fhir.validation.TimeTracker; 044import org.hl7.fhir.validation.instance.utils.NodeStack; 045 046public class StructureDefinitionValidator extends BaseValidator { 047 048 public class FhirPathSorter implements Comparator<ExpressionNode> { 049 050 @Override 051 public int compare(ExpressionNode arg0, ExpressionNode arg1) { 052 return arg0.toString().compareTo(arg1.toString()); 053 } 054 055 } 056 057 private FHIRPathEngine fpe; 058 private boolean wantCheckSnapshotUnchanged; 059 060 public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged, XVerExtensionManager xverManager, Coding jurisdiction) { 061 super(context, xverManager); 062 source = Source.InstanceValidator; 063 this.fpe = fpe; 064 this.timeTracker = timeTracker; 065 this.wantCheckSnapshotUnchanged = wantCheckSnapshotUnchanged; 066 this.jurisdiction = jurisdiction; 067 } 068 069 public boolean validateStructureDefinition(List<ValidationMessage> errors, Element src, NodeStack stack) { 070 boolean ok = true; 071 StructureDefinition sd = null; 072 String typeName = null; 073 try { 074 sd = loadAsSD(src); 075 List<ElementDefinition> snapshot = sd.getSnapshot().getElement(); 076 sd.setSnapshot(null); 077 typeName = sd.getTypeName(); 078 StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); 079 if (warning(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), base != null, I18nConstants.UNABLE_TO_FIND_BASE__FOR_, sd.getBaseDefinition(), "StructureDefinition, so can't check the differential")) { 080 if (rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasDerivation(), I18nConstants.SD_MUST_HAVE_DERIVATION, sd.getUrl())) { 081 boolean bok = base.getAbstract() || sd.hasKind() && sd.getKind() == base.getKind(); 082 rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), bok, I18nConstants.SD_CONSTRAINED_KIND_NO_MATCH, sd.getKind().toCode(), base.getKind().toCode(), base.getType(), base.getUrl()); 083 if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { 084 rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasType() && sd.getType().equals(base.getType()), I18nConstants.SD_CONSTRAINED_TYPE_NO_MATCH, sd.getType(), base.getType()); 085 List<ValidationMessage> msgs = new ArrayList<>(); 086 ProfileUtilities pu = new ProfileUtilities(context, msgs, null); 087 pu.setXver(xverManager); 088 pu.setNewSlicingProcessing(!sd.hasFhirVersion() || VersionUtilities.isR4Plus(sd.getFhirVersion().toCode())); 089 pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir/R4/", sd.getName()); 090 if (msgs.size() > 0) { 091 for (ValidationMessage msg : msgs) { 092 // we need to set the location for the context 093 String loc = msg.getLocation(); 094 if (loc.contains("#")) { 095 msg.setLocation(stack.getLiteralPath()+".differential.element.where(path = '"+loc.substring(loc.indexOf("#")+1)+"')"); 096 } else { 097 msg.setLocation(stack.getLiteralPath()); 098 } 099 errors.add(msg); 100 ok = false; 101 } 102 } 103 if (!snapshot.isEmpty() && wantCheckSnapshotUnchanged) { 104 int was = snapshot.size(); 105 int is = sd.getSnapshot().getElement().size(); 106 ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), was == is, I18nConstants.SNAPSHOT_EXISTING_PROBLEM, was, is) && ok; 107 } 108 } else { 109 rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasType() && !sd.getType().equals(base.getType()), I18nConstants.SD_SPECIALIZED_TYPE_MATCHES, sd.getType(), base.getType()); 110 } 111 } else { 112 ok = false; 113 } 114 if ("constraint".equals(src.getChildValue("derivation"))) { 115 ok = rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), base.getKindElement().primitiveValue().equals(src.getChildValue("kind")), 116 I18nConstants.SD_DERIVATION_KIND_MISMATCH, base.getKindElement().primitiveValue(), src.getChildValue("kind")) && ok; 117 } 118 } 119 } catch (FHIRException | IOException e) { 120 rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage()); 121 ok = false; 122 } 123 List<Element> differentials = src.getChildrenByName("differential"); 124 List<Element> snapshots = src.getChildrenByName("snapshot"); 125 for (Element differential : differentials) { 126 ok = validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd, typeName) && ok; 127 } 128 for (Element snapshot : snapshots) { 129 ok = validateElementList(errors, snapshot, stack.push(snapshot, -1, null, null), true, true, sd, typeName) && ok; 130 } 131 return ok; 132 } 133 134 private boolean validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName) { 135 boolean ok = true; 136 List<Element> elements = elementList.getChildrenByName("element"); 137 int cc = 0; 138 for (Element element : elements) { 139 ok = validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd, typeName) && ok; 140 cc++; 141 } 142 return ok; 143 } 144 145 private boolean validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd, String typeName) { 146 boolean ok = true; 147 boolean typeMustSupport = false; 148 String path = element.getNamedChildValue("path"); 149 rule(errors, "2022-11-02", IssueType.NOTFOUND, stack.getLiteralPath(), typeName == null || path == null || path.equals(typeName) || path.startsWith(typeName+"."), I18nConstants.SD_PATH_TYPE_MISMATCH, typeName, path); 150 List<Element> types = element.getChildrenByName("type"); 151 Set<String> typeCodes = new HashSet<>(); 152 Set<String> characteristics = new HashSet<>(); 153 154 for (Element type : types) { 155 if (hasMustSupportExtension(type)) { 156 typeMustSupport = true; 157 } 158 String tc = type.getChildValue("code"); 159 if (type.hasExtension(ToolingExtensions.EXT_FHIR_TYPE)) { 160 tc = type.getExtensionValue(ToolingExtensions.EXT_FHIR_TYPE).primitiveValue(); 161 } 162 if (Utilities.noString(tc) && type.hasChild("code")) { 163 throw new Error("WTF?"); 164// if (type.getNamedChild("code").hasExtension(" http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type")) { 165// tc = "*"; 166// } 167 } 168 typeCodes.add(tc); 169 Set<String> tcharacteristics = new HashSet<>(); 170 StructureDefinition tsd = context.fetchTypeDefinition(tc); 171 if (tsd != null && tsd.hasExtension(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) { 172 for (Extension ext : tsd.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) { 173 tcharacteristics.add(ext.getValue().primitiveValue()); 174 } 175 } else { 176 // nothing specified, so infer from known types 177 addCharacteristics(tcharacteristics, tc); 178 } 179 characteristics.addAll(tcharacteristics); 180 if (type.hasChildren("targetProfile")) { 181 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), tcharacteristics.contains("has-target") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "targetProfile", tc) && ok; 182 } 183 // check the stated profile - must be a constraint on the type 184 if (snapshot || sd != null) { 185 ok = validateElementType(errors, type, stack.push(type, -1, null, null), sd, path) && ok; 186 } 187 } 188 if (typeMustSupport) { 189 if (snapshot) { 190 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_SNAPSHOT, path) && ok; 191 } else { 192 hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), hasSnapshot || "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_DIFF, path); 193 } 194 } 195 if (element.hasChild("binding")) { 196 if (!typeCodes.isEmpty()) { 197 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("can-bind") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Binding", typeCodes) && ok; 198 } 199 Element binding = element.getNamedChild("binding"); 200 ok = validateBinding(errors, binding, stack.push(binding, -1, null, null), typeCodes, snapshot, path) && ok; 201 } else { 202 // this is a good idea but there's plenty of cases where the rule isn't met; maybe one day it's worth investing the time to exclude these cases and bring this rule back 203// String bt = boundType(typeCodes); 204// hint(errors, UNKNOWN_DATE_TIME, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bt == null, I18nConstants.SD_ED_SHOULD_BIND, element.getNamedChildValue("path"), bt); 205 } 206 if (!typeCodes.isEmpty()) { 207 if (element.hasChild("maxLength")) { 208 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-length") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MaxLength", typeCodes) && ok; 209 } 210 if (element.hasExtension(ToolingExtensions.EXT_MIN_LENGTH)) { 211 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-length") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MinLength Extension", typeCodes) && ok; 212 } 213 if (element.hasChild("minValue")) { 214 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-range") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MinValue", typeCodes) && ok; 215 } 216 if (element.hasChild("maxValue")) { 217 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-range") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "MaxValue", typeCodes) && ok; 218 } 219 if (element.hasExtension(ToolingExtensions.EXT_MAX_DECIMALS)) { 220 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("is-continuous") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Max Decimal Places Extension", typeCodes) && ok; 221 } 222 if (element.hasExtension(ToolingExtensions.EXT_MAX_SIZE)) { 223 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), characteristics.contains("has-size") , I18nConstants.SD_ILLEGAL_CHARACTERISTICS, "Max Size", typeCodes) && ok; 224 } 225 } 226 // in a snapshot, we validate that fixedValue, pattern, and defaultValue, if present, are all of the right type 227 if (snapshot && (element.getIdBase() != null) && (element.getIdBase().contains("."))) { 228 if (rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !typeCodes.isEmpty() || element.hasChild("contentReference"), I18nConstants.SD_NO_TYPES_OR_CONTENTREF, element.getIdBase())) { 229 // if we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint 230 boolean repeating = !Utilities.existsInList(element.getChildValue("max"), "0", "1"); 231 232 Element v = element.getNamedChild("defaultValue"); 233 if (v != null) { 234 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "defaultValue", v.fhirType(), typeCodes) && ok; 235 } 236 v = element.getNamedChild("fixed"); 237 if (v != null) { 238 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "fixed", v.fhirType(), typeCodes) && ok; 239 hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_HINT, element.getIdBase(), "fixed"); 240 if (isPrimitiveType(v.fhirType())) { 241 warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_WARNING_DOTNET, element.getIdBase(), "fixed"); 242 } else { 243 warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), false, I18nConstants.SD_VALUE_COMPLEX_FIXED, v.fhirType()); 244 } 245 } 246 v = element.getNamedChild("pattern"); 247 if (v != null) { 248 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "pattern", v.fhirType(), typeCodes) && ok; 249 hint(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_HINT, element.getIdBase(), "pattern"); 250 if (isPrimitiveType(v.fhirType())) { 251 warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), !repeating, I18nConstants.SD_VALUE_TYPE_REPEAT_WARNING_DOTNET, element.getIdBase(), "pattern"); 252 } 253 } 254 } 255 // if we see fixed[x] or pattern[x] applied to a repeating element, we'll give the user a hint 256 } 257 return ok; 258 } 259 260 private boolean addCharacteristics(Set<String> set, String tc) { 261 switch (tc) { 262 case "boolean" : return addCharacteristicsForType(set); 263 case "integer" : return addCharacteristicsForType(set, "has-range", "has-length"); 264 case "integer64" : return addCharacteristicsForType(set, "has-range", "has-length"); 265 case "decimal" :return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length"); 266 case "base64Binary" : return addCharacteristicsForType(set, "has-size"); 267 case "instant" : return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length"); 268 case "string" : return addCharacteristicsForType(set, "has-length", "do-translations", "can-bind"); 269 case "uri" : return addCharacteristicsForType(set, "has-length", "can-bind"); 270 case "date" :return addCharacteristicsForType(set, "has-range", "has-length"); 271 case "dateTime" : return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length"); 272 case "time" :return addCharacteristicsForType(set, "has-range", "is-continuous", "has-length"); 273 case "canonical" :return addCharacteristicsForType(set, "has-target", "has-length"); 274 case "code" :return addCharacteristicsForType(set, "has-length", "can-bind"); 275 case "id" :return addCharacteristicsForType(set, "has-length"); 276 case "markdown" :return addCharacteristicsForType(set, "do-translations", "has-length"); 277 case "oid" :return addCharacteristicsForType(set, "has-length", "can-bind"); 278 case "positiveInt" :return addCharacteristicsForType(set, "has-range", "has-length"); 279 case "unsignedInt" :return addCharacteristicsForType(set, "has-range", "has-length"); 280 case "url" :return addCharacteristicsForType(set, "has-length", "can-bind"); 281 case "uuid" :return addCharacteristicsForType(set, "has-length", "can-bind"); 282 case "xhtml" :return addCharacteristicsForType(set); 283 case "Address" :return addCharacteristicsForType(set, "do-translations"); 284 case "Age" : return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 285 case "Annotation" :return addCharacteristicsForType(set); 286 case "Attachment" :return addCharacteristicsForType(set, "has-size", "do-translations"); 287 case "CodeableConcept" :return addCharacteristicsForType(set, "can-bind", "do-translations"); 288 case "CodeableReference" : return addCharacteristicsForType(set, "has-target", "can-bind", "do-translations"); 289 case "Coding" : return addCharacteristicsForType(set, "can-bind", "do-translations"); 290 case "ContactPoint" :return addCharacteristicsForType(set); 291 case "Count" :return addCharacteristicsForType(set, "has-range"); 292 case "Distance" :return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 293 case "Duration" : return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 294 case "HumanName" :return addCharacteristicsForType(set); 295 case "Identifier" : return addCharacteristicsForType(set); 296 case "Money" : return addCharacteristicsForType(set, "has-range", "is-continuous"); 297 case "Period" : return addCharacteristicsForType(set); 298 case "Quantity" :return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 299 case "Range" :return addCharacteristicsForType(set, "has-units", "can-bind", "has-units"); 300 case "Ratio" :return addCharacteristicsForType(set, "has-units"); 301 case "RatioRange" : return addCharacteristicsForType(set, "has-units"); 302 case "Reference" : return addCharacteristicsForType(set, "has-target"); 303 case "SampledData" :return addCharacteristicsForType(set); 304 case "Signature" : return addCharacteristicsForType(set); 305 case "Timing" : return addCharacteristicsForType(set); 306 case "ContactDetail" :return addCharacteristicsForType(set); 307 case "Contributor" :return addCharacteristicsForType(set); 308 case "DataRequirement" :return addCharacteristicsForType(set); 309 case "Expression" : return addCharacteristicsForType(set); 310 case "ParameterDefinition" : return addCharacteristicsForType(set); 311 case "RelatedArtifact" :return addCharacteristicsForType(set); 312 case "TriggerDefinition" :return addCharacteristicsForType(set); 313 case "UsageContext" :return addCharacteristicsForType(set); 314 case "Dosage" : return addCharacteristicsForType(set); 315 case "Meta" :return addCharacteristicsForType(set); 316 case "Resource" :return addCharacteristicsForType(set); 317 case "Extension" :return addCharacteristicsForType(set, "can-bind"); 318 case "Narrative" :return addCharacteristicsForType(set); 319 case "MoneyQuantity" :return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 320 case "SimpleQuantity" :return addCharacteristicsForType(set, "has-range", "is-continuous", "can-bind", "has-units"); 321 case "MarketingStatus" :return addCharacteristicsForType(set); 322 case "ExtendedContactDetail" :return addCharacteristicsForType(set); 323 case "VirtualServiceDetail" :return addCharacteristicsForType(set); 324 case "Availability" :return addCharacteristicsForType(set); 325 case "MonetaryComponent" :return addCharacteristicsForType(set); 326 case "ElementDefinition" :return addCharacteristicsForType(set); 327 328 case "BackboneElement" :return addCharacteristicsForType(set); 329 case "Element" :return addCharacteristicsForType(set); 330 case "Base" :return addCharacteristicsForType(set); 331 default: 332// if (!context.getResourceNames().contains(tc)) { 333// System.out.println("Unhandled data type in addCharacteristics: "+tc); 334// } 335 return addCharacteristicsForType(set); 336 } 337 } 338 339 340 private boolean addCharacteristicsForType(Set<String> set, String... cl) { 341 for (String c : cl) { 342 set.add(c); 343 } 344 return true; 345 } 346 347 private boolean isPrimitiveType(String fhirType) { 348 StructureDefinition sd = context.fetchTypeDefinition(fhirType); 349 return sd != null && sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 350 } 351 352 private String boundType(Set<String> typeCodes) { 353 for (String tc : typeCodes) { 354 if (Utilities.existsInList(tc, "code", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) { 355 return tc; 356 } 357 } 358 return null; 359 } 360 361 private String bindableType(Set<String> typeCodes) { 362 String ret = boundType(typeCodes); 363 if (ret != null) { 364 return ret; 365 } 366 for (String tc : typeCodes) { 367 if (Utilities.existsInList(tc, "string", "uri", "CodeableConcept", "Quantity", "CodeableReference")) { 368 return tc; 369 } 370 StructureDefinition sd = context.fetchTypeDefinition(tc); 371 if (sd != null) { 372 if (sd.hasExtension(ToolingExtensions.EXT_BINDING_STYLE)) { 373 return tc; 374 } 375 for (Extension ext : sd.getExtensionsByUrl(ToolingExtensions.EXT_TYPE_CHARACTERISTICS)) { 376 if ("can-bind".equals(ext.getValue().primitiveValue())) { 377 return tc; 378 } 379 } 380 } 381 } 382 return null; 383 } 384 385 private boolean validateBinding(List<ValidationMessage> errors, Element binding, NodeStack stack, Set<String> typeCodes, boolean snapshot, String path) { 386 boolean ok = true; 387 if (bindableType(typeCodes) == null) { 388 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot, I18nConstants.SD_ED_BIND_NO_BINDABLE, path, typeCodes.toString()) && ok; 389 } 390 if (!snapshot) { 391 Set<String> bindables = getListofBindableTypes(typeCodes); 392 hint(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), bindables.size() <= 1, I18nConstants.SD_ED_BIND_MULTIPLE_TYPES, path, typeCodes.toString()); 393 } 394 395 if (binding.hasChild("valueSet")) { 396 Element valueSet = binding.getNamedChild("valueSet"); 397 String ref = valueSet.hasPrimitiveValue() ? valueSet.primitiveValue() : valueSet.getNamedChildValue("reference"); 398 if (warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || ref != null, I18nConstants.SD_ED_SHOULD_BIND_WITH_VS, path)) { 399 Resource vs = context.fetchResource(Resource.class, ref); 400 401 // just because we can't resolve it directly doesn't mean that terminology server can't. Check with it 402 403 if (warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs != null || serverSupportsValueSet(ref), I18nConstants.SD_ED_BIND_UNKNOWN_VS, path, ref)) { 404 if (vs != null) { 405 ok = rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs instanceof ValueSet, I18nConstants.SD_ED_BIND_NOT_VS, path, ref, vs.fhirType()) && ok; 406 } 407 } 408 } 409 } 410 return ok; 411 } 412 413 private Set<String> getListofBindableTypes(Set<String> types) { 414 Set<String> res = new HashSet<>(); 415 for (String s : types) { 416 if (Utilities.existsInList(s, "code", "string", "url", "uri", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) { 417 res.add(s); 418 } 419 } 420 return res; 421 } 422 423 private boolean serverSupportsValueSet(String ref) { 424 ValidationResult vr = context.validateCode(new ValidationOptions().checkValueSetOnly().setVsAsUrl().noClient(), new Coding("http://loinc.org", "5792-7", null), new ValueSet().setUrl(ref)); 425 return vr.getErrorClass() == null; 426 } 427 428 private boolean validateElementType(List<ValidationMessage> errors, Element type, NodeStack stack, StructureDefinition sd, String path) { 429 boolean ok = true; 430 String code = type.getNamedChildValue("code"); 431 if (code == null && path != null) { 432 code = getTypeCodeFromSD(sd, path); 433 } 434 if (code != null) { 435 List<Element> profiles = type.getChildrenByName("profile"); 436 if (VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion()) ) { 437 for (Element profile : profiles) { 438 ok = validateProfileTypeOrTarget(errors, profile, code, stack.push(profile, -1, null, null), path) && ok; 439 } 440 441 } else { 442 for (Element profile : profiles) { 443 ok = validateTypeProfile(errors, profile, code, stack.push(profile, -1, null, null), path) && ok; 444 } 445 profiles = type.getChildrenByName("targetProfile"); 446 for (Element profile : profiles) { 447 ok = validateTargetProfile(errors, profile, code, stack.push(profile, -1, null, null), path) && ok; 448 } 449 } 450 } 451 return true; 452 } 453 454 private boolean validateProfileTypeOrTarget(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 455 boolean ok = true; 456 String p = profile.primitiveValue(); 457 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 458 if (code.equals("Reference")) { 459 if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 460 StructureDefinition t = determineBaseType(sd); 461 if (t == null) { 462 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok; 463 } else { 464 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok; 465 } 466 } 467 } else { 468 if (sd == null ) { 469 sd = getXverExt(errors, stack.getLiteralPath(), profile, p); 470 } 471 if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 472 StructureDefinition t = determineBaseType(sd); 473 if (t == null) { 474 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok; 475 } else { 476 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok; 477 if (t.getType().equals("Extension")) { 478 boolean isModifierDefinition = checkIsModifierExtension(sd); 479 boolean isModifierContext = path.endsWith(".modifierExtension"); 480 if (isModifierDefinition) { 481 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_NOT_MODIFIER, p, t, code, path) && ok; 482 } else { 483 ok =rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_IS_MODIFIER, p, t, code, path) && ok; 484 } 485 } 486 } 487 } 488 } 489 return ok; 490 } 491 492 private String getTypeCodeFromSD(StructureDefinition sd, String path) { 493 ElementDefinition ed = null; 494 for (ElementDefinition t : sd.getSnapshot().getElement()) { 495 if (t.hasPath() && t.getPath().equals(path)) { 496 if (ed == null) { 497 ed = t; 498 } else { 499 return null; // more than one match, we don't know which is which 500 } 501 } 502 } 503 return ed != null && ed.getType().size() == 1 ? ed.getTypeFirstRep().getCode() : null; 504 } 505 506 private boolean validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 507 boolean ok = true; 508 String p = profile.primitiveValue(); 509 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 510 if (sd == null ) { 511 sd = getXverExt(errors, stack.getLiteralPath(), profile, p); 512 } 513 if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 514 StructureDefinition t = determineBaseType(sd); 515 if (t == null) { 516 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok; 517 } else if (!isInstanceOf(t, code)) { 518 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path) && ok; 519 } else { 520 if (t.getType().equals("Extension")) { 521 boolean isModifierDefinition = checkIsModifierExtension(sd); 522 boolean isModifierContext = path.endsWith(".modifierExtension"); 523 if (isModifierDefinition) { 524 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_NOT_MODIFIER, p, t, code, path) && ok; 525 } else { 526 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), !isModifierContext, I18nConstants.SD_ED_TYPE_PROFILE_IS_MODIFIER, p, t, code, path) && ok; 527 } 528 } 529 } 530 } 531 return ok; 532 } 533 534 private boolean checkIsModifierExtension(StructureDefinition t) { 535 return t.getSnapshot().getElementFirstRep().getIsModifier(); 536 } 537 538 private boolean validateTargetProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 539 boolean ok = true; 540 String p = profile.primitiveValue(); 541 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 542 if (code.equals("Reference") || code.equals("CodeableReference")) { 543 if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 544 StructureDefinition t = determineBaseType(sd); 545 if (t == null) { 546 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok; 547 } else { 548 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Resource") && ok; 549 } 550 } 551 } else if (code.equals("canonical")) { 552 if (warning(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 553 StructureDefinition t = determineBaseType(sd); 554 if (t == null) { 555 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p) && ok; 556 } else if (!VersionUtilities.isR5Ver(context.getVersion())) { 557 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()) || "Resource".equals(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource") && ok; 558 } else { 559 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource") && ok; 560 } 561 } 562 } else { 563 ok = rule(errors, NO_RULE_DATE, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_NO_TARGET_PROFILE, code) && ok; 564 } 565 return ok; 566 } 567 568 private boolean isInstanceOf(StructureDefinition sd, String code) { 569 while (sd != null) { 570 if (sd.getType().equals(code)) { 571 return true; 572 } 573 if (sd.getUrl().equals(code)) { 574 return true; 575 } 576 sd = sd.hasBaseDefinition() ? context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()) : null; 577 if (!(VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion())) && sd != null && !sd.getAbstract() && sd.getKind() != StructureDefinitionKind.LOGICAL) { 578 sd = null; 579 } 580 } 581 582 return false; 583 } 584 585 private StructureDefinition determineBaseType(StructureDefinition sd) { 586 while (sd != null && sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { 587 sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); 588 } 589 return sd; 590 } 591 592 private boolean hasMustSupportExtension(Element type) { 593 if ("true".equals(getExtensionValue(type, ToolingExtensions.EXT_MUST_SUPPORT))) { 594 return true; 595 } 596 List<Element> profiles = type.getChildrenByName("profile"); 597 for (Element profile : profiles) { 598 if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { 599 return true; 600 } 601 } 602 profiles = type.getChildrenByName("targetProfile"); 603 for (Element profile : profiles) { 604 if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { 605 return true; 606 } 607 } 608 return false; 609 } 610 611 private String getExtensionValue(Element element, String url) { 612 List<Element> extensions = element.getChildrenByName("extension"); 613 for (Element extension : extensions) { 614 if (url.equals(extension.getNamedChildValue("url"))) { 615 return extension.getNamedChildValue("value"); 616 } 617 } 618 return null; 619 } 620 621 private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException { 622 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 623 Manager.compose(context, src, bs, FhirFormat.JSON, OutputStyle.NORMAL, null); 624 if (VersionUtilities.isR2Ver(context.getVersion())) { 625 org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(bs.toByteArray()); 626 return (StructureDefinition) VersionConvertorFactory_10_50.convertResource(r2); 627 } 628 if (VersionUtilities.isR2BVer(context.getVersion())) { 629 org.hl7.fhir.dstu2016may.model.Resource r2b = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(bs.toByteArray()); 630 return (StructureDefinition) VersionConvertorFactory_14_50.convertResource(r2b); 631 } 632 if (VersionUtilities.isR3Ver(context.getVersion())) { 633 org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(bs.toByteArray()); 634 return (StructureDefinition) VersionConvertorFactory_30_50.convertResource(r3); 635 } 636 if (VersionUtilities.isR4Ver(context.getVersion())) { 637 org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(bs.toByteArray()); 638 return (StructureDefinition) VersionConvertorFactory_40_50.convertResource(r4); 639 } 640 return (StructureDefinition) new org.hl7.fhir.r5.formats.JsonParser().parse(bs.toByteArray()); 641 } 642 643}