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}