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.conv10_50.VersionConvertor_10_50; 012import org.hl7.fhir.convertors.conv14_50.VersionConvertor_14_50; 013import org.hl7.fhir.convertors.conv30_50.VersionConvertor_30_50; 014import org.hl7.fhir.convertors.factory.*; 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.Resource; 027import org.hl7.fhir.r5.model.StructureDefinition; 028import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; 029import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; 030import org.hl7.fhir.r5.model.ValueSet; 031import org.hl7.fhir.r5.utils.FHIRPathEngine; 032import org.hl7.fhir.r5.utils.ToolingExtensions; 033import org.hl7.fhir.r5.utils.XVerExtensionManager; 034import org.hl7.fhir.utilities.Utilities; 035import org.hl7.fhir.utilities.VersionUtilities; 036import org.hl7.fhir.utilities.i18n.I18nConstants; 037import org.hl7.fhir.utilities.validation.ValidationMessage; 038import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 039import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 040import org.hl7.fhir.utilities.validation.ValidationOptions; 041import org.hl7.fhir.validation.BaseValidator; 042import org.hl7.fhir.validation.TimeTracker; 043import org.hl7.fhir.validation.instance.utils.NodeStack; 044 045public class StructureDefinitionValidator extends BaseValidator { 046 047 public class FhirPathSorter implements Comparator<ExpressionNode> { 048 049 @Override 050 public int compare(ExpressionNode arg0, ExpressionNode arg1) { 051 return arg0.toString().compareTo(arg1.toString()); 052 } 053 054 } 055 056 private FHIRPathEngine fpe; 057 private boolean wantCheckSnapshotUnchanged; 058 059 public StructureDefinitionValidator(IWorkerContext context, TimeTracker timeTracker, FHIRPathEngine fpe, boolean wantCheckSnapshotUnchanged, XVerExtensionManager xverManager) { 060 super(context, xverManager); 061 source = Source.InstanceValidator; 062 this.fpe = fpe; 063 this.timeTracker = timeTracker; 064 this.wantCheckSnapshotUnchanged = wantCheckSnapshotUnchanged; 065 } 066 067 public void validateStructureDefinition(List<ValidationMessage> errors, Element src, NodeStack stack) { 068 StructureDefinition sd = null; 069 try { 070 sd = loadAsSD(src); 071 List<ElementDefinition> snapshot = sd.getSnapshot().getElement(); 072 sd.setSnapshot(null); 073 StructureDefinition base = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); 074 if (warning(errors, IssueType.NOTFOUND, stack.getLiteralPath(), base != null, I18nConstants.UNABLE_TO_FIND_BASE__FOR_, sd.getBaseDefinition(), "StructureDefinition, so can't check the differential")) { 075 if (rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), sd.hasDerivation(), I18nConstants.SD_MUST_HAVE_DERIVATION, sd.getUrl())) { 076 if (sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { 077 List<ValidationMessage> msgs = new ArrayList<>(); 078 ProfileUtilities pu = new ProfileUtilities(context, msgs, null); 079 pu.setXver(xverManager); 080 pu.generateSnapshot(base, sd, sd.getUrl(), "http://hl7.org/fhir", sd.getName()); 081 if (msgs.size() > 0) { 082 for (ValidationMessage msg : msgs) { 083 // we need to set the location for the context 084 String loc = msg.getLocation(); 085 if (loc.contains("#")) { 086 msg.setLocation(stack.getLiteralPath()+".differential.element.where(path = '"+loc.substring(loc.indexOf("#")+1)+"')"); 087 } else { 088 msg.setLocation(stack.getLiteralPath()); 089 } 090 errors.add(msg); 091 } 092 } 093 if (!snapshot.isEmpty() && wantCheckSnapshotUnchanged) { 094 int was = snapshot.size(); 095 int is = sd.getSnapshot().getElement().size(); 096 rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), was == is, I18nConstants.SNAPSHOT_EXISTING_PROBLEM, was, is); 097 } 098 } 099 } 100 if ("constraint".equals(src.getChildValue("derivation"))) { 101 rule(errors, IssueType.NOTFOUND, stack.getLiteralPath(), base.getKindElement().primitiveValue().equals(src.getChildValue("kind")), 102 I18nConstants.SD_DERIVATION_KIND_MISMATCH, base.getKindElement().primitiveValue(), src.getChildValue("kind")); 103 } 104 } 105 } catch (FHIRException | IOException e) { 106 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.ERROR_GENERATING_SNAPSHOT, e.getMessage()); 107 } 108 List<Element> differentials = src.getChildrenByName("differential"); 109 List<Element> snapshots = src.getChildrenByName("snapshot"); 110 for (Element differential : differentials) { 111 validateElementList(errors, differential, stack.push(differential, -1, null, null), false, snapshots.size() > 0, sd); 112 } 113 for (Element snapshot : snapshots) { 114 validateElementList(errors, snapshot, stack.push(snapshot, -1, null, null), true, true, sd); 115 } 116 } 117 118 private void validateElementList(List<ValidationMessage> errors, Element elementList, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd) { 119 List<Element> elements = elementList.getChildrenByName("element"); 120 int cc = 0; 121 for (Element element : elements) { 122 validateElementDefinition(errors, element, stack.push(element, cc, null, null), snapshot, hasSnapshot, sd); 123 cc++; 124 } 125 } 126 127 private void validateElementDefinition(List<ValidationMessage> errors, Element element, NodeStack stack, boolean snapshot, boolean hasSnapshot, StructureDefinition sd) { 128 boolean typeMustSupport = false; 129 List<Element> types = element.getChildrenByName("type"); 130 Set<String> typeCodes = new HashSet<>(); 131 for (Element type : types) { 132 if (hasMustSupportExtension(type)) { 133 typeMustSupport = true; 134 } 135 String tc = type.getChildValue("code"); 136 if (type.hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type")) { 137 tc = type.getExtensionValue("http://hl7.org/fhir/StructureDefinition/structuredefinition-fhir-type").primitiveValue(); 138 } 139 if (Utilities.noString(tc) && type.hasChild("code")) { 140 if (type.getNamedChild("code").hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type")) { 141 tc = "*"; 142 } 143 } 144 typeCodes.add(tc); 145 // check the stated profile - must be a constraint on the type 146 if (snapshot || sd != null) { 147 validateElementType(errors, type, stack.push(type, -1, null, null), sd, element.getChildValue("path")); 148 } 149 } 150 if (typeMustSupport) { 151 if (snapshot) { 152 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_SNAPSHOT, element.getNamedChildValue("path")); 153 } else { 154 hint(errors, IssueType.EXCEPTION, stack.getLiteralPath(), hasSnapshot || "true".equals(element.getChildValue("mustSupport")), I18nConstants.SD_NESTED_MUST_SUPPORT_DIFF, element.getNamedChildValue("path")); 155 } 156 } 157 if (element.hasChild("binding")) { 158 Element binding = element.getNamedChild("binding"); 159 validateBinding(errors, binding, stack.push(binding, -1, null, null), typeCodes, snapshot, element.getNamedChildValue("path")); 160 } else { 161 // 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 162// String bt = boundType(typeCodes); 163// hint(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bt == null, I18nConstants.SD_ED_SHOULD_BIND, element.getNamedChildValue("path"), bt); 164 } 165 // in a snapshot, we validate that fixedValue, pattern, and defaultValue, if present, are all of the right type 166 if (snapshot && (element.getIdBase() != null) && (element.getIdBase().contains("."))) { 167 if (rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), !typeCodes.isEmpty() || element.hasChild("contentReference"), I18nConstants.SD_NO_TYPES_OR_CONTENTREF, element.getIdBase())) { 168 Element v = element.getNamedChild("defaultValue"); 169 if (v != null) { 170 rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "defaultValue", v.fhirType(), typeCodes); 171 } 172 v = element.getNamedChild("fixed"); 173 if (v != null) { 174 rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "fixed", v.fhirType(), typeCodes); 175 } 176 v = element.getNamedChild("pattern"); 177 if (v != null) { 178 rule(errors, IssueType.EXCEPTION, stack.push(v, -1, null, null).getLiteralPath(), typeCodes.contains(v.fhirType()), I18nConstants.SD_VALUE_TYPE_IILEGAL, element.getIdBase(), "pattern", v.fhirType(), typeCodes); 179 } 180 } 181 } 182 } 183 184 private String boundType(Set<String> typeCodes) { 185 for (String tc : typeCodes) { 186 if (Utilities.existsInList(tc, "code", "Coding", "CodeableConcept", "Quantity", "CodeableReference")) { 187 return tc; 188 } 189 } 190 return null; 191 } 192 193 private String bindableType(Set<String> typeCodes) { 194 String ret = boundType(typeCodes); 195 if (ret != null) { 196 return ret; 197 } 198 for (String tc : typeCodes) { 199 if (Utilities.existsInList(tc, "string", "uri", "CodeableConcept", "Quantity", "CodeableReference")) { 200 return tc; 201 } 202 StructureDefinition sd = context.fetchTypeDefinition(tc); 203 if (sd != null) { 204 if (sd.hasExtension(ToolingExtensions.EXT_BINDING_METHOD)) { 205 return tc; 206 } 207 } 208 } 209 return null; 210 } 211 212 private void validateBinding(List<ValidationMessage> errors, Element binding, NodeStack stack, Set<String> typeCodes, boolean snapshot, String path) { 213 rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || bindableType(typeCodes) != null, I18nConstants.SD_ED_BIND_NO_BINDABLE, path, typeCodes.toString()); 214 if (binding.hasChild("valueSet")) { 215 Element valueSet = binding.getNamedChild("valueSet"); 216 String ref = valueSet.hasPrimitiveValue() ? valueSet.primitiveValue() : valueSet.getNamedChildValue("reference"); 217 if (warning(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), !snapshot || ref != null, I18nConstants.SD_ED_SHOULD_BIND_WITH_VS, path)) { 218 Resource vs = context.fetchResource(Resource.class, ref); 219 220 // just because we can't resolve it directly doesn't mean that terminology server can't. Check with it 221 222 if (warning(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs != null || serverSupportsValueSet(ref), I18nConstants.SD_ED_BIND_UNKNOWN_VS, path, ref)) { 223 if (vs != null) { 224 rule(errors, IssueType.BUSINESSRULE, stack.getLiteralPath(), vs instanceof ValueSet, I18nConstants.SD_ED_BIND_NOT_VS, path, ref, vs.fhirType()); 225 } 226 } 227 } 228 } 229 } 230 231 private boolean serverSupportsValueSet(String ref) { 232 ValidationResult vr = context.validateCode(new ValidationOptions().checkValueSetOnly().setVsAsUrl().noClient(), new Coding("http://loinc.org", "5792-7", null), new ValueSet().setUrl(ref)); 233 return vr.getErrorClass() == null; 234 } 235 236 private void validateElementType(List<ValidationMessage> errors, Element type, NodeStack stack, StructureDefinition sd, String path) { 237 String code = type.getNamedChildValue("code"); 238 if (code == null && path != null) { 239 code = getTypeCodeFromSD(sd, path); 240 } 241 if (code != null) { 242 List<Element> profiles = type.getChildrenByName("profile"); 243 if (VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion()) ) { 244 for (Element profile : profiles) { 245 validateProfileTypeOrTarget(errors, profile, code, stack.push(profile, -1, null, null), path); 246 } 247 248 } else { 249 for (Element profile : profiles) { 250 validateTypeProfile(errors, profile, code, stack.push(profile, -1, null, null), path); 251 } 252 profiles = type.getChildrenByName("targetProfile"); 253 for (Element profile : profiles) { 254 validateTargetProfile(errors, profile, code, stack.push(profile, -1, null, null), path); 255 } 256 } 257 } 258 } 259 260 private void validateProfileTypeOrTarget(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 261 String p = profile.primitiveValue(); 262 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 263 if (code.equals("Reference")) { 264 if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 265 StructureDefinition t = determineBaseType(sd); 266 if (t == null) { 267 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); 268 } else { 269 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path); 270 } 271 } 272 } else { 273 if (sd == null ) { 274 sd = getXverExt(errors, stack.getLiteralPath(), profile, p); 275 } 276 if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 277 StructureDefinition t = determineBaseType(sd); 278 if (t == null) { 279 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); 280 } else { 281 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), isInstanceOf(t, code), I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path); 282 } 283 } 284 } 285 } 286 287 private String getTypeCodeFromSD(StructureDefinition sd, String path) { 288 ElementDefinition ed = null; 289 for (ElementDefinition t : sd.getSnapshot().getElement()) { 290 if (t.hasPath() && t.getPath().equals(path)) { 291 if (ed == null) { 292 ed = t; 293 } else { 294 return null; // more than one match, we don't know which is which 295 } 296 } 297 } 298 return ed != null && ed.getType().size() == 1 ? ed.getTypeFirstRep().getCode() : null; 299 } 300 301 private void validateTypeProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 302 String p = profile.primitiveValue(); 303 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 304 if (sd == null ) { 305 sd = getXverExt(errors, stack.getLiteralPath(), profile, p); 306 } 307 if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 308 StructureDefinition t = determineBaseType(sd); 309 if (t == null) { 310 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); 311 } else if (!isInstanceOf(t, code)) { 312 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_WRONG, p, t, code, path); 313 } 314 } 315 } 316 317 private void validateTargetProfile(List<ValidationMessage> errors, Element profile, String code, NodeStack stack, String path) { 318 String p = profile.primitiveValue(); 319 StructureDefinition sd = context.fetchResource(StructureDefinition.class, p); 320 if (code.equals("Reference")) { 321 if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 322 StructureDefinition t = determineBaseType(sd); 323 if (t == null) { 324 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); 325 } else { 326 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd.getKind() == StructureDefinitionKind.RESOURCE, I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Resource"); 327 } 328 } 329 } else if (code.equals("canonical")) { 330 if (warning(errors, IssueType.EXCEPTION, stack.getLiteralPath(), sd != null, I18nConstants.SD_ED_TYPE_PROFILE_UNKNOWN, p)) { 331 StructureDefinition t = determineBaseType(sd); 332 if (t == null) { 333 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_PROFILE_NOTYPE, p); 334 } else if (!VersionUtilities.isR5Ver(context.getVersion())) { 335 rule(errors, 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"); 336 } else { 337 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), VersionUtilities.getCanonicalResourceNames(context.getVersion()).contains(t.getType()), I18nConstants.SD_ED_TYPE_PROFILE_WRONG_TARGET, p, t, code, path, "Canonical Resource"); 338 } 339 } 340 } else { 341 rule(errors, IssueType.EXCEPTION, stack.getLiteralPath(), false, I18nConstants.SD_ED_TYPE_NO_TARGET_PROFILE, code); 342 } 343 } 344 345 private boolean isInstanceOf(StructureDefinition sd, String code) { 346 while (sd != null) { 347 if (sd.getType().equals(code)) { 348 return true; 349 } 350 if (sd.getUrl().equals(code)) { 351 return true; 352 } 353 sd = sd.hasBaseDefinition() ? context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()) : null; 354 if (!(VersionUtilities.isR2Ver(context.getVersion()) || VersionUtilities.isR2BVer(context.getVersion())) && sd != null && !sd.getAbstract() && sd.getKind() != StructureDefinitionKind.LOGICAL) { 355 sd = null; 356 } 357 } 358 359 return false; 360 } 361 362 private StructureDefinition determineBaseType(StructureDefinition sd) { 363 while (sd != null && sd.getDerivation() == TypeDerivationRule.CONSTRAINT) { 364 sd = context.fetchResource(StructureDefinition.class, sd.getBaseDefinition()); 365 } 366 return sd; 367 } 368 369 private boolean hasMustSupportExtension(Element type) { 370 if ("true".equals(getExtensionValue(type, ToolingExtensions.EXT_MUST_SUPPORT))) { 371 return true; 372 } 373 List<Element> profiles = type.getChildrenByName("profile"); 374 for (Element profile : profiles) { 375 if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { 376 return true; 377 } 378 } 379 profiles = type.getChildrenByName("targetProfile"); 380 for (Element profile : profiles) { 381 if ("true".equals(getExtensionValue(profile, ToolingExtensions.EXT_MUST_SUPPORT))) { 382 return true; 383 } 384 } 385 return false; 386 } 387 388 private String getExtensionValue(Element element, String url) { 389 List<Element> extensions = element.getChildrenByName("extension"); 390 for (Element extension : extensions) { 391 if (url.equals(extension.getNamedChildValue("url"))) { 392 return extension.getNamedChildValue("value"); 393 } 394 } 395 return null; 396 } 397 398 private StructureDefinition loadAsSD(Element src) throws FHIRException, IOException { 399 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 400 Manager.compose(context, src, bs, FhirFormat.JSON, OutputStyle.NORMAL, null); 401 if (VersionUtilities.isR2Ver(context.getVersion())) { 402 org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(bs.toByteArray()); 403 return (StructureDefinition) VersionConvertorFactory_10_50.convertResource(r2); 404 } 405 if (VersionUtilities.isR2BVer(context.getVersion())) { 406 org.hl7.fhir.dstu2016may.model.Resource r2b = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(bs.toByteArray()); 407 return (StructureDefinition) VersionConvertorFactory_14_50.convertResource(r2b); 408 } 409 if (VersionUtilities.isR3Ver(context.getVersion())) { 410 org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(bs.toByteArray()); 411 return (StructureDefinition) VersionConvertorFactory_30_50.convertResource(r3); 412 } 413 if (VersionUtilities.isR4Ver(context.getVersion())) { 414 org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(bs.toByteArray()); 415 return (StructureDefinition) VersionConvertorFactory_40_50.convertResource(r4); 416 } 417 return (StructureDefinition) new org.hl7.fhir.r5.formats.JsonParser().parse(bs.toByteArray()); 418 } 419 420}