Merge branch 'master' of https://github.com/eevaturkka/hapi-fhir into eevaturkka-master

This commit is contained in:
James Agnew 2019-02-05 14:26:41 -05:00
commit 47123196a0
12 changed files with 786 additions and 59 deletions

View File

@ -16,14 +16,14 @@
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId> <artifactId>hapi-fhir-base</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
</dependency> </dependency>
<!-- Server --> <!-- Server -->
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-server</artifactId> <artifactId>hapi-fhir-server</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
@ -35,43 +35,43 @@
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2</artifactId> <artifactId>hapi-fhir-structures-dstu2</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId> <artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2.1</artifactId> <artifactId>hapi-fhir-structures-dstu2.1</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu3</artifactId> <artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId> <artifactId>hapi-fhir-structures-r4</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId> <artifactId>hapi-fhir-validation-resources-dstu2</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>ca.uhn.hapi.fhir</groupId> <groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu3</artifactId> <artifactId>hapi-fhir-validation-resources-dstu3</artifactId>
<version>3.7.0-SNAPSHOT</version> <version>${project.version}</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>

View File

@ -16013,6 +16013,7 @@ public class VersionConvertor_30_40 {
tgt.setType(convertQuestionnaireItemType(src.getType())); tgt.setType(convertQuestionnaireItemType(src.getType()));
for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent t : src.getEnableWhen()) for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent t : src.getEnableWhen())
tgt.addEnableWhen(convertQuestionnaireItemEnableWhenComponent(t)); tgt.addEnableWhen(convertQuestionnaireItemEnableWhenComponent(t));
tgt.setEnableBehavior(EnableWhenBehavior.ANY);
if (src.hasRequired()) if (src.hasRequired())
tgt.setRequired(src.getRequired()); tgt.setRequired(src.getRequired());
if (src.hasRepeats()) if (src.hasRepeats())
@ -16029,6 +16030,9 @@ public class VersionConvertor_30_40 {
tgt.addInitial().setValue(convertType(src.getInitial())); tgt.addInitial().setValue(convertType(src.getInitial()));
for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent t : src.getItem()) for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent t : src.getItem())
tgt.addItem(convertQuestionnaireItemComponent(t)); tgt.addItem(convertQuestionnaireItemComponent(t));
for (org.hl7.fhir.dstu3.model.Extension t : src.getModifierExtension()) {
tgt.addModifierExtension(convertExtension(t));
}
return tgt; return tgt;
} }
@ -16131,8 +16135,10 @@ public class VersionConvertor_30_40 {
tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EXISTS); tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EXISTS);
tgt.setAnswer(convertType(src.getHasAnswerElement())); tgt.setAnswer(convertType(src.getHasAnswerElement()));
} }
else if (src.hasAnswer()) else if (src.hasAnswer()) {
tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EQUAL);
tgt.setAnswer(convertType(src.getAnswer())); tgt.setAnswer(convertType(src.getAnswer()));
}
return tgt; return tgt;
} }

View File

