Validator enhancements

This commit is contained in:
James Agnew 2016-01-08 16:23:27 -05:00
parent e436254c32
commit 552842e547
9 changed files with 2163 additions and 47 deletions

View File

@ -1,10 +1,8 @@
package ca.uhn.fhir.jpa.config;
import org.hl7.fhir.dstu21.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu21.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu21.hapi.validation.FhirQuestionnaireResponseValidator;
import org.hl7.fhir.dstu21.hapi.validation.IValidationSupport;
import org.hl7.fhir.dstu21.hapi.validation.ValidationSupportChain;
import org.hl7.fhir.dstu21.validation.IResourceValidator.BestPracticeWarningLevel;
/*
@ -28,8 +26,6 @@ import org.hl7.fhir.dstu21.validation.IResourceValidator.BestPracticeWarningLeve
*/
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@ -40,6 +36,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.dao.FhirSearchDao;
import ca.uhn.fhir.jpa.dao.IFhirSystemDao;
import ca.uhn.fhir.jpa.dao.ISearchDao;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu21;
import ca.uhn.fhir.validation.IValidatorModule;
@Configuration
@ -94,15 +91,9 @@ public class BaseDstu21Config extends BaseConfig {
return module;
}
@Bean
@Bean(autowire=Autowire.BY_NAME)
public IValidationSupport validationSupportChainDstu21() {
return new ValidationSupportChain(defaultProfileValidationSupport(), jpaValidationSupportDstu21());
// return new ValidationSupportChain();
}
@Bean(destroyMethod="flush")
public DefaultProfileValidationSupport defaultProfileValidationSupport() {
return new DefaultProfileValidationSupport();
return new JpaValidationSupportChainDstu21();
}
}

View File

@ -0,0 +1,31 @@
package ca.uhn.fhir.jpa.validation;
import javax.annotation.PostConstruct;
import org.hl7.fhir.dstu21.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu21.hapi.validation.ValidationSupportChain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class JpaValidationSupportChainDstu21 extends ValidationSupportChain {
@Autowired
@Qualifier("myJpaValidationSupportDstu21")
public ca.uhn.fhir.jpa.dao.IJpaValidationSupportDstu21 myJpaValidationSupportDstu21;
public JpaValidationSupportChainDstu21() {
super();
}
private DefaultProfileValidationSupport myDefaultProfileValidationSupport = new DefaultProfileValidationSupport();
@PostConstruct
public void postConstruct() {
addValidationSupport(myDefaultProfileValidationSupport);
addValidationSupport(myJpaValidationSupportDstu21);
}
public void flush() {
myDefaultProfileValidationSupport.flush();
}
}

View File

@ -31,6 +31,7 @@ import ca.uhn.fhir.jpa.config.WebsocketDstu21Config;
import ca.uhn.fhir.jpa.dao.dstu21.BaseJpaDstu21Test;
import ca.uhn.fhir.jpa.provider.JpaConformanceProviderDstu21;
import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider;
import ca.uhn.fhir.jpa.validation.JpaValidationSupportChainDstu21;
import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
import ca.uhn.fhir.rest.client.IGenericClient;
import ca.uhn.fhir.rest.client.ServerValidationModeEnum;
@ -46,7 +47,7 @@ public abstract class BaseResourceProviderDstu21Test extends BaseJpaDstu21Test {
private static Server ourServer;
protected static String ourServerBase;
protected static RestfulServer ourRestServer;
private static DefaultProfileValidationSupport myValidationSupport;
private static JpaValidationSupportChainDstu21 myValidationSupport;
public BaseResourceProviderDstu21Test() {
super();
@ -133,7 +134,7 @@ public abstract class BaseResourceProviderDstu21Test extends BaseJpaDstu21Test {
server.start();
WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(subsServletHolder.getServlet().getServletConfig().getServletContext());
myValidationSupport = wac.getBean(DefaultProfileValidationSupport.class);
myValidationSupport = wac.getBean(JpaValidationSupportChainDstu21.class);
ourClient = myFhirCtx.newRestfulGenericClient(ourServerBase);
ourClient.registerInterceptor(new LoggingInterceptor(true));

View File

@ -66,7 +66,7 @@ public class ResourceProviderQuestionnaireResponseDstu21Test extends BaseResourc
ourClient.create().resource(qr1).execute();
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.toString(), containsString("Answer to question with linkId[link1] found of type [DecimalType] but this is invalid for question of type [string]"));
assertThat(e.toString(), containsString("Answer value must be of type string"));
}
}
@ -88,7 +88,7 @@ public class ResourceProviderQuestionnaireResponseDstu21Test extends BaseResourc
ourClient.create().resource(qr1).execute();
fail();
} catch (UnprocessableEntityException e) {
assertThat(e.toString(), containsString("Answer to question with linkId[link1] found of type [DecimalType] but this is invalid for question of type [string]"));
assertThat(e.toString(), containsString("Answer value must be of type string"));
}
}

