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}