@ -31,6 +31,8 @@ import org.hl7.fhir.r4.utils.INarrativeGenerator;
import org.hl7.fhir.r4.utils.IResourceValidator; import org.hl7.fhir.r4.utils.IResourceValidator;
import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel; import org.hl7.fhir.r4.utils.IResourceValidator.BestPracticeWarningLevel;
import org.hl7.fhir.r4.utils.IResourceValidator.IdStatus; import org.hl7.fhir.r4.utils.IResourceValidator.IdStatus;
import org.hl7.fhir.r4.validation.DefaultEnableWhenEvaluator;
import org.hl7.fhir.r4.validation.IEnableWhenEvaluator;
import org.hl7.fhir.r4.validation.InstanceValidator; import org.hl7.fhir.r4.validation.InstanceValidator;
import org.hl7.fhir.utilities.TranslationServices; import org.hl7.fhir.utilities.TranslationServices;
import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -47,6 +49,7 @@ import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@SuppressWarnings({"PackageAccessibility", "Duplicates"}) @SuppressWarnings({"PackageAccessibility", "Duplicates"})
public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule { public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule {
@ -60,6 +63,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
private IValidationSupport myValidationSupport; private IValidationSupport myValidationSupport;
private boolean noTerminologyChecks = false; private boolean noTerminologyChecks = false;
private volatile WorkerContextWrapper myWrappedWorkerContext; private volatile WorkerContextWrapper myWrappedWorkerContext;
private Function<IWorkerContext, IEnableWhenEvaluator> enableWhenEvaluatorSupplier = ctx -> new DefaultEnableWhenEvaluator();
private boolean errorForUnknownProfiles;
private List<String> extensionDomains = Collections.emptyList(); private List<String> extensionDomains = Collections.emptyList();
/** /**
@ -222,6 +228,14 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
return myAnyExtensionsAllowed; return myAnyExtensionsAllowed;
} }
public boolean isErrorForUnknownProfiles() {
return errorForUnknownProfiles;
}
public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) {
this.errorForUnknownProfiles = errorForUnknownProfiles;
}
/** /**
* If set to {@literal true} (default is true) extensions which are not known to the * If set to {@literal true} (default is true) extensions which are not known to the
* validator (e.g. because they have not been explicitly declared in a profile) will * validator (e.g. because they have not been explicitly declared in a profile) will
@ -238,6 +252,15 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
return noTerminologyChecks; return noTerminologyChecks;
} }
/**
* Sets a customized {@link IEnableWhenEvaluator} which is injected to created InstanceValidators
* @param myEnableWhenEvaluator
*/
public void setEnableWhenEvaluatorSupplier(
Function<IWorkerContext, IEnableWhenEvaluator> enableWhenEvaluatorSupplier) {
this.enableWhenEvaluatorSupplier = enableWhenEvaluatorSupplier;
}
/** /**
* If set to {@literal true} (default is false) the valueSet will not be validate * If set to {@literal true} (default is false) the valueSet will not be validate
*/ */
@ -270,7 +293,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid
v.setAnyExtensionsAllowed(isAnyExtensionsAllowed()); v.setAnyExtensionsAllowed(isAnyExtensionsAllowed());
v.setResourceIdRule(IdStatus.OPTIONAL); v.setResourceIdRule(IdStatus.OPTIONAL);
v.setNoTerminologyChecks(isNoTerminologyChecks()); v.setNoTerminologyChecks(isNoTerminologyChecks());
v.getExtensionDomains().addAll(extensionDomains); v.setMyEnableWhenEvaluator(enableWhenEvaluatorSupplier.apply(wrappedWorkerContext));
v.setErrorForUnknownProfiles(isErrorForUnknownProfiles());
v.addExtensionDomains(extensionDomains);
List<ValidationMessage> messages = new ArrayList<>(); List<ValidationMessage> messages = new ArrayList<>();

View File

@ -2539,6 +2539,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress);
else else
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId());
} }
} }

View File

@ -390,3 +390,4 @@ public class QuestionnaireResponseValidator extends BaseValidator {
return allowedAnswerTypes; return allowedAnswerTypes;
} }
} }

View File

@ -0,0 +1,48 @@
package org.hl7.fhir.instance.validation;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemEnableWhenComponent;
public class EnableWhenResult {
private final boolean enabled;
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;
private final Element answerItem;
private final String linkId;
/**
* Evaluation result of enableWhen condition
*
* @param enabled
* Evaluation result
* @param linkId
* LinkId of the questionnaire item
* @param enableWhenCondition
* Evaluated enableWhen condition
* @param answerItem
* item in QuestionnaireResponse
*/
public EnableWhenResult(boolean enabled, String linkId, QuestionnaireItemEnableWhenComponent enableWhenCondition,
Element answerItem) {
this.enabled = enabled;
this.linkId = linkId;
this.answerItem = answerItem;
this.enableWhenCondition = enableWhenCondition;
}
public boolean isEnabled() {
return enabled;
}
public String getLinkId() {
return linkId;
}
public Element getAnswerItem() {
return answerItem;
}
public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() {
return enableWhenCondition;
}
}

View File

