001package org.hl7.fhir.validation.instance; 002 003/* 004 Copyright (c) 2011+, HL7, Inc. 005 All rights reserved. 006 007 Redistribution and use in source and binary forms, with or without modification, 008 are permitted provided that the following conditions are met: 009 010 * Redistributions of source code must retain the above copyright notice, this 011 list of conditions and the following disclaimer. 012 * Redistributions in binary form must reproduce the above copyright notice, 013 this list of conditions and the following disclaimer in the documentation 014 and/or other materials provided with the distribution. 015 * Neither the name of HL7 nor the names of its contributors may be used to 016 endorse or promote products derived from this software without specific 017 prior written permission. 018 019 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 022 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 024 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 025 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 026 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 027 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 028 POSSIBILITY OF SUCH DAMAGE. 029 030 */ 031import java.util.ArrayList; 032import java.util.List; 033import java.util.stream.Collectors; 034 035import org.hl7.fhir.exceptions.FHIRException; 036import org.hl7.fhir.r5.elementmodel.Element; 037import org.hl7.fhir.r5.model.BooleanType; 038import org.hl7.fhir.r5.model.Coding; 039import org.hl7.fhir.r5.model.DataType; 040import org.hl7.fhir.r5.model.Expression; 041import org.hl7.fhir.r5.model.ExpressionNode; 042import org.hl7.fhir.r5.model.Factory; 043import org.hl7.fhir.r5.model.PrimitiveType; 044import org.hl7.fhir.r5.model.Quantity; 045import org.hl7.fhir.r5.model.Questionnaire.EnableWhenBehavior; 046import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent; 047import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemEnableWhenComponent; 048import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemOperator; 049import org.hl7.fhir.r5.utils.FHIRPathEngine; 050import org.hl7.fhir.validation.instance.type.QuestionnaireValidator.QuestionnaireWithContext; 051import org.hl7.fhir.validation.instance.utils.ValidatorHostContext; 052 053import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 054 055/** 056 * Evaluates Questionnaire.item.enableWhen against a QuestionnaireResponse. 057 * Ignores possible modifierExtensions and extensions. 058 */ 059public class EnableWhenEvaluator { 060 public static final String LINKID_ELEMENT = "linkId"; 061 public static final String ITEM_ELEMENT = "item"; 062 public static final String ANSWER_ELEMENT = "answer"; 063 064 065 public static class QuestionnaireAnswerPair { 066 private QuestionnaireItemComponent q; 067 private Element a; 068 069 public QuestionnaireAnswerPair(QuestionnaireItemComponent q, Element a) { 070 super(); 071 this.q = q; 072 this.a = a; 073 } 074 075 public QuestionnaireItemComponent getQ() { 076 return q; 077 } 078 079 public Element getA() { 080 return a; 081 } 082 083 } 084 085 public static class QStack extends ArrayList<QuestionnaireAnswerPair> { 086 087 private static final long serialVersionUID = 1L; 088 private QuestionnaireWithContext q; 089 private Element a; 090 091 public QStack(QuestionnaireWithContext q, Element a) { 092 super(); 093 this.q = q; 094 this.a = a; 095 } 096 097 098 public QuestionnaireWithContext getQ() { 099 return q; 100 } 101 102 103 public Element getA() { 104 return a; 105 } 106 107 108 public QStack push(QuestionnaireItemComponent q, Element a) { 109 QStack self = new QStack(this.q, this.a); 110 self.addAll(this); 111 self.add(new QuestionnaireAnswerPair(q, a)); 112 return self; 113 } 114 } 115 116 public static class EnableWhenResult { 117 private final boolean enabled; 118 private final QuestionnaireItemEnableWhenComponent enableWhenCondition; 119 120 /** 121 * Evaluation result of enableWhen condition 122 * 123 * @param enabled Evaluation result 124 * @param enableWhenCondition Evaluated enableWhen condition 125 */ 126 public EnableWhenResult(boolean enabled, QuestionnaireItemEnableWhenComponent enableWhenCondition) { 127 this.enabled = enabled; 128 this.enableWhenCondition = enableWhenCondition; 129 } 130 131 public boolean isEnabled() { 132 return enabled; 133 } 134 135 public QuestionnaireItemEnableWhenComponent getEnableWhenCondition() { 136 return enableWhenCondition; 137 } 138 } 139 140 /** 141 * 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 142 * <p> 143 * the itembeing validated is in the context of the stack. For root items, the stack is empty. 144 * <p> 145 * The context Questionnaire and QuestionnaireResponse are always available 146 */ 147 public boolean isQuestionEnabled(ValidatorHostContext hostContext, QuestionnaireItemComponent qitem, QStack qstack, FHIRPathEngine engine) { 148 if (hasExpressionExtension(qitem)) { 149 String expr = getExpression(qitem); 150 ExpressionNode node = engine.parse(expr); 151 return engine.evaluateToBoolean(hostContext, qstack.a, qstack.a, qstack.a, node); 152 } 153 154 if (!qitem.hasEnableWhen()) { 155 return true; 156 } 157 158 List<EnableWhenResult> evaluationResults = new ArrayList<>(); 159 for (QuestionnaireItemEnableWhenComponent enableCondition : qitem.getEnableWhen()) { 160 evaluationResults.add(evaluateCondition(enableCondition, qitem, qstack)); 161 } 162 return checkConditionResults(evaluationResults, qitem); 163 } 164 165 166 private boolean hasExpressionExtension(QuestionnaireItemComponent qitem) { 167 return qitem.hasExtension("http://phr.kanta.fi/StructureDefinition/fiphr-ext-questionnaire-enablewhen") || // finnish extension 168 qitem.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression"); // sdc extension 169 } 170 171 private String getExpression(QuestionnaireItemComponent qitem) { 172 if (qitem.hasExtension("http://phr.kanta.fi/StructureDefinition/fiphr-ext-questionnaire-enablewhen")) 173 return qitem.getExtensionString("http://phr.kanta.fi/StructureDefinition/fiphr-ext-questionnaire-enablewhen"); 174 if (qitem.hasExtension("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression")) { 175 Expression expr = (Expression) qitem.getExtensionByUrl("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression").getValue(); 176 if ("text/fhirpath".equals(expr.getLanguage())) { 177 return expr.getExpression(); 178 } else { 179 throw new FHIRException("Unsupported language '" + expr.getLanguage() + "' for enableWhen extension http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-enableWhenExpression"); 180 } 181 } 182 throw new Error("How did you get here?"); 183 } 184 185 186 public boolean checkConditionResults(List<EnableWhenResult> evaluationResults, QuestionnaireItemComponent questionnaireItem) { 187 if ((questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ANY) || evaluationResults.size() == 1) { 188 return evaluationResults.stream().anyMatch(EnableWhenResult::isEnabled); 189 } 190 if (questionnaireItem.hasEnableBehavior() && questionnaireItem.getEnableBehavior() == EnableWhenBehavior.ALL) { 191 return evaluationResults.stream().allMatch(EnableWhenResult::isEnabled); 192 } 193 //TODO: Throw exception? enableBehavior is mandatory when there are multiple conditions 194 return true; 195 } 196 197 198 protected EnableWhenResult evaluateCondition(QuestionnaireItemEnableWhenComponent enableCondition, QuestionnaireItemComponent qitem, QStack qstack) { 199 List<Element> answerItems = findQuestionAnswers(qstack, qitem, enableCondition); 200 QuestionnaireItemOperator operator = enableCondition.getOperator(); 201 if (operator == QuestionnaireItemOperator.EXISTS) { 202 DataType answer = enableCondition.getAnswer(); 203 if (!(answer instanceof BooleanType)) { 204 throw new UnprocessableEntityException("Exists-operator requires answerBoolean"); 205 } 206 return new EnableWhenResult(((BooleanType) answer).booleanValue() != answerItems.isEmpty(), enableCondition); 207 } 208 boolean result = false; 209 for (Element answer : answerItems) { 210 result = result || evaluateAnswer(answer, enableCondition.getAnswer(), enableCondition.getOperator()); 211 } 212 return new EnableWhenResult(result, enableCondition); 213 } 214 215 private DataType convertToType(Element element) throws FHIRException { 216 if (element.fhirType().equals("BackboneElement")) { 217 return null; 218 } 219 DataType b = new Factory().create(element.fhirType()); 220 if (b instanceof PrimitiveType) { 221 ((PrimitiveType<?>) b).setValueAsString(element.primitiveValue()); 222 } else { 223 for (Element child : element.getChildren()) { 224 if (!isExtension(child)) { 225 b.setProperty(child.getName(), convertToType(child)); 226 } 227 } 228 } 229 return b; 230 } 231 232 233 private boolean isExtension(Element element) { 234 return "Extension".equals(element.fhirType()); 235 } 236 237 protected boolean evaluateAnswer(Element answer, DataType expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) { 238 DataType actualAnswer; 239 if (isExtension(answer)) { 240 return false; 241 } 242 try { 243 actualAnswer = convertToType(answer); 244 if (actualAnswer == null) { 245 return false; 246 } 247 } catch (FHIRException e) { 248 throw new UnprocessableEntityException("Unexpected answer type", e); 249 } 250 if (!actualAnswer.getClass().equals(expectedAnswer.getClass())) { 251 throw new UnprocessableEntityException("Expected answer and actual answer have incompatible types"); 252 } 253 if (expectedAnswer instanceof Coding) { 254 return compareCodingAnswer((Coding) expectedAnswer, (Coding) actualAnswer, questionnaireItemOperator); 255 } else if ((expectedAnswer instanceof PrimitiveType)) { 256 return comparePrimitiveAnswer((PrimitiveType<?>) actualAnswer, (PrimitiveType<?>) expectedAnswer, questionnaireItemOperator); 257 } else if (expectedAnswer instanceof Quantity) { 258 return compareQuantityAnswer((Quantity) actualAnswer, (Quantity) expectedAnswer, questionnaireItemOperator); 259 } 260 // TODO: Attachment, reference? 261 throw new UnprocessableEntityException("Unimplemented answer type: " + expectedAnswer.getClass()); 262 } 263 264 265 private boolean compareQuantityAnswer(Quantity actualAnswer, Quantity expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) { 266 return compareComparable(actualAnswer.getValue(), expectedAnswer.getValue(), questionnaireItemOperator); 267 } 268 269 270 private boolean comparePrimitiveAnswer(PrimitiveType<?> actualAnswer, PrimitiveType<?> expectedAnswer, QuestionnaireItemOperator questionnaireItemOperator) { 271 if (actualAnswer.getValue() instanceof Comparable) { 272 return compareComparable((Comparable<?>) actualAnswer.getValue(), (Comparable<?>) expectedAnswer.getValue(), questionnaireItemOperator); 273 } else if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL) { 274 return actualAnswer.equalsShallow(expectedAnswer); 275 } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) { 276 return !actualAnswer.equalsShallow(expectedAnswer); 277 } 278 throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison"); 279 } 280 281 @SuppressWarnings({"rawtypes", "unchecked"}) 282 private boolean compareComparable(Comparable actual, Comparable expected, 283 QuestionnaireItemOperator questionnaireItemOperator) { 284 int result = actual.compareTo(expected); 285 286 if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL) { 287 return result == 0; 288 } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) { 289 return result != 0; 290 } else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_OR_EQUAL) { 291 return result >= 0; 292 } else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_OR_EQUAL) { 293 return result <= 0; 294 } else if (questionnaireItemOperator == QuestionnaireItemOperator.LESS_THAN) { 295 return result < 0; 296 } else if (questionnaireItemOperator == QuestionnaireItemOperator.GREATER_THAN) { 297 return result > 0; 298 } 299 300 throw new UnprocessableEntityException("Bad operator for PrimitiveType comparison: " + questionnaireItemOperator.toCode()); 301 302 } 303 304 /** 305 * Recursively look for answers to questions with the given link id, working upwards given the context 306 * <p> 307 * For discussion about this, see https://chat.fhir.org/#narrow/stream/179255-questionnaire/topic/enable-when 308 * <p> 309 * - 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 310 * - work up from sourceQ until you find the Q group that also contains targetQ - this is groupQ 311 * - work up from sourceA until you find the QR group that matches groupQ - this is groupA 312 * - any targetA in groupA are input for the enableWhen decision 313 */ 314 private List<Element> findQuestionAnswers(QStack qstack, QuestionnaireItemComponent sourceQ, QuestionnaireItemEnableWhenComponent ew) { 315 QuestionnaireItemComponent targetQ = qstack.getQ().q().getQuestion(ew.getQuestion()); 316 if (targetQ != null) { 317 QuestionnaireItemComponent groupQ = qstack.getQ().q().getCommonGroup(sourceQ, targetQ); 318 if (groupQ == null) { // root is Q itself 319 return findOnItem(qstack.getA(), ew.getQuestion()); 320 } else { 321 for (int i = qstack.size() - 1; i >= 0; i--) { 322 if (qstack.get(i).getQ() == groupQ) { 323 // group A 324 return findOnItem(qstack.get(i).getA(), ew.getQuestion()); 325 } 326 } 327 } 328 } 329 return new ArrayList<>(); 330 } 331 332 private List<Element> findOnItem(Element focus, String question) { 333 List<Element> retVal = new ArrayList<>(); 334 List<Element> items = focus.getChildren(ITEM_ELEMENT); 335 for (Element item : items) { 336 if (hasLinkId(item, question)) { 337 List<Element> answers = extractAnswer(item); 338 retVal.addAll(answers); 339 } 340 retVal.addAll(findOnItem(item, question)); 341 } 342 // didn't find it? look inside the items on the answers too 343 List<Element> answerChildren = focus.getChildren(ANSWER_ELEMENT); 344 for (Element answer : answerChildren) { 345 retVal.addAll(findOnItem(answer, question)); 346 } 347 348 // In case the question with the enableWhen is a direct child of the question with 349 // the answer that it depends on. There is an example of this in the 350 // "BO_ConsDrop" question in this test case: 351 // https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-validation/src/test/resources/dstu3/fmc03-questionnaire.json 352 if (hasLinkId(focus, question)) { 353 List<Element> answers = extractAnswer(focus); 354 retVal.addAll(answers); 355 } 356 357 return retVal; 358 } 359 360 361 private List<Element> extractAnswer(Element item) { 362 return item.getChildrenByName(ANSWER_ELEMENT) 363 .stream() 364 .flatMap(c -> c.getChildren().stream()) 365 .collect(Collectors.toList()); 366 } 367 368 private boolean compareCodingAnswer(Coding expectedAnswer, Coding actualAnswer, QuestionnaireItemOperator questionnaireItemOperator) { 369 boolean result = compareSystems(expectedAnswer, actualAnswer) && compareCodes(expectedAnswer, actualAnswer); 370 if (questionnaireItemOperator == QuestionnaireItemOperator.EQUAL) { 371 return result == true; 372 } else if (questionnaireItemOperator == QuestionnaireItemOperator.NOT_EQUAL) { 373 return result == false; 374 } 375 throw new UnprocessableEntityException("Bad operator for Coding comparison"); 376 } 377 378 private boolean compareCodes(Coding expectedCoding, Coding value) { 379 if (expectedCoding.hasCode() != value.hasCode()) { 380 return false; 381 } 382 if (expectedCoding.hasCode()) { 383 return expectedCoding.getCode().equals(value.getCode()); 384 } 385 return true; 386 } 387 388 private boolean compareSystems(Coding expectedCoding, Coding value) { 389 if (expectedCoding.hasSystem() && !value.hasSystem()) { 390 return false; 391 } 392 if (expectedCoding.hasSystem()) { 393 return expectedCoding.getSystem().equals(value.getSystem()); 394 } 395 return true; 396 } 397 398 private boolean hasLinkId(Element item, String linkId) { 399 Element linkIdChild = item.getNamedChild(LINKID_ELEMENT); 400 if (linkIdChild != null && linkIdChild.getValue().equals(linkId)) { 401 return true; 402 } 403 return false; 404 } 405}