diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index c2795fa0544..c9aa6d46be1 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -16,14 +16,14 @@ ca.uhn.hapi.fhir hapi-fhir-base - 3.7.0-SNAPSHOT + ${project.version} ca.uhn.hapi.fhir hapi-fhir-server - 3.7.0-SNAPSHOT + ${project.version} true @@ -35,43 +35,43 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-structures-dstu2.1 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 3.7.0-SNAPSHOT + ${project.version} true ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 3.7.0-SNAPSHOT + ${project.version} true diff --git a/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java index e97fb121e3c..579acd9a82a 100644 --- a/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java +++ b/hapi-fhir-converter/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java @@ -16013,6 +16013,7 @@ public class VersionConvertor_30_40 { tgt.setType(convertQuestionnaireItemType(src.getType())); for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent t : src.getEnableWhen()) tgt.addEnableWhen(convertQuestionnaireItemEnableWhenComponent(t)); + tgt.setEnableBehavior(EnableWhenBehavior.ANY); if (src.hasRequired()) tgt.setRequired(src.getRequired()); if (src.hasRepeats()) @@ -16029,6 +16030,9 @@ public class VersionConvertor_30_40 { tgt.addInitial().setValue(convertType(src.getInitial())); for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent t : src.getItem()) tgt.addItem(convertQuestionnaireItemComponent(t)); + for (org.hl7.fhir.dstu3.model.Extension t : src.getModifierExtension()) { + tgt.addModifierExtension(convertExtension(t)); + } return tgt; } @@ -16131,8 +16135,10 @@ public class VersionConvertor_30_40 { tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EXISTS); tgt.setAnswer(convertType(src.getHasAnswerElement())); } - else if (src.hasAnswer()) - tgt.setAnswer(convertType(src.getAnswer())); + else if (src.hasAnswer()) { + tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EQUAL); + tgt.setAnswer(convertType(src.getAnswer())); + } return tgt; } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java index 146b9d737fc..725d085dc49 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/hapi/validation/FhirInstanceValidator.java @@ -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.BestPracticeWarningLevel; 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.utilities.TranslationServices; import org.hl7.fhir.utilities.validation.ValidationMessage; @@ -47,6 +49,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.Function; @SuppressWarnings({"PackageAccessibility", "Duplicates"}) public class FhirInstanceValidator extends BaseValidatorBridge implements IValidatorModule { @@ -60,6 +63,9 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid private IValidationSupport myValidationSupport; private boolean noTerminologyChecks = false; private volatile WorkerContextWrapper myWrappedWorkerContext; + private Function enableWhenEvaluatorSupplier = ctx -> new DefaultEnableWhenEvaluator(); + + private boolean errorForUnknownProfiles; private List extensionDomains = Collections.emptyList(); /** @@ -221,6 +227,14 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid public boolean isAnyExtensionsAllowed() { 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 @@ -237,6 +251,15 @@ public class FhirInstanceValidator extends BaseValidatorBridge implements IValid public boolean isNoTerminologyChecks() { return noTerminologyChecks; } + + /** + * Sets a customized {@link IEnableWhenEvaluator} which is injected to created InstanceValidators + * @param myEnableWhenEvaluator + */ + public void setEnableWhenEvaluatorSupplier( + Function enableWhenEvaluatorSupplier) { + this.enableWhenEvaluatorSupplier = enableWhenEvaluatorSupplier; + } /** * 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.setResourceIdRule(IdStatus.OPTIONAL); v.setNoTerminologyChecks(isNoTerminologyChecks()); - v.getExtensionDomains().addAll(extensionDomains); + v.setMyEnableWhenEvaluator(enableWhenEvaluatorSupplier.apply(wrappedWorkerContext)); + v.setErrorForUnknownProfiles(isErrorForUnknownProfiles()); + v.addExtensionDomains(extensionDomains); List messages = new ArrayList<>(); diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java index 36df4e23dfe..8fc9f6a0489 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/InstanceValidator.java @@ -2539,6 +2539,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); else rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); + } } diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java index ef430b4d3ef..1209c78c8a4 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java @@ -390,3 +390,4 @@ public class QuestionnaireResponseValidator extends BaseValidator { return allowedAnswerTypes; } } + diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java new file mode 100644 index 00000000000..84afebe2980 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java @@ -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; + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java new file mode 100644 index 00000000000..d070570c816 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java @@ -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 evaluationResults = questionnaireItem.getEnableWhen() + .stream() + .map(enableCondition -> evaluateCondition(enableCondition, questionnaireResponse, + questionnaireItem.getLinkId())) + .collect(Collectors.toList()); + return checkConditionResults(evaluationResults, questionnaireItem); + } + + + public boolean checkConditionResults(List 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 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 findQuestionAnswers(Element questionnaireResponse, String question) { + List 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 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 findSubItems(Element item) { + List 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; + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/EnableWhenResult.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/EnableWhenResult.java new file mode 100644 index 00000000000..4f6d5d7f7db --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/EnableWhenResult.java @@ -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; + } +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java new file mode 100644 index 00000000000..be567c9d8d4 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java @@ -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); + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java index 9654b1c0d95..1821523b812 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java @@ -15,6 +15,8 @@ import java.util.UUID; import org.apache.commons.lang3.NotImplementedException; 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.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; @@ -254,6 +256,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private IEvaluationContext externalHostServices; private boolean noExtensibleWarnings; private String serverBase; + + private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator(); /* * Keeps track of whether a particular profile has been checked or not yet @@ -2464,6 +2468,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat public void setAllowXsiLocation(boolean allowXsiLocation) { this.allowXsiLocation = allowXsiLocation; } + + public void setMyEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) { + this.myEnableWhenEvaluator = myEnableWhenEvaluator; + } /** * @@ -2734,21 +2742,21 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat 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")) { 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 errors, Element element, NodeStack stack, boolean inProgress) { + private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { String text = element.getNamedChildValue("text"); rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId()); List answers = new ArrayList(); element.getNamedChildren("answer", answers); 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 - 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) 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 break; } - validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress); + validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot); } 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"); @@ -2832,16 +2840,20 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat 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()); } 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 errors, List elements, NodeStack stack, boolean inProgress) { +private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List answers) { + return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP; +} + + private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { 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()); for (Element element : elements) { NodeStack ns = stack.push(element, -1, null, null); - validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); + validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot); } } @@ -2852,8 +2864,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } return -1; } - - private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress) { + + private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { List items = new ArrayList(); element.getNamedChildren("item", items); // now, sort into stacks @@ -2866,9 +2878,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (index == -1) { QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); if (qItem != null) { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place"); + rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)); NodeStack ns = stack.push(item, -1, null, null); - validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); + validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot); } else 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"); lastIndex = index; - List mapItem = map.get(linkId); - if (mapItem == null) { - mapItem = new ArrayList(); - map.put(linkId, mapItem); - } + + List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); + mapItem.add(item); } } @@ -2890,13 +2900,25 @@ 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 for (QuestionnaireItemComponent qItem : qItems) { List mapItem = map.get(qItem.getLinkId()); - if (mapItem != null) - validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); - else - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); + if (mapItem != null){ + 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()); + 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()); + } + } } } +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 errors, Element answer, NodeStack stack) { } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java new file mode 100644 index 00000000000..8568fbe271e --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java @@ -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; + } +} diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java index 32e77320c39..e50b14f40bc 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/QuestionnaireResponseValidatorDstu3Test.java @@ -7,35 +7,37 @@ import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ResultSeverityEnum; import ca.uhn.fhir.validation.SingleValidationMessage; import ca.uhn.fhir.validation.ValidationResult; -import org.apache.commons.io.IOUtils; + 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.CodeValidationResult; import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.CodeSystem.CodeSystemContentMode; import org.hl7.fhir.dstu3.model.CodeSystem.ConceptDefinitionComponent; 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.QuestionnaireItemType; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseStatus; import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; -import java.util.stream.Collectors; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; 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.mockito.ArgumentMatchers.contains; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; @@ -43,23 +45,26 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; 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"); 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 IdType ID_VS_SCHOOLTYPE = new IdType("ValueSet/schooltype"); private static final String SYSTEMURI_ICC_SCHOOLTYPE = "http://ehealthinnovation/icc/ns/schooltype"; private static DefaultProfileValidationSupport myDefaultValidationSupport = new DefaultProfileValidationSupport(); - private static FhirContext ourCtx = FhirContext.forDstu3(); + private static FhirContext ourCtx; private FhirInstanceValidator myInstanceVal; private FhirValidator myVal; private IValidationSupport myValSupport; - private IWorkerContext myWorkerCtx; - + + @BeforeClass + public static void beforeClass() { + ourCtx = FhirContext.forDstu3(); + } + @Before public void before() { myValSupport = mock(IValidationSupport.class); - // new DefaultProfileValidationSupport(); - myWorkerCtx = new HapiWorkerContext(ourCtx, myValSupport); myVal = ourCtx.newValidator(); myVal.setValidateAgainstStandardSchema(false); @@ -128,7 +133,7 @@ public class QuestionnaireResponseValidatorDstu3Test { answerValues[11] = new Coding().setSystem("http://codesystems.com/system").setCode("code0"); answerValues[12] = new StringType("some value"); 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); for (int i = 0; i < itemCnt; i++) { @@ -138,7 +143,7 @@ public class QuestionnaireResponseValidatorDstu3Test { reset(myValSupport); Questionnaire q = new Questionnaire(); 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.fetchResource(any(FhirContext.class), eq(ValueSet.class), eq("http://somevalueset"))).thenReturn(options); myInstanceVal.flushCaches(); @@ -155,7 +160,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); 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]); ValidationResult errors = myVal.validateWithResult(qa); @@ -171,7 +176,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); 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")); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q); @@ -184,11 +189,11 @@ public class QuestionnaireResponseValidatorDstu3Test { @Test public void testCodedAnswer() { - String questionnaireRef = "http://example.com/Questionnaire/q1"; + String questionnaireRef = QUESTIONNAIRE_URL; Questionnaire q = new Questionnaire(); 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.setContent(CodeSystemContentMode.COMPLETE); @@ -252,7 +257,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); - qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL); QuestionnaireResponseItemComponent qaGroup = qa.addItem(); qaGroup.addItem().setLinkId("link0").addAnswer().setValue(new StringType("FOO")); @@ -262,6 +267,43 @@ public class QuestionnaireResponseValidatorDstu3Test { ourLog.info(errors.toString()); 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 public void testItemWithNoType() { @@ -272,7 +314,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); - qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL); QuestionnaireResponseItemComponent qaGroup = qa.addItem().setLinkId("link0"); qaGroup.addItem().setLinkId("link1").addAnswer().setValue(new StringType("FOO")); @@ -293,7 +335,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); 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")); String reference = qa.getQuestionnaire().getReference(); @@ -303,10 +345,271 @@ public class QuestionnaireResponseValidatorDstu3Test { ourLog.info(errors.toString()); 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 public void testEmbeddedItemInChoice() { - String questionnaireRef = "http://example.com/Questionnaire/q1"; + String questionnaireRef = QUESTIONNAIRE_URL; String valueSetRef = "http://somevalueset"; String codeSystemUrl = "http://codesystems.com/system"; String codeValue = "code0"; @@ -362,7 +665,7 @@ public class QuestionnaireResponseValidatorDstu3Test { @Test public void testEmbeddedItemInOpenChoice() { - String questionnaireRef = "http://example.com/Questionnaire/q1"; + String questionnaireRef = QUESTIONNAIRE_URL; String valueSetRef = "http://somevalueset"; String codeSystemUrl = "http://codesystems.com/system"; String codeValue = "code0"; @@ -418,7 +721,7 @@ public class QuestionnaireResponseValidatorDstu3Test { @Test public void testEmbeddedItemInString() { - String questionnaireRef = "http://example.com/Questionnaire/q1"; + String questionnaireRef = QUESTIONNAIRE_URL; // create the questionnaire QuestionnaireItemComponent item1 = new QuestionnaireItemComponent(); @@ -519,7 +822,7 @@ public class QuestionnaireResponseValidatorDstu3Test { .setType(QuestionnaireItemType.STRING) .setRequired(true); - String reference = "http://example.com/Questionnaire/q1"; + String reference = QUESTIONNAIRE_URL; when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(reference))) .thenReturn(q); @@ -543,7 +846,7 @@ public class QuestionnaireResponseValidatorDstu3Test { @Test public void testOpenchoiceAnswer() { - String questionnaireRef = "http://example.com/Questionnaire/q1"; + String questionnaireRef = QUESTIONNAIRE_URL; Questionnaire q = new Questionnaire(); QuestionnaireItemComponent item = q.addItem(); @@ -666,7 +969,7 @@ public class QuestionnaireResponseValidatorDstu3Test { QuestionnaireResponse qa = new QuestionnaireResponse(); 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")); 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(); qa.setStatus(QuestionnaireResponseStatus.COMPLETED); - qa.getQuestionnaire().setReference("http://example.com/Questionnaire/q1"); + qa.getQuestionnaire().setReference(QUESTIONNAIRE_URL); qa.addItem().setLinkId("link1").addItem().setLinkId("link2"); when(myValSupport.fetchResource(any(FhirContext.class), eq(Questionnaire.class), eq(qa.getQuestionnaire().getReference()))).thenReturn(q);