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}