diff --git a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java index c38fcf44e..2a044e39e 100644 --- a/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java +++ b/org.hl7.fhir.convertors/src/main/java/org/hl7/fhir/convertors/VersionConvertor_30_40.java @@ -33,6 +33,7 @@ import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.r4.model.BooleanType; +import org.hl7.fhir.r4.model.Questionnaire; import org.hl7.fhir.r4.model.CodeableConcept; import org.hl7.fhir.r4.model.Enumeration; import org.hl7.fhir.r4.model.Expression.ExpressionLanguage; @@ -76,7 +77,7 @@ import org.hl7.fhir.utilities.Utilities; public class VersionConvertor_30_40 { - private static List CANONICAL_URLS = new ArrayList(); + private static List CANONICAL_URLS = new ArrayList<>(); static { CANONICAL_URLS.add("http://hl7.org/fhir/StructureDefinition/11179-permitted-value-conceptmap"); CANONICAL_URLS.add("http://hl7.org/fhir/StructureDefinition/11179-permitted-value-valueset"); @@ -16013,6 +16014,7 @@ public class VersionConvertor_30_40 { tgt.setType(convertQuestionnaireItemType(src.getType())); for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemEnableWhenComponent t : src.getEnableWhen()) tgt.addEnableWhen(convertQuestionnaireItemEnableWhenComponent(t)); + tgt.setEnableBehavior(Questionnaire.EnableWhenBehavior.ANY); if (src.hasRequired()) tgt.setRequired(src.getRequired()); if (src.hasRepeats()) @@ -16029,6 +16031,9 @@ public class VersionConvertor_30_40 { tgt.addInitial().setValue(convertType(src.getInitial())); for (org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemComponent t : src.getItem()) tgt.addItem(convertQuestionnaireItemComponent(t)); + for (org.hl7.fhir.dstu3.model.Extension t : src.getModifierExtension()) { + tgt.addModifierExtension(convertExtension(t)); + } return tgt; } @@ -16131,8 +16136,10 @@ public class VersionConvertor_30_40 { tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EXISTS); tgt.setAnswer(convertType(src.getHasAnswerElement())); } - else if (src.hasAnswer()) - tgt.setAnswer(convertType(src.getAnswer())); + else if (src.hasAnswer()) { + tgt.setOperator(org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemOperator.EQUAL); + tgt.setAnswer(convertType(src.getAnswer())); + } return tgt; } diff --git a/org.hl7.fhir.validation.cli/pom.xml b/org.hl7.fhir.validation.cli/pom.xml index 6aafb46e0..5bf52803b 100644 --- a/org.hl7.fhir.validation.cli/pom.xml +++ b/org.hl7.fhir.validation.cli/pom.xml @@ -66,9 +66,10 @@ shade - + standalone - + org.hl7.fhir.r4.validation.Validator diff --git a/org.hl7.fhir.validation.cli/src/main/java/.keep.16 b/org.hl7.fhir.validation.cli/src/main/java/.keep.16 deleted file mode 100644 index e69de29bb..000000000 diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/BaseValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/BaseValidator.java index a563defc8..7caab98c6 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/BaseValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/BaseValidator.java @@ -72,8 +72,8 @@ public class BaseValidator { */ protected boolean fail(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.FATAL)); - } + addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.FATAL); + } return thePass; } @@ -87,8 +87,8 @@ public class BaseValidator { protected boolean fail(List errors, IssueType type, List pathParts, boolean thePass, String msg) { if (!thePass) { String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.FATAL); + } return thePass; } @@ -102,8 +102,8 @@ public class BaseValidator { protected boolean fail(List errors, IssueType type, List pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { if (!thePass) { String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL)); - } + addValidationMessage(errors, type, -1, -1, path, formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL); + } return thePass; } @@ -116,8 +116,8 @@ public class BaseValidator { */ protected boolean fail(List errors, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.FATAL)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.FATAL); + } return thePass; } @@ -145,8 +145,8 @@ public class BaseValidator { */ protected boolean hint(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.INFORMATION); + } return thePass; } @@ -160,15 +160,16 @@ public class BaseValidator { protected boolean hint(List errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { if (!thePass) { String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, message, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION); + } return thePass; } protected boolean txHint(List errors, String txLink, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { if (!thePass) { String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(Source.TerminologyEngine, type, line, col, path, message, IssueSeverity.INFORMATION).setTxLink(txLink)); + addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION, Source.TerminologyEngine) + .setTxLink(txLink); } return thePass; } @@ -184,8 +185,8 @@ public class BaseValidator { if (!thePass) { String path = toPath(pathParts); String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION); + } return thePass; } @@ -198,8 +199,8 @@ public class BaseValidator { */ protected boolean hint(List errors, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.INFORMATION); + } return thePass; } @@ -213,8 +214,8 @@ public class BaseValidator { protected boolean rule(List errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { if (!thePass) { String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, message, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, line, col, path, message, IssueSeverity.ERROR); + } return thePass; } @@ -236,8 +237,8 @@ public class BaseValidator { protected boolean rule(List errors, IssueType type, List pathParts, boolean thePass, String msg) { if (!thePass) { String path = toPath(pathParts); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.ERROR); + } return thePass; } @@ -252,8 +253,8 @@ public class BaseValidator { if (!thePass) { String path = toPath(pathParts); String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.ERROR); + } return thePass; } @@ -266,15 +267,15 @@ public class BaseValidator { */ protected boolean rule(List errors, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.ERROR); + } return thePass; } - static public boolean rule(List errors, Source source, IssueType type, String path, boolean thePass, String msg) { + public boolean rule(List errors, Source source, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.ERROR, source); + } return thePass; } @@ -287,8 +288,8 @@ public class BaseValidator { */ protected boolean rule(List errors, IssueType type, String path, boolean thePass, String msg, String html) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.ERROR)); - } + addValidationMessage(errors, type, path, msg, html, IssueSeverity.ERROR); + } return thePass; } @@ -330,13 +331,25 @@ public class BaseValidator { protected boolean warning(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { if (!thePass) { msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.WARNING)); - } + IssueSeverity severity = IssueSeverity.WARNING; + addValidationMessage(errors, type, line, col, path, msg, severity); + } return thePass; } - /** + protected void addValidationMessage(List errors, IssueType type, int line, int col, String path, String msg, IssueSeverity theSeverity) { + Source source = this.source; + addValidationMessage(errors, type, line, col, path, msg, theSeverity, source); + } + + protected ValidationMessage addValidationMessage(List errors, IssueType type, int line, int col, String path, String msg, IssueSeverity theSeverity, Source theSource) { + ValidationMessage validationMessage = new ValidationMessage(theSource, type, line, col, path, msg, theSeverity); + errors.add(validationMessage); + return validationMessage; + } + + /** * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails * * @param thePass @@ -355,8 +368,8 @@ public class BaseValidator { protected boolean warningOrError(boolean isError, List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { if (!thePass) { msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, isError ? IssueSeverity.ERROR : IssueSeverity.WARNING)); - } + addValidationMessage(errors, type, line, col, path, msg, isError ? IssueSeverity.ERROR : IssueSeverity.WARNING); + } return thePass; } @@ -372,8 +385,8 @@ public class BaseValidator { if (!thePass) { String path = toPath(pathParts); String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.WARNING)); - } + addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.WARNING); + } return thePass; } @@ -386,8 +399,8 @@ public class BaseValidator { */ protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.WARNING)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.WARNING); + } return thePass; } @@ -400,8 +413,8 @@ public class BaseValidator { */ protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg, String html) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING)); - } + addValidationMessage(errors, type, path, msg, html, IssueSeverity.WARNING); + } return thePass; } @@ -415,8 +428,8 @@ public class BaseValidator { protected boolean warning(List errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { if (!thePass) { msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.WARNING)); - } + addValidationMessage(errors, type, path, msg, html, IssueSeverity.WARNING); + } return thePass; } @@ -431,8 +444,8 @@ public class BaseValidator { protected boolean suppressedwarning(List errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { if (!thePass) { msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, line, col, path, msg, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.INFORMATION); + } return thePass; } @@ -448,8 +461,8 @@ public class BaseValidator { if (!thePass) { String path = toPath(pathParts); String message = formatMessage(theMessage, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, message, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION); + } return thePass; } @@ -462,8 +475,8 @@ public class BaseValidator { */ protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.INFORMATION); + } return thePass; } @@ -476,12 +489,17 @@ public class BaseValidator { */ protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg, String html) { if (!thePass) { - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.INFORMATION)); - } + IssueSeverity severity = IssueSeverity.INFORMATION; + addValidationMessage(errors, type, path, msg, html, severity); + } return thePass; } - /** + protected void addValidationMessage(List errors, IssueType type, String path, String msg, String html, IssueSeverity theSeverity) { + errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, theSeverity)); + } + + /** * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails * * @param thePass @@ -491,8 +509,8 @@ public class BaseValidator { protected boolean suppressedwarning(List errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { if (!thePass) { msg = formatMessage(msg, theMessageArguments); - errors.add(new ValidationMessage(source, type, -1, -1, path, msg, html, IssueSeverity.INFORMATION)); - } + addValidationMessage(errors, type, path, msg, html, IssueSeverity.INFORMATION); + } return thePass; } diff --git a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java new file mode 100644 index 000000000..9eb98ad60 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/DefaultEnableWhenEvaluator.java @@ -0,0 +1,216 @@ +package org.hl7.fhir.r4.validation; + +import java.util.*; +import java.util.stream.*; + +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.*; + +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){ + 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); + } + + public Type convertToType(Element element) throws FHIRException { + 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); + } 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"); + + } + + private List findQuestionAnswers(Element questionnaireResponse, String question) { + List matchingItems = questionnaireResponse.getChildren(ITEM_ELEMENT) + .stream() + .flatMap(i -> findSubItems(i).stream()) + .filter(i -> hasLinkId(i, question)) + .collect(Collectors.toList()); + return matchingItems + .stream() + .flatMap(e -> extractAnswer(e).stream()) + .collect(Collectors.toList()); + } + + 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 List findSubItems(Element item) { + List results = item.getChildren(LINKID_ELEMENT) + .stream() + .flatMap(i -> findSubItems(i).stream()) + .collect(Collectors.toList()); + results.add(item); + return results; + } + + 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/r4/validation/EnableWhenResult.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/EnableWhenResult.java new file mode 100644 index 000000000..4f6d5d7f7 --- /dev/null +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/EnableWhenResult.java @@ -0,0 +1,48 @@ +package org.hl7.fhir.r4.validation; + + +import org.hl7.fhir.r4.elementmodel.Element; +import org.hl7.fhir.r4.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/r4/validation/IEnableWhenEvaluator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/IEnableWhenEvaluator.java new file mode 100644 index 000000000..be567c9d8 --- /dev/null +++ b/org.hl7.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/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java index 760accbf6..15ece9bee 100644 --- a/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java +++ b/org.hl7.fhir.validation/src/main/java/org/hl7/fhir/r4/validation/InstanceValidator.java @@ -35,6 +35,8 @@ import java.util.UUID; import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.exceptions.*; import org.hl7.fhir.convertors.VersionConvertorConstants; import org.hl7.fhir.exceptions.DefinitionException; import org.hl7.fhir.exceptions.FHIRException; @@ -274,6 +276,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat private IEvaluationContext externalHostServices; private boolean noExtensibleWarnings; private String serverBase; + + private IEnableWhenEvaluator myEnableWhenEvaluator = new DefaultEnableWhenEvaluator(); /* * Keeps track of whether a particular profile has been checked or not yet @@ -2498,6 +2502,10 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat public void setAllowXsiLocation(boolean allowXsiLocation) { this.allowXsiLocation = allowXsiLocation; } + + public void setEnableWhenEvaluator(IEnableWhenEvaluator myEnableWhenEvaluator) { + this.myEnableWhenEvaluator = myEnableWhenEvaluator; + } /** * @@ -2776,21 +2784,21 @@ 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); + validateQuestionannaireResponseItems(qsrc, qsrc.getItem(), errors, element, stack, inProgress, element); } } } - private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress) { + private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { 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()); List answers = new ArrayList(); element.getNamedChildren("answer", answers); if (inProgress) - warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); + warning(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId()); else - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), (answers.size() > 0) || !qItem.getRequired(), "No response answer found for required item "+qItem.getLinkId()); + rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), "No response answer found for required item "+qItem.getLinkId()); 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"); @@ -2865,7 +2873,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat // no validation break; } - validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress); + validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot); } 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"); @@ -2874,16 +2882,20 @@ 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); + validateQuestionannaireResponseItems(qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot); } } - private void validateQuestionannaireResponseItem(Questionnaire qsrc, QuestionnaireItemComponent qItem, List errors, List elements, NodeStack stack, boolean inProgress) { +private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List answers) { + 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) { 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()); for (Element element : elements) { NodeStack ns = stack.push(element, -1, null, null); - validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress); + validateQuestionannaireResponseItem(qsrc, qItem, errors, element, ns, inProgress, questionnaireResponseRoot); } } @@ -2894,8 +2906,8 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat } return -1; } - - private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress) { + + private void validateQuestionannaireResponseItems(Questionnaire qsrc, List qItems, List errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot) { List items = new ArrayList(); element.getNamedChildren("item", items); // now, sort into stacks @@ -2908,9 +2920,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat if (index == -1) { QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId); if (qItem != null) { - rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, "Structural Error: item is in the wrong place"); + 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, element, ns, inProgress); + validateQuestionannaireResponseItem(qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot); } else rule(errors, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, "LinkId \""+linkId+"\" not found in questionnaire"); @@ -2919,11 +2931,9 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat { rule(errors, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, "Structural Error: items are out of order"); lastIndex = index; - List mapItem = map.get(linkId); - if (mapItem == null) { - mapItem = new ArrayList(); - map.put(linkId, mapItem); - } + + List mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>()); + mapItem.add(item); } } @@ -2932,13 +2942,25 @@ 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) - validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress); - else - rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); + if (mapItem != null){ + rule(errors, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot), "Item has answer, even though it is not enabled "+qItem.getLinkId()); + validateQuestionannaireResponseItem(qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot); + } else { + //item is missing, is the question enabled? + if (myEnableWhenEvaluator.isQuestionEnabled(qItem, questionnaireResponseRoot)) { + rule(errors, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), !qItem.getRequired(), "No response found for required item "+qItem.getLinkId()); + } + } } } +private String misplacedItemError(QuestionnaireItemComponent qItem) { + return qItem.hasLinkId() ? + String.format("Structural Error: item with linkid %s is in the wrong place", qItem.getLinkId()) + : + "Structural Error: item is in the wrong place"; +} + private void validateQuestionnaireResponseItemQuantity( List errors, Element answer, NodeStack stack) { } @@ -3009,8 +3031,11 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat long t = System.nanoTime(); ValidationResult res = context.validateCode(c, vs); txTime = txTime + (System.nanoTime() - t); - if (!res.isOk()) - txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided ("+c.getSystem()+"::"+c.getCode()+") is not in the options value set in the questionnaire"); + if (!res.isOk()) { + txRule(errors, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "The value provided (" + c.getSystem() + "::" + c.getCode() + ") is not in the options value set in the questionnaire"); + } else if (res.getSeverity() != null) { + super.addValidationMessage(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity(), Source.TerminologyEngine); + } } catch (Exception e) { warning(errors, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, "Error " + e.getMessage() + " validating Coding against Questionnaire Options"); }