diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java index 48d16bca1..f6cc428d6 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java @@ -381,6 +381,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator(); private String executionId; + private XVerExtensionManager xverManager; /* * Keeps track of whether a particular profile has been checked or not yet @@ -1551,25 +1552,53 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } } - private StructureDefinition checkExtension(ValidatorHostContext hostContext, List errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws FHIRException, IOException { + private StructureDefinition checkExtension(ValidatorHostContext hostContext, List errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack, String extensionUrl) throws FHIRException, IOException { String url = element.getNamedChildValue("url"); boolean isModifier = element.getName().equals("modifierExtension"); long t = System.nanoTime(); - StructureDefinition ex = context.fetchResource(StructureDefinition.class, url); + StructureDefinition ex = Utilities.isAbsoluteUrl(url) ? context.fetchResource(StructureDefinition.class, url) : null; sdTime = sdTime + (System.nanoTime() - t); if (ex == null) { - if (!url.startsWith("http://hl7.org/fhir/4.0/StructureDefinition/extension-")) - if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) - hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url), "Unknown extension " + url); - } else { - if (def.getIsModifier()) + if (xverManager == null) { + xverManager = new XVerExtensionManager(context); + } + if (xverManager.matchingUrl(url)) { + switch (xverManager.status(url)) { + case BadVersion: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' evaluation state is not valid (invalidVersion \""+xverManager.getVersion(url)+"\")"); + break; + case Unknown: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' evaluation state is not valid (unknown Element id \""+xverManager.getElementId(url)+"\")"); + break; + case Invalid: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' evaluation state is not valid (Element id \""+xverManager.getElementId(url)+"\" is valid, but cannot be used in a cross-version paradigm because there has been no changes across the relevant versions)"); + break; + case Valid: + ex = xverManager.makeDefinition(url); + context.generateSnapshot(ex); + context.cacheResource(ex); + break; + default: + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, "Extension url '" + url + "' evaluation state illegal"); + break; + } + } else if (extensionUrl != null && !isAbsolute(url)) { + if (extensionUrl.equals(profile.getUrl())) { + rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", hasExtensionSlice(profile, url), "Sub-extension url '" + url + "' is not defined by the Extension "+profile.getUrl()); + } + } else if (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path, allowUnknownExtension(url), "The extension " + url + " is unknown, and not allowed here")) { + hint(errors, IssueType.STRUCTURE, element.line(), element.col(), path, isKnownExtension(url), "Unknown extension " + url); + } + } + if (ex != null) { + if (def.getIsModifier()) { rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", ex.getSnapshot().getElement().get(0).getIsModifier(), "Extension modifier mismatch: the extension element is labelled as a modifier, but the underlying extension is not"); - else + } else { rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", !ex.getSnapshot().getElement().get(0).getIsModifier(), "Extension modifier mismatch: the extension element is not labelled as a modifier, but the underlying extension is"); - + } // two questions // 1. can this extension be used here? checkExtensionContext(errors, element, /* path+"[url='"+url+"']", */ ex, stack, ex.getUrl()); @@ -1590,12 +1619,21 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat rule(errors, IssueType.STRUCTURE, element.line(), element.col(), path + "[url='" + url + "']", allowedTypes.contains(actualType), "The Extension '" + url + "' definition allows for the types "+allowedTypes.toString()+" but found type "+actualType); // 3. is the content of the extension valid? - validateElement(hostContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false, true); + validateElement(hostContext, errors, ex, ex.getSnapshot().getElement().get(0), null, null, resource, element, "Extension", stack, false, true, url); } return ex; } + private boolean hasExtensionSlice(StructureDefinition profile, String sliceName) { + for (ElementDefinition ed : profile.getSnapshot().getElement()) { + if (ed.getPath().equals("Extension.extension.url") && ed.hasFixed() && sliceName.equals(ed.getFixed().primitiveValue())) { + return true; + } + } + return false; + } + private String getExtensionType(Element element) { for (Element e : element.getChildren()) { if (e.getName().startsWith("value")) { @@ -3232,7 +3270,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat (rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), defn.hasSnapshot(), "StructureDefinition has no snapshot - validation is against the snapshot, so it must be provided"))) { // Don't need to validate against the resource if there's a profile because the profile snapshot will include the relevant parts of the resources - validateElement(hostContext, errors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true); + validateElement(hostContext, errors, defn, defn.getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true, null); } // specific known special validations @@ -3253,7 +3291,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // todo: re-enable this when I can deal with the impact... (GG) // if (!profileUsage.getProfile().getType().equals(resource.fhirType())) // throw new FHIRException("Profile type mismatch - resource is "+resource.fhirType()+", and profile is for "+profileUsage.getProfile().getType()); - validateElement(hostContext, errors, profileUsage.getProfile(), profileUsage.getProfile().getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true); + validateElement(hostContext, errors, profileUsage.getProfile(), profileUsage.getProfile().getSnapshot().getElement().get(0), null, null, resource, element, element.getName(), stack, false, true, null); } } @@ -4287,7 +4325,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L // firstBase = ebase == null ? base : ebase; private void validateElement(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, StructureDefinition cprofile, ElementDefinition context, - Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext) throws FHIRException, FHIRException, IOException { + Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, String extensionUrl) throws FHIRException, FHIRException, IOException { // element.markValidation(profile, definition); if (debug) { @@ -4324,12 +4362,12 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L // 5. inspect each child for validity for (ElementInfo ei : children) { - checkChild(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei); + checkChild(hostContext, errors, profile, definition, resource, element, actualType, stack, inCodeableConcept, checkDisplayInContext, ei, extensionUrl); } } public void checkChild(ValidatorHostContext hostContext, List errors, StructureDefinition profile, ElementDefinition definition, - Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei) + Element resource, Element element, String actualType, NodeStack stack, boolean inCodeableConcept, boolean checkDisplayInContext, ElementInfo ei, String extensionUrl) throws FHIRException, IOException, DefinitionException { List profiles = new ArrayList(); if (ei.definition != null) { @@ -4405,6 +4443,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L String eiPath = ei.path; assert(eiPath.equals(localStackLiterapPath)) : "ei.path: " + ei.path + " - localStack.getLiteralPath: " + localStackLiterapPath; boolean thisIsCodeableConcept = false; + String thisExtension = null; boolean checkDisplay = true; checkInvariants(hostContext, errors, profile, ei.definition, resource, ei.element, localStack, true); @@ -4431,8 +4470,17 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L } else if (type.equals("Reference")) { checkReference(hostContext, errors, ei.path, ei.element, profile, ei.definition, actualType, localStack); // We only check extensions if we're not in a complex extension or if the element we're dealing with is not defined as part of that complex extension - } else if (type.equals("Extension") && ei.element.getChildValue("url") != null && ei.element.getChildValue("url").contains("/")) { - checkExtension(hostContext, errors, ei.path, resource, ei.element, ei.definition, profile, localStack); + } else if (type.equals("Extension")) { + Element eurl = ei.element.getNamedChild("url"); + if (rule(errors, IssueType.INVALID, ei.path, eurl != null, "Extension.url is required")) { + String url = eurl.primitiveValue(); + thisExtension = url; + if (rule(errors, IssueType.INVALID, ei.path, !Utilities.noString(url), "Extension.url is required")) { + if (rule(errors, IssueType.INVALID, ei.path, (extensionUrl != null) || Utilities.isAbsoluteUrl(url), "Extension.url must be an absolute URL")) { + checkExtension(hostContext, errors, ei.path, resource, ei.element, ei.definition, profile, localStack, extensionUrl); + } + } + } } else if (type.equals("Resource")) { validateContains(hostContext, errors, ei.path, ei.definition, definition, resource, ei.element, localStack, idStatusForEntry(element, ei)); // if // (str.matches(".*([.,/])work\\1$")) @@ -4450,7 +4498,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L } } else { if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), stack.getLiteralPath(), ei.definition != null, "Unrecognised Content " + ei.name)) - validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, false, true); + validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, false, true, null); } StructureDefinition p = null; boolean elementValidated = false; @@ -4490,7 +4538,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L p = this.context.fetchResource(StructureDefinition.class, typeProfile); if (rule(errors, IssueType.STRUCTURE, ei.line(), ei.col(), ei.path, p != null, "Unknown profile " + typeProfile)) { List profileErrors = new ArrayList(); - validateElement(hostContext, profileErrors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay); + validateElement(hostContext, profileErrors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); if (hasErrors(profileErrors)) badProfiles.put(typeProfile, profileErrors); else @@ -4522,15 +4570,15 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L if (p!=null) { if (!elementValidated) { if (ei.element.getSpecial() == SpecialElement.BUNDLE_ENTRY || ei.element.getSpecial() == SpecialElement.BUNDLE_OUTCOME || ei.element.getSpecial() == SpecialElement.PARAMETER ) - validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, ei.element, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay); + validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, ei.element, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); else - validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay); + validateElement(hostContext, errors, p, getElementByTail(p, tail), profile, ei.definition, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); } int index = profile.getSnapshot().getElement().indexOf(ei.definition); if (index < profile.getSnapshot().getElement().size() - 1) { String nextPath = profile.getSnapshot().getElement().get(index+1).getPath(); if (!nextPath.equals(ei.definition.getPath()) && nextPath.startsWith(ei.definition.getPath())) - validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay); + validateElement(hostContext, errors, profile, ei.definition, null, null, resource, ei.element, type, localStack, thisIsCodeableConcept, checkDisplay, thisExtension); } } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java index 8d0b8cc5a..6bcfc4d8e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java @@ -406,6 +406,8 @@ public class ValidationEngine implements IValidatorResourceFetcher { context = SimpleWorkerContext.fromDefinitions(source, loaderForVersion()); context.setAllowLoadingDuplicates(true); // because of Forge context.setExpansionProfile(makeExpProfile()); + NpmPackage npm = pcm.loadPackage("hl7.fhir.xver-extensions", "0.0.1"); + context.loadFromPackage(npm, null); grabNatives(source, "http://hl7.org/fhir"); } @@ -1614,7 +1616,8 @@ public class ValidationEngine implements IValidatorResourceFetcher { if (context.fetchResource(Resource.class, url) != null) return true; if (Utilities.existsInList(url, "http://hl7.org/fhir/sid/us-ssn", "http://hl7.org/fhir/sid/cvx", "http://hl7.org/fhir/sid/ndc", "http://hl7.org/fhir/sid/us-npi", - "http://hl7.org/fhir/sid/icd-10-vn", "http://hl7.org/fhir/sid/icd-10-cm", "http://hl7.org/fhir/sid/icd-9-cm", "http://hl7.org/fhir/w5", "http://hl7.org/fhir/fivews", "http://hl7.org/fhir/workflow")) { + "http://hl7.org/fhir/sid/icd-10-vn", "http://hl7.org/fhir/sid/icd-10-cm", "http://hl7.org/fhir/sid/icd-9-cm", "http://hl7.org/fhir/w5", "http://hl7.org/fhir/fivews", + "http://hl7.org/fhir/workflow", "http://hl7.org/fhir/ConsentPolicy/opt-out", "http://hl7.org/fhir/ConsentPolicy/opt-in")) { return true; } return false; diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/XVerExtensionManager.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/XVerExtensionManager.java new file mode 100644 index 000000000..4c91cce5e --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/XVerExtensionManager.java @@ -0,0 +1,156 @@ +package org.hl7.fhir.r5.validation; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.r5.context.IWorkerContext; +import org.hl7.fhir.r5.model.ElementDefinition; +import org.hl7.fhir.r5.model.ElementDefinition.TypeRefComponent; +import org.hl7.fhir.r5.model.Enumerations.FHIRVersion; +import org.hl7.fhir.r5.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r5.model.StructureDefinition; +import org.hl7.fhir.r5.model.StructureDefinition.ExtensionContextType; +import org.hl7.fhir.r5.model.StructureDefinition.StructureDefinitionKind; +import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule; +import org.hl7.fhir.r5.model.UriType; +import org.hl7.fhir.r5.validation.XVerExtensionManager.XVerExtensionStatus; +import org.hl7.fhir.utilities.json.JsonTrackingParser; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +public class XVerExtensionManager { + + public enum XVerExtensionStatus { + BadVersion, Unknown, Invalid, Valid + } + + private Map lists = new HashMap<>(); + private IWorkerContext context; + + public XVerExtensionManager(IWorkerContext context) { + this.context = context; + } + + public XVerExtensionStatus status(String url) throws IOException { + String v = url.substring(20, 23); + String e = url.substring(54); + if (!lists.containsKey(v)) { + if (context.getBinaries().containsKey("xver-paths-"+v+".json")) { + lists.put(v, JsonTrackingParser.parseJson(context.getBinaries().get("xver-paths-"+v+".json"))); + } else { + return XVerExtensionStatus.BadVersion; + } + } + JsonObject root = lists.get(v); + JsonObject path = root.getAsJsonObject(e); + if (path == null) { + return XVerExtensionStatus.Unknown; + } + if (path.has("elements") || path.has("types")) { + return XVerExtensionStatus.Valid; + } else { + return XVerExtensionStatus.Invalid; + } + } + + public String getElementId(String url) { + return url.substring(54); + } + + public StructureDefinition makeDefinition(String url) { + String v = url.substring(20, 23); + String e = url.substring(54); + JsonObject root = lists.get(v); + JsonObject path = root.getAsJsonObject(e); + + StructureDefinition sd = new StructureDefinition(); + sd.setUrl(url); + sd.setVersion(context.getVersion()); + sd.setFhirVersion(FHIRVersion.fromCode(context.getVersion())); + sd.setKind(StructureDefinitionKind.COMPLEXTYPE); + sd.setType("Extension"); + sd.setDerivation(TypeDerivationRule.CONSTRAINT); + sd.setName("Extension-"+v+"-"+e); + sd.setTitle("Extension Definition for "+e+" for Version "+v); + sd.setStatus(PublicationStatus.ACTIVE); + sd.setExperimental(false); + sd.setDate(new Date()); + sd.setPublisher("FHIR Project"); + sd.setPurpose("Defined so the validator can validate cross version extensions (see http://hl7.org/fhir/versions.html#extensions)"); + sd.setAbstract(false); + sd.addContext().setType(ExtensionContextType.ELEMENT).setExpression(head(e)); + sd.setBaseDefinition("http://hl7.org/fhir/StructureDefinition/Extension"); + if (path.has("types")) { + sd.getDifferential().addElement().setPath("Extension.extension").setMax("0"); + sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url)); + ElementDefinition val = sd.getDifferential().addElement().setPath("Extension.value[x]").setMin(1); + populateTypes(path, val); + } else if (path.has("elements")) { + for (JsonElement i : path.getAsJsonArray("elements")) { + String s = i.getAsString(); + sd.getDifferential().addElement().setPath("Extension.extension").setSliceName(s); + sd.getDifferential().addElement().setPath("Extension.extension.extension").setMax("0"); + sd.getDifferential().addElement().setPath("Extension.extension.url").setFixed(new UriType(s)); + ElementDefinition val = sd.getDifferential().addElement().setPath("Extension.extension.value[x]").setMin(1); + JsonObject elt = root.getAsJsonObject(e+"."+s); + if (!elt.has("types")) { + throw new FHIRException("Internal error - nested elements not supported yet"); + } + populateTypes(elt, val); + } + sd.getDifferential().addElement().setPath("Extension.url").setFixed(new UriType(url)); + sd.getDifferential().addElement().setPath("Extension.value[x]").setMax("0"); + } else { + throw new FHIRException("Internal error - attempt to define extension for "+url+" when it is invalid"); + } + return sd; + } + + public void populateTypes(JsonObject path, ElementDefinition val) { + for (JsonElement i : path.getAsJsonArray("types")) { + String s = i.getAsString(); + if (s.contains("(")) { + String t = s.substring(0, s.indexOf("(")); + TypeRefComponent tr = val.addType().setCode(s); + s = s.substring(t.length()+1); + for (String p : s.substring(0, s.length()-1).split("\\|")) { + tr.addTargetProfile("http://hl7.org/fhir/StructureDefinition/"+p); + } + } else { + val.addType().setCode(s); + } + } + } + + private String head(String id) { + if (id.contains(".")) { + return id.substring(0, id.lastIndexOf(".")); + } else { + return id; + } + } + + public String getVersion(String url) { + return url.substring(20, 23); + } + + public boolean matchingUrl(String url) { + if (url == null || url.length() < 56) { + return false; + } + String pfx = url.substring(0, 20); + String v = url.substring(20, 23); + String sfx = url.substring(23, 54); + return pfx.equals("http://hl7.org/fhir/") && + isVersionPattern(v) && sfx.equals("/StructureDefinition/extension-"); + } + + private boolean isVersionPattern(String v) { + return v.length() == 3 && Character.isDigit(v.charAt(0)) && v.charAt(1) == '.' && Character.isDigit(v.charAt(2)); + } + +}