Add validation module for QuestionnaireAnswers

This commit is contained in:
James Agnew 2015-07-15 17:28:12 -04:00
parent 818c40498c
commit 3bba0c0425
7 changed files with 374 additions and 61 deletions

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.validation;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public interface IResourceLoader {
/**
* Load the latest version of a given resource
*
* @param theType
* The type of the resource to load
* @param theId
* The ID of the resource to load
* @throws ResourceNotFoundException
* If the resource is not known
*/
public <T extends IBaseResource> T load(Class<T> theType, IIdType theId) throws ResourceNotFoundException;
}

View File

@ -23,7 +23,7 @@ package ca.uhn.fhir.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.EncodingEnum;
interface IValidationContext<T> {
public interface IValidationContext<T> {
FhirContext getFhirContext();

View File

@ -218,7 +218,6 @@
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<fork>true</fork>
<verbose>true</verbose>
</configuration>
</plugin>
</plugins>

View File

@ -0,0 +1,51 @@
package ca.uhn.fhir.validation;
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.validation.ValidationMessage;
import ca.uhn.fhir.model.api.Bundle;
/**
* Base class for a bridge between the RI validation tools and HAPI
*/
abstract class BaseValidatorBridge implements IValidator {
public BaseValidatorBridge() {
super();
}
private void doValidate(IValidationContext<?> theCtx) {
List<ValidationMessage> messages = validate(theCtx);
for (ValidationMessage riMessage : messages) {
SingleValidationMessage hapiMessage = new SingleValidationMessage();
if (riMessage.getCol() != -1) {
hapiMessage.setLocationCol(riMessage.getCol());
}
if (riMessage.getLine() != -1) {
hapiMessage.setLocationLine(riMessage.getLine());
}
hapiMessage.setLocationString(riMessage.getLocation());
hapiMessage.setMessage(riMessage.getMessage());
if (riMessage.getLevel() != null) {
hapiMessage.setSeverity(ResultSeverityEnum.fromCode(riMessage.getLevel().toCode()));
}
theCtx.addValidationMessage(hapiMessage);
}
}
protected abstract List<ValidationMessage> validate(IValidationContext<?> theCtx);
@Override
public void validateBundle(IValidationContext<Bundle> theCtx) {
doValidate(theCtx);
}
@Override
public void validateResource(IValidationContext<IBaseResource> theCtx) {
doValidate(theCtx);
}
}

View File

@ -14,7 +14,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.StructureDefinition;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.ValidationMessage;
import org.w3c.dom.Document;
@ -24,7 +23,6 @@ import org.xml.sax.InputSource;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.rest.server.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
@ -32,9 +30,9 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
public class FhirInstanceValidator implements IValidator {
public class FhirInstanceValidator extends BaseValidatorBridge implements IValidator {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class);
static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidator.class);
private DocumentBuilderFactory myDocBuilderFactory;
@ -43,7 +41,39 @@ public class FhirInstanceValidator implements IValidator {
myDocBuilderFactory.setNamespaceAware(true);
}
List<ValidationMessage> validate(FhirContext theCtx, String theInput, EncodingEnum theEncoding) {
private String determineResourceName(Document theDocument) {
Element root = null;
NodeList list = theDocument.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
if (list.item(i) instanceof Element) {
root = (Element) list.item(i);
break;
}
}
root = theDocument.getDocumentElement();
return root.getLocalName();
}
private StructureDefinition loadProfileOrReturnNull(List<ValidationMessage> theMessages, FhirContext theCtx, String theResourceName) {
if (isBlank(theResourceName)) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("Could not determine resource type from request. Content appears invalid."));
return null;
}
String profileCpName = "/org/hl7/fhir/instance/model/profile/" + theResourceName.toLowerCase() + ".profile.xml";
String profileText;
try {
profileText = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream(profileCpName), "UTF-8");
} catch (IOException e1) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("No profile found for resource type " + theResourceName));
return null;
}
StructureDefinition profile = theCtx.newXmlParser().parseResource(StructureDefinition.class, profileText);
return profile;
}
protected List<ValidationMessage> validate(FhirContext theCtx, String theInput, EncodingEnum theEncoding) {
WorkerContext workerContext = new WorkerContext();
org.hl7.fhir.instance.validation.InstanceValidator v;
try {
@ -97,61 +127,9 @@ public class FhirInstanceValidator implements IValidator {
return messages;
}
private StructureDefinition loadProfileOrReturnNull(List<ValidationMessage> theMessages, FhirContext theCtx, String theResourceName) {
if (isBlank(theResourceName)) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("Could not determine resource type from request. Content appears invalid."));
return null;
}
String profileCpName = "/org/hl7/fhir/instance/model/profile/" + theResourceName.toLowerCase() + ".profile.xml";
String profileText;
try {
profileText = IOUtils.toString(FhirInstanceValidator.class.getResourceAsStream(profileCpName), "UTF-8");
} catch (IOException e1) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("No profile found for resource type " + theResourceName));
return null;
}
StructureDefinition profile = theCtx.newXmlParser().parseResource(StructureDefinition.class, profileText);
return profile;
}
private String determineResourceName(Document theDocument) {
Element root = null;
NodeList list = theDocument.getChildNodes();
for (int i = 0; i < list.getLength(); i++) {
if (list.item(i) instanceof Element) {
root = (Element) list.item(i);
break;
}
}
root = theDocument.getDocumentElement();
return root.getLocalName();
}
@Override
public void validateResource(IValidationContext<IBaseResource> theCtx) {
List<ValidationMessage> messages = validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding());
for (ValidationMessage riMessage : messages) {
SingleValidationMessage hapiMessage = new SingleValidationMessage();
if (riMessage.getCol() != -1) {
hapiMessage.setLocationCol(riMessage.getCol());
}
if (riMessage.getLine() != -1) {
hapiMessage.setLocationLine(riMessage.getLine());
}
hapiMessage.setLocationString(riMessage.getLocation());
hapiMessage.setMessage(riMessage.getMessage());
if (riMessage.getLevel() != null) {
hapiMessage.setSeverity(ResultSeverityEnum.fromCode(riMessage.getLevel().toCode()));
}
theCtx.addValidationMessage(hapiMessage);
}
}
@Override
public void validateBundle(IValidationContext<Bundle> theContext) {
// nothing for now
protected List<ValidationMessage> validate(IValidationContext<?> theCtx) {
return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding());
}
}

