More work on QA validator

This commit is contained in:
James Agnew 2015-07-30 19:16:16 -04:00
parent ab2129d651
commit c49941060b
8 changed files with 245 additions and 13 deletions

View File

@ -37,12 +37,31 @@ import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.instance.model.annotations.Child;
import org.hl7.fhir.instance.model.annotations.Description;
import org.hl7.fhir.instance.model.annotations.DatatypeDef;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.annotations.Block;
import org.hl7.fhir.instance.model.api.*;
/**
* Base definition for all elements in a resource.
*/
public abstract class Element extends Base implements IBaseHasExtensions {
/**
* Returns an unmodifiable list containing all extensions on this element which
* match the given URL.
*
* @param theUrl The URL. Must not be blank or null.
* @return an unmodifiable list containing all extensions on this element which
* match the given URL
*/
public List<Extension> getExtensionsByUrl(String theUrl) {
Validate.notBlank(theUrl, "theUrl must not be blank or null");
ArrayList<Extension> retVal = new ArrayList<Extension>();
for (Extension next : getExtension()) {
if (theUrl.equals(next.getUrl())) {
retVal.add(next);
}
}
return Collections.unmodifiableList(retVal);
}
/**
* unique id for the element within a resource (for internal references).

View File

@ -14,6 +14,7 @@ import org.hl7.fhir.instance.client.ResourceFormat;
import org.hl7.fhir.instance.model.Bundle;
import org.hl7.fhir.instance.model.ConceptMap;
import org.hl7.fhir.instance.model.Conformance;
import org.hl7.fhir.instance.model.DataElement;
import org.hl7.fhir.instance.model.ElementDefinition.TypeRefComponent;
import org.hl7.fhir.instance.model.OperationOutcome;
import org.hl7.fhir.instance.model.Parameters;
@ -52,6 +53,7 @@ public class WorkerContext implements NameResolver {
private ITerminologyServices terminologyServices = new NullTerminologyServices();
private IFHIRClient client = new NullClient();
private Map<String, ValueSet> codeSystems = new HashMap<String, ValueSet>();
private Map<String, DataElement> dataElements = new HashMap<String, DataElement>();
private Map<String, ValueSet> valueSets = new HashMap<String, ValueSet>();
private Map<String, ConceptMap> maps = new HashMap<String, ConceptMap>();
private Map<String, StructureDefinition> profiles = new HashMap<String, StructureDefinition>();
@ -97,6 +99,10 @@ public class WorkerContext implements NameResolver {
return codeSystems;
}
public Map<String, DataElement> getDataElements() {
return dataElements;
}
public Map<String, ValueSet> getValueSets() {
return valueSets;
}

View File

@ -302,4 +302,20 @@ public class BaseValidator {
return thePass;
}
/**
* Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails
*
* @param thePass
* Set this parameter to <code>false</code> if the validation does not pass
* @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation)
*/
protected boolean warning(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) {
if (!thePass) {
String path = toPath(pathParts);
String message = formatMessage(theMessage, theMessageArguments);
errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.WARNING));
}
return thePass;
}
}

View File

@ -15,9 +15,12 @@ import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.Attachment;
import org.hl7.fhir.instance.model.BooleanType;
import org.hl7.fhir.instance.model.Coding;
import org.hl7.fhir.instance.model.DataElement;
import org.hl7.fhir.instance.model.DateTimeType;
import org.hl7.fhir.instance.model.DateType;
import org.hl7.fhir.instance.model.DecimalType;
import org.hl7.fhir.instance.model.ElementDefinition;
import org.hl7.fhir.instance.model.Extension;
import org.hl7.fhir.instance.model.InstantType;
import org.hl7.fhir.instance.model.IntegerType;
import org.hl7.fhir.instance.model.OperationOutcome.IssueSeverity;
@ -42,8 +45,6 @@ import org.hl7.fhir.instance.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.instance.model.valuesets.IssueType;
import org.hl7.fhir.instance.utils.WorkerContext;
import ca.uhn.fhir.context.FhirContext;
/**
* Validates that an instance of {@link QuestionnaireAnswers} is valid against the {@link Questionnaire} that it claims to conform to.
*
@ -58,6 +59,7 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
* ****************************************************************
*/
private static final List<String> EMPTY_PATH = Collections.emptyList();
private WorkerContext myWorkerCtx;
public QuestionnaireAnswersValidator(WorkerContext theWorkerCtx) {
@ -192,29 +194,54 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
private void validateQuestion(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.GroupComponent theAnsGroup,
LinkedList<String> thePathStack, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
String linkId = theQuestion.getLinkId();
QuestionComponent question = theQuestion;
String linkId = question.getLinkId();
if (!fail(theErrors, IssueType.INVALID, thePathStack, isNotBlank(linkId), "Questionnaire is invalid, question found with no link ID")) {
return;
}
AnswerFormat type = theQuestion.getType();
AnswerFormat type = question.getType();
if (type == null) {
if (theQuestion.getGroup().isEmpty()) {
rule(theErrors, IssueType.INVALID, thePathStack, false, "Questionnaire in invalid, no type and no groups specified for question with link ID[{0}]", linkId);
return;
// Support old format/casing and new
List<Extension> extensions = question.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-deReference");
if (extensions.isEmpty()) {
extensions = question.getExtensionsByUrl("http://hl7.org/fhir/StructureDefinition/questionnaire-dereference");
}
if (extensions.isEmpty() == false) {
if (extensions.size() > 1) {
warning(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Questionnaire is invalid, element contains multiple extensions with URL 'questionnaire-dereference', maximum one may be contained in a single element");
}
return;
/*
* Hopefully we will implement this soon...
*/
// Extension ext = extensions.get(0);
// Reference ref = (Reference) ext.getValue();
// DataElement de = myWorkerCtx.getDataElements().get(ref.getReference());
// if (de.getElement().size() != 1) {
// warning(theErrors, IssueType.BUSINESSRULE, EMPTY_PATH, false, "DataElement {0} has wrong number of elements: {1}", ref.getReference(), de.getElement().size());
// }
// ElementDefinition element = de.getElement().get(0);
// question = toQuestion(element);
} else {
if (question.getGroup().isEmpty()) {
rule(theErrors, IssueType.INVALID, thePathStack, false, "Questionnaire is invalid, no type and no groups specified for question with link ID[{0}]", linkId);
return;
}
type = AnswerFormat.NULL;
}
type = AnswerFormat.NULL;
}
List<org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent> answers = findAnswersByLinkId(theAnsGroup.getQuestion(), linkId);
if (answers.size() > 1) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Multiple answers repetitions found with linkId[{0}]", linkId);
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Multiple answers repetitions found with linkId[{0}]", linkId);
}
if (answers.size() == 0) {
if (theValidateRequired) {
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
rule(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
} else {
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !theQuestion.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
hint(theErrors, IssueType.BUSINESSRULE, thePathStack, !question.getRequired(), "Missing answer to required question with linkId[{0}]", linkId);
}
return;
}
@ -222,13 +249,14 @@ public class QuestionnaireAnswersValidator extends BaseValidator {
org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent answerQuestion = answers.get(0);
try {
thePathStack.add("question[" + answers.indexOf(answerQuestion) + "]");
validateQuestionAnswers(theErrors, theQuestion, thePathStack, type, answerQuestion, theAnswers, theValidateRequired);
validateQuestionGroups(theErrors, theQuestion, answerQuestion, thePathStack, theAnswers, theValidateRequired);
validateQuestionAnswers(theErrors, question, thePathStack, type, answerQuestion, theAnswers, theValidateRequired);
validateQuestionGroups(theErrors, question, answerQuestion, thePathStack, theAnswers, theValidateRequired);
} finally {
thePathStack.removeLast();
}
}
private void validateQuestionGroups(List<ValidationMessage> theErrors, QuestionComponent theQuestion, org.hl7.fhir.instance.model.QuestionnaireAnswers.QuestionComponent theAnswerQuestion,
LinkedList<String> thePathSpec, QuestionnaireAnswers theAnswers, boolean theValidateRequired) {
validateGroups(theErrors, theQuestion.getGroup(), theAnswerQuestion.getGroup(), thePathSpec, theAnswers, theValidateRequired);

View File

@ -4,11 +4,13 @@ import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
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.DataElement;
import org.hl7.fhir.instance.model.Questionnaire;
import org.hl7.fhir.instance.model.Questionnaire.AnswerFormat;
import org.hl7.fhir.instance.model.Questionnaire.GroupComponent;
@ -24,6 +26,7 @@ import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.DataFormatException;
public class QuestionnaireAnswersValidatorTest {
private static final FhirContext ourCtx = FhirContext.forDstu2Hl7Org();
@ -56,6 +59,20 @@ public class QuestionnaireAnswersValidatorTest {
assertThat(errors.toString(), containsString("Answer to question with linkId[link0] found of type [StringType] but this is invalid for question of type [boolean]"));
}
@Test
public void testExtensionDereference() throws Exception {
Questionnaire q = ourCtx.newJsonParser().parseResource(Questionnaire.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-q.json")));
QuestionnaireAnswers qa = ourCtx.newXmlParser().parseResource(QuestionnaireAnswers.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-qa.xml")));
DataElement de = ourCtx.newJsonParser().parseResource(DataElement.class, IOUtils.toString(getClass().getResourceAsStream("/dereference-de.json")));
myWorkerCtx.getQuestionnaires().put(qa.getQuestionnaire().getReference(), q);
myWorkerCtx.getDataElements().put("DataElement/4771", de);
List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
myVal.validate(errors, qa);
ourLog.info(errors.toString());
assertEquals(errors.toString(), errors.size(), 0);
}
@Test
public void testGroupWithNoLinkIdInQuestionnaireAnswers() {

View File

@ -0,0 +1,30 @@
{
"resourceType":"DataElement",
"id":"4770",
"meta":{
"versionId":"1",
"lastUpdated":"2015-07-09T03:28:33.831-04:00"
},
"text":{
"status":"generated",
"div":"<div xmlns=\"http://www.w3.org/1999/xhtml\"><!-- Snipped for brevity --></div>"
},
"identifier":{
"value":"Age Question"
},
"version":"1.0",
"name":"QuestionSample",
"status":"active",
"publisher":"EDIFECS",
"element":[
{
"path":"DataElement.question",
"label":"What is your age?",
"type":[
{
"code":"positiveInt"
}
]
}
]
}

View File

@ -0,0 +1,63 @@
{
"resourceType":"Questionnaire",
"id":"4772",
"meta":{
"versionId":"1",
"lastUpdated":"2015-07-09T03:30:29.589-04:00"
},
"identifier":[
{
"value":"My First Questionnaire"
}
],
"date":"2015-07-09T11:32:03+04:00",
"publisher":"Edifecs",
"group":{
"title":"Main Group",
"group":[
{
"text":"Common",
"question":[
{
"linkId":"Link0",
"text":"What is your name?",
"type":"string"
},
{
"linkId":"Link1",
"text":"What is your age?",
"type":"integer"
},
{
"linkId":"Link2",
"text":"Do you smoke?",
"type":"boolean"
},
{
"extension":[
{
"url":"http://hl7.org/fhir/StructureDefinition/questionnaire-deReference",
"valueReference":{
"reference":"DataElement/4770"
}
}
],
"linkId":"Link3"
},
{
"extension":[
{
"url":"http://hl7.org/fhir/StructureDefinition/questionnaire-deReference",
"valueReference":{
"reference":"DataElement/4771"
}
}
],
"linkId":"Link4"
}
]
}
]
}
}

View File

@ -0,0 +1,53 @@
<QuestionnaireAnswers xmlns="http://hl7.org/fhir">
<identifier>
<value value="4772"/>
</identifier>
<questionnaire>
<reference value="Questionnaire/4772"/>
</questionnaire>
<subject>
<reference value="Organization/4769"/>
</subject>
<authored value="2015-07-29T20:39:17+04:00"/>
<group>
<title value="Main Group"/>
<group>
<text value="Common"/>
<question>
<linkId value="Link0"/>
<text value="What is your name?"/>
<answer>
<valueString value="swewe"/>
</answer>
</question>
<question>
<linkId value="Link1"/>
<text value="What is your age?"/>
<answer>
<valueInteger value="1"/>
</answer>
</question>
<question>
<linkId value="Link2"/>
<text value="Do you smoke?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</question>
<question>
<linkId value="Link3"/>
<text value="What is your age?"/>
<answer>
<valueInteger value="23"/>
</answer>
</question>
<question>
<linkId value="Link4"/>
<text value="Do you smoke?"/>
<answer>
<valueBoolean value="false"/>
</answer>
</question>
</group>
</group>
</QuestionnaireAnswers>