001package org.hl7.fhir.validation.instance.type;
002
003import static org.apache.commons.lang3.StringUtils.isBlank;
004import static org.apache.commons.lang3.StringUtils.isNotBlank;
005
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.List;
009import java.util.Map;
010
011import org.hl7.fhir.exceptions.FHIRException;
012import org.hl7.fhir.r5.context.IWorkerContext;
013import org.hl7.fhir.r5.context.IWorkerContext.ValidationResult;
014import org.hl7.fhir.r5.elementmodel.Element;
015import org.hl7.fhir.r5.elementmodel.ObjectConverter;
016import org.hl7.fhir.r5.model.Coding;
017import org.hl7.fhir.r5.model.DateType;
018import org.hl7.fhir.r5.model.IntegerType;
019import org.hl7.fhir.r5.model.Questionnaire;
020import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemAnswerOptionComponent;
021import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemComponent;
022import org.hl7.fhir.r5.model.Questionnaire.QuestionnaireItemType;
023import org.hl7.fhir.r5.model.StringType;
024import org.hl7.fhir.r5.model.TimeType;
025import org.hl7.fhir.r5.model.ValueSet;
026import org.hl7.fhir.r5.utils.FHIRPathEngine;
027import org.hl7.fhir.r5.utils.XVerExtensionManager;
028import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier;
029import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.ValidationContextResourceProxy;
030import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
031import org.hl7.fhir.utilities.Utilities;
032import org.hl7.fhir.utilities.VersionUtilities;
033import org.hl7.fhir.utilities.i18n.I18nConstants;
034import org.hl7.fhir.utilities.validation.ValidationMessage;
035import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity;
036import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType;
037import org.hl7.fhir.utilities.validation.ValidationMessage.Source;
038import org.hl7.fhir.utilities.validation.ValidationOptions;
039import org.hl7.fhir.validation.BaseValidator;
040import org.hl7.fhir.validation.TimeTracker;
041import org.hl7.fhir.validation.cli.utils.QuestionnaireMode;
042import org.hl7.fhir.validation.instance.EnableWhenEvaluator;
043import org.hl7.fhir.validation.instance.EnableWhenEvaluator.QStack;
044import org.hl7.fhir.validation.instance.utils.NodeStack;
045import org.hl7.fhir.validation.instance.utils.ValidatorHostContext;
046
047import ca.uhn.fhir.util.ObjectUtil;
048
049public class QuestionnaireValidator extends BaseValidator {
050
051 
052  public class ElementWithIndex {
053
054    private Element element;
055    private int index;
056
057    public ElementWithIndex(Element element, int index) {
058      this.element = element;
059      this.index = index;
060    }
061
062    public Element getElement() {
063      return element;
064    }
065
066    public int getIndex() {
067      return index;
068    }
069
070  }
071
072  public static class QuestionnaireWithContext {
073    private Questionnaire q;
074    private Element container;
075    private String containerPath;
076
077    public static QuestionnaireWithContext fromQuestionnaire(Questionnaire q) {
078      if (q == null) {
079        return null;
080      }
081      QuestionnaireWithContext res = new QuestionnaireWithContext();
082      res.q = q;
083      return res;
084    }
085
086    public static QuestionnaireWithContext fromContainedResource(String path, Element e, Questionnaire q) {
087      if (q == null) {
088        return null;
089      }
090      QuestionnaireWithContext res = new QuestionnaireWithContext();
091      res.q = q;
092      res.container = e;
093      res.containerPath = path;
094      return res;
095    }
096    
097    public Questionnaire q() {
098      return q;
099    }
100
101  }
102
103  private EnableWhenEvaluator myEnableWhenEvaluator;
104  private FHIRPathEngine fpe;
105  private QuestionnaireMode questionnaireMode;
106
107  public QuestionnaireValidator(IWorkerContext context, EnableWhenEvaluator myEnableWhenEvaluator, FHIRPathEngine fpe, TimeTracker timeTracker, QuestionnaireMode questionnaireMode, XVerExtensionManager xverManager, Coding jurisdiction) {
108    super(context, xverManager);
109    source = Source.InstanceValidator;
110    this.myEnableWhenEvaluator = myEnableWhenEvaluator;
111    this.fpe = fpe;
112    this.timeTracker = timeTracker;
113    this.questionnaireMode = questionnaireMode;
114    this.jurisdiction = jurisdiction;
115  }
116
117  public boolean validateQuestionannaire(List<ValidationMessage> errors, Element element, Element element2, NodeStack stack) {
118    ArrayList<Element> parents = new ArrayList<>();
119    parents.add(element);
120    return validateQuestionannaireItem(errors, element, element, stack, parents);    
121  }
122  
123  private boolean validateQuestionannaireItem(List<ValidationMessage> errors, Element element, Element questionnaire, NodeStack stack, List<Element> parents) {
124    boolean ok = true;
125    List<Element> list = getItems(element);
126    for (int i = 0; i < list.size(); i++) {
127      Element e = list.get(i);
128      NodeStack ns = stack.push(e, i, e.getProperty().getDefinition(), e.getProperty().getDefinition());
129      ok = validateQuestionnaireElement(errors, ns, questionnaire, e, parents) && ok;
130      List<Element> np = new ArrayList<Element>();
131      np.add(e);
132      np.addAll(parents);
133      ok = validateQuestionannaireItem(errors, e, questionnaire, ns, np) && ok;
134    }
135    return ok;
136  }
137
138  private boolean validateQuestionnaireElement(List<ValidationMessage> errors, NodeStack ns, Element questionnaire, Element item, List<Element> parents) {
139    boolean ok = true;
140    // R4+
141    if ((VersionUtilities.isR4Plus(context.getVersion())) && (item.hasChildren("enableWhen"))) {
142      List<Element> ewl = item.getChildren("enableWhen");
143      for (Element ew : ewl) {
144        String ql = ew.getNamedChildValue("question");
145        if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), ql != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOLINK)) {
146          Element tgt = getQuestionById(item, ql);
147          if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt == null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_ISINNER)) {
148            tgt = getQuestionById(questionnaire, ql);
149            if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != null, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_NOTARGET, ql, item.getChildValue("linkId"))) {
150              if (rule(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), tgt != item, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_SELF)) {
151                if (!isBefore(item, tgt, parents)) {
152                  warning(errors, NO_RULE_DATE, IssueType.BUSINESSRULE, ns.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_Q_ENABLEWHEN_AFTER, ql);
153                }
154              } else {
155                ok = false;
156              }
157            } else {
158              ok = false;
159            }
160          } else {
161            ok = false;
162          }
163        } else {
164          ok = false;
165        }
166      }
167    }
168    return ok;
169  }
170
171  private boolean isBefore(Element item, Element tgt, List<Element> parents) {
172    // we work up the list, looking for tgt in the children of the parents
173    if (parents.contains(tgt)) {
174      // actually, if the target is a parent, that's automatically ok
175      return true;
176    }
177    for (Element p : parents) {
178      int i = findIndex(p, item);
179      int t = findIndex(p, tgt);
180      if (i > -1 && t > -1) {
181        return i > t;
182      }
183    }
184    return false; // unsure... shouldn't ever get to this point;
185  }
186
187
188  private int findIndex(Element parent, Element descendant) {
189    for (int i = 0; i < parent.getChildren().size(); i++) {
190      if (parent.getChildren().get(i) == descendant || isChild(parent.getChildren().get(i), descendant))
191        return i;
192    }
193    return -1;
194  }
195
196  private boolean isChild(Element element, Element descendant) {
197    for (Element e : element.getChildren()) {
198      if (e == descendant)
199        return true;
200      if (isChild(e, descendant))
201        return true;
202    }
203    return false;
204  }
205
206  private Element getQuestionById(Element focus, String ql) {
207    List<Element> list = getItems(focus);
208    for (Element item : list) {
209      String v = item.getNamedChildValue("linkId");
210      if (ql.equals(v))
211        return item;
212      Element tgt = getQuestionById(item, ql);
213      if (tgt != null)
214        return tgt;
215    }
216    return null;
217
218  }
219
220  private List<Element> getItems(Element element) {
221    List<Element> list = new ArrayList<>();
222    element.getNamedChildren("item", list);
223    return list;
224  }
225
226  public boolean validateQuestionannaireResponse(ValidatorHostContext hostContext, List<ValidationMessage> errors, Element element, NodeStack stack) throws FHIRException {
227    if (questionnaireMode == QuestionnaireMode.NONE) {
228      return true;
229    }
230    boolean ok = true;
231    Element q = element.getNamedChild("questionnaire");
232    String questionnaire = null;
233    if (q != null) {
234      /*
235       * q.getValue() is correct for R4 content, but we'll also accept the second
236       * option just in case we're validating raw STU3 content. Being lenient here
237       * isn't the end of the world since if someone is actually doing the reference
238       * wrong in R4 content it'll get flagged elsewhere by the validator too
239       */
240      if (isNotBlank(q.getValue())) {
241        questionnaire = q.getValue();
242      } else if (isNotBlank(q.getChildValue("reference"))) {
243        questionnaire = q.getChildValue("reference");
244      }
245    }
246    boolean qok;
247    if (questionnaireMode == QuestionnaireMode.REQUIRED) {
248      qok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), questionnaire != null, I18nConstants.QUESTIONNAIRE_QR_Q_NONE);
249      ok = qok;
250    } else {
251      qok = hint(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), questionnaire != null, I18nConstants.QUESTIONNAIRE_QR_Q_NONE);
252    }
253    if (qok) {
254      QuestionnaireWithContext qsrc = null;
255      if (questionnaire.startsWith("#")) {
256        qsrc = QuestionnaireWithContext.fromContainedResource(stack.getLiteralPath(), element, (Questionnaire) loadContainedResource(errors, stack.getLiteralPath(), element, questionnaire.substring(1), Questionnaire.class));        
257      } else {
258        qsrc = QuestionnaireWithContext.fromQuestionnaire(context.fetchResource(Questionnaire.class, questionnaire));          
259      }
260      if (questionnaireMode == QuestionnaireMode.REQUIRED) {
261        qok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, I18nConstants.QUESTIONNAIRE_QR_Q_NOTFOUND, questionnaire);
262        ok = qok && ok;
263      } else if (questionnaire.startsWith("http://example.org") || questionnaire.startsWith("https://example.org")) {
264        qok = hint(errors, NO_RULE_DATE, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, I18nConstants.QUESTIONNAIRE_QR_Q_NOTFOUND, questionnaire);
265      } else {
266        qok = warning(errors, NO_RULE_DATE, IssueType.REQUIRED, q.line(), q.col(), stack.getLiteralPath(), qsrc != null, I18nConstants.QUESTIONNAIRE_QR_Q_NOTFOUND, questionnaire);
267      }
268      if (qok) {
269        boolean inProgress = "in-progress".equals(element.getNamedChildValue("status"));
270        ok = validateQuestionannaireResponseItems(hostContext, qsrc, qsrc.q().getItem(), errors, element, stack, inProgress, element, new QStack(qsrc, element)) && ok;
271      }
272    }
273    return ok;
274  }
275
276  private boolean validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
277    BooleanValue ok = new BooleanValue(true);
278    
279    String text = element.getNamedChildValue("text");
280    ok.see(rule(errors, NO_RULE_DATE, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), Utilities.noString(text) || text.equals(qItem.getText()), I18nConstants.QUESTIONNAIRE_QR_ITEM_TEXT, qItem.getLinkId()));
281
282    List<Element> answers = new ArrayList<Element>();
283    element.getNamedChildren("answer", answers);
284    if (inProgress)
285      warning(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
286    else if (myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe)) {
287      ok.see(rule(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId()));
288    } else if (!answers.isEmpty()) { // items without answers should be allowed, but not items with answers to questions that are disabled
289      // it appears that this is always a duplicate error - it will always already have been reported, so no need to report it again?
290      // GDG 2019-07-13
291//      rule(errors, UNKNOWN_DATE_TIME, IssueType.INVALID, element.line(), element.col(), stack.getLiteralPath(), !isAnswerRequirementFulfilled(qItem, answers), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED, qItem.getLinkId());
292    }
293
294    if (answers.size() > 1) {
295      ok.see(rule(errors, NO_RULE_DATE, IssueType.INVALID, answers.get(1).line(), answers.get(1).col(), stack.getLiteralPath(), qItem.getRepeats(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEA));
296    }
297    
298    int i = 0;
299    for (Element answer : answers) {
300      NodeStack ns = stack.push(answer, i, null, null);
301      if (qItem.getType() != null) {
302        switch (qItem.getType()) {
303          case GROUP:
304            ok.see(rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, answer.line(), answer.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_GROUP));
305            break;
306          case DISPLAY:  // nothing
307            break;
308          case BOOLEAN:
309            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "boolean");
310            break;
311          case DECIMAL:
312            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "decimal");
313            break;
314          case INTEGER:
315            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "integer");
316            break;
317          case DATE:
318            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "date");
319            break;
320          case DATETIME:
321            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "dateTime");
322            break;
323          case TIME:
324            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "time");
325            break;
326          case STRING:
327            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "string");
328            break;
329          case TEXT:
330            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "text");
331            break;
332          case URL:
333            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "uri");
334            break;
335          case ATTACHMENT:
336            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "Attachment");
337            break;
338          case REFERENCE:
339            validateQuestionnaireResponseItemType(errors, answer, ns, ok, "Reference");
340            break;
341          case QUANTITY:
342            if ("Quantity".equals(validateQuestionnaireResponseItemType(errors, answer, ns, ok, "Quantity")))
343              if (qItem.hasExtension("???"))
344                validateQuestionnaireResponseItemQuantity(errors, answer, ns);
345            break;
346          case CODING:
347            String itemType = validateQuestionnaireResponseItemType(errors, answer, ns, ok, "Coding", "date", "time", "integer", "string");
348            if (itemType != null) {
349              if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, false);
350              else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
351              else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
352              else if (itemType.equals("integer"))
353                ok.see(checkOption(errors, answer, ns, qsrc, qItem, "integer"));
354              else if (itemType.equals("string")) checkOption(errors, answer, ns, qsrc, qItem, "string");
355            }
356            break;
357//          case OPENCHOICE:
358//            itemType = validateQuestionnaireResponseItemType(errors, answer, ns, "Coding", "date", "time", "integer", "string");
359//            if (itemType != null) {
360//              if (itemType.equals("Coding")) validateAnswerCode(errors, answer, ns, qsrc, qItem, true);
361//              else if (itemType.equals("date")) checkOption(errors, answer, ns, qsrc, qItem, "date");
362//              else if (itemType.equals("time")) checkOption(errors, answer, ns, qsrc, qItem, "time");
363//              else if (itemType.equals("integer"))
364//                checkOption(errors, answer, ns, qsrc, qItem, "integer");
365//              else if (itemType.equals("string"))
366//                checkOption(errors, answer, ns, qsrc, qItem, "string", true);
367//            }
368//            break;
369//          case QUESTION:
370          case NULL:
371            // no validation
372            break;
373        case QUESTION:
374          throw new Error("Shouldn't get here?");
375        }
376      }
377      if (qItem.getType() != QuestionnaireItemType.GROUP) {
378        // if it's a group, we already have an error before getting here, so no need to hammer away on that 
379        validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, answer, stack, inProgress, questionnaireResponseRoot, qstack);
380      }
381      i++;
382    }
383    if (qItem.getType() == null) {
384      ok.see(fail(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTYPE, qItem.getLinkId()));
385    } else if (qItem.getType() == QuestionnaireItemType.DISPLAY) {
386      List<Element> items = new ArrayList<Element>();
387      element.getNamedChildren("item", items);
388      ok.see(rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_DISPLAY, qItem.getLinkId()));
389    } else if (qItem.getType() != QuestionnaireItemType.GROUP) {
390      List<Element> items = new ArrayList<Element>();
391      element.getNamedChildren("item", items);
392      ok.see(rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, element.line(), element.col(), stack.getLiteralPath(), items.isEmpty(), I18nConstants.QUESTIONNAIRE_QR_ITEM_GROUP_ANSWER, qItem.getLinkId()));
393    } else {
394      ok.see(validateQuestionannaireResponseItems(hostContext, qsrc, qItem.getItem(), errors, element, stack, inProgress, questionnaireResponseRoot, qstack));
395    }
396    return ok.isValue();
397  }
398
399  private boolean isAnswerRequirementFulfilled(QuestionnaireItemComponent qItem, List<Element> answers) {
400    return !answers.isEmpty() || !qItem.getRequired() || qItem.getType() == QuestionnaireItemType.GROUP;
401  }
402
403  private boolean validateQuestionnaireResponseItem(ValidatorHostContext hostcontext, QuestionnaireWithContext qsrc, QuestionnaireItemComponent qItem, List<ValidationMessage> errors, List<ElementWithIndex> elements, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
404    boolean ok = true;
405    if (elements.size() > 1) {
406      ok = rulePlural(errors, NO_RULE_DATE, IssueType.INVALID, elements.get(1).getElement().line(), elements.get(1).getElement().col(), stack.getLiteralPath(), qItem.getRepeats(), elements.size(), I18nConstants.QUESTIONNAIRE_QR_ITEM_ONLYONEI, qItem.getLinkId()) && ok;
407    }
408    for (ElementWithIndex element : elements) {
409      NodeStack ns = stack.push(element.getElement(), element.getIndex(), null, null);
410      ok = validateQuestionnaireResponseItem(hostcontext, qsrc, qItem, errors, element.getElement(), ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, element.getElement())) && ok;
411    }
412    return ok;
413  }
414
415  private int getLinkIdIndex(List<QuestionnaireItemComponent> qItems, String linkId) {
416    for (int i = 0; i < qItems.size(); i++) {
417      if (linkId.equals(qItems.get(i).getLinkId()))
418        return i;
419    }
420    return -1;
421  }
422
423  private boolean validateQuestionannaireResponseItems(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List<QuestionnaireItemComponent> qItems, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QStack qstack) {
424    boolean ok = true;
425    List<Element> items = new ArrayList<Element>();
426    element.getNamedChildren("item", items);
427    // now, sort into stacks
428    Map<String, List<ElementWithIndex>> map = new HashMap<String, List<ElementWithIndex>>();
429    int lastIndex = -1;
430    int counter = 0;
431    for (Element item : items) {
432      String linkId = item.getNamedChildValue("linkId");
433      if (rule(errors, NO_RULE_DATE, IssueType.REQUIRED, item.line(), item.col(), stack.getLiteralPath(), !Utilities.noString(linkId), I18nConstants.QUESTIONNAIRE_QR_ITEM_NOLINKID)) {
434        int index = getLinkIdIndex(qItems, linkId);
435        if (index == -1) {
436          QuestionnaireItemComponent qItem = findQuestionnaireItem(qsrc, linkId);
437          if (qItem != null) {
438            ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index > -1, misplacedItemError(qItem)) && ok;
439            NodeStack ns = stack.push(item, counter, null, null);
440            ok = validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, item, ns, inProgress, questionnaireResponseRoot, qstack.push(qItem, item)) && ok;
441          } else
442            ok = rule(errors, NO_RULE_DATE, IssueType.NOTFOUND, item.line(), item.col(), stack.getLiteralPath(), index > -1, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTFOUND, linkId) && ok;
443        } else {
444          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, item.line(), item.col(), stack.getLiteralPath(), index >= lastIndex, I18nConstants.QUESTIONNAIRE_QR_ITEM_ORDER) && ok;
445          lastIndex = index;
446
447          // If an item has a child called "linkId" but no child called "answer",
448          // we'll treat it as not existing for the purposes of enableWhen validation
449          if (item.hasChildren("answer") || item.hasChildren("item")) {
450            List<ElementWithIndex> mapItem = map.computeIfAbsent(linkId, key -> new ArrayList<>());
451            mapItem.add(new ElementWithIndex(item, counter));
452          }
453        }
454      } else {
455        ok = false;
456      }
457      counter++;
458    }
459
460    // ok, now we have a list of known items, grouped by linkId. We've made an error for anything out of order
461    for (QuestionnaireItemComponent qItem : qItems) {
462      List<ElementWithIndex> mapItem = map.get(qItem.getLinkId());
463      ok = validateQuestionnaireResponseItem(hostContext, qsrc, errors, element, stack, inProgress, questionnaireResponseRoot, qItem, mapItem, qstack) && ok;
464    }
465    return ok;
466  }
467
468  public boolean validateQuestionnaireResponseItem(ValidatorHostContext hostContext, QuestionnaireWithContext qsrc, List<ValidationMessage> errors, Element element, NodeStack stack, boolean inProgress, Element questionnaireResponseRoot, QuestionnaireItemComponent qItem, List<ElementWithIndex> mapItem, QStack qstack) {
469    boolean ok = true;
470    boolean enabled = myEnableWhenEvaluator.isQuestionEnabled(hostContext, qItem, qstack, fpe);
471    if (mapItem != null) {
472      if (!enabled) {
473        for (ElementWithIndex e : mapItem) {
474          NodeStack ns = stack.push(e.getElement(), e.getElement().getIndex(), e.getElement().getProperty().getDefinition(), e.getElement().getProperty().getDefinition());
475          ok = rule(errors, NO_RULE_DATE, IssueType.INVALID, e.getElement().line(), e.getElement().col(), ns.getLiteralPath(), enabled, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTENABLED2, qItem.getLinkId()) && ok;
476        }
477      }
478
479      // Recursively validate child items
480      ok = validateQuestionnaireResponseItem(hostContext, qsrc, qItem, errors, mapItem, stack, inProgress, questionnaireResponseRoot, qstack) && ok;
481
482    } else {
483
484      // item is missing, is the question enabled?
485      if (enabled && qItem.getRequired()) {
486        String message = context.formatMessage(I18nConstants.QUESTIONNAIRE_QR_ITEM_MISSING, qItem.getLinkId());
487        if (inProgress) {
488          warning(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message);
489        } else {
490          ok = rule(errors, NO_RULE_DATE, IssueType.REQUIRED, element.line(), element.col(), stack.getLiteralPath(), false, message) && ok;
491        }
492      }
493
494    }
495    return ok;
496  }
497
498  private String misplacedItemError(QuestionnaireItemComponent qItem) {
499    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";
500  }
501
502  private void validateQuestionnaireResponseItemQuantity(List<ValidationMessage> errors, Element answer, NodeStack stack) {
503
504  }
505
506  private String validateQuestionnaireResponseItemType(List<ValidationMessage> errors, Element element, NodeStack stack, BooleanValue ok, String... types) {
507    List<Element> values = new ArrayList<Element>();
508    element.getNamedChildrenWithWildcard("value[x]", values);
509    for (int i = 0; i < types.length; i++) {
510      if (types[i].equals("text")) {
511        types[i] = "string";
512      }
513    }
514    if (values.size() > 0) {
515      NodeStack ns = stack.push(values.get(0), -1, null, null);
516      CommaSeparatedStringBuilder l = new CommaSeparatedStringBuilder();
517      for (String s : types) {
518        l.append(s);
519        if (values.get(0).getName().equals("value" + Utilities.capitalize(s)))
520          return (s);
521      }
522
523      ok.see(rulePlural(errors, NO_RULE_DATE, IssueType.STRUCTURE, values.get(0).line(), values.get(0).col(), ns.getLiteralPath(), false, types.length, I18nConstants.QUESTIONNAIRE_QR_ITEM_WRONGTYPE, l.toString()));
524    }
525    return null;
526  }
527
528  private QuestionnaireItemComponent findQuestionnaireItem(QuestionnaireWithContext qSrc, String linkId) {
529    return findItem(qSrc.q.getItem(), linkId);
530  }
531
532  private QuestionnaireItemComponent findItem(List<QuestionnaireItemComponent> list, String linkId) {
533    for (QuestionnaireItemComponent item : list) {
534      if (linkId.equals(item.getLinkId()))
535        return item;
536      QuestionnaireItemComponent result = findItem(item.getItem(), linkId);
537      if (result != null)
538        return result;
539    }
540    return null;
541  }
542
543  private boolean validateAnswerCode(List<ValidationMessage> errors, Element value, NodeStack stack, QuestionnaireWithContext qSrc, String ref, boolean theOpenChoice) {
544    boolean ok = true;
545    ValueSet vs = null;
546    if (ref.startsWith("#") && qSrc.container != null) {
547      vs = (ValueSet) loadContainedResource(errors, qSrc.containerPath, qSrc.container, ref.substring(1), ValueSet.class);
548    } else {
549      vs = resolveBindingReference(qSrc.q(), ref, qSrc.q().getUrl(), qSrc.q());
550    }
551    if (warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), vs != null, I18nConstants.TERMINOLOGY_TX_VALUESET_NOTFOUND, describeReference(ref))) {
552      try {
553        Coding c = ObjectConverter.readAsCoding(value);
554        if (isBlank(c.getCode()) && isBlank(c.getSystem()) && isNotBlank(c.getDisplay())) {
555          if (theOpenChoice) {
556            return ok;
557          }
558        }
559
560        long t = System.nanoTime();
561        ValidationContextCarrier vc = makeValidationContext(errors, qSrc);
562        ValidationResult res = context.validateCode(new ValidationOptions(stack.getWorkingLang()), c, vs, vc);
563        timeTracker.tx(t, "vc "+c.getSystem()+"#"+c.getCode()+" '"+c.getDisplay()+"'");
564        if (!res.isOk()) {
565          ok = txRule(errors, NO_RULE_DATE, res.getTxLink(), IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_BADOPTION, c.getSystem(), c.getCode()) && ok;
566        } else if (res.getSeverity() != null) {
567          super.addValidationMessage(errors, NO_RULE_DATE, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity(), Source.TerminologyEngine, null);
568        } else if (res.getMessage() != null) {
569          super.addValidationMessage(errors, NO_RULE_DATE, IssueType.INFORMATIONAL, value.line(), value.col(), stack.getLiteralPath(), res.getMessage(), res.getSeverity() == null ? IssueSeverity.INFORMATION : res.getSeverity(), Source.TerminologyEngine, null);          
570        }
571      } catch (Exception e) {
572        warning(errors, NO_RULE_DATE, IssueType.CODEINVALID, value.line(), value.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_CODING, e.getMessage());
573      }
574    }
575    return ok;
576  }
577
578  private ValidationContextCarrier makeValidationContext(List<ValidationMessage> errors, QuestionnaireWithContext qSrc) {
579    ValidationContextCarrier vc = new ValidationContextCarrier();
580    if (qSrc.container == null) {
581      vc.getResources().add(new ValidationContextResourceProxy(qSrc.q));
582    } else {
583      vc.getResources().add(new ValidationContextResourceProxy(errors, qSrc.containerPath, qSrc.container, this));
584    }
585    return vc;
586  }
587
588  private boolean validateAnswerCode(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean theOpenChoice) {
589    Element v = answer.getNamedChild("valueCoding");
590    NodeStack ns = stack.push(v, -1, null, null);
591    if (qItem.getAnswerOption().size() > 0)
592      checkCodingOption(errors, answer, stack, qSrc, qItem, theOpenChoice);
593      //      validateAnswerCode(errors, v, stack, qItem.getOption());
594    else if (qItem.hasAnswerValueSet())
595      return validateAnswerCode(errors, v, stack, qSrc, qItem.getAnswerValueSet(), theOpenChoice);
596    else
597      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONS);
598    return true;
599  }
600
601  private boolean checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type) {
602    return checkOption(errors, answer, stack, qSrc, qItem, type, false);
603  }
604
605  private boolean checkOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, String type, boolean openChoice) {
606    if (type.equals("integer")) return checkIntegerOption(errors, answer, stack, qSrc, qItem, openChoice);
607    else if (type.equals("date")) return checkDateOption(errors, answer, stack, qSrc, qItem, openChoice);
608    else if (type.equals("time")) return checkTimeOption(errors, answer, stack, qSrc, qItem, openChoice);
609    else if (type.equals("string")) return checkStringOption(errors, answer, stack, qSrc, qItem, openChoice);
610    else if (type.equals("Coding")) return checkCodingOption(errors, answer, stack, qSrc, qItem, openChoice);
611    return true;
612  }
613
614  private boolean checkIntegerOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
615    boolean ok = true;
616    Element v = answer.getNamedChild("valueInteger");
617    NodeStack ns = stack.push(v, -1, null, null);
618    if (qItem.getAnswerOption().size() > 0) {
619      List<IntegerType> list = new ArrayList<IntegerType>();
620      for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
621        try {
622          list.add(components.getValueIntegerType());
623        } catch (FHIRException e) {
624          // If it's the wrong type, just keep going
625        }
626      }
627      if (list.isEmpty() && !openChoice) {
628        ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSINTEGER) && ok;
629      } else {
630        boolean found = false;
631        for (IntegerType item : list) {
632          if (item.getValue() == Integer.parseInt(v.primitiveValue())) {
633            found = true;
634            break;
635          }
636        }
637        if (!found) {
638          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOINTEGER, v.primitiveValue()) && ok;
639        }
640      }
641    } else {
642      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_INTNOOPTIONS);
643    }
644    return ok;
645  }
646
647  private boolean checkDateOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
648    boolean ok = true;
649    Element v = answer.getNamedChild("valueDate");
650    NodeStack ns = stack.push(v, -1, null, null);
651    if (qItem.getAnswerOption().size() > 0) {
652      List<DateType> list = new ArrayList<DateType>();
653      for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
654        try {
655          list.add(components.getValueDateType());
656        } catch (FHIRException e) {
657          // If it's the wrong type, just keep going
658        }
659      }
660      if (list.isEmpty() && !openChoice) {
661        ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSDATE) && ok;
662      } else {
663        boolean found = false;
664        for (DateType item : list) {
665          if (item.getValue().equals(v.primitiveValue())) {
666            found = true;
667            break;
668          }
669        }
670        if (!found) {
671          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NODATE, v.primitiveValue()) && ok;
672        }
673      }
674    } else {
675      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_DATENOOPTIONS);
676    }
677    return ok;
678  }
679
680  private boolean checkTimeOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
681    boolean ok = true;
682    Element v = answer.getNamedChild("valueTime");
683    NodeStack ns = stack.push(v, -1, null, null);
684    if (qItem.getAnswerOption().size() > 0) {
685      List<TimeType> list = new ArrayList<TimeType>();
686      for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
687        try {
688          list.add(components.getValueTimeType());
689        } catch (FHIRException e) {
690          // If it's the wrong type, just keep going
691        }
692      }
693      if (list.isEmpty() && !openChoice) {
694        ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSTIME) && ok;
695      } else {
696        boolean found = false;
697        for (TimeType item : list) {
698          if (item.getValue().equals(v.primitiveValue())) {
699            found = true;
700            break;
701          }
702        }
703        if (!found) {
704          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOTIME, v.primitiveValue()) && ok;
705        }
706      }
707    } else {
708      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_TIMENOOPTIONS);
709    }
710    return ok;
711  }
712
713  private boolean checkStringOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
714    boolean ok = true;
715    Element v = answer.getNamedChild("valueString");
716    NodeStack ns = stack.push(v, -1, null, null);
717    if (qItem.getAnswerOption().size() > 0) {
718      List<StringType> list = new ArrayList<StringType>();
719      for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
720        try {
721          if (components.getValue() != null) {
722            list.add(components.getValueStringType());
723          }
724        } catch (FHIRException e) {
725          // If it's the wrong type, just keep going
726        }
727      }
728      if (!openChoice) {
729        if (list.isEmpty()) {
730          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSSTRING) && ok;
731        } else {
732          boolean found = false;
733          for (StringType item : list) {
734            if (item.getValue().equals((v.primitiveValue()))) {
735              found = true;
736              break;
737            }
738          }
739          if (!found) {
740            ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOSTRING, v.primitiveValue()) && ok;
741          }
742        }
743      }
744    } else {
745      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_STRINGNOOPTIONS);
746    }
747    return ok;
748  }
749
750  private boolean checkCodingOption(List<ValidationMessage> errors, Element answer, NodeStack stack, QuestionnaireWithContext qSrc, QuestionnaireItemComponent qItem, boolean openChoice) {
751    boolean ok = true;
752    
753    Element v = answer.getNamedChild("valueCoding");
754    String system = v.getNamedChildValue("system");
755    String code = v.getNamedChildValue("code");
756    NodeStack ns = stack.push(v, -1, null, null);
757    if (qItem.getAnswerOption().size() > 0) {
758      List<Coding> list = new ArrayList<Coding>();
759      for (QuestionnaireItemAnswerOptionComponent components : qItem.getAnswerOption()) {
760        try {
761          if (components.getValue() != null) {
762            list.add(components.getValueCoding());
763          }
764        } catch (FHIRException e) {
765          // If it's the wrong type, just keep going
766        }
767      }
768      if (list.isEmpty() && !openChoice) {
769        ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOOPTIONSCODING) && ok;
770      } else {
771        boolean found = false;
772        for (Coding item : list) {
773          if (ObjectUtil.equals(item.getSystem(), system) && ObjectUtil.equals(item.getCode(), code)) {
774            found = true;
775            break;
776          }
777        }
778        if (!found) {
779          ok = rule(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), found, I18nConstants.QUESTIONNAIRE_QR_ITEM_NOCODING, system, code) && ok;
780        }
781      }
782    } else {
783      hint(errors, NO_RULE_DATE, IssueType.STRUCTURE, v.line(), v.col(), stack.getLiteralPath(), false, I18nConstants.QUESTIONNAIRE_QR_ITEM_CODINGNOOPTIONS);
784    }
785    return ok;
786  }
787
788
789}