Cross version extennsion support + improve extension validation

This commit is contained in:
Grahame Grieve 2019-12-19 10:50:25 +11:00
parent 4922522adf
commit 3ce532968a
3 changed files with 230 additions and 23 deletions

View File

@ -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<ValidationMessage> errors, String path, Element resource, Element element, ElementDefinition def, StructureDefinition profile, NodeStack stack) throws FHIRException, IOException {
private StructureDefinition checkExtension(ValidatorHostContext hostContext, List<ValidationMessage> 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<ValidationMessage> 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<ValidationMessage> 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<String> profiles = new ArrayList<String>();
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<ValidationMessage> profileErrors = new ArrayList<ValidationMessage>();
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);
}
}
}

View File

@ -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;

View File

@ -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<String, JsonObject> 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));
}
}