@ -0,0 +1,216 @@
package org.hl7.fhir.r4.validation;
import java.util.*;
import java.util.stream.*;
import org.hl7.fhir.exceptions.FHIRException;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.model.*;
import org.hl7.fhir.r4.model.Questionnaire.*;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
/**
* Evaluates Questionnaire.item.enableWhen against a QuestionnaireResponse.
* Ignores possible modifierExtensions and extensions.
*
*/
public class DefaultEnableWhenEvaluator implements IEnableWhenEvaluator {
public static final String LINKID_ELEMENT = "linkId";
public static final String ITEM_ELEMENT = "item";
public static final String ANSWER_ELEMENT = "answer";
@Override
public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem, Element questionnaireResponse) {
if (!questionnaireItem.hasEnableWhen()) {
return true;
}
List<EnableWhenResult> evaluationResults = questionnaireItem.getEnableWhen()
.stream()
.map(enableCondition -> evaluateCondition(enableCondition, questionnaireResponse,
questionnaireItem.getLinkId()))
.collect(Collectors.toList());
return checkConditionResults(evaluationResults, questionnaireItem);
}
public boolean checkConditionResults(List<EnableWhenResult> evaluationResults,
QuestionnaireItemComponent questionnaireItem) {
if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY){
return evaluationResults.stream().anyMatch(EnableWhenResult::isEnabled);
} if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ALL){
return evaluationResults.stream().allMatch(EnableWhenResult::isEnabled);
}
//TODO: Throw exception? enableBehavior is mandatory when there are multiple conditions
return true;
}
protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition,
Element questionnaireResponse, String linkId) {
//TODO: Fix EnableWhenResult stuff
List<Element> answerItems = findQuestionAnswers(questionnaireResponse,
enableCondition.getQuestion());
QuestionnaireItemOperator operator = enableCondition.getOperator();
if (operator == QuestionnaireItemOperator.EXISTS){
Type answer = enableCondition.getAnswer();
if (!(answer instanceof BooleanType)){
throw new UnprocessableEntityException("Exists-operator requires answerBoolean");
}
return new EnableWhenResult(((BooleanType)answer).booleanValue() != answerItems.isEmpty(),
linkId, enableCondition, questionnaireResponse);
}
boolean result = answerItems
.stream()
.anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()));
return new EnableWhenResult(result, linkId, enableCondition, questionnaireResponse);
}
public Type convertToType(Element element) {
Type b = new Factory().create(element.fhirType());
if (b instanceof PrimitiveType) {
((PrimitiveType<?>) b).setValueAsString(element.primitiveValue());
} else {
for (Element child : element.getChildren()) {
if (!isExtension(child)) {
b.setProperty(child.getName(), convertToType(child));
}
}
}
return b;
}
private boolean isExtension(Element element) {
return "Extension".equals(element.fhirType());
}
protected boolean evaluateAnswer(Element answer, Type expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
Type actualAnswer;
if (isExtension(answer)) {
return false;
}
try {
actualAnswer = convertToType(answer);
} catch (FHIRException e) {
throw new UnprocessableEntityException("Unexpected answer type", e);
}
if (!actualAnswer.getClass().equals(expectedAnswer.getClass())) {
throw new UnprocessableEntityException("Expected answer and actual answer have incompatible types");
}
if (expectedAnswer instanceof Coding) {
return compareCodingAnswer((Coding)expectedAnswer, (Coding)actualAnswer, questionnaireItemOperator);
} else if ((expectedAnswer instanceof PrimitiveType)) {
return comparePrimitiveAnswer((PrimitiveType<?>)actualAnswer, (PrimitiveType<?>)expectedAnswer, questionnaireItemOperator);
} else if (expectedAnswer instanceof Quantity) {
return compareQuantityAnswer((Quantity)actualAnswer, (Quantity)expectedAnswer, questionnaireItemOperator);
}
// TODO: Attachment, reference?
throw new UnprocessableEntityException("Unimplemented answer type: " + expectedAnswer.getClass());
}
private boolean compareQuantityAnswer(Quantity actualAnswer, Quantity expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
return compareComparable(actualAnswer.getValue(), expectedAnswer.getValue(), questionnaireItemOperator);
}
private boolean comparePrimitiveAnswer(PrimitiveType<?> actualAnswer, PrimitiveType<?> expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
if (actualAnswer.getValue() instanceof Comparable){
return compareComparable((Comparable)actualAnswer.getValue(), (Comparable) expectedAnswer.getValue(), questionnaireItemOperator);
} else if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
return actualAnswer.equalsShallow(expectedAnswer);
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
return !actualAnswer.equalsShallow(expectedAnswer);
}
throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private boolean compareComparable(Comparable actual, Comparable expected,
QuestionnaireItemOperator questionnaireItemOperator) {
int result = actual.compareTo(expected);
if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
return result == 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
return result != 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_OR_EQUAL){
return result >= 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_OR_EQUAL){
return result <= 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_THAN){
return result < 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_THAN){
return result > 0;
}
throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison");
}
private List<Element> findQuestionAnswers(Element questionnaireResponse, String question) {
List<Element> matchingItems = questionnaireResponse.getChildren(ITEM_ELEMENT)
.stream()
.flatMap(i -> findSubItems(i).stream())
.filter(i -> hasLinkId(i, question))
.collect(Collectors.toList());
return matchingItems
.stream()
.flatMap(e -> extractAnswer(e).stream())
.collect(Collectors.toList());
}
private List<Element> extractAnswer(Element item) {
return item.getChildrenByName(ANSWER_ELEMENT)
.stream()
.flatMap(c -> c.getChildren().stream())
.collect(Collectors.toList());
}
private boolean compareCodingAnswer(Coding expectedAnswer, Coding actualAnswer, QuestionnaireItemOperator questionnaireItemOperator) {
boolean result = compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer);
if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
return result == true;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
return result == false;
}
throw new UnprocessableEntityException("Bad operator for Coding comparison");
}
private boolean compareCodes(Coding expectedCoding, Coding value) {
if (expectedCoding.hasCode() != value.hasCode()) {
return false;
}
if (expectedCoding.hasCode()) {
return expectedCoding.getCode().equals(value.getCode());
}
return true;
}
private boolean compareSystems(Coding expectedCoding, Coding value) {
if (expectedCoding.hasSystem() && !value.hasSystem()) {
return false;
}
if (expectedCoding.hasSystem()) {
return expectedCoding.getSystem().equals(value.getSystem());
}
return true;
}
private List<Element> findSubItems(Element item) {
List<Element> results = item.getChildren(LINKID_ELEMENT)
.stream()
.flatMap(i -> findSubItems(i).stream())
.collect(Collectors.toList());
results.add(item);
return results;
}
private boolean hasLinkId(Element item, String linkId) {
Element linkIdChild = item.getNamedChild(LINKID_ELEMENT);
if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){
return true;
}
return false;
}
}