View File

@ -0,0 +1,139 @@
package ca.uhn.fhir.validation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.ResourceReferenceInfo;
public class FhirQuestionnaireAnswersValidator extends BaseValidatorBridge {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirQuestionnaireAnswersValidator.class);
private IResourceLoader myResourceLoader;
/**
* Set the class which will be used to load linked resources from the <code>QuestionnaireAnswers</code>. Specifically, if the <code>QuestionnaireAnswers</code> refers to an external (non-contained)
* <code>Questionnaire</code>, or to any external (non-contained) <code>ValueSet</code>, the resource loader will be used to fetch those resources during the validation.
*
* @param theResourceLoader
* The resourceloader to use. May be <code>null</code> if no resource loader should be used (in which case any <code>QuestionaireAnswers</code> with external references will fail to
* validate.)
*/
public void setResourceLoader(IResourceLoader theResourceLoader) {
myResourceLoader = theResourceLoader;
}
@Override
protected List<ValidationMessage> validate(IValidationContext<?> theCtx) {
Object resource = theCtx.getResource();
if (!(theCtx.getResource() instanceof IBaseResource)) {
ourLog.debug("Not validating object of type {}", theCtx.getResource().getClass());
return Collections.emptyList();
}
if (resource instanceof QuestionnaireAnswers) {
return doValidate(theCtx, (QuestionnaireAnswers) resource);
}
RuntimeResourceDefinition def = theCtx.getFhirContext().getResourceDefinition((IBaseResource) resource);
if ("QuestionnaireAnswers".equals(def.getName()) == false) {
return Collections.emptyList();
}
/*
* If we have a non-RI structure, convert it
*/
IParser p = theCtx.getFhirContext().newJsonParser();
String string = p.encodeResourceToString((IBaseResource) resource);
QuestionnaireAnswers qa = p.parseResource(QuestionnaireAnswers.class, string);
return doValidate(theCtx, qa);
}
private List<ValidationMessage> doValidate(IValidationContext<?> theValCtx, QuestionnaireAnswers theResource) {
WorkerContext workerCtx = new WorkerContext();
ArrayList<ValidationMessage> retVal = new ArrayList<ValidationMessage>();
if (!loadReferences(theResource, workerCtx, theValCtx, retVal)) {
return retVal;
}
QuestionnaireAnswersValidator val = new QuestionnaireAnswersValidator(workerCtx);
val.validate(retVal, theResource);
return retVal;
}
private boolean loadReferences(IBaseResource theResource, WorkerContext theWorkerCtx, IValidationContext<?> theValCtx, ArrayList<ValidationMessage> theMessages) {
List<ResourceReferenceInfo> refs = theValCtx.getFhirContext().newTerser().getAllResourceReferences(theResource);
List<IBaseResource> newResources = new ArrayList<IBaseResource>();
for (ResourceReferenceInfo nextRefInfo : refs) {
IIdType nextRef = nextRefInfo.getResourceReference().getReferenceElement();
String resourceType = nextRef.getResourceType();
if ("ValueSet".equals(resourceType)) {
if (!theWorkerCtx.getValueSets().containsKey(nextRef.getValue())) {
ValueSet resource = tryToLoad(ValueSet.class, nextRef, theMessages);
if (resource == null) {
return false;
}
theWorkerCtx.getValueSets().put(nextRef.getValue(), resource);
newResources.add(resource);
}
} else if ("Questionnaire".equals(resourceType)) {
if (!theWorkerCtx.getQuestionnaires().containsKey(nextRef.getValue())) {
Questionnaire resource = tryToLoad(Questionnaire.class, nextRef, theMessages);
if (resource == null) {
return false;
}
theWorkerCtx.getQuestionnaires().put(nextRef.getValue(), resource);
newResources.add(resource);
}
}
}
for (IBaseResource nextAddedResource : newResources) {
boolean outcome = loadReferences(nextAddedResource, theWorkerCtx, theValCtx, theMessages);
if (!outcome) {
return false;
}
}
return true;
}
private <T extends IBaseResource> T tryToLoad(Class<T> theType, IIdType theReference, List<ValidationMessage> theMessages) {
if (myResourceLoader == null) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("No resource loader present, could not load " + theReference));
return null;
}
try {
T retVal = myResourceLoader.load(theType, theReference);
if (retVal == null) {
throw new IllegalStateException("ResourceLoader returned null. This is a bug with the resourceloader. Reference was: " + theReference);
}
return retVal;
} catch (ResourceNotFoundException e) {
theMessages.add(new ValidationMessage().setLevel(IssueSeverity.FATAL).setMessage("Reference could not be found: " + theReference));
return null;
}
}
}

