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