View File

@ -2,7 +2,6 @@ package ca.uhn.fhirtest.config;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@ -10,10 +9,7 @@ import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.commons.lang3.time.DateUtils;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
@ -30,7 +26,6 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseValidatingInterceptor;
import ca.uhn.fhir.validation.IValidatorModule;
import ca.uhn.fhir.validation.ResultSeverityEnum;
@Configuration

View File

@ -1,9 +1,12 @@
package org.hl7.fhir.dstu21.validation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.dstu21.exceptions.DefinitionException;
import org.hl7.fhir.dstu21.exceptions.FHIRException;
import org.hl7.fhir.dstu21.formats.FormatUtilities;
@ -13,6 +16,7 @@ import org.hl7.fhir.dstu21.model.Base;
import org.hl7.fhir.dstu21.model.CodeableConcept;
import org.hl7.fhir.dstu21.model.Coding;
import org.hl7.fhir.dstu21.model.ContactPoint;
import org.hl7.fhir.dstu21.model.DomainResource;
import org.hl7.fhir.dstu21.model.ElementDefinition;
import org.hl7.fhir.dstu21.model.ElementDefinition.ConstraintSeverity;
import org.hl7.fhir.dstu21.model.ElementDefinition.ElementDefinitionBindingComponent;
@ -28,6 +32,9 @@ import org.hl7.fhir.dstu21.model.OperationOutcome.IssueType;
import org.hl7.fhir.dstu21.model.Period;
import org.hl7.fhir.dstu21.model.Property;
import org.hl7.fhir.dstu21.model.Quantity;
import org.hl7.fhir.dstu21.model.Questionnaire;
import org.hl7.fhir.dstu21.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.dstu21.model.Questionnaire.QuestionnaireItemType;
import org.hl7.fhir.dstu21.model.Range;
import org.hl7.fhir.dstu21.model.Ratio;
import org.hl7.fhir.dstu21.model.Reference;
@ -461,7 +468,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing (cc)")) {
if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
ValueSet valueset = resolveBindingReference(binding.getValueSet());
ValueSet valueset = resolveBindingReference(profile, binding.getValueSet());
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) {
try {
CodeableConcept cc = readAsCodeableConcept(element);
@ -532,7 +539,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
ElementDefinitionBindingComponent binding = theElementCntext.getBinding();
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, binding != null, "Binding for " + path + " missing")) {
if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
ValueSet valueset = resolveBindingReference(binding.getValueSet());
ValueSet valueset = resolveBindingReference(profile, binding.getValueSet());
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, valueset != null, "ValueSet " + describeReference(binding.getValueSet()) + " not found")) {
try {
Coding c = readAsCoding(element);
@ -848,7 +855,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
checkFixedValue(errors, path + ".end", focus.getNamedChild("end"), fixed.getEndElement(), "end");
}
private void checkPrimitive(List<ValidationMessage> errors, String path, String type, ElementDefinition context, WrapperElement e) {
private void checkPrimitive(List<ValidationMessage> errors, String path, String type, ElementDefinition context, WrapperElement e, StructureDefinition profile) {
if (type.equals("uri")) {
rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("oid:"), "URI values cannot start with oid:");
rule(errors, IssueType.INVALID, e.line(), e.col(), path, !e.getAttribute("value").startsWith("uuid:"), "URI values cannot start with uuid:");
@ -882,13 +889,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
if (context.hasBinding()) {
checkPrimitiveBinding(errors, path, type, context, e);
checkPrimitiveBinding(errors, path, type, context, e, profile);
}
// for nothing to check
}
// note that we don't check the type here; it could be string, uri or code.
private void checkPrimitiveBinding(List<ValidationMessage> errors, String path, String type, ElementDefinition elementContext, WrapperElement element) {
private void checkPrimitiveBinding(List<ValidationMessage> errors, String path, String type, ElementDefinition elementContext, WrapperElement element, StructureDefinition profile) {
if (!element.hasAttribute("value"))
return;
@ -898,7 +905,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// firstly, resolve the value set
ElementDefinitionBindingComponent binding = elementContext.getBinding();
if (binding.hasValueSet() && binding.getValueSet() instanceof Reference) {
ValueSet vs = resolveBindingReference(binding.getValueSet());
ValueSet vs = resolveBindingReference(profile, binding.getValueSet());
if (warning(errors, IssueType.CODEINVALID, element.line(), element.col(), path, vs != null, "ValueSet {0} not found", describeReference(binding.getValueSet()))) {
ValidationResult vr = context.validateCode(null, value, null, vs);
if (!vr.isOk()) {
@ -1157,6 +1164,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private ElementDefinition getCriteriaForDiscriminator(String path, ElementDefinition ed, String discriminator, StructureDefinition profile) throws DefinitionException {
List<ElementDefinition> childDefinitions = getChildMap(profile, ed);
List<ElementDefinition> snapshot = null;
int index;
if (childDefinitions.isEmpty()) {
// going to look at the type
if (ed.getType().size() == 0)
@ -1173,14 +1181,14 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
type = context.fetchResource(StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + ed.getType().get(0).getCode());
snapshot = type.getSnapshot().getElement();
ed = snapshot.get(0);
index = 0;
} else {
snapshot = profile.getSnapshot().getElement();
snapshot = childDefinitions;
index = -1;
}
String originalPath = ed.getPath();
String goal = originalPath + "." + discriminator;
int index = snapshot.indexOf(ed);
assert(index > -1);
index++;
while (index < snapshot.size() && !snapshot.get(index).getPath().equals(originalPath)) {
if (snapshot.get(index).getPath().equals(goal))
@ -1344,11 +1352,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
private ValueSet resolveBindingReference(Type reference) {
private ValueSet resolveBindingReference(DomainResource ctxt, Type reference) {
if (reference instanceof UriType)
return context.fetchResource(ValueSet.class, ((UriType) reference).getValue().toString());
else if (reference instanceof Reference)
else if (reference instanceof Reference) {
String s = ((Reference) reference).getReference();
if (s.startsWith("#")) {
for (Resource c : ctxt.getContained()) {
if (c.getId().equals(s.substring(1)) && (c instanceof ValueSet))
return (ValueSet) c;
}
return null;
} else
return context.fetchResource(ValueSet.class, ((Reference) reference).getReference());
}
else
return null;
}
@ -1490,9 +1507,239 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
validateBundle(errors, element, stack);
if (element.getResourceType().equals("Observation"))
validateObservation(errors, element, stack);
if (element.getResourceType().equals("QuestionnaireResponse"))
validateQuestionannaireResponse(errors, element, stack);
}
}
private void validateQuestionannaireResponse(List<ValidationMessage> errors, WrapperElement element, NodeStack stack) {
WrapperElement q = element.getNamedChild("questionnaire");
if (hint(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), q != null, "No questionnaire is identified, so no validation can be performed against the base questionnaire")) {
Questionnaire qsrc = context.fetchResource(Questionnaire.class, q.getNamedChildValue("reference"));
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire could not be resolved, so no validation can be performed against the base questionnaire"))
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack);
}
}
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, WrapperElement element, NodeStack stack) {
String text = element.getNamedChildValue("text");
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId());
List<WrapperElement> answers = new ArrayList<InstanceValidator.WrapperElement>();
element.getNamedChildren("answer", answers);
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId());
if (answers.size() > 1)
rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed");
for (WrapperElement answer : answers) {
NodeStack ns = stack.push(answer, -1, null, null);
switch (qItem.getType()) {
case GROUP:
rule(errors, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, "Items of type group should not have answers");
break;
case DISPLAY: // nothing
break;
case BOOLEAN:
validateQuestionnaireResponseItemType(errors, answer, ns, "boolean");
break;
case DECIMAL:
validateQuestionnaireResponseItemType(errors, answer, ns, "decimal");
break;
case INTEGER:
validateQuestionnaireResponseItemType(errors, answer, ns, "integer");
break;
case DATE:
validateQuestionnaireResponseItemType(errors, answer, ns, "date");
break;
case DATETIME:
validateQuestionnaireResponseItemType(errors, answer, ns, "dateTime");
break;
case INSTANT:
validateQuestionnaireResponseItemType(errors, answer, ns, "instant");
break;
case TIME:
validateQuestionnaireResponseItemType(errors, answer, ns, "time");
break;
case STRING:
validateQuestionnaireResponseItemType(errors, answer, ns, "string");
break;
case TEXT:
validateQuestionnaireResponseItemType(errors, answer, ns, "text");
break;
case URL:
validateQuestionnaireResponseItemType(errors, answer, ns, "uri");
break;
case ATTACHMENT:
validateQuestionnaireResponseItemType(errors, answer, ns, "Attachment");
break;
case REFERENCE:
validateQuestionnaireResponseItemType(errors, answer, ns, "Reference");
break;
case QUANTITY:
if (validateQuestionnaireResponseItemType(errors, answer, ns, "Quantity").equals("Quantity"))
if (qItem.hasExtension("???"))
validateQuestionnaireResponseItemQuantity(errors, answer, ns);
break;
case CHOICE:
if (validateQuestionnaireResponseItemType(errors, answer, ns, "Coding").equals("Coding"))
validateAnswerCode(errors, answer, ns, qsrc, qItem);
break;
case OPENCHOICE:
if (validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "string").equals("Coding"))
validateAnswerCode(errors, answer, ns, qsrc, qItem);
break;
}
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack);
}
if (qItem.getType() == QuestionnaireItemType.GROUP)
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack);
else {
List<WrapperElement> items = new ArrayList<InstanceValidator.WrapperElement>();
element.getNamedChildren("item", items);
for (WrapperElement item : items) {
NodeStack ns = stack.push(item, -1, null, null);
rule(errors, IssueType.STRUCTURE, answers.get(0).line(), answers.get(0).col(), stack.getLiteralPath(), false, "Items not of type group should not have items");
}
}
}
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<WrapperElement> elements, NodeStack stack) {
if (elements.size() > 1)
rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed");
for (WrapperElement element : elements) {
NodeStack ns = stack.push(element, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns);
}
}
private int getLinkIdIndex(List<QuestionnaireItemComponent> qItems, String linkId) {
for (int i = 0; i < qItems.size(); i++) {
if (linkId.equals(qItems.get(i).getLinkId()))
return i;
}
return -1;
}
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, WrapperElement element, NodeStack stack) {
List<WrapperElement> items = new ArrayList<WrapperElement>();
element.getNamedChildren("item", items);
// now, sort into stacks
Map<String, List<WrapperElement>> map = new HashMap<String, List<WrapperElement>>();
int lastIndex = -1;
for (WrapperElement item : items) {
String linkId = item.getNamedChildValue("linkId");
if (rule(errors, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), "No LinkId, so can't be validated")) {
int index = getLinkIdIndex(qItems, linkId);
if (index == -1) {
QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId);
if (qItem != null) {
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place");
NodeStack ns = stack.push(item, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns);
}
else
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
}
else
{
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order");
lastIndex = index;
List<WrapperElement> mapItem = map.get(linkId);
if (mapItem == null) {
mapItem = new ArrayList<WrapperElement>();
map.put(linkId, mapItem);
}
mapItem.add(item);
}
}
}
// ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order
for (QuestionnaireItemComponent qItem : qItems) {
List<WrapperElement> mapItem = map.get(qItem.getLinkId());
if (mapItem != null)
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack);
else
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId());
}
}
private void validateQuestionnaireResponseItemQuantity( List<ValidationMessage> errors, WrapperElement answer, NodeStack stack) {
}
private String validateQuestionnaireResponseItemType(List<ValidationMessage> errors, WrapperElement element, NodeStack stack, String... types) {
List<WrapperElement> values = new ArrayList<WrapperElement>();
element.getNamedChildrenWithWildcard("value[x]", values);
if (values.size() > 0) {
NodeStack ns = stack.push(values.get(0), -1, null, null);
CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder();
for (String s : types) {
l.append(s);
if (values.get(0).getName().equals("value"+Utilities.capitalize(s)))
return(s);
}
if (types.length == 1)
rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be of type "+types[0]);
else
rule(errors, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, "Answer value must be one of the types "+l.toString());
}
return null;
}
private QuestionnaireItemComponent findQuestionnaireItem(Questionnaire qSrc, String linkId) {
return findItem(qSrc.getItem(), linkId);
}
private QuestionnaireItemComponent findItem(List<QuestionnaireItemComponent> list, String linkId) {
for (QuestionnaireItemComponent item : list) {
if (linkId.equals(item.getLinkId()))
return item;
QuestionnaireItemComponent result = findItem(item.getItem(), linkId);
if (result != null)
return result;
}
return null;
}
private void validateAnswerCode(List<ValidationMessage> errors, WrapperElement value, NodeStack stack, List<Coding> optionList) {
String system = value.getNamedChildValue("system");
String code = value.getNamedChildValue("code");
boolean found = false;
for (Coding c : optionList) {
if (StringUtils.equals(c.getSystem(), system) && StringUtils.equals(c.getCode(), code)) {
found = true;
break;
}
}
rule(errors, IssueType.STRUCTURE, value.line(), value.col(), stack.getLiteralPath(), found, "The code "+system+"::"+code+" is not a valid option");
}
private void validateAnswerCode(List<ValidationMessage> errors, WrapperElement value, NodeStack stack, Questionnaire qSrc, Reference ref) {
ValueSet vs = resolveBindingReference(qSrc, ref);
if (warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, "ValueSet " + describeReference(ref) + " not found")) {
try {
Coding c = readAsCoding(value);
ValidationResult res = context.validateCode(c, vs);
if (!res.isOk())
rule(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided is not in the options value set in the questionnaire");
} catch (Exception e) {
warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options");
}
}
}
private void validateAnswerCode( List<ValidationMessage> errors, WrapperElement answer, NodeStack stack, Questionnaire qSrc, QuestionnaireItemComponent qItem) {
WrapperElement v = answer.getNamedChild("valueCoding");
NodeStack ns = stack.push(v, -1, null, null);
if (qItem.getOption().size() > 0)
validateAnswerCode(errors, v, stack, qItem.getOption());
else if (qItem.hasOptions())
validateAnswerCode(errors, v, stack, qSrc, qItem.getOptions());
else
hint(errors, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, "Cannot validate options because no option or options are provided");
}
private String tail(String path) {
return path.substring(path.lastIndexOf(".") + 1);
}
@ -1895,8 +2142,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), composition.getResourceType().equals("Composition"),
"The first entry in a document must be a composition")) {
// the composition subject and section references must resolve in the bundle
validateBundleReference(errors, entries, composition.getNamedChild("subject"), "Composition Subject", stack.push(composition.getNamedChild("subject"), -1, null, null), fullUrl, "Composition",
id);
WrapperElement elem = composition.getNamedChild("subject");
if (rule(errors, IssueType.INVALID, composition.line(), composition.col(), stack.getLiteralPath(), elem == null, "A document composition must have a subject"))
validateBundleReference(errors, entries, elem, "Composition Subject", stack.push(elem, -1, null, null), fullUrl, "Composition", id);
validateSections(errors, entries, composition, stack, fullUrl, id);
}
}
@ -1922,7 +2170,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !empty(element), "Elements must have some content (@value, extensions, or children elements)");
checkInvariants(errors, stack.literalPath, profile, definition, null, null, resource, element);
checkInvariants(errors, stack.getLiteralPath(), profile, definition, null, null, resource, element);
// get the list of direct defined children, including slices
List<ElementDefinition> childDefinitions = getChildMap(profile, definition.getName(), definition.getPath(), definition.getNameReference());
@ -2039,7 +2287,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (type != null) {
if (isPrimitiveType(type))
checkPrimitive(errors, ei.path, type, ei.definition, ei.element);
checkPrimitive(errors, ei.path, type, ei.definition, ei.element, profile);
else {
if (type.equals("Identifier"))
checkIdentifier(errors, ei.path, ei.element, ei.definition);

View File

@ -9,6 +9,7 @@ import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
@ -23,6 +24,7 @@ import org.hl7.fhir.dstu21.hapi.validation.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu21.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu21.hapi.validation.IValidationSupport;
import org.hl7.fhir.dstu21.hapi.validation.IValidationSupport.CodeValidationResult;
import org.hl7.fhir.dstu21.hapi.validation.ValidationSupportChain;
import org.hl7.fhir.dstu21.model.CodeType;
import org.hl7.fhir.dstu21.model.Observation;
import org.hl7.fhir.dstu21.model.Observation.ObservationStatus;
@ -33,6 +35,7 @@ import org.hl7.fhir.dstu21.model.ValueSet.ConceptDefinitionComponent;
import org.hl7.fhir.dstu21.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.dstu21.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -62,6 +65,11 @@ public class FhirInstanceValidatorDstu21Test {
myValidConcepts.add(theSystem + "___" + theCode);
}
@After
public void after() {
myDefaultValidationSupport.flush();
}
@SuppressWarnings("unchecked")
@Before
public void before() {
@ -69,14 +77,17 @@ public class FhirInstanceValidatorDstu21Test {
myVal.setValidateAgainstStandardSchema(false);
myVal.setValidateAgainstStandardSchematron(false);
myInstanceVal = new FhirInstanceValidator(myDefaultValidationSupport);
myMockSupport = mock(IValidationSupport.class);
ValidationSupportChain validationSupport = new ValidationSupportChain(myMockSupport, myDefaultValidationSupport);
myInstanceVal = new FhirInstanceValidator(validationSupport);
myVal.registerValidatorModule(myInstanceVal);
mySupportedCodeSystemsForExpansion = new HashMap<String, ValueSet.ValueSetExpansionComponent>();
myValidConcepts = new ArrayList<String>();
myMockSupport = mock(IValidationSupport.class);
when(myMockSupport.expandValueSet(any(FhirContext.class), any(ConceptSetComponent.class))).thenAnswer(new Answer<ValueSetExpansionComponent>() {
@Override
public ValueSetExpansionComponent answer(InvocationOnMock theInvocation) throws Throwable {
@ -100,9 +111,14 @@ public class FhirInstanceValidatorDstu21Test {
when(myMockSupport.fetchResource(any(FhirContext.class), any(Class.class), any(String.class))).thenAnswer(new Answer<IBaseResource>() {
@Override
public IBaseResource answer(InvocationOnMock theInvocation) throws Throwable {
IBaseResource retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class<IBaseResource>) theInvocation.getArguments()[1],
(String) theInvocation.getArguments()[2]);
ourLog.info("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], theInvocation.getArguments()[2], retVal });
IBaseResource retVal;
String id = (String) theInvocation.getArguments()[2];
if ("Questionnaire/q_jon".equals(id)) {
retVal = ourCtx.newJsonParser().parseResource(IOUtils.toString(FhirInstanceValidatorDstu21Test.class.getResourceAsStream("/q_jon.json")));
} else {
retVal = myDefaultValidationSupport.fetchResource((FhirContext) theInvocation.getArguments()[0], (Class<IBaseResource>) theInvocation.getArguments()[1], id);
}
ourLog.info("fetchResource({}, {}) : {}", new Object[] { theInvocation.getArguments()[1], id, retVal });
return retVal;
}
});
@ -214,7 +230,7 @@ public class FhirInstanceValidatorDstu21Test {
ValidationResult output = null;
int passes = 1;
for (int i = 0; i < passes; i++) {
ourLog.info("Pass {}", i+1);
ourLog.info("Pass {}", i + 1);
output = myVal.validateWithResult(input);
}
@ -241,6 +257,16 @@ public class FhirInstanceValidatorDstu21Test {
assertEquals("Element is unknown or does not match any slice", output.getMessages().get(0).getMessage());
}
@Test
public void testValidateQuestionnaireResponse() throws IOException {
String input = IOUtils.toString(FhirInstanceValidatorDstu21Test.class.getResourceAsStream("/qr_jon.xml"));
ValidationResult output = myVal.validateWithResult(input);
assertEquals(output.toString(), 12, output.getMessages().size());
ourLog.info(output.getMessages().get(0).getLocationString());
ourLog.info(output.getMessages().get(0).getMessage());
}
@Test
public void testValidateResourceFailingInvariant() {
Observation input = new Observation();
@ -322,7 +348,7 @@ public class FhirInstanceValidatorDstu21Test {
addValidConcept("http://loinc.org", "1234567");
Observation input = new Observation();
// input.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/devicemetricobservation");
// input.getMeta().addProfile("http://hl7.org/fhir/StructureDefinition/devicemetricobservation");
input.addIdentifier().setSystem("http://acme").setValue("12345");
input.getEncounter().setReference("http://foo.com/Encounter/9");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,455 @@
<QuestionnaireResponse xmlns="http://hl7.org/fhir">
<identifier>
<value value="1234567890"/>
</identifier>
<questionnaire>
<reference value="Questionnaire/q_jon"/>
</questionnaire>
<status value="completed"/>
<subject>
<reference value="http://fhirtest.uhn.ca/baseDstu2.1/Patient/proband"/>
</subject>
<author>
<reference value="http://fhirtest.uhn.ca/baseDstu2.1/Practitioner/f007"/>
</author>
<authored value="2016-01-08"/>
<item>
<linkId value="root"/>
<item>
<linkId value="g1"/>
<text value="CLINICAL INFORMATION"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="1.1"/>
</extension>
<linkId value="1.1"/>
<text value="Patient Clinical Information"/>
<answer>
<valueString value="Previous chest X-RAY was suspicious. Heavy smoker, 15 pack years."/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="1.2"/>
</extension>
<linkId value="1.2"/>
<text value="Previous Examination (Date and Modality)"/>
<answer>
<valueString value="Chest XR - September 23, 2015"/>
</answer>
</item>
</item>
<item>
<linkId value="g2"/>
<text value="IMAGING PROCEDURE DESCRIPTION"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="2.1"/>
</extension>
<linkId value="2.1"/>
<text value="Overall Image Quality:"/>
<answer>
<valueCoding>
<code value="2.1a"/>
<display value="Adequate"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="2.2"/>
</extension>
<linkId value="2.2"/>
<text value="Intravenous Contrast Used?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="2.3"/>
</extension>
<linkId value="2.3"/>
<text value="Additional Comments"/>
<answer>
<valueString value="n/a."/>
</answer>
</item>
</item>
<item>
<linkId value="g3"/>
<text value="FINDINGS"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3"/>
</extension>
<linkId value="g3.0"/>
<text value="T Category"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.1"/>
</extension>
<linkId value="g3.1"/>
<text value="Location of Main Nodule/Mass (Primary tumor, or Reference tumor)"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.1.1"/>
</extension>
<linkId value="q3.1.1"/>
<text value="Location of main nodule/mass:"/>
<answer>
<valueCoding>
<code value="3.1.1a"/>
<display value="Peripheral"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.1.2"/>
</extension>
<linkId value="3.1.2"/>
<answer>
<valueString value="Right Upper Lung, Posterior segment."/>
</answer>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.2"/>
</extension>
<linkId value="g3.2"/>
<text value="Size and characteristics of main nodule/mass"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.2.1"/>
</extension>
<linkId value="3.2.1"/>
<text value="Size of the nodule/mass:"/>
<answer>
<valueCoding>
<code value="3.2.1a"/>
<display value="Solid nodule/mass"/>
</valueCoding>
</answer>
<item><!-- Measurement, image, series, etc for all 3 answer choices goes here. Conditonal. -->
<linkId value="g3.2.1"/>
<item><!-- Group for answer choice a -->
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-enableWhen"><!-- Note that this extension is at the group level. -->
<extension url="#question">
<valueString value="3.2.1"/>
</extension>
<extension url="#answer">
<valueCoding>
<code value="3.2.1a"/>
</valueCoding>
</extension>
</extension>
<linkId value="g3.2.1a"/>
<item>
<linkId value="3.2.1a"/>
<text value="largest dimension:"/>
<answer>
<valueInteger value="20"/>
</answer>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl">
<valueCodeableConcept>
<coding>
<system value="http://hl7.org/fhir/ValueSet/questionnaire-item-control"/>
<code value="unit"/>
</coding>
</valueCodeableConcept>
</extension>
<text value="mm"/>
</item>
</item>
<item>
<linkId value="3.2.1a.image"/>
<text value="image"/>
<answer>
<valueString value="32"/>
</answer>
</item>
<item>
<linkId value="3.2.1a.series"/>
<text value="series"/>
<answer>
<valueString value="3"/>
</answer>
</item>
</item>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.2.2"/>
</extension>
<linkId value="3.2.2"/>
<text value="Plane in which the mass was measured:"/>
<answer>
<valueCoding>
<code value="3.2.2a"/>
<display value="Axial"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.2.3"/>
</extension>
<linkId value="3.2.3"/>
<answer>
<valueString value="Solid attenuation of mass, margins spiculated."/>
</answer>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.3"/>
</extension>
<linkId value="g3.3"/>
<text value="Structures directly involved"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.3.1"/>
</extension>
<linkId value="3.3.1"/>
<text value="State if there is bronchial involvement:"/>
<answer>
<valueCoding>
<code value="3.3.1b"/>
<display value="No"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.3.2"/>
</extension>
<linkId value="3.3.2"/>
<text value="Is there direct involvement of any other anatomical structures?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.3.3"/>
</extension>
<linkId value="3.3.3"/>
<text value="Are there additional suspicious pulmonary nodules?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="3.3.4"/>
</extension>
<linkId value="3.3.4"/>
<text value="Other notable intrathoracic findings (eg lymphangitis carcinomatosis):"/>
<answer>
<valueString value="n/a"/>
</answer>
</item>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="4"/>
</extension>
<linkId value="4.0"/>
<text value="N Category"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="4.1"/>
</extension>
<linkId value="4.1"/>
<text value="Are there enlarged lymph nodes?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="4.2"/>
</extension>
<linkId value="4.2"/>
<text value="Other notable findings:"/>
<answer>
<valueString value="Liner / scar atelectasis lingua, anterior segment of right upper lobe and bilateral lower lobes."/>
</answer>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="5"/>
</extension>
<linkId value="5.0"/>
<text value="M Category (Suspicious Extrathoracic Findings (M1b))"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="5.1"/>
</extension>
<linkId value="5.1"/>
<text value="Are there suspicious extrathoracic findings?"/>
<answer>
<valueBoolean value="true"/>
</answer>
<item>
<linkId value="g5.1.yes"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-enableWhen">
<extension url="#question">
<valueString value="5.1"/>
</extension>
<extension url="#answer">
<valueBoolean value="true"/>
</extension>
</extension>
<linkId value="5.1.yes"/>
<text value="Applicable Structures and Descriptions:"/>
<answer>
<valueCoding>
<code value="5.1.yes.d"/>
<display value="Other"/>
</valueCoding>
</answer>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-enableWhen">
<extension url="#question">
<valueString value="5.1.yes"/>
</extension>
<extension url="#answer">
<valueCoding>
<code value="5.1.yes.d"/>
</valueCoding>
</extension>
</extension>
<linkId value="5.1.yes.d.description"/>
<text value="Description of structures:"/>
<answer>
<valueString value="3 cm calcified thyroid nodule on TUL left."/>
</answer>
</item>
</item>
</item>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="6"/>
</extension>
<linkId value="6.0"/>
<text value="Additional Findings"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="6.1"/>
</extension>
<linkId value="6.1"/>
<text value="Are there additional findings?"/>
<answer>
<valueBoolean value="true"/>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="6.2"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-enableWhen">
<extension url="#question">
<valueString value="6.1"/>
</extension>
<extension url="#answer">
<valueBoolean value="true"/>
</extension>
</extension>
<linkId value="6.2"/>
<text value="Findings and Descriptions:"/>
<answer>
<valueString value="Oligemic changes in right upper lobe from possible old pulmonary embolus."/>
</answer>
</item>
</item>
</item>
<item>
<linkId value="g7"/>
<text value="IMPRESSIONS"/>
<item>
<linkId value="g7.1"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="7.1"/>
</extension>
<linkId value="7.1"/>
<text value="Impression/Summary:"/>
<answer>
<valueString value="Spiculated mass in posterior segment of right upper lobe that has increasing size from 16 to 20 mm."/>
</answer>
</item>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="7.2"/>
</extension>
<linkId value="g7.2"/>
<text value="Radiologic Staging (TNM Version 7th edition)"/>
<item>
<linkId value="g7.20"/>
<text value="If this is a biopsy proven carcinoma, the preliminary radiologic stage is:"/>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="i)"/>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-questionControl" >
<valueCodeableConcept>
<coding>
<code value="radio-button"/>
<display value="Radio Button"/>
</coding>
</valueCodeableConcept>
</extension>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-choiceOrientation" >
<valueCode value="horizontal"/>
</extension>
<linkId value="7.2i"/>
<text value="Primary Tumour (T):"/>
<answer>
<valueCoding>
<code value="T1a"/>
<display value="T1a"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="ii)"/>
</extension>
<linkId value="7.2ii"/>
<text value="Regional Lymph Nodes (N):"/>
<answer>
<valueCoding>
<code value="N0"/>
<display value="N0"/>
</valueCoding>
</answer>
</item>
<item>
<extension url="http://hl7.org/fhir/StructureDefinition/questionnaire-label">
<valueString value="iii)"/>
</extension>
<linkId value="7.2iii"/>
<text value="Distant Metastasis (M):"/>
<answer>
<valueCoding>
<code value="M0"/>
<display value="M0"/>
</valueCoding>
</answer>
</item>
</item>
</item>
</item>
</item>
</QuestionnaireResponse>