View File

@ -0,0 +1,48 @@
package org.hl7.fhir.r4.validation;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemEnableWhenComponent;
public class EnableWhenResult {
private final boolean enabled;
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;
private final Element answerItem;
private final String linkId;
/**
* Evaluation result of enableWhen condition
*
* @param enabled
* Evaluation result
* @param linkId
* LinkId of the questionnaire item
* @param enableWhenCondition
* Evaluated enableWhen condition
* @param responseItem
* item in QuestionnaireResponse
*/
public EnableWhenResult(boolean enabled, String linkId, QuestionnaireItemEnableWhenComponent enableWhenCondition,
Element answerItem) {
this.enabled = enabled;
this.linkId = linkId;
this.answerItem = answerItem;
this.enableWhenCondition = enableWhenCondition;
}
public boolean isEnabled() {
return enabled;
}
public String getLinkId() {
return linkId;
}
public Element getAnswerItem() {
return answerItem;
}
public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() {
return enableWhenCondition;
}
}

View File

@ -0,0 +1,10 @@
package org.hl7.fhir.r4.validation;
import org.hl7.fhir.r4.elementmodel.Element;
import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent;
public interface IEnableWhenEvaluator {
public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem,
Element questionnaireResponse);
}

View File

@ -15,6 +15,8 @@ import java.util.UUID;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.exceptions.*;
import org.hl7.fhir.convertors.VersionConvertorConstants; import org.hl7.fhir.convertors.VersionConvertorConstants;
import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.DefinitionException;
import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.FHIRException;
@ -255,6 +257,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean noExtensibleWarnings; private boolean noExtensibleWarnings;
private String serverBase; private String serverBase;
private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator();
/* /*
* Keeps track of whether a particular profile has been checked or not yet * Keeps track of whether a particular profile has been checked or not yet
*/ */
@ -2465,6 +2469,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
this.allowXsiLocation = allowXsiLocation; this.allowXsiLocation = allowXsiLocation;
} }
public void setMyEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) {
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
}
/** /**
* *
* @param element * @param element
@ -2734,21 +2742,21 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
sdTime = sdTime + (System.nanoTime() - t); sdTime = sdTime + (System.nanoTime() - t);
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire \""+questionnaire+"\" could not be resolved, so no validation can be performed against the base questionnaire")) { if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire \""+questionnaire+"\" could not be resolved, so no validation can be performed against the base questionnaire")) {
boolean inProgress = "in-progress".equals(element.getNamedChildValue("status")); boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress); validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, element);
} }
} }
} }
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) { private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
String text = element.getNamedChildValue("text"); 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()); 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<Element> answers = new ArrayList<Element>(); List<Element> answers = new ArrayList<Element>();
element.getNamedChildren("answer", answers); element.getNamedChildren("answer", answers);
if (inProgress) if (inProgress)
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
else else
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
if (answers.size() > 1) 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"); 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");
@ -2823,7 +2831,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// no validation // no validation
break; break;
} }
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress); validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot);
} }
if (qItem.getType() == null) { if (qItem.getType() == null) {
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type"); fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type");
@ -2832,16 +2840,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
element.getNamedChildren("item", items); element.getNamedChildren("item", items);
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId()); rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId());
} else { } else {
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress); validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot);
} }
} }
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress) { private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List<Element> answers) {
return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP;
}
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
if (elements.size() > 1) 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 - " + qItem.getLinkId()); rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed - " + qItem.getLinkId());
for (Element element : elements) { for (Element element : elements) {
NodeStack ns = stack.push(element, -1, null, null); NodeStack ns = stack.push(element, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot);
} }
} }
@ -2853,7 +2865,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return -1; return -1;
} }
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress) { private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
List<Element> items = new ArrayList<Element>(); List<Element> items = new ArrayList<Element>();
element.getNamedChildren("item", items); element.getNamedChildren("item", items);
// now, sort into stacks // now, sort into stacks
@ -2866,9 +2878,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
if (index == -1) { if (index == -1) {
QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId);
if (qItem != null) { if (qItem != null) {
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place"); rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
NodeStack ns = stack.push(item, -1, null, null); NodeStack ns = stack.push(item, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot);
} }
else else
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire"); rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
@ -2877,11 +2889,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
{ {
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order"); rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order");
lastIndex = index; lastIndex = index;
List<Element> mapItem = map.get(linkId);
if (mapItem == null) { List<Element> mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>());
mapItem = new ArrayList<Element>();
map.put(linkId, mapItem);
}
mapItem.add(item); mapItem.add(item);
} }
} }
@ -2890,12 +2900,24 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order // 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) { for (QuestionnaireItemComponent qItem : qItems) {
List<Element> mapItem = map.get(qItem.getLinkId()); List<Element> mapItem = map.get(qItem.getLinkId());
if (mapItem != null) if (mapItem != null){
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot), "Item has answer, even though it is not enabled "+qItem.getLinkId());
else validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot);
} else {
//item is missing, is the question enabled?
if (myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot)) {
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId());
} }
} }
}
}
private String misplacedItemError(QuestionnaireItemComponent qItem) {
return qItem.hasLinkId() ?
String.format("Structural Error: item with linkid %s is in the wrong place", qItem.getLinkId())
:
"Structural Error: item is in the wrong place";
}
private void validateQuestionnaireResponseItemQuantity( List<ValidationMessage> errors, Element answer, NodeStack stack) { private void validateQuestionnaireResponseItemQuantity( List<ValidationMessage> errors, Element answer, NodeStack stack) {

View File

@ -0,0 +1,47 @@
package org.hl7.fhir.dstu3.hapi.validation;
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent;
public class EnableWhenResult {
private final boolean enabled;
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;
private final QuestionnaireResponseItemComponent responseItem;
private final String linkId;
/**
* Evaluation result of enableWhen condition
*
* @param enabled
* Evaluation result
* @param linkId
* LinkId of the questionnaire item
* @param enableWhenCondition
* Evaluated enableWhen condition
* @param responseItem
* item in QuestionnaireResponse
*/
public EnableWhenResult(boolean enabled, String linkId, QuestionnaireItemEnableWhenComponent enableWhenCondition,
QuestionnaireResponseItemComponent responseItem) {
this.enabled = enabled;
this.linkId = linkId;
this.responseItem = responseItem;
this.enableWhenCondition = enableWhenCondition;
}
public boolean isEnabled() {
return enabled;
}
public String getLinkId() {
return linkId;
}
public QuestionnaireResponseItemComponent getResponseItem() {
return responseItem;
}
public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() {
return enableWhenCondition;
}
}