View File

@ -0,0 +1,124 @@
package ca.uhn.fhir.validation;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.QuestionnaireAnswers;
import org.hl7.fhir.instance.model.Reference;
import org.hl7.fhir.instance.model.StringType;
import org.hl7.fhir.instance.model.ValueSet;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.utils.WorkerContext;
import org.hl7.fhir.instance.validation.QuestionnaireAnswersValidator;
import org.hl7.fhir.instance.validation.ValidationMessage;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
public class QuestionnaireAnswersValidatorIntegrationTest {
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireAnswersValidatorIntegrationTest.class);
private IResourceLoader myResourceLoaderMock;
private FhirValidator myVal;
@Before
public void before() {
myResourceLoaderMock = mock(IResourceLoader.class);
FhirQuestionnaireAnswersValidator qaVal = new FhirQuestionnaireAnswersValidator();
qaVal.setResourceLoader(myResourceLoaderMock);
myVal = ourCtx.newValidator();
myVal.setValidateAgainstStandardSchema(false);
myVal.setValidateAgainstStandardSchematron(false);
myVal.registerValidator(qaVal);
}
@Test
public void testAnswerWithWrongType() {
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(true).setType(AnswerFormat.BOOLEAN);
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
QuestionnaireAnswers qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1");
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
ValidationResult result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@Test
public void testCodedAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1";
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset/ValueSet/123"));
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
ValueSet options = new ValueSet();
options.getDefine().setSystem("urn:system").addConcept().setCode("code0");
when(myResourceLoaderMock.load(Mockito.eq(ValueSet.class), Mockito.eq(new IdType("http://somevalueset/ValueSet/123")))).thenReturn(options);
QuestionnaireAnswers qa;
// Good code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code0"));
ValidationResult result = myVal.validateWithResult(qa);
assertEquals(result.getMessages().toString(), 0, result.getMessages().size());
// Bad code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
result = myVal.validateWithResult(qa);
ourLog.info(result.getMessages().toString());
assertThat(result.getMessages().toString(), containsString("myLocationString=QuestionnaireAnswers.group(0).question(0).answer(0)"));
assertThat(result.getMessages().toString(), containsString("myMessage=Question with linkId[link0] has answer with system[urn:system] and code[code1] but this is not a valid answer for ValueSet[http://somevalueset/ValueSet/123]"));
}
@Test
public void testUnknownValueSet() {
String questionnaireRef = "http://example.com/Questionnaire/q1";
Questionnaire q = new Questionnaire();
q.getGroup().addQuestion().setLinkId("link0").setRequired(false).setType(AnswerFormat.CHOICE).setOptions(new Reference("http://somevalueset/ValueSet/123"));
when(myResourceLoaderMock.load(Mockito.eq(Questionnaire.class), Mockito.eq(new IdType("http://example.com/Questionnaire/q1")))).thenReturn(q);
when(myResourceLoaderMock.load(Mockito.eq(ValueSet.class), Mockito.eq(new IdType("http://somevalueset/ValueSet/123")))).thenThrow(new ResourceNotFoundException("Unknown"));
QuestionnaireAnswers qa;
// Bad code
qa = new QuestionnaireAnswers();
qa.getQuestionnaire().setReference(questionnaireRef);
qa.getGroup().addQuestion().setLinkId("link0").addAnswer().setValue(new Coding().setSystem("urn:system").setCode("code1"));
ValidationResult result = myVal.validateWithResult(qa);
assertThat(result.getMessages().toString(), containsString("myMessage=Reference could not be found: http://some"));
}
}