fix validation problems with questionnaires, and allow tests to still run even when offline
This commit is contained in:
parent
da434e310b
commit
58da7557d8
|
@ -10180,6 +10180,10 @@ The primary difference between a medication statement and a medication administr
|
|||
public String toCode(int len) {
|
||||
return toCode().substring(0, len);
|
||||
}
|
||||
|
||||
public static boolean isR4Plus(String version) {
|
||||
return version != null && (version.startsWith("4.") || version.startsWith("5."));
|
||||
}
|
||||
}
|
||||
|
||||
public static class FHIRVersionEnumFactory implements EnumFactory<FHIRVersion> {
|
||||
|
|
|
@ -1970,10 +1970,46 @@ public class Questionnaire extends MetadataResource {
|
|||
, maxLength, answerValueSet, answerOption, initial, item);
|
||||
}
|
||||
|
||||
public String fhirType() {
|
||||
return "Questionnaire.item";
|
||||
public String fhirType() {
|
||||
return "Questionnaire.item";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public QuestionnaireItemComponent getQuestion(String linkId) {
|
||||
if (linkId == null)
|
||||
return null;
|
||||
for (QuestionnaireItemComponent i : getItem()) {
|
||||
if (i.getLinkId().equals(linkId))
|
||||
return i;
|
||||
QuestionnaireItemComponent t = i.getQuestion(linkId);
|
||||
if (t != null)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public QuestionnaireItemComponent getCommonGroup(QuestionnaireItemComponent q1, QuestionnaireItemComponent q2) {
|
||||
if (q1 == null || q2 == null)
|
||||
return null;
|
||||
for (QuestionnaireItemComponent i : getItem()) {
|
||||
QuestionnaireItemComponent t = i.getCommonGroup(q1, q2);
|
||||
if (t != null)
|
||||
return t;
|
||||
}
|
||||
if (containsQuestion(q1) && containsQuestion(q2))
|
||||
return this;
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean containsQuestion(QuestionnaireItemComponent q) {
|
||||
if (q == this)
|
||||
return true;
|
||||
for (QuestionnaireItemComponent i : getItem()) {
|
||||
if (i.containsQuestion(q))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -5222,6 +5258,29 @@ public class Questionnaire extends MetadataResource {
|
|||
*/
|
||||
public static final ca.uhn.fhir.rest.gclient.TokenClientParam STATUS = new ca.uhn.fhir.rest.gclient.TokenClientParam(SP_STATUS);
|
||||
|
||||
|
||||
public QuestionnaireItemComponent getQuestion(String linkId) {
|
||||
if (linkId == null)
|
||||
return null;
|
||||
for (QuestionnaireItemComponent i : getItem()) {
|
||||
if (i.getLinkId().equals(linkId))
|
||||
return i;
|
||||
QuestionnaireItemComponent t = i.getQuestion(linkId);
|
||||
if (t != null)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public QuestionnaireItemComponent getCommonGroup(QuestionnaireItemComponent q1, QuestionnaireItemComponent q2) {
|
||||
for (QuestionnaireItemComponent i : getItem()) {
|
||||
QuestionnaireItemComponent t = i.getCommonGroup(q1, q2);
|
||||
if (t != null)
|
||||
return t;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
package org.hl7.fhir.r5.validation;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.*;
|
||||
import org.hl7.fhir.r5.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) || evaluationResults.size() == 1){
|
||||
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);
|
||||
}
|
||||
|
||||
private Type convertToType(Element element) throws FHIRException {
|
||||
if (element.fhirType().equals("BackboneElement")) {
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
if (actualAnswer == null) {
|
||||
return false;
|
||||
}
|
||||
} 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: "+questionnaireItemOperator.toCode());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively look for answers to questions with the given link id
|
||||
*/
|
||||
private List<Element> findQuestionAnswers(Element questionnaireResponse, String question) {
|
||||
List<Element> retVal = new ArrayList<>();
|
||||
|
||||
List<Element> items = questionnaireResponse.getChildren(ITEM_ELEMENT);
|
||||
for (Element next : items) {
|
||||
if (hasLinkId(next, question)) {
|
||||
List<Element> answers = extractAnswer(next);
|
||||
retVal.addAll(answers);
|
||||
}
|
||||
retVal.addAll(findQuestionAnswers(next, question));
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
|
||||
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 boolean hasLinkId(Element item, String linkId) {
|
||||
Element linkIdChild = item.getNamedChild(LINKID_ELEMENT);
|
||||
if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
package org.hl7.fhir.r5.validation;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
import org.hl7.fhir.exceptions.FHIRException;
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.*;
|
||||
import org.hl7.fhir.r5.model.Questionnaire.*;
|
||||
|
||||
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";
|
||||
public static final String ITEM_ELEMENT = "item";
|
||||
public static final String ANSWER_ELEMENT = "answer";
|
||||
|
||||
|
||||
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;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
|
||||
public Questionnaire getQ() {
|
||||
return q;
|
||||
}
|
||||
|
||||
|
||||
public Element getA() {
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
public QStack push(QuestionnaireItemComponent q, Element a) {
|
||||
QStack self = new QStack(this.q, this.a);
|
||||
self.addAll(this);
|
||||
self.add(new QuestionnaireAnswerPair(q, a));
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EnableWhenResult {
|
||||
private final boolean enabled;
|
||||
private final QuestionnaireItemEnableWhenComponent enableWhenCondition;
|
||||
|
||||
/**
|
||||
* 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, QuestionnaireItemEnableWhenComponent enableWhenCondition) {
|
||||
this.enabled = enabled;
|
||||
this.enableWhenCondition = enableWhenCondition;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() {
|
||||
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.
|
||||
*
|
||||
* The context Questionnaire and QuestionnaireResponse are always available
|
||||
*
|
||||
* @param questionnaireItem
|
||||
* @param questionnaireResponse
|
||||
* @param qstack
|
||||
* @return
|
||||
*/
|
||||
public boolean isQuestionEnabled(QuestionnaireItemComponent qitem, QStack qstack) {
|
||||
if (!qitem.hasEnableWhen()) {
|
||||
return true;
|
||||
}
|
||||
List<EnableWhenResult> evaluationResults = qitem.getEnableWhen()
|
||||
.stream()
|
||||
.map(enableCondition -> evaluateCondition(enableCondition, qitem, qstack))
|
||||
.collect(Collectors.toList());
|
||||
return checkConditionResults(evaluationResults, qitem);
|
||||
}
|
||||
|
||||
|
||||
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){
|
||||
return evaluationResults.stream().allMatch(EnableWhenResult::isEnabled);
|
||||
}
|
||||
//TODO: Throw exception? enableBehavior is mandatory when there are multiple conditions
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition, QuestionnaireItemComponent qitem, QStack qstack) {
|
||||
List<Element> answerItems = findQuestionAnswers(qstack, qitem, enableCondition);
|
||||
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(), enableCondition);
|
||||
}
|
||||
boolean result = answerItems
|
||||
.stream()
|
||||
.anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()));
|
||||
return new EnableWhenResult(result, enableCondition);
|
||||
}
|
||||
|
||||
private Type convertToType(Element element) throws FHIRException {
|
||||
if (element.fhirType().equals("BackboneElement")) {
|
||||
return null;
|
||||
}
|
||||
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);
|
||||
if (actualAnswer == null) {
|
||||
return false;
|
||||
}
|
||||
} 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: "+questionnaireItemOperator.toCode());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively look for answers to questions with the given link id, working upwards given the context
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
private List<Element> findQuestionAnswers(QStack qstack, QuestionnaireItemComponent sourceQ, QuestionnaireItemEnableWhenComponent ew) {
|
||||
QuestionnaireItemComponent targetQ = qstack.getQ().getQuestion(ew.getQuestion());
|
||||
if (targetQ != null) {
|
||||
QuestionnaireItemComponent groupQ = qstack.getQ().getCommonGroup(sourceQ, targetQ);
|
||||
if (groupQ == null) { // root is Q itself
|
||||
return findOnItem(qstack.getA(), ew.getQuestion());
|
||||
} else {
|
||||
for (int i = qstack.size() - 1; i >= 0; i--) {
|
||||
if (qstack.get(i).getQ() == groupQ) {
|
||||
// group A
|
||||
return findOnItem(qstack.get(i).getA(), ew.getQuestion());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private List<Element> findOnItem(Element focus, String question) {
|
||||
List<Element> retVal = new ArrayList<>();
|
||||
List<Element> items = focus.getChildren(ITEM_ELEMENT);
|
||||
for (Element item : items) {
|
||||
if (hasLinkId(item, question)) {
|
||||
List<Element> answers = extractAnswer(item);
|
||||
retVal.addAll(answers);
|
||||
}
|
||||
retVal.addAll(findOnItem(item, question));
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
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 boolean hasLinkId(Element item, String linkId) {
|
||||
Element linkIdChild = item.getNamedChild(LINKID_ELEMENT);
|
||||
if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package org.hl7.fhir.r5.validation;
|
||||
|
||||
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.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;
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package org.hl7.fhir.r5.validation;
|
||||
|
||||
import org.hl7.fhir.r5.elementmodel.Element;
|
||||
import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
|
||||
|
||||
public interface IEnableWhenEvaluator {
|
||||
public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem,
|
||||
Element questionnaireResponse);
|
||||
|
||||
}
|
|
@ -132,6 +132,7 @@ import org.hl7.fhir.r5.utils.ToolingExtensions;
|
|||
import org.hl7.fhir.r5.utils.ValidationProfileSet;
|
||||
import org.hl7.fhir.r5.utils.ValidationProfileSet.ProfileRegistration;
|
||||
import org.hl7.fhir.r5.utils.Version;
|
||||
import org.hl7.fhir.r5.validation.EnableWhenEvaluator.QStack;
|
||||
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
|
||||
import org.hl7.fhir.utilities.Utilities;
|
||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||
|
@ -292,7 +293,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
private boolean noExtensibleWarnings;
|
||||
private String serverBase;
|
||||
|
||||
private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator();
|
||||
private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator();
|
||||
|
||||
/*
|
||||
* Keeps track of whether a particular profile has been checked or not yet
|
||||
|
@ -2620,10 +2621,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
this.allowXsiLocation = allowXsiLocation;
|
||||
}
|
||||
|
||||
public void setEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) {
|
||||
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param element
|
||||
|
@ -2886,6 +2883,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
validateBundle(errors, element, stack);
|
||||
else if (element.getType().equals("Observation"))
|
||||
validateObservation(errors, element, stack);
|
||||
else if (element.getType().equals("Questionnaire"))
|
||||
validateQuestionannaire(errors, element, stack);
|
||||
else if (element.getType().equals("QuestionnaireResponse"))
|
||||
validateQuestionannaireResponse(errors, element, stack);
|
||||
else if (element.getType().equals("CodeSystem"))
|
||||
|
@ -2901,6 +2900,88 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
}
|
||||
}
|
||||
|
||||
private void validateQuestionannaire(List<ValidationMessage> errors, Element element, NodeStack stack) {
|
||||
List<Element> list = getItems(element);
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Element e = list.get(i);
|
||||
NodeStack ns = stack.push(element, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
|
||||
validateQuestionnaireElement(errors, ns, element, e, new ArrayList<>());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents) {
|
||||
// R4+
|
||||
if (FHIRVersion.isR4Plus(context.getVersion())) {
|
||||
if (item.hasChild("enableWhen")) {
|
||||
Element ew = item.getNamedChild("enableWhen");
|
||||
String ql = ew.getNamedChildValue("question");
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, ql != null, "Questions with an enableWhen must have a value for the question link")) {
|
||||
Element tgt = getQuestionById(item, ql);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt == null, "Questions with an enableWhen cannot refer to an inner question for it's enableWhen condition")) {
|
||||
tgt = getQuestionById(questionnaire, ql);
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != null, "Unable to find "+ql+" target for this question enableWhen")) {
|
||||
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != item, "Target for this question enableWhen can't reference itself")) {
|
||||
warning(errors, IssueType.BUSINESSRULE, ns.literalPath, isBefore(item, tgt, parents), "The target of this enableWhen rule ("+ql+") comes after the question itself");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isBefore(Element item, Element tgt, List<Element> parents) {
|
||||
// we work up the list, looking for tgt in the children of the parents
|
||||
for (Element p : parents) {
|
||||
int i = findIndex(p, item);
|
||||
int t = findIndex(p, tgt);
|
||||
if (i > -1 && t > -1) {
|
||||
return i > t;
|
||||
}
|
||||
}
|
||||
return false; // unsure... shouldn't ever get to this point;
|
||||
}
|
||||
|
||||
|
||||
private int findIndex(Element parent, Element descendant) {
|
||||
for (int i = 0; i < parent.getChildren().size(); i++) {
|
||||
if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isChild(Element element, Element descendant) {
|
||||
for (Element e : element.getChildren()) {
|
||||
if (e == descendant)
|
||||
return true;
|
||||
if (isChild(element, descendant))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Element getQuestionById(Element focus, String ql) {
|
||||
List<Element> list = getItems(focus);
|
||||
for (Element item : list) {
|
||||
String v = item.getNamedChildValue("linkId");
|
||||
if (ql.equals(v))
|
||||
return item;
|
||||
Element tgt = getQuestionById(item, ql);
|
||||
if (tgt != null)
|
||||
return tgt;
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private List<Element> getItems(Element element) {
|
||||
List<Element> list = new ArrayList<>();
|
||||
element.getNamedChildren("item", list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private void checkLang(Element resource, NodeStack stack) {
|
||||
String lang = resource.getNamedChildValue("language");
|
||||
if (!Utilities.noString(lang))
|
||||
|
@ -2981,7 +3062,7 @@ 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, element);
|
||||
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3035,7 +3116,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
return null;
|
||||
}
|
||||
|
||||
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
|
||||
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
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());
|
||||
|
||||
|
@ -3043,10 +3124,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
element.getNamedChildren("answer", answers);
|
||||
if (inProgress)
|
||||
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
|
||||
else if (myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot))
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
|
||||
else if (!answers.isEmpty()) // items without answers should be allowed, but not items with answers to questions that are disabled
|
||||
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), "Item has answer, even though it is not enabled "+qItem.getLinkId());
|
||||
else if (myEnableWhenEvaluator.isQuestionEnabled(qItem, qstack)) {
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
|
||||
} else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled
|
||||
// it appears that this is always a duplicate error - it will always already have beeb reported, so no need to report it again?
|
||||
// GDG 2019-07-13
|
||||
// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), "Item has answer (2), even though it is not enabled "+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");
|
||||
|
@ -3122,7 +3206,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
// no validation
|
||||
break;
|
||||
}
|
||||
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot);
|
||||
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
}
|
||||
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");
|
||||
|
@ -3131,7 +3215,7 @@ 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, questionnaireResponseRoot);
|
||||
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3139,12 +3223,14 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
|
|||
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) {
|
||||
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
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());
|
||||
int i = 0;
|
||||
for (Element element : elements) {
|
||||
NodeStack ns = stack.push(element, -1, null, null);
|
||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot);
|
||||
NodeStack ns = stack.push(element, i, null, null);
|
||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3156,7 +3242,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
|
|||
return -1;
|
||||
}
|
||||
|
||||
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
|
||||
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
|
||||
List<Element> items = new ArrayList<Element>();
|
||||
element.getNamedChildren("item", items);
|
||||
// now, sort into stacks
|
||||
|
@ -3171,7 +3257,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
|
|||
if (qItem != null) {
|
||||
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, item, ns, inProgress, questionnaireResponseRoot);
|
||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item));
|
||||
}
|
||||
else
|
||||
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
|
||||
|
@ -3191,20 +3277,26 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
|
|||
// 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<Element> mapItem = map.get(qItem.getLinkId());
|
||||
validateQuestionannaireResponseItem(qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem);
|
||||
validateQuestionannaireResponseItem(qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack);
|
||||
}
|
||||
}
|
||||
|
||||
public void validateQuestionannaireResponseItem(Questionnaire qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<Element> mapItem) {
|
||||
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot);
|
||||
public void validateQuestionannaireResponseItem(Questionnaire qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<Element> mapItem, QStack qstack) {
|
||||
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, qstack);
|
||||
if (mapItem != null){
|
||||
if (!enabled)
|
||||
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), enabled, "Item has answer, even though it is not enabled (item id = '"+qItem.getLinkId()+"')");
|
||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot);
|
||||
if (!enabled) {
|
||||
int i = 0;
|
||||
for (Element e : mapItem) {
|
||||
NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
|
||||
rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, "Item has answer, even though it is not enabled (item id = '"+qItem.getLinkId()+"')");
|
||||
i++;
|
||||
}
|
||||
}
|
||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||
} else {
|
||||
//item is missing, is the question enabled?
|
||||
if (enabled && qItem.getRequired()) {
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "No response found for required item (item id = '"+qItem.getLinkId()+"')");
|
||||
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "No response found for required item with id = '"+qItem.getLinkId()+"'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -313,6 +313,13 @@ public class ValidationEngine {
|
|||
this.anyExtensionsAllowed = anyExtensionsAllowed;
|
||||
}
|
||||
|
||||
public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version, boolean canRunWithoutTerminologyServer) throws Exception {
|
||||
pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
|
||||
loadInitialDefinitions(src);
|
||||
context.setCanRunWithoutTerminology(canRunWithoutTerminologyServer);
|
||||
setTerminologyServer(txsrvr, txLog, version);
|
||||
}
|
||||
|
||||
public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version) throws Exception {
|
||||
pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
|
||||
loadInitialDefinitions(src);
|
||||
|
@ -649,8 +656,16 @@ public class ValidationEngine {
|
|||
context.setTlogging(false);
|
||||
if (url == null) {
|
||||
context.setCanRunWithoutTerminology(true);
|
||||
} else
|
||||
context.connectToTSServer(TerminologyClientFactory.makeClient(url, version), log);
|
||||
} else {
|
||||
try {
|
||||
context.connectToTSServer(TerminologyClientFactory.makeClient(url, version), log);
|
||||
} catch (Exception e) {
|
||||
if (context.isCanRunWithoutTerminology()) {
|
||||
System.out.println("Running without Terminology Server (error: "+e.getMessage()+")");
|
||||
} else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void loadProfile(String src) throws Exception {
|
||||
|
|
|
@ -109,16 +109,15 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
|
|||
|
||||
if (ve == null || !v.equals(veVersion)) {
|
||||
if (v.equals("5.0"))
|
||||
ve = new ValidationEngine("hl7.fhir.core#current", DEF_TX, null, FhirPublication.R5);
|
||||
ve = new ValidationEngine("hl7.fhir.core#current", DEF_TX, null, FhirPublication.R5, true);
|
||||
else if (v.equals("3.0"))
|
||||
ve = new ValidationEngine("hl7.fhir.core#3.0.1", DEF_TX, null, FhirPublication.STU3);
|
||||
ve = new ValidationEngine("hl7.fhir.core#3.0.1", DEF_TX, null, FhirPublication.STU3, true);
|
||||
else if (v.equals("4.0"))
|
||||
ve = new ValidationEngine("hl7.fhir.core#4.0.0", DEF_TX, null, FhirPublication.R4);
|
||||
ve = new ValidationEngine("hl7.fhir.core#4.0.0", DEF_TX, null, FhirPublication.R4, true);
|
||||
else if (v.equals("1.0"))
|
||||
ve = new ValidationEngine("hl7.fhir.core#1.0.2", DEF_TX, null, FhirPublication.DSTU2);
|
||||
ve = new ValidationEngine("hl7.fhir.core#1.0.2", DEF_TX, null, FhirPublication.DSTU2, true);
|
||||
else
|
||||
throw new Exception("unknown version "+v);
|
||||
ve.getContext().setCanRunWithoutTerminology(true);
|
||||
TestingUtilities.fcontext = ve.getContext();
|
||||
veVersion = v;
|
||||
}
|
||||
|
|
|
@ -614,10 +614,22 @@
|
|||
"errorCount": 0
|
||||
}
|
||||
},
|
||||
"questionnaire-enableWhen-test-invalid.xml": {
|
||||
"errorCount": 1,
|
||||
"warningCount" : 0
|
||||
},
|
||||
"questionnaire-enableWhen-test-order.xml": {
|
||||
"errorCount": 0,
|
||||
"warningCount" : 1
|
||||
},
|
||||
"questionnaireResponse-enableWhen-test.xml": {
|
||||
"questionnaire": "questionnaire-enableWhen-test.xml",
|
||||
"errorCount": 0
|
||||
},
|
||||
"questionnaireResponse-enableWhen-test-nested.xml": {
|
||||
"questionnaire": "questionnaire-enableWhen-test-nested.xml",
|
||||
"errorCount": 2
|
||||
},
|
||||
"questionnaireResponse-enableWhen-test3.xml": {
|
||||
"version": "3.0",
|
||||
"questionnaire": "questionnaire-enableWhen-test3.xml",
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Questionnaire xmlns="http://hl7.org/fhir">
|
||||
<id value="questionnaire-enableWhen-test" />
|
||||
<language value="en-US" />
|
||||
<text>
|
||||
<status value="generated" />
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
|
||||
<p>enableWhen test</p>
|
||||
</div>
|
||||
</text>
|
||||
<url value="http://hl7.org/fhir/us/hai/Questionnaire/questionnaire-enableWhen-test" />
|
||||
<name value="TestQuestionnnaire"/>
|
||||
<status value="draft" />
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<text value="group" />
|
||||
<type value="group" />
|
||||
<enableWhen>
|
||||
<question value="condition" />
|
||||
<operator value="="/>
|
||||
<answerBoolean value="true" />
|
||||
</enableWhen>
|
||||
<repeats value="true" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<type value="boolean" />
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<type value="boolean" />
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
</item>
|
||||
</Questionnaire>
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Questionnaire xmlns="http://hl7.org/fhir">
|
||||
<id value="questionnaire-enableWhen-test" />
|
||||
<language value="en-US" />
|
||||
<text>
|
||||
<status value="generated" />
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
|
||||
<p>enableWhen test</p>
|
||||
</div>
|
||||
</text>
|
||||
<url value="http://hl7.org/fhir/us/hai/Questionnaire/questionnaire-enableWhen-test" />
|
||||
<status value="draft" />
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<text value="group" />
|
||||
<type value="group" />
|
||||
<repeats value="true" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<type value="boolean" />
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<type value="boolean" />
|
||||
<enableWhen>
|
||||
<question value="condition" />
|
||||
<operator value="="/>
|
||||
<answerBoolean value="true" />
|
||||
</enableWhen>
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
</item>
|
||||
</Questionnaire>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Questionnaire xmlns="http://hl7.org/fhir">
|
||||
<id value="questionnaire-enableWhen-test" />
|
||||
<language value="en-US" />
|
||||
<text>
|
||||
<status value="generated" />
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
|
||||
<p>enableWhen test</p>
|
||||
</div>
|
||||
</text>
|
||||
<url value="http://hl7.org/fhir/us/hai/Questionnaire/questionnaire-enableWhen-test" />
|
||||
<name value="TestQuestionnnaire"/>
|
||||
<status value="draft" />
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<type value="boolean" />
|
||||
<enableWhen>
|
||||
<question value="condition" />
|
||||
<operator value="="/>
|
||||
<answerBoolean value="true" />
|
||||
</enableWhen>
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<type value="boolean" />
|
||||
<required value="true" />
|
||||
<repeats value="false" />
|
||||
</item>
|
||||
</Questionnaire>
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<QuestionnaireResponse xmlns="http://hl7.org/fhir">
|
||||
<id value="questionnaireResponse-enableWhen-test" />
|
||||
<text>
|
||||
<status value="generated" />
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" lang="en-US">
|
||||
<p>enableWhen test response</p>
|
||||
</div>
|
||||
</text>
|
||||
<questionnaire value="http://hl7.org/fhir/us/hai/Questionnaire/questionnaire-enableWhen-test" />
|
||||
<status value="completed" />
|
||||
<!-- group 0: spacer -->
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<answer>
|
||||
<valueBoolean value="true" />
|
||||
</answer>
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<answer>
|
||||
<valueBoolean value="false" />
|
||||
</answer>
|
||||
</item>
|
||||
</item>
|
||||
<!-- group 1: condition true, present (valid) -->
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<answer>
|
||||
<valueBoolean value="true" />
|
||||
</answer>
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<answer>
|
||||
<valueBoolean value="false" />
|
||||
</answer>
|
||||
</item>
|
||||
</item>
|
||||
<!-- group 2: condition false, present (invalid) -->
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<answer>
|
||||
<valueBoolean value="false" />
|
||||
</answer>
|
||||
</item>
|
||||
<item>
|
||||
<linkId value="enable-when" />
|
||||
<text value="enable when condition true" />
|
||||
<answer>
|
||||
<valueBoolean value="false" />
|
||||
</answer>
|
||||
</item>
|
||||
</item>
|
||||
<!-- group 3: condition true, absent (invalid) -->
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<answer>
|
||||
<valueBoolean value="true" />
|
||||
</answer>
|
||||
</item>
|
||||
</item>
|
||||
<!-- group 4: condition false, absent (valid) -->
|
||||
<item>
|
||||
<linkId value="group" />
|
||||
<item>
|
||||
<linkId value="condition" />
|
||||
<text value="condition" />
|
||||
<answer>
|
||||
<valueBoolean value="false" />
|
||||
</answer>
|
||||
</item>
|
||||
</item>
|
||||
</QuestionnaireResponse>
|
Loading…
Reference in New Issue