fix validation problems with questionnaires, and allow tests to still run even when offline

This commit is contained in:
Grahame Grieve 2019-07-13 20:50:53 +10:00
parent da434e310b
commit 58da7557d8
14 changed files with 741 additions and 313 deletions

View File

@ -10180,6 +10180,10 @@ The primary difference between a medication statement and a medication administr
public String toCode(int len) {
return toCode().substring(0, len);
}
public static boolean isR4Plus(String version) {
return version != null && (version.startsWith("4.") || version.startsWith("5."));
}
}
public static class FHIRVersionEnumFactory implements EnumFactory<FHIRVersion> {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -132,6 +132,7 @@ import org.hl7.fhir.r5.utils.ToolingExtensions;
import org.hl7.fhir.r5.utils.ValidationProfileSet;
import org.hl7.fhir.r5.utils.ValidationProfileSet.ProfileRegistration;
import org.hl7.fhir.r5.utils.Version;
import org.hl7.fhir.r5.validation.EnableWhenEvaluator.QStack;
import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
import org.hl7.fhir.utilities.Utilities;
import org.hl7.fhir.utilities.validation.ValidationMessage;
@ -292,7 +293,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
private boolean noExtensibleWarnings;
private String serverBase;
private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator();
private EnableWhenEvaluator myEnableWhenEvaluator = new EnableWhenEvaluator();
/*
* Keeps track of whether a particular profile has been checked or not yet
@ -2620,10 +2621,6 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
this.allowXsiLocation = allowXsiLocation;
}
public void setEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) {
this.myEnableWhenEvaluator = myEnableWhenEvaluator;
}
/**
*
* @param element
@ -2886,6 +2883,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
validateBundle(errors, element, stack);
else if (element.getType().equals("Observation"))
validateObservation(errors, element, stack);
else if (element.getType().equals("Questionnaire"))
validateQuestionannaire(errors, element, stack);
else if (element.getType().equals("QuestionnaireResponse"))
validateQuestionannaireResponse(errors, element, stack);
else if (element.getType().equals("CodeSystem"))
@ -2901,6 +2900,88 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
}
}
private void validateQuestionannaire(List<ValidationMessage> errors, Element element, NodeStack stack) {
List<Element> list = getItems(element);
for (int i = 0; i < list.size(); i++) {
Element e = list.get(i);
NodeStack ns = stack.push(element, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
validateQuestionnaireElement(errors, ns, element, e, new ArrayList<>());
}
}
private void validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents) {
// R4+
if (FHIRVersion.isR4Plus(context.getVersion())) {
if (item.hasChild("enableWhen")) {
Element ew = item.getNamedChild("enableWhen");
String ql = ew.getNamedChildValue("question");
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, ql != null, "Questions with an enableWhen must have a value for the question link")) {
Element tgt = getQuestionById(item, ql);
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt == null, "Questions with an enableWhen cannot refer to an inner question for it's enableWhen condition")) {
tgt = getQuestionById(questionnaire, ql);
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != null, "Unable to find "+ql+" target for this question enableWhen")) {
if (rule(errors, IssueType.BUSINESSRULE, ns.literalPath, tgt != item, "Target for this question enableWhen can't reference itself")) {
warning(errors, IssueType.BUSINESSRULE, ns.literalPath, isBefore(item, tgt, parents), "The target of this enableWhen rule ("+ql+") comes after the question itself");
}
}
}
}
}
}
}
private boolean isBefore(Element item, Element tgt, List<Element> parents) {
// we work up the list, looking for tgt in the children of the parents
for (Element p : parents) {
int i = findIndex(p, item);
int t = findIndex(p, tgt);
if (i > -1 && t > -1) {
return i > t;
}
}
return false; // unsure... shouldn't ever get to this point;
}
private int findIndex(Element parent, Element descendant) {
for (int i = 0; i < parent.getChildren().size(); i++) {
if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant))
return i;
}
return -1;
}
private boolean isChild(Element element, Element descendant) {
for (Element e : element.getChildren()) {
if (e == descendant)
return true;
if (isChild(element, descendant))
return true;
}
return false;
}
private Element getQuestionById(Element focus, String ql) {
List<Element> list = getItems(focus);
for (Element item : list) {
String v = item.getNamedChildValue("linkId");
if (ql.equals(v))
return item;
Element tgt = getQuestionById(item, ql);
if (tgt != null)
return tgt;
}
return null;
}
private List<Element> getItems(Element element) {
List<Element> list = new ArrayList<>();
element.getNamedChildren("item", list);
return list;
}
private void checkLang(Element resource, NodeStack stack) {
String lang = resource.getNamedChildValue("language");
if (!Utilities.noString(lang))
@ -2981,7 +3062,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
sdTime = sdTime + (System.nanoTime() - t);
if (warning(errors, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, "The questionnaire \""+questionnaire+"\" could not be resolved, so no validation can be performed against the base questionnaire")) {
boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, element);
validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element));
}
}
}
@ -3035,7 +3116,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
return null;
}
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
String text = element.getNamedChildValue("text");
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), "If text exists, it must match the questionnaire definition for linkId "+qItem.getLinkId());
@ -3043,10 +3124,13 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
element.getNamedChildren("answer", answers);
if (inProgress)
warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
else if (myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot))
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
else if (!answers.isEmpty()) // items without answers should be allowed, but not items with answers to questions that are disabled
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), "Item has answer, even though it is not enabled "+qItem.getLinkId());
else if (myEnableWhenEvaluator.isQuestionEnabled(qItem, qstack)) {
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId());
} else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled
// it appears that this is always a duplicate error - it will always already have beeb reported, so no need to report it again?
// GDG 2019-07-13
// rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), "Item has answer (2), even though it is not enabled "+qItem.getLinkId());
}
if (answers.size() > 1)
rule(errors, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response answer item with this linkId allowed");
@ -3122,7 +3206,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
// no validation
break;
}
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot);
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
}
if (qItem.getType() == null) {
fail(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "Definition for item "+qItem.getLinkId() + " does not contain a type");
@ -3131,7 +3215,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
element.getNamedChildren("item", items);
rule(errors, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), "Items not of type DISPLAY should not have items - linkId {0}", qItem.getLinkId());
} else {
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot);
validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack);
}
}
@ -3139,12 +3223,14 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP;
}
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<Element> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
if (elements.size() > 1)
rule(errors, IssueType.INVALID, elements.get(1).line(), elements.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), "Only one response item with this linkId allowed - " + qItem.getLinkId());
int i = 0;
for (Element element : elements) {
NodeStack ns = stack.push(element, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot);
NodeStack ns = stack.push(element, i, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element));
i++;
}
}
@ -3156,7 +3242,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
return -1;
}
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) {
private void validateQuestionannaireResponseItems(Questionnaire qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
List<Element> items = new ArrayList<Element>();
element.getNamedChildren("item", items);
// now, sort into stacks
@ -3171,7 +3257,7 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
if (qItem != null) {
rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem));
NodeStack ns = stack.push(item, -1, null, null);
validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot);
validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item));
}
else
rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire");
@ -3191,20 +3277,26 @@ private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, L
// ok, now we have a list of known items, grouped by linkId. We"ve made an error for anything out of order
for (QuestionnaireItemComponent qItem : qItems) {
List<Element> mapItem = map.get(qItem.getLinkId());
validateQuestionannaireResponseItem(qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem);
validateQuestionannaireResponseItem(qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack);
}
}
public void validateQuestionannaireResponseItem(Questionnaire qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<Element> mapItem) {
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot);
public void validateQuestionannaireResponseItem(Questionnaire qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<Element> mapItem, QStack qstack) {
boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(qItem, qstack);
if (mapItem != null){
if (!enabled)
rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), enabled, "Item has answer, even though it is not enabled (item id = '"+qItem.getLinkId()+"')");
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot);
if (!enabled) {
int i = 0;
for (Element e : mapItem) {
NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
rule(errors, IssueType.INVALID, e.line(), e.col(), ns.getLiteralPath(), enabled, "Item has answer, even though it is not enabled (item id = '"+qItem.getLinkId()+"')");
i++;
}
}
validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack);
} else {
//item is missing, is the question enabled?
if (enabled && qItem.getRequired()) {
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "No response found for required item (item id = '"+qItem.getLinkId()+"')");
rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, "No response found for required item with id = '"+qItem.getLinkId()+"'");
}
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>