View File

@ -7,35 +7,37 @@ import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.ResultSeverityEnum;
import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.SingleValidationMessage;
import ca.uhn.fhir.validation.ValidationResult; import ca.uhn.fhir.validation.ValidationResult;
import org.apache.commons.io.IOUtils;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
import org.hl7.fhir.dstu3.context.IWorkerContext;
import org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport.CodeValidationResult; import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport.CodeValidationResult;
import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode; import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode;
import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent;
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent;
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent;
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemOptionComponent; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemOptionComponent;
import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent;
import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Before; import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.BOOLEAN;
import static org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType.CHOICE;
import static org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus.COMPLETED;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -43,23 +45,26 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
public class QuestionnaireResponseValidatorDstu3Test { public class QuestionnaireResponseValidatorDstu3Test {
private static final String QUESTIONNAIRE_URL = "http://example.com/Questionnaire/q1";
public static final IdType ID_ICC_QUESTIONNAIRE_SETUP = new IdType("Questionnaire/profile"); public static final IdType ID_ICC_QUESTIONNAIRE_SETUP = new IdType("Questionnaire/profile");
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorDstu3Test.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(QuestionnaireResponseValidatorDstu3Test.class);
private static final String CODE_ICC_SCHOOLTYPE_PT = "PT"; private static final String CODE_ICC_SCHOOLTYPE_PT = "PT";
private static final IdType ID_VS_SCHOOLTYPE = new IdType("ValueSet/schooltype"); private static final IdType ID_VS_SCHOOLTYPE = new IdType("ValueSet/schooltype");
private static final String SYSTEMURI_ICC_SCHOOLTYPE = "http://ehealthinnovation/icc/ns/schooltype"; private static final String SYSTEMURI_ICC_SCHOOLTYPE = "http://ehealthinnovation/icc/ns/schooltype";
private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport();
private static FhirContext ourCtx = FhirContext.forDstu3(); private static FhirContext ourCtx;
private FhirInstanceValidator myInstanceVal; private FhirInstanceValidator myInstanceVal;
private FhirValidator myVal; private FhirValidator myVal;
private IValidationSupport myValSupport; private IValidationSupport myValSupport;
private IWorkerContext myWorkerCtx;
@BeforeClass
public static void beforeClass() {
ourCtx = FhirContext.forDstu3();
}
@Before @Before
public void before() { public void before() {
myValSupport = mock(IValidationSupport.class); myValSupport = mock(IValidationSupport.class);
// new DefaultProfileValidationSupport();
myWorkerCtx = new HapiWorkerContext(ourCtx, myValSupport);
myVal = ourCtx.newValidator(); myVal = ourCtx.newValidator();
myVal.setValidateAgainstStandardSchema(false); myVal.setValidateAgainstStandardSchema(false);
@ -128,7 +133,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
answerValues[11] = new Coding().setSystem("http://codesystems.com/system").setCode("code0"); answerValues[11] = new Coding().setSystem("http://codesystems.com/system").setCode("code0");
answerValues[12] = new StringType("some value"); answerValues[12] = new StringType("some value");
answerValues[13] = new Attachment().setData("some data".getBytes()).setContentType("txt"); answerValues[13] = new Attachment().setData("some data".getBytes()).setContentType("txt");
answerValues[14] = new Reference("http://example.com/Questionnaire/q1"); answerValues[14] = new Reference(QUESTIONNAIRE_URL);
answerValues[15] = new Quantity(42); answerValues[15] = new Quantity(42);
for (int i = 0; i < itemCnt; i++) { for (int i = 0; i < itemCnt; i++) {
@ -138,7 +143,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
reset(myValSupport); reset(myValSupport);
Questionnaire q = new Questionnaire(); Questionnaire q = new Questionnaire();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class),
eq("http://example.com/Questionnaire/q1"))).thenReturn(q); eq(QUESTIONNAIRE_URL))).thenReturn(q);
when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem); when(myValSupport.fetchCodeSystem(any(FhirContext.class), eq("http://codesystems.com/system"))).thenReturn(codeSystem);
when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); when(myValSupport.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options);
myInstanceVal.flushCaches(); myInstanceVal.flushCaches();
@ -155,7 +160,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.INPROGRESS); qa.setStatus(QuestionnaireResponseStatus.INPROGRESS);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId(linkId).addAnswer().setValue(answerValues[i]); qa.addItem().setLinkId(linkId).addAnswer().setValue(answerValues[i]);
ValidationResult errors = myVal.validateWithResult(qa); ValidationResult errors = myVal.validateWithResult(qa);
@ -171,7 +176,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);
@ -184,11 +189,11 @@ public class QuestionnaireResponseValidatorDstu3Test {
@Test @Test
public void testCodedAnswer() { public void testCodedAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = QUESTIONNAIRE_URL;
Questionnaire q = new Questionnaire(); Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setOptions(new Reference("http://somevalueset")); q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.CHOICE).setOptions(new Reference("http://somevalueset"));
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq("http://example.com/Questionnaire/q1"))).thenReturn(q); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(QUESTIONNAIRE_URL))).thenReturn(q);
CodeSystem codeSystem = new CodeSystem(); CodeSystem codeSystem = new CodeSystem();
codeSystem.setContent(CodeSystemContentMode.COMPLETE); codeSystem.setContent(CodeSystemContentMode.COMPLETE);
@ -252,7 +257,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
QuestionnaireResponseItemComponent qaGroup = qa.addItem(); QuestionnaireResponseItemComponent qaGroup = qa.addItem();
qaGroup.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); qaGroup.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
@ -263,6 +268,43 @@ public class QuestionnaireResponseValidatorDstu3Test {
assertThat(errors.toString(), containsString("No LinkId, so can't be validated")); assertThat(errors.toString(), containsString("No LinkId, so can't be validated"));
} }
@Test
public void testMissingAnswerInNestedStructureIsReported() throws Exception {
Questionnaire q = new Questionnaire();
q.addItem().setType(QuestionnaireItemType.GROUP).setRequired(true)
.addItem().setType(QuestionnaireItemType.GROUP).setRequired(true)
.addItem().setType(QuestionnaireItemType.BOOLEAN).setLinkId("link0").setRequired(true);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
assertThat(errors.toString(), Matchers.not(containsString("No issues")));
}
@Test
public void testGroupMarkedAsRequiredIsOk() throws Exception {
Questionnaire q = new Questionnaire();
q.addItem().setType(QuestionnaireItemType.GROUP).setRequired(true).setLinkId("link1")
.addItem().setType(QuestionnaireItemType.BOOLEAN).setLinkId("link0").setRequired(true);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link1")
.addItem().setLinkId("link0").addAnswer().setValue(new BooleanType(true));
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
assertThat(errors.toString(), containsString("No issues"));
}
@Test @Test
public void testItemWithNoType() { public void testItemWithNoType() {
Questionnaire q = new Questionnaire(); Questionnaire q = new Questionnaire();
@ -272,7 +314,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
QuestionnaireResponseItemComponent qaGroup = qa.addItem().setLinkId("link0"); QuestionnaireResponseItemComponent qaGroup = qa.addItem().setLinkId("link0");
qaGroup.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); qaGroup.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
@ -293,7 +335,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
String reference = qa.getQuestionnaire().getReference(); String reference = qa.getQuestionnaire().getReference();
@ -304,9 +346,270 @@ public class QuestionnaireResponseValidatorDstu3Test {
assertThat(errors.toString(), containsString("No response found for required item link0")); assertThat(errors.toString(), containsString("No response found for required item link0"));
} }
@Test
public void testEnableWhenWithHasAnswerTrueDisablesQuestionWhenNoAnswerIsPresent() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.STRING);
q.addItem().setLinkId("link1").setRequired(true).addEnableWhen().setQuestion("link0").setHasAnswer(true);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testRequiredQuestionQuantityWithEnableWhenHidesQuestionHasAnswerTrue() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.QUANTITY);
//link1 question is enabled when link0 has answer
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
item1.setLinkId("link1").setRequired(true);
q.addItem(item1);
QuestionnaireItemEnableWhenComponent enable = new QuestionnaireItemEnableWhenComponent();
item1.addEnableWhen(enable);
enable.setQuestion("link0");
enable.setHasAnswer(true);
enable.setAnswer(new Quantity().setValue(1L));
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testRequiredQuestionQuantityWithEnableWhenHidesQuestionValue() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.QUANTITY);
//link1 question is enabled when link0 has answer
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
item1.setLinkId("link1").setRequired(true);
q.addItem(item1);
QuestionnaireItemEnableWhenComponent enable = new QuestionnaireItemEnableWhenComponent();
item1.addEnableWhen(enable);
enable.setQuestion("link0");
enable.setAnswer(new Quantity().setValue(2L));
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link0").addAnswer().setValue(new Quantity().setValue(1L));
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testRequiredQuestionQuantityWithEnableWhenEnablesQuestionValue() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.QUANTITY);
//link1 question is enabled when link0 has answer
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
item1.setLinkId("link1").setRequired(true);
q.addItem(item1);
QuestionnaireItemEnableWhenComponent enable = new QuestionnaireItemEnableWhenComponent();
item1.addEnableWhen(enable);
enable.setQuestion("link0");
enable.setAnswer(new Quantity().setValue(1L));
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link0").addAnswer().setValue(new Quantity().setValue(1L));
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No response found for required item link1"));
}
@Test
public void testRequiredQuestionWithEnableWhenHasAnswerTrueWithAnswer() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.STRING);
// create the questionnaire
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
item1.setLinkId("link1").setRequired(true).setType(QuestionnaireItemType.STRING);
q.addItem(item1);
QuestionnaireItemEnableWhenComponent enable = new QuestionnaireItemEnableWhenComponent();
item1.addEnableWhen(enable);
enable.setQuestion("link0");
enable.setHasAnswer(true);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("BAR"));
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testRequiredQuestionWithEnableWheHidesRequiredQuestionnHasAnswerFalse() {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.STRING);
// create the questionnaire
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
item1.setLinkId("link1").setRequired(true);
q.addItem(item1);
QuestionnaireItemEnableWhenComponent enable = new QuestionnaireItemEnableWhenComponent();
item1.addEnableWhen(enable);
enable.setQuestion("link0");
enable.setHasAnswer(false);
QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
// link1 should be disabled, because the enableWhen enables it when link0 doesn't haven an answer
qa.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO"));
String reference = qa.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qa);
ourLog.info(errors.toString());
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testGivenQuestionIsNotEnabledWithEnableWhenAnswersAreReportedAsErrors() throws Exception {
Questionnaire q = new Questionnaire();
q.addItem().setLinkId("link0").setRequired(false).setType(QuestionnaireItemType.STRING);
q.addItem().setLinkId("link2").setRequired(false).setType(QuestionnaireItemType.STRING).addEnableWhen().setQuestion("link0").setHasAnswer(true);
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setStatus(QuestionnaireResponseStatus.COMPLETED);
qr.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qr.addItem().setLinkId("link2").addAnswer().setValue(new StringType("FOO"));
String reference = qr.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qr);
assertThat(errors.toString(), Matchers.not(containsString("No issues")));
}
@Test
public void testGivenQuestionnaireResponseHasSiblingItemsWhenTheyShouldBeChildItems() throws Exception {
Questionnaire q = new Questionnaire();
QuestionnaireItemComponent item = q.addItem().setLinkId("link0").setRequired(true).setType(QuestionnaireItemType.GROUP);
item.addItem().setLinkId("link1").setRequired(true).setType(QuestionnaireItemType.STRING);
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setStatus(QuestionnaireResponseStatus.COMPLETED);
qr.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qr.addItem().setLinkId("link0").setText("Text");
qr.addItem().setLinkId("link1").addAnswer().setValue(new StringType("Answer"));
String reference = qr.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qr);
assertThat(errors.toString(), Matchers.not(containsString("No issues")));
assertTrue("Must contain structural error about misplaced link1 item",
errors.getMessages().stream().filter(vm -> vm.getMessage().contains("Structural Error"))
.anyMatch(vm -> vm.getMessage().contains("link1")));
}
@Test
public void testAnswerIsValueCodingWithExtensionInside() throws Exception {
Questionnaire q = new Questionnaire();
Coding qcoding = new Coding();
qcoding.setCode("1293");
q.addItem().setLinkId("1B").setRequired(true).setType(CHOICE).addOption().setValue(qcoding);
q.addItem().setLinkId("2B").setType(BOOLEAN).addEnableWhen().setQuestion("1B").setAnswer(qcoding);
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setStatus(COMPLETED);
qr.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
QuestionnaireResponseItemComponent qrItem = qr.addItem().setLinkId("1B");
Coding coding = new Coding();
coding.setCode("1293");
QuestionnaireResponseItemAnswerComponent answer = qrItem.addAnswer();
answer.setValue(coding);
coding.addExtension("http://hl7.org/fhir/StructureDefinition/questionnaire-hidden", new BooleanType(true));
qr.addItem().setLinkId("2B").addAnswer().setValue(new BooleanType(true));
String reference = qr.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qr);
assertThat(errors.toString(), containsString("No issues"));
}
@Test
public void testChoiceItemsEnableWhenHasNoSystemYetAnswerHasSystem() throws Exception {
Questionnaire q = new Questionnaire();
Coding qcoding = new Coding();
qcoding.setCode("male");
qcoding.setSystem("http://hl7.org/fhir/administrative-gender");
q.addItem().setLinkId("1B").setRequired(true).setType(CHOICE).addOption().setValue(qcoding);
Coding enablewhenCoding = new Coding();
enablewhenCoding.setCode("male");
q.addItem().setLinkId("2B").setType(BOOLEAN).addEnableWhen().setQuestion("1B").setAnswer(enablewhenCoding);
QuestionnaireResponse qr = new QuestionnaireResponse();
qr.setStatus(COMPLETED);
qr.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
QuestionnaireResponseItemComponent qrItem = qr.addItem().setLinkId("1B");
Coding coding = new Coding();
coding.setCode("male");
coding.setSystem("http://hl7.org/fhir/administrative-gender");
QuestionnaireResponseItemAnswerComponent answer = qrItem.addAnswer();
answer.setValue(coding);
qr.addItem().setLinkId("2B").addAnswer().setValue(new BooleanType(true));
String reference = qr.getQuestionnaire().getReference();
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))).thenReturn(q);
ValidationResult errors = myVal.validateWithResult(qr);
assertThat(errors.toString(), containsString("No issues"));
}
@Test @Test
public void testEmbeddedItemInChoice() { public void testEmbeddedItemInChoice() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = QUESTIONNAIRE_URL;
String valueSetRef = "http://somevalueset"; String valueSetRef = "http://somevalueset";
String codeSystemUrl = "http://codesystems.com/system"; String codeSystemUrl = "http://codesystems.com/system";
String codeValue = "code0"; String codeValue = "code0";
@ -362,7 +665,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
@Test @Test
public void testEmbeddedItemInOpenChoice() { public void testEmbeddedItemInOpenChoice() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = QUESTIONNAIRE_URL;
String valueSetRef = "http://somevalueset"; String valueSetRef = "http://somevalueset";
String codeSystemUrl = "http://codesystems.com/system"; String codeSystemUrl = "http://codesystems.com/system";
String codeValue = "code0"; String codeValue = "code0";
@ -418,7 +721,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
@Test @Test
public void testEmbeddedItemInString() { public void testEmbeddedItemInString() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = QUESTIONNAIRE_URL;
// create the questionnaire // create the questionnaire
QuestionnaireItemComponent item1 = new QuestionnaireItemComponent(); QuestionnaireItemComponent item1 = new QuestionnaireItemComponent();
@ -519,7 +822,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
.setType(QuestionnaireItemType.STRING) .setType(QuestionnaireItemType.STRING)
.setRequired(true); .setRequired(true);
String reference = "http://example.com/Questionnaire/q1"; String reference = QUESTIONNAIRE_URL;
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))) when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference)))
.thenReturn(q); .thenReturn(q);
@ -543,7 +846,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
@Test @Test
public void testOpenchoiceAnswer() { public void testOpenchoiceAnswer() {
String questionnaireRef = "http://example.com/Questionnaire/q1"; String questionnaireRef = QUESTIONNAIRE_URL;
Questionnaire q = new Questionnaire(); Questionnaire q = new Questionnaire();
QuestionnaireItemComponent item = q.addItem(); QuestionnaireItemComponent item = q.addItem();
@ -666,7 +969,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); qa.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO"));
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);
@ -684,7 +987,7 @@ public class QuestionnaireResponseValidatorDstu3Test {
QuestionnaireResponse qa = new QuestionnaireResponse(); QuestionnaireResponse qa = new QuestionnaireResponse();
qa.setStatus(QuestionnaireResponseStatus.COMPLETED); qa.setStatus(QuestionnaireResponseStatus.COMPLETED);
qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL);
qa.addItem().setLinkId("link1").addItem().setLinkId("link2"); qa.addItem().setLinkId("link1").addItem().setLinkId("link2");
when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);