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}