From 58da7557d8a7814da662a5b0a9746e7ef0d89f11 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 13 Jul 2019 20:50:53 +1000 Subject: [PATCH] fix validation problems with questionnaires, and allow tests to still run even when offline --- .../org/hl7/fhir/r5/model/Enumerations.java | 4 + .../org/hl7/fhir/r5/model/Questionnaire.java | 65 +++- .../DefaultEnableWhenEvaluator.java | 220 ------------ .../r5/validation/EnableWhenEvaluator.java | 327 ++++++++++++++++++ .../fhir/r5/validation/EnableWhenResult.java | 48 --- .../r5/validation/IEnableWhenEvaluator.java | 10 - .../fhir/r5/validation/InstanceValidator.java | 142 ++++++-- .../fhir/r5/validation/ValidationEngine.java | 19 +- .../validation/tests/ValidationTestSuite.java | 9 +- .../validation-examples/manifest.json | 12 + .../questionnaire-enableWhen-test-invalid.xml | 39 +++ .../questionnaire-enableWhen-test-nested.xml | 38 ++ .../questionnaire-enableWhen-test-order.xml | 33 ++ ...onnaireResponse-enableWhen-test-nested.xml | 88 +++++ 14 files changed, 741 insertions(+), 313 deletions(-) delete mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/DefaultEnableWhenEvaluator.java create mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenEvaluator.java delete mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenResult.java delete mode 100644 org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/IEnableWhenEvaluator.java create mode 100644 org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-invalid.xml create mode 100644 org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-nested.xml create mode 100644 org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-order.xml create mode 100644 org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaireResponse-enableWhen-test-nested.xml diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Enumerations.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Enumerations.java index 475d42e1e..efd31d9c3 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Enumerations.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Enumerations.java @@ -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 { diff --git a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Questionnaire.java b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Questionnaire.java index bef78af5a..cfc2c0b7e 100644 --- a/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Questionnaire.java +++ b/org.hl7.fhir.r5/src/main/java/org/hl7/fhir/r5/model/Questionnaire.java @@ -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; + } + } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/DefaultEnableWhenEvaluator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/DefaultEnableWhenEvaluator.java deleted file mode 100644 index 369f73f93..000000000 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/DefaultEnableWhenEvaluator.java +++ /dev/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 evaluationResults = questionnaireItem.getEnableWhen() - .stream() - .map(enableCondition -> evaluateCondition(enableCondition, questionnaireResponse, - questionnaireItem.getLinkId())) - .collect(Collectors.toList()); - return checkConditionResults(evaluationResults, questionnaireItem); - } - - - public boolean checkConditionResults(List evaluationResults, - QuestionnaireItemComponent questionnaireItem) { - if ((questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY) || 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 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 findQuestionAnswers(Element questionnaireResponse, String question) { - List retVal = new ArrayList<>(); - - List items = questionnaireResponse.getChildren(ITEM_ELEMENT); - for (Element next : items) { - if (hasLinkId(next, question)) { - List answers = extractAnswer(next); - retVal.addAll(answers); - } - retVal.addAll(findQuestionAnswers(next, question)); - } - - return retVal; - } - - private List extractAnswer(Element item) { - return item.getChildrenByName(ANSWER_ELEMENT) - .stream() - .flatMap(c -> c.getChildren().stream()) - .collect(Collectors.toList()); - } - - private boolean compareCodingAnswer(Coding expectedAnswer, Coding actualAnswer, QuestionnaireItemOperator questionnaireItemOperator) { - boolean result = compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer); - if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){ - return result == true; - } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){ - return result == false; - } - throw new UnprocessableEntityException("Bad operator for Coding comparison"); - } - - private boolean compareCodes(Coding expectedCoding, Coding value) { - if (expectedCoding.hasCode() != value.hasCode()) { - return false; - } - if (expectedCoding.hasCode()) { - return expectedCoding.getCode().equals(value.getCode()); - } - return true; - } - - private boolean compareSystems(Coding expectedCoding, Coding value) { - if (expectedCoding.hasSystem() && !value.hasSystem()) { - return false; - } - if (expectedCoding.hasSystem()) { - return expectedCoding.getSystem().equals(value.getSystem()); - } - return true; - } - - private boolean hasLinkId(Element item, String linkId) { - Element linkIdChild = item.getNamedChild(LINKID_ELEMENT); - if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){ - return true; - } - return false; - } -} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenEvaluator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenEvaluator.java new file mode 100644 index 000000000..56216b686 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenEvaluator.java @@ -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 { + + 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 evaluationResults = qitem.getEnableWhen() + .stream() + .map(enableCondition -> evaluateCondition(enableCondition, qitem, qstack)) + .collect(Collectors.toList()); + return checkConditionResults(evaluationResults, qitem); + } + + + public boolean checkConditionResults(List 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 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 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 findOnItem(Element focus, String question) { + List retVal = new ArrayList<>(); + List items = focus.getChildren(ITEM_ELEMENT); + for (Element item : items) { + if (hasLinkId(item, question)) { + List answers = extractAnswer(item); + retVal.addAll(answers); + } + retVal.addAll(findOnItem(item, question)); + } + return retVal; + } + + + private List extractAnswer(Element item) { + return item.getChildrenByName(ANSWER_ELEMENT) + .stream() + .flatMap(c -> c.getChildren().stream()) + .collect(Collectors.toList()); + } + + private boolean compareCodingAnswer(Coding expectedAnswer, Coding actualAnswer, QuestionnaireItemOperator questionnaireItemOperator) { + boolean result = compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer); + if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){ + return result == true; + } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){ + return result == false; + } + throw new UnprocessableEntityException("Bad operator for Coding comparison"); + } + + private boolean compareCodes(Coding expectedCoding, Coding value) { + if (expectedCoding.hasCode() != value.hasCode()) { + return false; + } + if (expectedCoding.hasCode()) { + return expectedCoding.getCode().equals(value.getCode()); + } + return true; + } + + private boolean compareSystems(Coding expectedCoding, Coding value) { + if (expectedCoding.hasSystem() && !value.hasSystem()) { + return false; + } + if (expectedCoding.hasSystem()) { + return expectedCoding.getSystem().equals(value.getSystem()); + } + return true; + } + + private boolean hasLinkId(Element item, String linkId) { + Element linkIdChild = item.getNamedChild(LINKID_ELEMENT); + if (linkIdChild != null && linkIdChild.getValue().equals(linkId)){ + return true; + } + return false; + } +} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenResult.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenResult.java deleted file mode 100644 index 2b7b9fe91..000000000 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/EnableWhenResult.java +++ /dev/null @@ -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; - } -} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/IEnableWhenEvaluator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/IEnableWhenEvaluator.java deleted file mode 100644 index 89b5b22d8..000000000 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/IEnableWhenEvaluator.java +++ /dev/null @@ -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); - -} diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java index 3c7ac881e..0d5a5df29 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/InstanceValidator.java @@ -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 errors, Element element, NodeStack stack) { + List 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 errors, NodeStack ns, Element questionnaire, Element item, List 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 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 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 getItems(Element element) { + List 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 errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { + private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List 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 errors, List elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { + private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List 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 qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { + private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) { List items = new ArrayList(); 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 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 errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List mapItem) { - boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot); + public void validateQuestionannaireResponseItem(Questionnaire qsrc, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List 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()+"'"); } } } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java index a18aff7af..8468a916e 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r5/validation/ValidationEngine.java @@ -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 { diff --git a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java index 11c29b978..9aafeb57d 100644 --- a/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java +++ b/org.hl7.fhir.validation/src/test/java/org/hl7/fhir/validation/tests/ValidationTestSuite.java @@ -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; } diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json b/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json index 3142e428e..2626a2695 100644 --- a/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/manifest.json @@ -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", diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-invalid.xml b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-invalid.xml new file mode 100644 index 000000000..efe16227b --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-invalid.xml @@ -0,0 +1,39 @@ + + + + + + +
+

enableWhen test

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-nested.xml b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-nested.xml new file mode 100644 index 000000000..d9fb3d2da --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-nested.xml @@ -0,0 +1,38 @@ + + + + + + +
+

enableWhen test

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-order.xml b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-order.xml new file mode 100644 index 000000000..cfc5db9eb --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaire-enableWhen-test-order.xml @@ -0,0 +1,33 @@ + + + + + + +
+

enableWhen test

+
+
+ + + + + + + + + + + + + + + + + + + + + + +
diff --git a/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaireResponse-enableWhen-test-nested.xml b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaireResponse-enableWhen-test-nested.xml new file mode 100644 index 000000000..484251f06 --- /dev/null +++ b/org.hl7.fhir.validation/src/test/resources/validation-examples/questionnaireResponse-enableWhen-test-nested.xml @@ -0,0 +1,88 @@ + + + + + +
+

enableWhen test response

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +