From cddb6b993c1951cdd2398a4150d6da338214ec7a Mon Sep 17 00:00:00 2001 From: Okko Kauhanen Date: Mon, 5 Nov 2018 17:03:50 +0200 Subject: [PATCH] Added operator evaluation for R4 enableWhen --- .../QuestionnaireResponseValidator.java | 7 +- .../instance/validation/EnableWhenResult.java | 2 +- .../validation/IEnableWhenEvaluator.java | 15 --- .../DefaultEnableWhenEvaluator.java | 115 +++++++++--------- .../r4/validation/IEnableWhenEvaluator.java | 10 ++ .../fhir/r4/validation/InstanceValidator.java | 15 +-- .../hapi/validation/EnableWhenResult.java | 47 +++++++ 7 files changed, 125 insertions(+), 86 deletions(-) delete mode 100644 hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/IEnableWhenEvaluator.java rename hapi-fhir-validation/src/main/java/org/hl7/fhir/{instance => r4}/validation/DefaultEnableWhenEvaluator.java (55%) create mode 100644 hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java index c92e55c0375..054bd549b5f 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/dstu3/validation/QuestionnaireResponseValidator.java @@ -36,8 +36,6 @@ import org.hl7.fhir.dstu3.model.Type; import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent; -import org.hl7.fhir.instance.validation.DefaultEnableWhenEvaluator; -import org.hl7.fhir.instance.validation.IEnableWhenEvaluator; import org.hl7.fhir.utilities.validation.ValidationMessage; import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; @@ -57,7 +55,6 @@ public class QuestionnaireResponseValidator extends BaseValidator { */ private IWorkerContext myWorkerCtx; - private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator(); // this is here not to introduce enabledWhen validation unless wanted private boolean skipEnabledCheck = true; @@ -229,13 +226,15 @@ public class QuestionnaireResponseValidator extends BaseValidator { List responseItems = findResponsesByLinkId(theResponseItems, linkId); if (responseItems.isEmpty()) { - if ((skipEnabledCheck || myEnableWhenEvaluator.isQuestionEnabled(nextQuestionnaireItem, theResponseItems)) && nextQuestionnaireItem.getRequired() ) { + if (nextQuestionnaireItem.getRequired()) { + if ((skipEnabledCheck /*|| myEnableWhenEvaluator.isQuestionEnabled(nextQuestionnaireItem, theResponseItems)*/) && nextQuestionnaireItem.getRequired() ) { if (theValidateRequired) { rule(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required {0} with linkId[{1}]", itemType, linkId); } else { hint(theErrors, IssueType.BUSINESSRULE, thePathStack, false, "Missing required {0} with linkId[{1}]", itemType, linkId); } } + } continue; } if (responseItems.size() > 1) { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java index 46246739596..84afebe2980 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/EnableWhenResult.java @@ -19,7 +19,7 @@ public class EnableWhenResult { * LinkId of the questionnaire item * @param enableWhenCondition * Evaluated enableWhen condition - * @param responseItem + * @param answerItem * item in QuestionnaireResponse */ public EnableWhenResult(boolean enabled, String linkId, QuestionnaireItemEnableWhenComponent enableWhenCondition, diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/IEnableWhenEvaluator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/IEnableWhenEvaluator.java deleted file mode 100644 index a81500dfb4b..00000000000 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/IEnableWhenEvaluator.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.hl7.fhir.instance.validation; - -import java.util.List; - -import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; -import org.hl7.fhir.r4.elementmodel.Element; - -public interface IEnableWhenEvaluator { - public boolean isQuestionEnabled(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent questionnaireItem, - Element questionnaireResponse); - public boolean isQuestionEnabled(QuestionnaireItemComponent item, List theResponseItems); - - -} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/DefaultEnableWhenEvaluator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java similarity index 55% rename from hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/DefaultEnableWhenEvaluator.java rename to hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java index b4cf1912c44..6e840f0a9b1 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/instance/validation/DefaultEnableWhenEvaluator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java @@ -1,13 +1,13 @@ -package org.hl7.fhir.instance.validation; +package org.hl7.fhir.r4.validation; import java.util.*; -import java.util.stream.Collectors; +import java.util.stream.*; -import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.elementmodel.Element; import org.hl7.fhir.r4.model.*; import org.hl7.fhir.r4.model.Questionnaire.*; + /** * Evaluates Questionnaire.item.enableWhen against a QuestionnaireResponse. * Ignores possible modifierExtensions and extensions. @@ -18,42 +18,6 @@ public class DefaultEnableWhenEvaluator implements IEnableWhenEvaluator { public static final String ITEM_ELEMENT = "item"; public static final String ANSWER_ELEMENT = "answer"; - @Override - public boolean isQuestionEnabled(org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent item, - List resp) { - boolean enabled = true; - if(item.hasEnableWhen()) { - enabled = false; - for(org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent enable : item.getEnableWhen()) { - if(enable.getHasAnswer()) { - // check if referenced question has answer - String itemId = enable.getQuestion(); - for(QuestionnaireResponseItemComponent respItem : resp) { - if(respItem.getLinkId().equalsIgnoreCase(itemId) && respItem.hasAnswer()) { - //TODO check answer value - enabled = true; - } - } - - } else { - // and if not - String itemId = enable.getQuestion(); - for(QuestionnaireResponseItemComponent respItem : resp) { - if(respItem.getLinkId().equalsIgnoreCase(itemId) && !respItem.hasAnswer()) { - - //TODO check answer value - - enabled = true; - } - } - - } - } - } - return enabled; - } - - @Override public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem, Element questionnaireResponse) { if (!questionnaireItem.hasEnableWhen()) { @@ -80,38 +44,69 @@ public class DefaultEnableWhenEvaluator implements IEnableWhenEvaluator { } - public EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition, + protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition, Element questionnaireResponse, String linkId) { //TODO: Fix EnableWhenResult stuff List answerItems = findQuestionAnswers(questionnaireResponse, - enableCondition.getQuestion()); - if (enableCondition.hasAnswer()) { - boolean result = answerItems.stream().anyMatch(answer -> evaluateAnswer(answer, enableCondition.getAnswer())); - - return new EnableWhenResult(result, linkId, enableCondition, questionnaireResponse); - - } - return new EnableWhenResult(false, linkId, enableCondition, questionnaireResponse); + enableCondition.getQuestion()); + QuestionnaireItemOperator operator = enableCondition.getOperator(); + if (operator == QuestionnaireItemOperator.EXISTS){ + Type answer = enableCondition.getAnswer(); + if (!(answer instanceof BooleanType)){ + throw new RuntimeException("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 boolean evaluateAnswer(Element answer, Type expectedAnswer) { - org.hl7.fhir.r4.model.Type actualAnswer; + protected boolean evaluateAnswer(Element answer, Type expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) { + Type actualAnswer; try { actualAnswer = answer.asType(); } catch (FHIRException e) { throw new RuntimeException("Unexpected answer type", e); } - if (!actualAnswer.getClass().isAssignableFrom(expectedAnswer.getClass())) { + if (!actualAnswer.getClass().equals(expectedAnswer.getClass())) { throw new RuntimeException("Expected answer and actual answer have incompatible types"); - } + } if (expectedAnswer instanceof Coding) { - return validateCodingAnswer((Coding)expectedAnswer, (Coding)actualAnswer); - } else if (expectedAnswer instanceof PrimitiveType) { - return actualAnswer.equalsShallow(expectedAnswer); + return compareCodingAnswer((Coding)expectedAnswer, (Coding)actualAnswer, questionnaireItemOperator); + } else if ((expectedAnswer instanceof PrimitiveType)) { + return comparePrimitiveAnswer((PrimitiveType)actualAnswer, (PrimitiveType)expectedAnswer, questionnaireItemOperator); } // TODO: Quantity, Attachment, reference? throw new RuntimeException("Unimplemented answer type: " + expectedAnswer.getClass()); } + private boolean comparePrimitiveAnswer(PrimitiveType actualAnswer, PrimitiveType expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) { + if (actualAnswer.getValue() instanceof Comparable){ + @SuppressWarnings({ "rawtypes", "unchecked" }) + int result = ((Comparable)actualAnswer.getValue()).compareTo(expectedAnswer.getValue()); + 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 RuntimeException("Bad operator for PrimitiveType comparison"); + } else if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL){ + return actualAnswer.equalsShallow(expectedAnswer); + } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL){ + return !actualAnswer.equalsShallow(expectedAnswer); + } + throw new RuntimeException("Bad operator for PrimitiveType comparison"); + } private List findQuestionAnswers(Element questionnaireResponse, String question) { List matchingItems = questionnaireResponse.getChildren(ITEM_ELEMENT) @@ -132,8 +127,14 @@ public class DefaultEnableWhenEvaluator implements IEnableWhenEvaluator { .collect(Collectors.toList()); } - private boolean validateCodingAnswer(Coding expectedAnswer, Coding actualAnswer) { - return compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer); + 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 RuntimeException("Bad operator for Coding comparison"); } private boolean compareCodes(Coding expectedCoding, Coding value) { diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java new file mode 100644 index 00000000000..be567c9d8d4 --- /dev/null +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java @@ -0,0 +1,10 @@ +package org.hl7.fhir.r4.validation; + +import org.hl7.fhir.r4.elementmodel.Element; +import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent; + +public interface IEnableWhenEvaluator { + public boolean isQuestionEnabled(QuestionnaireItemComponent questionnaireItem, + Element questionnaireResponse); + +} diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java index 269fac6d8f6..561ba3d314f 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java @@ -10,8 +10,6 @@ import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.exceptions.PathEngineException; import org.hl7.fhir.exceptions.TerminologyServiceException; -import org.hl7.fhir.instance.validation.DefaultEnableWhenEvaluator; -import org.hl7.fhir.instance.validation.IEnableWhenEvaluator; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.context.IWorkerContext.ValidationResult; @@ -2779,16 +2777,15 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // 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()); - if (mapItem != null) + if (mapItem != null){ validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); - else { - //item is missing, is the question enabled? - if(! myEnableWhenEvaluator.isQuestionEnabled(qItem, element)) { - - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); + } else { + //item is missing, is the question enabled? + if (!myEnableWhenEvaluator.isQuestionEnabled(qItem, element)) { + rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); } } - } + } } private void validateQuestionnaireResponseItemQuantity( List errors, Element answer, NodeStack stack) { diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java new file mode 100644 index 00000000000..8568fbe271e --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/EnableWhenResult.java @@ -0,0 +1,47 @@ +package org.hl7.fhir.dstu3.hapi.validation; + +import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse.QuestionnaireResponseItemComponent; + +public class EnableWhenResult { + private final boolean enabled; + private final QuestionnaireItemEnableWhenComponent enableWhenCondition; + private final QuestionnaireResponseItemComponent responseItem; + 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, + QuestionnaireResponseItemComponent responseItem) { + this.enabled = enabled; + this.linkId = linkId; + this.responseItem = responseItem; + this.enableWhenCondition = enableWhenCondition; + } + + public boolean isEnabled() { + return enabled; + } + + public String getLinkId() { + return linkId; + } + + public QuestionnaireResponseItemComponent getResponseItem() { + return responseItem; + } + + public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() { + return enableWhenCondition; + } +}