Refactoring ResourceValidationTracker

This commit is contained in:
markiantorno 2020-02-21 16:09:13 -05:00
parent 8c2679a200
commit e54d7556b1
2 changed files with 5306 additions and 5133 deletions

View File

@ -36,11 +36,13 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r5.model.Reference; import org.hl7.fhir.r5.model.Reference;
import org.hl7.fhir.convertors.*; import org.hl7.fhir.convertors.*;
import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.exceptions.PathEngineException;
import org.hl7.fhir.exceptions.TerminologyServiceException; import org.hl7.fhir.exceptions.TerminologyServiceException;
import org.hl7.fhir.r5.conformance.ProfileUtilities; import org.hl7.fhir.r5.conformance.ProfileUtilities;
import org.hl7.fhir.r5.context.IWorkerContext; import org.hl7.fhir.r5.context.IWorkerContext;
@ -114,6 +116,7 @@ import org.hl7.fhir.r5.model.StructureDefinition.TypeDerivationRule;
import org.hl7.fhir.r5.model.TimeType; import org.hl7.fhir.r5.model.TimeType;
import org.hl7.fhir.r5.model.Timing; import org.hl7.fhir.r5.model.Timing;
import org.hl7.fhir.r5.model.DataType; import org.hl7.fhir.r5.model.DataType;
import org.hl7.fhir.r5.model.TypeDetails;
import org.hl7.fhir.r5.model.UriType; import org.hl7.fhir.r5.model.UriType;
import org.hl7.fhir.r5.model.ValueSet; import org.hl7.fhir.r5.model.ValueSet;
import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent; import org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent;
@ -150,12 +153,11 @@ import ca.uhn.fhir.util.ObjectUtil;
/** /**
* Thinking of using this in a java program? Don't! * Thinking of using this in a java program? Don't!
* You should use one of the wrappers instead. Either in HAPI, or use ValidationEngine * You should use one of the wrappers instead. Either in HAPI, or use ValidationEngine
* * <p>
* Validation todo: * Validation todo:
* - support @default slices * - support @default slices
* *
* @author Grahame Grieve * @author Grahame Grieve
*
*/ */
/* /*
* todo: * todo:
@ -165,40 +167,159 @@ import ca.uhn.fhir.util.ObjectUtil;
public class InstanceValidator extends BaseValidator implements IResourceValidator { public class InstanceValidator extends BaseValidator implements IResourceValidator {
/** private class ValidatorHostServices implements IEvaluationContext {
* The validator keeps one of these classes for each resource it validates (e.g. when chasing references)
*
* It keeps track of the state of resource validation - a given resrouce may be validated agaisnt multiple
* profiles during a single validation, and it may be valid against some, and invalid against others.
*
* We don't want to keep doing the same validation, so we remember the outcomes for each combination
* of resource + profile
*
* Additionally, profile validation may be circular - e.g. a Patient profile that applies the same profile
* to linked patients, and 2 patient resources that link to each other. So this class also tracks validation
* in process.
*
* If the validator comes back to the same point, and validates the same instance against the same profile
* while it's already validating, it assumes it's valid - it cannot have new errors that wouldn't already
* be found in the first iteration - in other words, there's no errors
*
* @author graha
*
*/
private class ResourceValidationTracker {
private Map<String, List<ValidationMessage>> validations = new HashMap<>();
private void startValidating(StructureDefinition sd) { @Override
validations.put(sd.getUrl(), new ArrayList<ValidationMessage>()); public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
ValidatorHostContext c = (ValidatorHostContext) appContext;
if (externalHostServices != null)
return externalHostServices.resolveConstant(c.getAppContext(), name, beforeContext);
else
return null;
} }
private List<ValidationMessage> getOutcomes(StructureDefinition sd) { @Override
return validations.get(sd.getUrl()); public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
ValidatorHostContext c = (ValidatorHostContext) appContext;
if (externalHostServices != null)
return externalHostServices.resolveConstantType(c.getAppContext(), name);
else
return null;
} }
public void storeOutcomes(StructureDefinition sd, List<ValidationMessage> errors) { @Override
validations.put(sd.getUrl(), errors); public boolean log(String argument, List<Base> focus) {
if (externalHostServices != null)
return externalHostServices.log(argument, focus);
else
return false;
} }
@Override
public FunctionDetails resolveFunction(String functionName) {
throw new Error("Not done yet (ValidatorHostServices.resolveFunction): " + functionName);
}
@Override
public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
throw new Error("Not done yet (ValidatorHostServices.checkFunction)");
}
@Override
public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
throw new Error("Not done yet (ValidatorHostServices.executeFunction)");
}
@Override
public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException {
ValidatorHostContext c = (ValidatorHostContext) appContext;
if (refContext != null && refContext.hasUserData("validator.bundle.resolution")) {
return (Base) refContext.getUserData("validator.bundle.resolution");
}
if (c.getAppContext() instanceof Element) {
Element bnd = (Element) c.getAppContext();
Base res = resolveInBundle(url, bnd);
if (res != null)
return res;
}
Base res = resolveInBundle(url, c.getResource());
if (res != null)
return res;
res = resolveInBundle(url, c.getContainer());
if (res != null)
return res;
if (externalHostServices != null)
return externalHostServices.resolveReference(c.getAppContext(), url, refContext);
else if (fetcher != null)
try {
return fetcher.fetch(c.getAppContext(), url);
} catch (IOException e) {
throw new FHIRException(e);
}
else
throw new Error("Not done yet - resolve " + url + " locally (2)");
}
public Base resolveInBundle(String url, Element bnd) {
if (bnd == null)
return null;
if (bnd.fhirType().equals("Bundle")) {
for (Element be : bnd.getChildrenByName("entry")) {
Element res = be.getNamedChild("resource");
if (res != null) {
String fullUrl = be.getChildValue("fullUrl");
String rt = res.fhirType();
String id = res.getChildValue("id");
if (url.equals(fullUrl))
return res;
if (url.equals(rt + "/" + id))
return res;
}
}
}
return null;
}
@Override
public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
ValidatorHostContext ctxt = (ValidatorHostContext) appContext;
StructureDefinition sd = context.fetchResource(StructureDefinition.class, url);
if (sd == null) {
throw new FHIRException("Unable to resolve " + url);
}
InstanceValidator self = InstanceValidator.this;
List<ValidationMessage> valerrors = new ArrayList<ValidationMessage>();
if (item instanceof Resource) {
try {
Element e = new ObjectConverter(context).convert((Resource) item);
self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e));
} catch (IOException e1) {
throw new FHIRException(e1);
}
} else if (item instanceof Element) {
Element e = (Element) item;
if (e.isResource()) {
self.validateResource(new ValidatorHostContext(ctxt.getAppContext(), e), valerrors, e, e, sd, IdStatus.OPTIONAL, new NodeStack(e));
} else {
throw new FHIRException("Not supported yet");
}
} else
throw new NotImplementedException("Not done yet (ValidatorHostServices.conformsToProfile), when item is not an element");
boolean ok = true;
List<ValidationMessage> record = new ArrayList<>();
for (ValidationMessage v : valerrors) {
ok = ok && !v.getLevel().isError();
if (v.getLevel().isError() || v.isSlicingHint()) {
record.add(v);
}
}
if (!ok && !record.isEmpty()) {
ctxt.sliceNotes(url, record);
}
return ok;
}
@Override
public ValueSet resolveValueSet(Object appContext, String url) {
ValidatorHostContext c = (ValidatorHostContext) appContext;
if (c.getProfile() != null && url.startsWith("#")) {
for (Resource r : c.getProfile().getContained()) {
if (r.getId().equals(url.substring(1))) {
if (r instanceof ValueSet)
return (ValueSet) r;
else
throw new FHIRException("Reference " + url + " refers to a " + r.fhirType() + " not a ValueSet");
}
}
return null;
}
return context.fetchResource(ValueSet.class, url);
}
} }
private IWorkerContext context; private IWorkerContext context;
@ -254,7 +375,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
this.context = theContext; this.context = theContext;
this.externalHostServices = hostServices; this.externalHostServices = hostServices;
fpe = new FHIRPathEngine(context); fpe = new FHIRPathEngine(context);
validatorServices = new ValidatorHostServices(this); validatorServices = new ValidatorHostServices();
fpe.setHostServices(validatorServices); fpe.setHostServices(validatorServices);
if (theContext.getVersion().startsWith("3.0") || theContext.getVersion().startsWith("1.0")) if (theContext.getVersion().startsWith("3.0") || theContext.getVersion().startsWith("1.0"))
fpe.setLegacyMode(true); fpe.setLegacyMode(true);
@ -673,8 +794,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
hint(errors, IssueType.UNKNOWN, element.line(), element.col(), path, false, "Code System URI '" + system + "' is unknown so the code cannot be validated"); hint(errors, IssueType.UNKNOWN, element.line(), element.col(), path, false, "Code System URI '" + system + "' is unknown so the code cannot be validated");
return true; return true;
} } catch (Exception e) {
catch (Exception e) {
return true; return true;
} }
} }
@ -1401,7 +1521,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
for (StructureDefinitionContextComponent ctxt : fixContexts(extUrl, definition.getContext())) { for (StructureDefinitionContextComponent ctxt : fixContexts(extUrl, definition.getContext())) {
if (ok) { break; } if (ok) {
break;
}
if (ctxt.getType() == ExtensionContextType.ELEMENT) { if (ctxt.getType() == ExtensionContextType.ELEMENT) {
String en = ctxt.getExpression(); String en = ctxt.getExpression();
contexts.append("e:" + en); contexts.append("e:" + en);
@ -1411,7 +1533,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
ok = true; ok = true;
} }
for (String p : plist) { for (String p : plist) {
if (ok) {break; } if (ok) {
break;
}
if (p.equals(en)) { if (p.equals(en)) {
ok = true; ok = true;
} else { } else {
@ -2946,15 +3070,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
} }
/** /**
* * @param element - the candidate that might be in the slice
* @param element * @param path - for reporting any errors. the XPath for the element
* - the candidate that might be in the slice * @param slicer - the definition of how slicing is determined
* @param path * @param ed - the slice for which to test membership
* - for reporting any errors. the XPath for the element
* @param slicer
* - the definition of how slicing is determined
* @param ed
* - the slice for which to test membership
* @param errors * @param errors
* @param stack * @param stack
* @return * @return
@ -3145,7 +3264,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
for (Coding c : cc.getCoding()) { for (Coding c : cc.getCoding()) {
if (c.hasExtension()) if (c.hasExtension())
throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId()); throw new DefinitionException("Unsupported CodeableConcept pattern - extensions are not allowed - for discriminator(" + discriminator + ") for slice " + ed.getId());
if (firstCoding) firstCoding = false; else expression.append(" and "); if (firstCoding) firstCoding = false;
else expression.append(" and ");
expression.append(discriminator + ".coding.where("); expression.append(discriminator + ".coding.where(");
boolean first = true; boolean first = true;
if (c.hasSystem()) { if (c.hasSystem()) {
@ -3153,15 +3273,18 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
expression.append("system = '" + c.getSystem() + "'"); expression.append("system = '" + c.getSystem() + "'");
} }
if (c.hasVersion()) { if (c.hasVersion()) {
if (first) first = false; else expression.append(" and "); if (first) first = false;
else expression.append(" and ");
expression.append("version = '" + c.getVersion() + "'"); expression.append("version = '" + c.getVersion() + "'");
} }
if (c.hasCode()) { if (c.hasCode()) {
if (first) first = false; else expression.append(" and "); if (first) first = false;
else expression.append(" and ");
expression.append("code = '" + c.getCode() + "'"); expression.append("code = '" + c.getCode() + "'");
} }
if (c.hasDisplay()) { if (c.hasDisplay()) {
if (first) first = false; else expression.append(" and "); if (first) first = false;
else expression.append(" and ");
expression.append("display = '" + c.getDisplay() + "'"); expression.append("display = '" + c.getDisplay() + "'");
} }
expression.append(").exists()"); expression.append(").exists()");
@ -3501,6 +3624,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
iRest++; iRest++;
} }
} }
private void validateCodeSystem(List<ValidationMessage> errors, Element cs, NodeStack stack) { private void validateCodeSystem(List<ValidationMessage> errors, Element cs, NodeStack stack) {
String url = cs.getNamedChildValue("url"); String url = cs.getNamedChildValue("url");
String vsu = cs.getNamedChildValue("valueSet"); String vsu = cs.getNamedChildValue("valueSet");
@ -3558,7 +3682,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id); new JsonParser(context).compose(contained, bs, OutputStyle.NORMAL, id);
byte[] json = bs.toByteArray(); byte[] json = bs.toByteArray();
switch (v) { switch (v) {
case DSTU1: throw new FHIRException("Unsupported version R1"); case DSTU1:
throw new FHIRException("Unsupported version R1");
case DSTU2: case DSTU2:
org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json); org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json);
Resource r5 = VersionConvertor_10_50.convertResource(r2); Resource r5 = VersionConvertor_10_50.convertResource(r2);
@ -3674,7 +3799,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false); if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false);
else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer"); else if (itemType.equals("integer"))
checkOption(errors, answer, ns, qsrc, qItem, "integer");
else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string"); else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string");
} }
break; break;
@ -3684,8 +3810,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true); if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true);
else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date"); else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time"); else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
else if (itemType.equals("integer")) checkOption(errors, answer, ns, qsrc, qItem, "integer"); else if (itemType.equals("integer"))
else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string", true); checkOption(errors, answer, ns, qsrc, qItem, "integer");
else if (itemType.equals("string"))
checkOption(errors, answer, ns, qsrc, qItem, "string", true);
} }
break; break;
// case QUESTION: // case QUESTION:
@ -3746,12 +3874,9 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
NodeStack ns = stack.push(item, -1, null, null); NodeStack ns = stack.push(item, -1, null, null);
validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)); validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item));
} } else
else
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \"" + linkId + "\" not found in questionnaire"); rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \"" + linkId + "\" not found in questionnaire");
} } else {
else
{
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order"); rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order");
lastIndex = index; lastIndex = index;
@ -4168,6 +4293,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
* Check each resource entry to ensure that the entry's fullURL includes the resource's id * Check each resource entry to ensure that the entry's fullURL includes the resource's id
* value. Adds an ERROR ValidationMessge to errors List for a given entry if it references * value. Adds an ERROR ValidationMessge to errors List for a given entry if it references
* a resource and fullURL does not include the resource's id. * a resource and fullURL does not include the resource's id.
*
* @param errors List of ValidationMessage objects that new errors will be added to. * @param errors List of ValidationMessage objects that new errors will be added to.
* @param entries List of entry Element objects to be checked. * @param entries List of entry Element objects to be checked.
* @param stack Current NodeStack used to create path names in error detail messages. * @param stack Current NodeStack used to create path names in error detail messages.
@ -5006,7 +5132,8 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
} }
if (resp != null) { if (resp != null) {
return IdStatus.OPTIONAL; return IdStatus.OPTIONAL;
} if (method == null) { }
if (method == null) {
if (fullUrl == null) if (fullUrl == null)
return IdStatus.REQUIRED; return IdStatus.REQUIRED;
else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:")) else if (fullUrl.primitiveValue().startsWith("urn:uuid:") || fullUrl.primitiveValue().startsWith("urn:oid:"))
@ -5121,7 +5248,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
/* /*
* The actual base entry point for internal use (re-entrant) * The actual base entry point for internal use (re-entrant)
*/ */
public void validateResource(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack) throws FHIRException { private void validateResource(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element resource, Element element, StructureDefinition defn, IdStatus idstatus, NodeStack stack) throws FHIRException {
assert stack != null; assert stack != null;
assert resource != null; assert resource != null;
boolean ok = true; boolean ok = true;
@ -5224,7 +5351,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
return Calendar.getInstance().get(Calendar.YEAR); return Calendar.getInstance().get(Calendar.YEAR);
} }
public static class NodeStack { public class NodeStack {
private ElementDefinition definition; private ElementDefinition definition;
private Element element; private Element element;
private ElementDefinition extension; private ElementDefinition extension;
@ -5293,9 +5420,11 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
private NodeStack pushTarget(Element element, int count, ElementDefinition definition, ElementDefinition type) { private NodeStack pushTarget(Element element, int count, ElementDefinition definition, ElementDefinition type) {
return pushInternal(element, count, definition, type, "->"); return pushInternal(element, count, definition, type, "->");
} }
private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) { private NodeStack push(Element element, int count, ElementDefinition definition, ElementDefinition type) {
return pushInternal(element, count, definition, type, "."); return pushInternal(element, count, definition, type, ".");
} }
private NodeStack pushInternal(Element element, int count, ElementDefinition definition, ElementDefinition type, String sep) { private NodeStack pushInternal(Element element, int count, ElementDefinition definition, ElementDefinition type, String sep) {
NodeStack res = new NodeStack(); NodeStack res = new NodeStack();
res.parent = this; res.parent = this;

View File

@ -0,0 +1,44 @@
package org.hl7.fhir.r5.validation.instancevalidator.utils;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.hl7.fhir.utilities.validation.ValidationMessage;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The validator keeps one of these classes for each resource it validates (e.g. when chasing references)
* <p>
* It keeps track of the state of resource validation - a given resrouce may be validated agaisnt multiple
* profiles during a single validation, and it may be valid against some, and invalid against others.
* <p>
* We don't want to keep doing the same validation, so we remember the outcomes for each combination
* of resource + profile
* <p>
* Additionally, profile validation may be circular - e.g. a Patient profile that applies the same profile
* to linked patients, and 2 patient resources that link to each other. So this class also tracks validation
* in process.
* <p>
* If the validator comes back to the same point, and validates the same instance against the same profile
* while it's already validating, it assumes it's valid - it cannot have new errors that wouldn't already
* be found in the first iteration - in other words, there's no errors
*
* @author graha
*/
public class ResourceValidationTracker {
private Map<String, List<ValidationMessage>> validations = new HashMap<>();
public void startValidating(StructureDefinition sd) {
validations.put(sd.getUrl(), new ArrayList<ValidationMessage>());
}
public List<ValidationMessage> getOutcomes(StructureDefinition sd) {
return validations.get(sd.getUrl());
}
public void storeOutcomes(StructureDefinition sd, List<ValidationMessage> errors) {
validations.put(sd.getUrl(), errors);
}
}