From 3bba0c04251fcb5f9d226caa98aa3eb114fdbf31 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 15 Jul 2015 17:28:12 -0400 Subject: [PATCH] Add validation module for QuestionnaireAnswers --- .../uhn/fhir/validation/IResourceLoader.java | 22 +++ .../fhir/validation/IValidationContext.java | 2 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 1 - .../fhir/validation/BaseValidatorBridge.java | 51 +++++++ .../validation/FhirInstanceValidator.java | 96 +++++------- .../FhirQuestionnaireAnswersValidator.java | 139 ++++++++++++++++++ ...nnaireAnswersValidatorIntegrationTest.java | 124 ++++++++++++++++ 7 files changed, 374 insertions(+), 61 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IResourceLoader.java create mode 100644 hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/BaseValidatorBridge.java create mode 100644 hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirQuestionnaireAnswersValidator.java create mode 100644 hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireAnswersValidatorIntegrationTest.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IResourceLoader.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IResourceLoader.java new file mode 100644 index 00000000000..52b6c44ca3a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IResourceLoader.java @@ -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 load(Class theType, IIdType theId) throws ResourceNotFoundException; + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IValidationContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IValidationContext.java index 20f8671f14c..8a1a442ac43 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IValidationContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/IValidationContext.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.validation; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.server.EncodingEnum; -interface IValidationContext { +public interface IValidationContext { FhirContext getFhirContext(); diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 92bb302c275..4446bd8b43f 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -218,7 +218,6 @@ maven-compiler-plugin true - true diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/BaseValidatorBridge.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/BaseValidatorBridge.java new file mode 100644 index 00000000000..8db90714e19 --- /dev/null +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/BaseValidatorBridge.java @@ -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 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 validate(IValidationContext theCtx); + + @Override + public void validateBundle(IValidationContext theCtx) { + doValidate(theCtx); + } + + @Override + public void validateResource(IValidationContext theCtx) { + doValidate(theCtx); + } + +} \ No newline at end of file diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirInstanceValidator.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirInstanceValidator.java index f021864aa27..9bd657225b5 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirInstanceValidator.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirInstanceValidator.java @@ -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 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 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 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 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 theCtx) { - List 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 theContext) { - // nothing for now + protected List validate(IValidationContext theCtx) { + return validate(theCtx.getFhirContext(), theCtx.getResourceAsString(), theCtx.getResourceAsStringEncoding()); } } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirQuestionnaireAnswersValidator.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirQuestionnaireAnswersValidator.java new file mode 100644 index 00000000000..9769e6ea4f9 --- /dev/null +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/ca/uhn/fhir/validation/FhirQuestionnaireAnswersValidator.java @@ -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 QuestionnaireAnswers. Specifically, if the QuestionnaireAnswers refers to an external (non-contained) + * Questionnaire, or to any external (non-contained) ValueSet, the resource loader will be used to fetch those resources during the validation. + * + * @param theResourceLoader + * The resourceloader to use. May be null if no resource loader should be used (in which case any QuestionaireAnswers with external references will fail to + * validate.) + */ + public void setResourceLoader(IResourceLoader theResourceLoader) { + myResourceLoader = theResourceLoader; + } + + @Override + protected List 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 doValidate(IValidationContext theValCtx, QuestionnaireAnswers theResource) { + + WorkerContext workerCtx = new WorkerContext(); + ArrayList retVal = new ArrayList(); + + 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 theMessages) { + List refs = theValCtx.getFhirContext().newTerser().getAllResourceReferences(theResource); + + List newResources = new ArrayList(); + + 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 tryToLoad(Class theType, IIdType theReference, List 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; + } + } + +} diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireAnswersValidatorIntegrationTest.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireAnswersValidatorIntegrationTest.java new file mode 100644 index 00000000000..1c7c92e9ef7 --- /dev/null +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/validation/QuestionnaireAnswersValidatorIntegrationTest.java @@ -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")); + } + +}