This commit is contained in:
markiantorno 2020-02-24 15:38:18 -05:00
parent 3a1d621f65
commit 52246a8ce5
2 changed files with 71 additions and 79 deletions

View File

@ -9,9 +9,9 @@ package org.hl7.fhir.validation.instance;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -35,7 +35,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
/**
* Evaluates Questionnaire.item.enableWhen against a QuestionnaireResponse.
* Ignores possible modifierExtensions and extensions.
*
*/
public class EnableWhenEvaluator {
public static final String LINKID_ELEMENT = "linkId";
@ -46,26 +45,29 @@ public class EnableWhenEvaluator {
public static class QuestionnaireAnswerPair {
private QuestionnaireItemComponent q;
private Element a;
public QuestionnaireAnswerPair(QuestionnaireItemComponent q, Element a) {
super();
this.q = q;
this.a = a;
}
public QuestionnaireItemComponent getQ() {
return q;
}
public Element getA() {
return a;
}
}
public static class QStack extends ArrayList<QuestionnaireAnswerPair> {
private static final long serialVersionUID = 1L;
private Questionnaire q;
private Element a;
public QStack(Questionnaire q, Element a) {
super();
this.q = q;
@ -97,40 +99,30 @@ public class EnableWhenEvaluator {
/**
* 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
*
* @param enabled Evaluation result
* @param enableWhenCondition Evaluated enableWhen condition
*/
public EnableWhenResult(boolean enabled, QuestionnaireItemEnableWhenComponent enableWhenCondition) {
this.enabled = enabled;
this.enableWhenCondition = enableWhenCondition;
this.enabled = enabled;
this.enableWhenCondition = enableWhenCondition;
}
public boolean isEnabled() {
return enabled;
return enabled;
}
public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() {
return enableWhenCondition;
return enableWhenCondition;
}
}
}
/**
* the stack contains a set of QR items that represent the tree of the QR being validated, each tagged with the definition of the item from the Q for the QR being validated
*
* the itembeing validated is in the context of the stack. For root items, the stack is empty.
*
* <p>
* the itembeing validated is in the context of the stack. For root items, the stack is empty.
* <p>
* The context Questionnaire and QuestionnaireResponse are always available
*
* @param questionnaireItem
* @param questionnaireResponse
* @param qstack
* @return
*/
public boolean isQuestionEnabled(ValidatorHostContext hostContext, QuestionnaireItemComponent qitem, QStack qstack, FHIRPathEngine engine) {
if (hasExpressionExtension(qitem)) {
@ -138,22 +130,22 @@ public class EnableWhenEvaluator {
ExpressionNode node = engine.parse(expr);
return engine.evaluateToBoolean(hostContext, qstack.a, qstack.a, qstack.a, node);
}
if (!qitem.hasEnableWhen()) {
return true;
}
List<EnableWhenResult> evaluationResults = qitem.getEnableWhen()
.stream()
.map(enableCondition -> evaluateCondition(enableCondition, qitem, qstack))
.collect(Collectors.toList());
.stream()
.map(enableCondition -> evaluateCondition(enableCondition, qitem, qstack))
.collect(Collectors.toList());
return checkConditionResults(evaluationResults, qitem);
}
private boolean hasExpressionExtension(QuestionnaireItemComponent qitem) {
return qitem.hasExtension("http://phr.kanta.fi/StructureDefinition/fiphr-ext-questionnaire-enablewhen") || // finnish extension
qitem.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression"); // sdc extension
qitem.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression"); // sdc extension
}
private String getExpression(QuestionnaireItemComponent qitem) {
@ -164,17 +156,18 @@ public class EnableWhenEvaluator {
if ("text/fhirpath".equals(expr.getLanguage())) {
return expr.getExpression();
} else {
throw new FHIRException("Unsupported language '"+expr.getLanguage()+"' for enableWhen extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression");
throw new FHIRException("Unsupported language '" + expr.getLanguage() + "' for enableWhen extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression");
}
}
throw new Error("How did you get here?");
}
public boolean checkConditionResults(List<EnableWhenResult> evaluationResults, QuestionnaireItemComponent questionnaireItem) {
if ((questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY) || evaluationResults.size() == 1){
public boolean checkConditionResults(List<EnableWhenResult> evaluationResults, QuestionnaireItemComponent questionnaireItem) {
if ((questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY) || evaluationResults.size() == 1) {
return evaluationResults.stream().anyMatch(EnableWhenResult::isEnabled);
} if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ALL){
}
if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ALL) {
return evaluationResults.stream().allMatch(EnableWhenResult::isEnabled);
}
//TODO: Throw exception? enableBehavior is mandatory when there are multiple conditions
@ -183,18 +176,18 @@ public class EnableWhenEvaluator {
protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition, QuestionnaireItemComponent qitem, QStack qstack) {
List<Element> answerItems = findQuestionAnswers(qstack, qitem, enableCondition);
List<Element> answerItems = findQuestionAnswers(qstack, qitem, enableCondition);
QuestionnaireItemOperator operator = enableCondition.getOperator();
if (operator == QuestionnaireItemOperator.EXISTS){
if (operator == QuestionnaireItemOperator.EXISTS) {
DataType answer = enableCondition.getAnswer();
if (!(answer instanceof BooleanType)){
throw new UnprocessableEntityException("Exists-operator requires answerBoolean");
if (!(answer instanceof BooleanType)) {
throw new UnprocessableEntityException("Exists-operator requires answerBoolean");
}
return new EnableWhenResult(((BooleanType)answer).booleanValue() != answerItems.isEmpty(), enableCondition);
}
return new EnableWhenResult(((BooleanType) answer).booleanValue() != answerItems.isEmpty(), enableCondition);
}
boolean result = answerItems
.stream()
.anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()));
.stream()
.anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()));
return new EnableWhenResult(result, enableCondition);
}
@ -235,71 +228,71 @@ public class EnableWhenEvaluator {
}
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);
return compareCodingAnswer((Coding) expectedAnswer, (Coding) actualAnswer, questionnaireItemOperator);
} else if ((expectedAnswer instanceof PrimitiveType)) {
return comparePrimitiveAnswer((PrimitiveType<?>)actualAnswer, (PrimitiveType<?>)expectedAnswer, questionnaireItemOperator);
return comparePrimitiveAnswer((PrimitiveType<?>) actualAnswer, (PrimitiveType<?>) expectedAnswer, questionnaireItemOperator);
} else if (expectedAnswer instanceof Quantity) {
return compareQuantityAnswer((Quantity)actualAnswer, (Quantity)expectedAnswer, questionnaireItemOperator);
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) {
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){
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){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) {
return !actualAnswer.equalsShallow(expectedAnswer);
}
throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@SuppressWarnings({"rawtypes", "unchecked"})
private boolean compareComparable(Comparable actual, Comparable expected,
QuestionnaireItemOperator questionnaireItemOperator) {
QuestionnaireItemOperator questionnaireItemOperator) {
int result = actual.compareTo(expected);
if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){
if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL) {
return result == 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) {
return result != 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_OR_EQUAL){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_OR_EQUAL) {
return result >= 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_OR_EQUAL){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_OR_EQUAL) {
return result <= 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_THAN){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_THAN) {
return result < 0;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_THAN){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_THAN) {
return result > 0;
}
throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison: "+questionnaireItemOperator.toCode());
throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison: " + questionnaireItemOperator.toCode());
}
/**
* Recursively look for answers to questions with the given link id, working upwards given the context
*
* <p>
* For discussion about this, see https://chat.fhir.org/#narrow/stream/179255-questionnaire/topic/enable-when
*
- given sourceQ - question that contains the enableWhen reference and targetQ - question that the enableWhen references in the Q and also sourceA - answer for sourceQ and targetA - answer for targetQ in the QR
- work up from sourceQ until you find the Q group that also contains targetQ - this is groupQ
- work up from sourceA until you find the QR group that matches groupQ - this is groupA
- any targetA in groupA are input for the enableWhen decision
* <p>
* - given sourceQ - question that contains the enableWhen reference and targetQ - question that the enableWhen references in the Q and also sourceA - answer for sourceQ and targetA - answer for targetQ in the QR
* - work up from sourceQ until you find the Q group that also contains targetQ - this is groupQ
* - work up from sourceA until you find the QR group that matches groupQ - this is groupA
* - any targetA in groupA are input for the enableWhen decision
*/
private List<Element> findQuestionAnswers(QStack qstack, QuestionnaireItemComponent sourceQ, QuestionnaireItemEnableWhenComponent ew) {
QuestionnaireItemComponent targetQ = qstack.getQ().getQuestion(ew.getQuestion());
if (targetQ != null) {
if (targetQ != null) {
QuestionnaireItemComponent groupQ = qstack.getQ().getCommonGroup(sourceQ, targetQ);
if (groupQ == null) { // root is Q itself
return findOnItem(qstack.getA(), ew.getQuestion());
@ -341,16 +334,16 @@ public class EnableWhenEvaluator {
private List<Element> extractAnswer(Element item) {
return item.getChildrenByName(ANSWER_ELEMENT)
.stream()
.flatMap(c -> c.getChildren().stream())
.collect(Collectors.toList());
.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){
if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL) {
return result == true;
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){
} else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) {
return result == false;
}
throw new UnprocessableEntityException("Bad operator for Coding comparison");
@ -378,7 +371,7 @@ public class EnableWhenEvaluator {
private boolean hasLinkId(Element item, String linkId) {
Element linkIdChild = item.getNamedChild(LINKID_ELEMENT);
if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){
if (linkIdChild != null && linkIdChild.getValue().equals(linkId)) {
return true;
}
return false;

View File

@ -20,7 +20,6 @@ package org.hl7.fhir.validation.profile;
* #L%
*/
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;