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