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) {
|
public String toCode(int len) {
|
||||||
return toCode().substring(0, 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> {
|
public static class FHIRVersionEnumFactory implements EnumFactory<FHIRVersion> {
|
||||||
|
|
|
@ -1970,10 +1970,46 @@ public class Questionnaire extends MetadataResource {
|
||||||
, maxLength, answerValueSet, answerOption, initial, item);
|
, maxLength, answerValueSet, answerOption, initial, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String fhirType() {
|
public String fhirType() {
|
||||||
return "Questionnaire.item";
|
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 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;
|
||||||
import org.hl7.fhir.r5.utils.ValidationProfileSet.ProfileRegistration;
|
import org.hl7.fhir.r5.utils.ValidationProfileSet.ProfileRegistration;
|
||||||
import org.hl7.fhir.r5.utils.Version;
|
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.CommaSeparatedStringBuilder;
|
||||||
import org.hl7.fhir.utilities.Utilities;
|
import org.hl7.fhir.utilities.Utilities;
|
||||||
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
import org.hl7.fhir.utilities.validation.ValidationMessage;
|
||||||
|
@ -292,7 +293,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
private boolean noExtensibleWarnings;
|
private boolean noExtensibleWarnings;
|
||||||
private String serverBase;
|
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
|
* 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;
|
this.allowXsiLocation = allowXsiLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) {
|
|
||||||
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param element
|
* @param element
|
||||||
|
@ -2886,6 +2883,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
validateBundle(errors, element, stack);
|
validateBundle(errors, element, stack);
|
||||||
else if (element.getType().equals("Observation"))
|
else if (element.getType().equals("Observation"))
|
||||||
validateObservation(errors, element, stack);
|
validateObservation(errors, element, stack);
|
||||||
|
else if (element.getType().equals("Questionnaire"))
|
||||||
|
validateQuestionannaire(errors, element, stack);
|
||||||
else if (element.getType().equals("QuestionnaireResponse"))
|
else if (element.getType().equals("QuestionnaireResponse"))
|
||||||
validateQuestionannaireResponse(errors, element, stack);
|
validateQuestionannaireResponse(errors, element, stack);
|
||||||
else if (element.getType().equals("CodeSystem"))
|
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) {
|
private void checkLang(Element resource, NodeStack stack) {
|
||||||
String lang = resource.getNamedChildValue("language");
|
String lang = resource.getNamedChildValue("language");
|
||||||
if (!Utilities.noString(lang))
|
if (!Utilities.noString(lang))
|
||||||
|
@ -2981,7 +3062,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
sdTime = sdTime + (System.nanoTime() - t);
|
sdTime = sdTime + (System.nanoTime() - t);
|
||||||
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire \""+questionnaire+"\" could not be resolved, so no validation can be performed against the base questionnaire")) {
|
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire \""+questionnaire+"\" could not be resolved, so no validation can be performed against the base questionnaire")) {
|
||||||
boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
|
boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
|
||||||
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, 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;
|
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");
|
String text = element.getNamedChildValue("text");
|
||||||
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId());
|
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId());
|
||||||
|
|
||||||
|
@ -3043,10 +3124,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
element.getNamedChildren("answer", answers);
|
element.getNamedChildren("answer", answers);
|
||||||
if (inProgress)
|
if (inProgress)
|
||||||
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "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 if (myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot))
|
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());
|
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
|
} 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());
|
// 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)
|
if (answers.size() > 1)
|
||||||
rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed");
|
rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed");
|
||||||
|
@ -3122,7 +3206,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
// no validation
|
// no validation
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot);
|
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
|
||||||
}
|
}
|
||||||
if (qItem.getType() == null) {
|
if (qItem.getType() == null) {
|
||||||
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type");
|
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type");
|
||||||
|
@ -3131,7 +3215,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
||||||
element.getNamedChildren("item", items);
|
element.getNamedChildren("item", items);
|
||||||
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId());
|
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId());
|
||||||
} else {
|
} else {
|
||||||
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, 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;
|
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)
|
if (elements.size() > 1)
|
||||||
rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed - " + qItem.getLinkId());
|
rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed - " + qItem.getLinkId());
|
||||||
|
int i = 0;
|
||||||
for (Element element : elements) {
|
for (Element element : elements) {
|
||||||
NodeStack ns = stack.push(element, -1, null, null);
|
NodeStack ns = stack.push(element, i, null, null);
|
||||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot);
|
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;
|
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>();
|
List<Element> items = new ArrayList<Element>();
|
||||||
element.getNamedChildren("item", items);
|
element.getNamedChildren("item", items);
|
||||||
// now, sort into stacks
|
// now, sort into stacks
|
||||||
|
@ -3171,7 +3257,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
|
||||||
if (qItem != null) {
|
if (qItem != null) {
|
||||||
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
|
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
|
||||||
NodeStack ns = stack.push(item, -1, null, null);
|
NodeStack ns = stack.push(item, -1, null, null);
|
||||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot);
|
validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
|
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
|
||||||
|
@ -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
|
// ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order
|
||||||
for (QuestionnaireItemComponent qItem : qItems) {
|
for (QuestionnaireItemComponent qItem : qItems) {
|
||||||
List<Element> mapItem = map.get(qItem.getLinkId());
|
List<Element> mapItem = map.get(qItem.getLinkId());
|
||||||
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) {
|
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, questionnaireResponseRoot);
|
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, qstack);
|
||||||
if (mapItem != null){
|
if (mapItem != null){
|
||||||
if (!enabled)
|
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()+"')");
|
int i = 0;
|
||||||
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot);
|
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 {
|
} else {
|
||||||
//item is missing, is the question enabled?
|
//item is missing, is the question enabled?
|
||||||
if (enabled && qItem.getRequired()) {
|
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;
|
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 {
|
public ValidationEngine(String src, String txsrvr, String txLog, FhirPublication version) throws Exception {
|
||||||
pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
|
pcm = new PackageCacheManager(true, ToolsVersion.TOOLS_VERSION);
|
||||||
loadInitialDefinitions(src);
|
loadInitialDefinitions(src);
|
||||||
|
@ -649,8 +656,16 @@ public class ValidationEngine {
|
||||||
context.setTlogging(false);
|
context.setTlogging(false);
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
context.setCanRunWithoutTerminology(true);
|
context.setCanRunWithoutTerminology(true);
|
||||||
} else
|
} else {
|
||||||
context.connectToTSServer(TerminologyClientFactory.makeClient(url, version), log);
|
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 {
|
public void loadProfile(String src) throws Exception {
|
||||||
|
|
|
@ -109,16 +109,15 @@ public class ValidationTestSuite implements IEvaluationContext, IValidatorResour
|
||||||
|
|
||||||
if (ve == null || !v.equals(veVersion)) {
|
if (ve == null || !v.equals(veVersion)) {
|
||||||
if (v.equals("5.0"))
|
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"))
|
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"))
|
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"))
|
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
|
else
|
||||||
throw new Exception("unknown version "+v);
|
throw new Exception("unknown version "+v);
|
||||||
ve.getContext().setCanRunWithoutTerminology(true);
|
|
||||||
TestingUtilities.fcontext = ve.getContext();
|
TestingUtilities.fcontext = ve.getContext();
|
||||||
veVersion = v;
|
veVersion = v;
|
||||||
}
|
}
|
||||||
|
|
|
@ -614,10 +614,22 @@
|
||||||
"errorCount": 0
|
"errorCount": 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"questionnaire-enableWhen-test-invalid.xml": {
|
||||||
|
"errorCount": 1,
|
||||||
|
"warningCount" : 0
|
||||||
|
},
|
||||||
|
"questionnaire-enableWhen-test-order.xml": {
|
||||||
|
"errorCount": 0,
|
||||||
|
"warningCount" : 1
|
||||||
|
},
|
||||||
"questionnaireResponse-enableWhen-test.xml": {
|
"questionnaireResponse-enableWhen-test.xml": {
|
||||||
"questionnaire": "questionnaire-enableWhen-test.xml",
|
"questionnaire": "questionnaire-enableWhen-test.xml",
|
||||||
"errorCount": 0
|
"errorCount": 0
|
||||||
},
|
},
|
||||||
|
"questionnaireResponse-enableWhen-test-nested.xml": {
|
||||||
|
"questionnaire": "questionnaire-enableWhen-test-nested.xml",
|
||||||
|
"errorCount": 2
|
||||||
|
},
|
||||||
"questionnaireResponse-enableWhen-test3.xml": {
|
"questionnaireResponse-enableWhen-test3.xml": {
|
||||||
"version": "3.0",
|
"version": "3.0",
|
||||||
"questionnaire": "questionnaire-enableWhen-test3.xml",
|
"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