001package org.hl7.fhir.validation; 002 003 004/* 005 Copyright (c) 2011+, HL7, Inc. 006 All rights reserved. 007 008 Redistribution and use in source and binary forms, with or without modification, 009 are permitted provided that the following conditions are met: 010 011 * Redistributions of source code must retain the above copyright notice, this 012 list of conditions and the following disclaimer. 013 * Redistributions in binary form must reproduce the above copyright notice, 014 this list of conditions and the following disclaimer in the documentation 015 and/or other materials provided with the distribution. 016 * Neither the name of HL7 nor the names of its contributors may be used to 017 endorse or promote products derived from this software without specific 018 prior written permission. 019 020 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 021 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 022 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 023 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 024 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 025 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 026 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 027 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 028 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 029 POSSIBILITY OF SUCH DAMAGE. 030 031 */ 032 033import org.apache.commons.lang3.StringUtils; 034import org.hl7.fhir.convertors.factory.VersionConvertorFactory_10_50; 035import org.hl7.fhir.convertors.factory.VersionConvertorFactory_14_50; 036import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_50; 037import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; 038import org.hl7.fhir.exceptions.FHIRException; 039import org.hl7.fhir.r5.context.IWorkerContext; 040import org.hl7.fhir.r5.elementmodel.Element; 041import org.hl7.fhir.r5.elementmodel.JsonParser; 042import org.hl7.fhir.r5.formats.IParser.OutputStyle; 043import org.hl7.fhir.r5.model.*; 044import org.hl7.fhir.r5.terminologies.ValueSetUtilities; 045import org.hl7.fhir.r5.utils.XVerExtensionManager; 046import org.hl7.fhir.r5.utils.XVerExtensionManager.XVerExtensionStatus; 047import org.hl7.fhir.r5.utils.validation.ValidationContextCarrier.IValidationContextResourceLoader; 048import org.hl7.fhir.utilities.Utilities; 049import org.hl7.fhir.utilities.i18n.I18nConstants; 050import org.hl7.fhir.utilities.validation.ValidationMessage; 051import org.hl7.fhir.utilities.validation.ValidationMessage.IssueSeverity; 052import org.hl7.fhir.utilities.validation.ValidationMessage.IssueType; 053import org.hl7.fhir.utilities.validation.ValidationMessage.Source; 054import org.hl7.fhir.validation.cli.utils.ValidationLevel; 055import org.hl7.fhir.validation.instance.utils.IndexedElement; 056 057import java.io.ByteArrayOutputStream; 058import java.io.IOException; 059import java.util.ArrayList; 060import java.util.HashMap; 061import java.util.List; 062import java.util.Map; 063 064import static org.apache.commons.lang3.StringUtils.isBlank; 065 066public class BaseValidator implements IValidationContextResourceLoader { 067 068 public class TrackedLocationRelatedMessage { 069 private Object location; 070 private ValidationMessage vmsg; 071 public TrackedLocationRelatedMessage(Object location, ValidationMessage vmsg) { 072 super(); 073 this.location = location; 074 this.vmsg = vmsg; 075 } 076 public Object getLocation() { 077 return location; 078 } 079 public ValidationMessage getVmsg() { 080 return vmsg; 081 } 082 } 083 084 public class ValidationControl { 085 private boolean allowed; 086 private IssueSeverity level; 087 088 public ValidationControl(boolean allowed, IssueSeverity level) { 089 super(); 090 this.allowed = allowed; 091 this.level = level; 092 } 093 public boolean isAllowed() { 094 return allowed; 095 } 096 public IssueSeverity getLevel() { 097 return level; 098 } 099 } 100 101 protected final String META = "meta"; 102 protected final String ENTRY = "entry"; 103 protected final String LINK = "link"; 104 protected final String DOCUMENT = "document"; 105 protected final String RESOURCE = "resource"; 106 protected final String MESSAGE = "message"; 107 protected final String SEARCHSET = "searchset"; 108 protected final String ID = "id"; 109 protected final String FULL_URL = "fullUrl"; 110 protected final String PATH_ARG = ":0"; 111 protected final String TYPE = "type"; 112 protected final String BUNDLE = "Bundle"; 113 protected final String LAST_UPDATED = "lastUpdated"; 114 115 116 protected Source source; 117 protected IWorkerContext context; 118 protected TimeTracker timeTracker = new TimeTracker(); 119 protected XVerExtensionManager xverManager; 120 protected List<TrackedLocationRelatedMessage> trackedMessages = new ArrayList<>(); 121 protected List<ValidationMessage> messagesToRemove = new ArrayList<>(); 122 private ValidationLevel level = ValidationLevel.HINTS; 123 124 public BaseValidator(IWorkerContext context, XVerExtensionManager xverManager) { 125 super(); 126 this.context = context; 127 this.xverManager = xverManager; 128 if (this.xverManager == null) { 129 this.xverManager = new XVerExtensionManager(context); 130 } 131 132 } 133 134 private boolean doingLevel(IssueSeverity error) { 135 switch (error) { 136 case ERROR: 137 return level == null || level == ValidationLevel.ERRORS || level == ValidationLevel.WARNINGS || level == ValidationLevel.HINTS; 138 case FATAL: 139 return level == null || level == ValidationLevel.ERRORS || level == ValidationLevel.WARNINGS || level == ValidationLevel.HINTS; 140 case WARNING: 141 return level == null || level == ValidationLevel.WARNINGS || level == ValidationLevel.HINTS; 142 case INFORMATION: 143 return level == null || level == ValidationLevel.HINTS; 144 case NULL: 145 return true; 146 default: 147 return true; 148 } 149 } 150 151 private boolean doingErrors() { 152 return doingLevel(IssueSeverity.ERROR); 153 } 154 155 private boolean doingWarnings() { 156 return doingLevel(IssueSeverity.WARNING); 157 } 158 159 private boolean doingHints() { 160 return doingLevel(IssueSeverity.INFORMATION); 161 } 162 163 164 /** 165 * Use to control what validation the validator performs. 166 * Using this, you can turn particular kinds of validation on and off 167 * In addition, you can override the error | warning | hint level and make it a different level 168 * 169 * There is no way to do this using the command line validator; it's a service that is only 170 * offered when the validator is hosted in some other process 171 */ 172 private Map<String, ValidationControl> validationControl = new HashMap<>(); 173 174 /** 175 * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails 176 * 177 * @param thePass 178 * Set this parameter to <code>false</code> if the validation does not pass 179 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 180 */ 181 @Deprecated 182 protected boolean fail(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { 183 if (!thePass && doingErrors()) { 184 addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.FATAL, null); 185 } 186 return thePass; 187 } 188 189 protected boolean fail(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 190 if (!thePass && doingErrors()) { 191 String msg = context.formatMessage(theMessage, theMessageArguments); 192 addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.FATAL, theMessage); 193 } 194 return thePass; 195 } 196 197 /** 198 * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails 199 * 200 * @param thePass 201 * Set this parameter to <code>false</code> if the validation does not pass 202 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 203 */ 204 @Deprecated 205 protected boolean fail(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String msg) { 206 if (!thePass && doingErrors()) { 207 String path = toPath(pathParts); 208 addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.FATAL, null); 209 } 210 return thePass; 211 } 212 213 /** 214 * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails 215 * 216 * @param thePass 217 * Set this parameter to <code>false</code> if the validation does not pass 218 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 219 */ 220 @Deprecated 221 protected boolean fail(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { 222 if (!thePass && doingErrors()) { 223 String path = toPath(pathParts); 224 addValidationMessage(errors, type, -1, -1, path, context.formatMessage(theMessage, theMessageArguments), IssueSeverity.FATAL, theMessage); 225 } 226 return thePass; 227 } 228 229 /** 230 * Test a rule and add a {@link IssueSeverity#FATAL} validation message if the validation fails 231 * 232 * @param thePass 233 * Set this parameter to <code>false</code> if the validation does not pass 234 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 235 */ 236 @Deprecated 237 protected boolean fail(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) { 238 if (!thePass && doingErrors()) { 239 addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.FATAL, null); 240 } 241 return thePass; 242 } 243 //TODO: i18n 244 protected boolean grammarWord(String w) { 245 return w.equals("and") || w.equals("or") || w.equals("a") || w.equals("the") || w.equals("for") || w.equals("this") || w.equals("that") || w.equals("of"); 246 } 247 248 /** 249 * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails 250 * 251 * @param thePass 252 * Set this parameter to <code>false</code> if the validation does not pass 253 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 254 */ 255 protected boolean hint(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg) { 256 if (!thePass && doingHints()) { 257 String message = context.formatMessage(msg); 258 addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION, msg); 259 } 260 return thePass; 261 } 262 263 /** 264 * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails. And mark it as a slicing hint for later recovery if appropriate 265 * 266 * @param thePass 267 * Set this parameter to <code>false</code> if the validation does not pass 268 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 269 */ 270 //FIXME: formatMessage should be done here 271 protected boolean slicingHint(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, boolean isCritical, String msg, String html, String[] text) { 272 if (!thePass && doingHints()) { 273 addValidationMessage(errors, type, line, col, path, msg, IssueSeverity.INFORMATION, null).setSlicingHint(true).setSliceHtml(html, text).setCriticalSignpost(isCritical); 274 } 275 return thePass; 276 } 277 278 /** 279 * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails 280 * 281 * @param thePass 282 * Set this parameter to <code>false</code> if the validation does not pass 283 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 284 */ 285 protected boolean hint(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 286 if (!thePass && doingHints()) { 287 String message = context.formatMessage(theMessage, theMessageArguments); 288 addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION, theMessage); 289 } 290 return thePass; 291 } 292 293 protected ValidationMessage signpost(List<ValidationMessage> errors, IssueType type, int line, int col, String path, String theMessage, Object... theMessageArguments) { 294 String message = context.formatMessage(theMessage, theMessageArguments); 295 return addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION, theMessage).setSignpost(true); 296 } 297 298 protected boolean txHint(List<ValidationMessage> errors, String txLink, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 299 if (!thePass && doingHints()) { 300 String message = context.formatMessage(theMessage, theMessageArguments); 301 addValidationMessage(errors, type, line, col, path, message, IssueSeverity.INFORMATION, Source.TerminologyEngine, theMessage).setTxLink(txLink); 302 } 303 return thePass; 304 } 305 306 /** 307 * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails 308 * 309 * @param thePass 310 * Set this parameter to <code>false</code> if the validation does not pass 311 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 312 */ 313 protected boolean hint(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { 314 if (!thePass && doingHints()) { 315 String path = toPath(pathParts); 316 String message = context.formatMessage(theMessage, theMessageArguments); 317 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION, theMessage); 318 } 319 return thePass; 320 } 321 322 /** 323 * Test a rule and add a {@link IssueSeverity#INFORMATION} validation message if the validation fails 324 * 325 * @param thePass 326 * Set this parameter to <code>false</code> if the validation does not pass 327 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 328 */ 329 protected boolean hint(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 330 if (!thePass && doingHints()) { 331 String message = context.formatMessage(theMessage, theMessageArguments); 332 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION, null); 333 } 334 return thePass; 335 } 336 337 /** 338 * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails 339 * 340 * @param thePass 341 * Set this parameter to <code>false</code> if the validation does not pass 342 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 343 */ 344 protected boolean rule(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 345 if (!thePass && doingErrors()) { 346 String message = context.formatMessage(theMessage, theMessageArguments); 347 addValidationMessage(errors, type, line, col, path, message, IssueSeverity.ERROR, theMessage); 348 } 349 return thePass; 350 } 351 352 protected boolean txRule(List<ValidationMessage> errors, String txLink, IssueType type, int line, int col, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 353 if (!thePass && doingErrors()) { 354 String message = context.formatMessage(theMessage, theMessageArguments); 355 ValidationMessage vm = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, message, IssueSeverity.ERROR).setMessageId(theMessage); 356 if (checkMsgId(theMessage, vm)) { 357 errors.add(vm.setTxLink(txLink)); 358 } 359 } 360 return thePass; 361 } 362 363 /** 364 * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails 365 * 366 * @param thePass 367 * Set this parameter to <code>false</code> if the validation does not pass 368 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 369 */ 370 protected boolean rule(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String msg) { 371 if (!thePass && doingErrors()) { 372 String path = toPath(pathParts); 373 addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.ERROR, null); 374 } 375 return thePass; 376 } 377 378 /** 379 * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails 380 * 381 * @param thePass 382 * Set this parameter to <code>false</code> if the validation does not pass 383 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 384 */ 385 protected boolean rule(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { 386 if (!thePass && doingErrors()) { 387 String path = toPath(pathParts); 388 String message = context.formatMessage(theMessage, theMessageArguments); 389 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.ERROR, theMessage); 390 } 391 return thePass; 392 } 393 394 /** 395 * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails 396 * 397 * @param thePass 398 * Set this parameter to <code>false</code> if the validation does not pass 399 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 400 */ 401 402 403 protected boolean rule(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String theMessage, Object... theMessageArguments) { 404 if (!thePass && doingErrors()) { 405 String message = context.formatMessage(theMessage, theMessageArguments); 406 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.ERROR, theMessage); 407 } 408 return thePass; 409 } 410 411 public boolean rule(List<ValidationMessage> errors, Source source, IssueType type, String path, boolean thePass, String msg) { 412 if (!thePass && doingErrors()) { 413 addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.ERROR, source, null); 414 } 415 return thePass; 416 } 417 418 /** 419 * Test a rule and add a {@link IssueSeverity#ERROR} validation message if the validation fails 420 * 421 * @param thePass 422 * Set this parameter to <code>false</code> if the validation does not pass 423 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 424 */ 425 protected boolean ruleHtml(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html) { 426 if (!thePass && doingErrors()) { 427 msg = context.formatMessage(msg, null); 428 html = context.formatMessage(html, null); 429 addValidationMessage(errors, type, path, msg, html, IssueSeverity.ERROR, null); 430 } 431 return thePass; 432 } 433 434 protected String splitByCamelCase(String s) { 435 StringBuilder b = new StringBuilder(); 436 for (int i = 0; i < s.length(); i++) { 437 char c = s.charAt(i); 438 if (Character.isUpperCase(c) && !(i == 0 || Character.isUpperCase(s.charAt(i-1)))) 439 b.append(' '); 440 b.append(c); 441 } 442 return b.toString(); 443 } 444 445 protected String stripPunctuation(String s, boolean numbers) { 446 StringBuilder b = new StringBuilder(); 447 for (char c : s.toCharArray()) { 448 int t = Character.getType(c); 449 if (t == Character.UPPERCASE_LETTER || t == Character.LOWERCASE_LETTER || t == Character.TITLECASE_LETTER || t == Character.MODIFIER_LETTER || t == Character.OTHER_LETTER || (t == Character.DECIMAL_DIGIT_NUMBER && numbers) || (t == Character.LETTER_NUMBER && numbers) || c == ' ') 450 b.append(c); 451 } 452 return b.toString(); 453 } 454 455 private String toPath(List<String> pathParts) { 456 if (pathParts == null || pathParts.isEmpty()) { 457 return ""; 458 } 459 return "//" + StringUtils.join(pathParts, '/'); 460 } 461 462 /** 463 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 464 * 465 * @param thePass 466 * Set this parameter to <code>false</code> if the validation does not pass 467 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 468 */ 469 protected boolean warning(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { 470 if (!thePass && doingWarnings()) { 471 String nmsg = context.formatMessage(msg, theMessageArguments); 472 IssueSeverity severity = IssueSeverity.WARNING; 473 addValidationMessage(errors, type, line, col, path, nmsg, severity, msg); 474 } 475 return thePass; 476 477 } 478 479 protected ValidationMessage addValidationMessage(List<ValidationMessage> errors, IssueType type, int line, int col, String path, String msg, IssueSeverity theSeverity, String id) { 480 Source source = this.source; 481 return addValidationMessage(errors, type, line, col, path, msg, theSeverity, source, id); 482 } 483 484 protected ValidationMessage addValidationMessage(List<ValidationMessage> errors, IssueType type, int line, int col, String path, String msg, IssueSeverity theSeverity, Source theSource, String id) { 485 ValidationMessage validationMessage = new ValidationMessage(theSource, type, line, col, path, msg, theSeverity).setMessageId(id); 486 if (doingLevel(theSeverity) && checkMsgId(id, validationMessage)) { 487 errors.add(validationMessage); 488 } 489 return validationMessage; 490 } 491 492 public boolean checkMsgId(String id, ValidationMessage vm) { 493 if (id != null && validationControl.containsKey(id)) { 494 ValidationControl control = validationControl.get(id); 495 if (control.level != null) { 496 vm.setLevel(control.level); 497 } 498 return control.isAllowed(); 499 } 500 return true; 501 } 502 503 /** 504 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 505 * 506 * @param thePass 507 * Set this parameter to <code>false</code> if the validation does not pass 508 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 509 */ 510 protected boolean txWarning(List<ValidationMessage> errors, String txLink, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { 511 if (!thePass && doingWarnings()) { 512 String nmsg = context.formatMessage(msg, theMessageArguments); 513 ValidationMessage vmsg = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, nmsg, IssueSeverity.WARNING).setTxLink(txLink).setMessageId(msg); 514 if (checkMsgId(msg, vmsg)) { 515 errors.add(vmsg); 516 } 517 } 518 return thePass; 519 520 } 521 522 /** 523 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails. Also, keep track of it later in case we want to remove it if we find a required binding for this element later 524 * 525 * @param thePass 526 * Set this parameter to <code>false</code> if the validation does not pass 527 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 528 */ 529 protected boolean txWarningForLaterRemoval(Object location, List<ValidationMessage> errors, String txLink, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { 530 if (!thePass && doingWarnings()) { 531 String nmsg = context.formatMessage(msg, theMessageArguments); 532 ValidationMessage vmsg = new ValidationMessage(Source.TerminologyEngine, type, line, col, path, nmsg, IssueSeverity.WARNING).setTxLink(txLink).setMessageId(msg); 533 if (checkMsgId(msg, vmsg)) { 534 errors.add(vmsg); 535 } 536 trackedMessages.add(new TrackedLocationRelatedMessage(location, vmsg)); 537 } 538 return thePass; 539 540 } 541 542 protected void removeTrackedMessagesForLocation(List<ValidationMessage> errors, Object location, String path) { 543 List<TrackedLocationRelatedMessage> messages = new ArrayList<>(); 544 for (TrackedLocationRelatedMessage m : trackedMessages) { 545 if (m.getLocation() == location) { 546 messages.add(m); 547 messagesToRemove.add(m.getVmsg()); 548 } 549 } 550 trackedMessages.removeAll(messages); 551 } 552 553 protected boolean warningOrError(boolean isError, List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { 554 if (!thePass) { 555 String nmsg = context.formatMessage(msg, theMessageArguments); 556 IssueSeverity lvl = isError ? IssueSeverity.ERROR : IssueSeverity.WARNING; 557 if (doingLevel(lvl)) { 558 addValidationMessage(errors, type, line, col, path, nmsg, lvl, msg); 559 } 560 } 561 return thePass; 562 563 } 564 565 /** 566 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 567 * 568 * @param thePass 569 * Set this parameter to <code>false</code> if the validation does not pass 570 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 571 */ 572 protected boolean warning(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { 573 if (!thePass && doingWarnings()) { 574 String path = toPath(pathParts); 575 String message = context.formatMessage(theMessage, theMessageArguments); 576 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.WARNING, theMessage); 577 } 578 return thePass; 579 } 580 581 /** 582 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 583 * 584 * @param thePass 585 * Set this parameter to <code>false</code> if the validation does not pass 586 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 587 */ 588 protected boolean warning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, Object... theMessageArguments) { 589 if (!thePass && doingWarnings()) { 590 String message = context.formatMessage(msg, theMessageArguments); 591 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.WARNING, null); 592 } 593 return thePass; 594 } 595 596 /** 597 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 598 * 599 * @param thePass 600 * Set this parameter to <code>false</code> if the validation does not pass 601 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 602 */ 603 protected boolean warningOrHint(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, boolean warning, String msg, Object... theMessageArguments) { 604 if (!thePass) { 605 String message = context.formatMessage(msg, theMessageArguments); 606 IssueSeverity lvl = warning ? IssueSeverity.WARNING : IssueSeverity.INFORMATION; 607 if (doingLevel(lvl)) { 608 addValidationMessage(errors, type, -1, -1, path, message, lvl, null); 609 } 610 } 611 return thePass; 612 } 613 614 /** 615 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 616 * 617 * @param thePass 618 * Set this parameter to <code>false</code> if the validation does not pass 619 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 620 */ 621 protected boolean warningHtml(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html) { 622 if (!thePass && doingWarnings()) { 623 addValidationMessage(errors, type, path, msg, html, IssueSeverity.WARNING, null); 624 } 625 return thePass; 626 } 627 628 /** 629 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 630 * 631 * @param thePass 632 * Set this parameter to <code>false</code> if the validation does not pass 633 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 634 */ 635 protected boolean warningHtml(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { 636 if (!thePass && doingWarnings()) { 637 String nmsg = context.formatMessage(msg, theMessageArguments); 638 addValidationMessage(errors, type, path, nmsg, html, IssueSeverity.WARNING, msg); 639 } 640 return thePass; 641 } 642 643 //--------- 644 /** 645 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 646 * 647 * @param thePass 648 * Set this parameter to <code>false</code> if the validation does not pass 649 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 650 */ 651 protected boolean suppressedwarning(List<ValidationMessage> errors, IssueType type, int line, int col, String path, boolean thePass, String msg, Object... theMessageArguments) { 652 if (!thePass && doingWarnings()) { 653 String nmsg = context.formatMessage(msg, theMessageArguments); 654 addValidationMessage(errors, type, line, col, path, nmsg, IssueSeverity.INFORMATION, msg); 655 } 656 return thePass; 657 658 } 659 660 /** 661 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 662 * 663 * @param thePass 664 * Set this parameter to <code>false</code> if the validation does not pass 665 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 666 */ 667 protected boolean suppressedwarning(List<ValidationMessage> errors, IssueType type, List<String> pathParts, boolean thePass, String theMessage, Object... theMessageArguments) { 668 if (!thePass && doingWarnings()) { 669 String path = toPath(pathParts); 670 String message = context.formatMessage(theMessage, theMessageArguments); 671 addValidationMessage(errors, type, -1, -1, path, message, IssueSeverity.INFORMATION, theMessage); 672 } 673 return thePass; 674 } 675 676 /** 677 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 678 * 679 * @param thePass 680 * Set this parameter to <code>false</code> if the validation does not pass 681 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 682 */ 683 protected boolean suppressedwarning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg) { 684 if (!thePass && doingWarnings()) { 685 addValidationMessage(errors, type, -1, -1, path, msg, IssueSeverity.INFORMATION, null); 686 } 687 return thePass; 688 } 689 690 /** 691 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 692 * 693 * @param thePass 694 * Set this parameter to <code>false</code> if the validation does not pass 695 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 696 */ 697 protected boolean suppressedwarning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html) { 698 if (!thePass && doingWarnings()) { 699 IssueSeverity severity = IssueSeverity.INFORMATION; 700 addValidationMessage(errors, type, path, msg, html, severity, null); 701 } 702 return thePass; 703 } 704 705 protected void addValidationMessage(List<ValidationMessage> errors, IssueType type, String path, String msg, String html, IssueSeverity theSeverity, String id) { 706 ValidationMessage vm = new ValidationMessage(source, type, -1, -1, path, msg, html, theSeverity); 707 if (checkMsgId(id, vm)) { 708 if (doingLevel(theSeverity)) { 709 errors.add(vm.setMessageId(id)); 710 } 711 } 712 } 713 714 /** 715 * Test a rule and add a {@link IssueSeverity#WARNING} validation message if the validation fails 716 * 717 * @param thePass 718 * Set this parameter to <code>false</code> if the validation does not pass 719 * @return Returns <code>thePass</code> (in other words, returns <code>true</code> if the rule did not fail validation) 720 */ 721 protected boolean suppressedwarning(List<ValidationMessage> errors, IssueType type, String path, boolean thePass, String msg, String html, Object... theMessageArguments) { 722 if (!thePass && doingWarnings()) { 723 String nmsg = context.formatMessage(msg, theMessageArguments); 724 addValidationMessage(errors, type, path, nmsg, html, IssueSeverity.INFORMATION, msg); 725 } 726 return thePass; 727 } 728 729 730 protected ValueSet resolveBindingReference(DomainResource ctxt, String reference, String uri) { 731 if (reference != null) { 732 if (reference.startsWith("#")) { 733 for (Resource c : ctxt.getContained()) { 734 if (c.getId().equals(reference.substring(1)) && (c instanceof ValueSet)) 735 return (ValueSet) c; 736 } 737 return null; 738 } else { 739 long t = System.nanoTime(); 740 ValueSet fr = context.fetchResource(ValueSet.class, reference); 741 if (fr == null) { 742 if (!Utilities.isAbsoluteUrl(reference)) { 743 reference = resolve(uri, reference); 744 fr = context.fetchResource(ValueSet.class, reference); 745 } 746 } 747 if (fr == null) { 748 fr = ValueSetUtilities.generateImplicitValueSet(reference); 749 } 750 751 timeTracker.tx(t, "vs "+uri); 752 return fr; 753 } 754 } else 755 return null; 756 } 757 758 759 private String resolve(String uri, String ref) { 760 if (isBlank(uri)) { 761 return ref; 762 } 763 String[] up = uri.split("\\/"); 764 String[] rp = ref.split("\\/"); 765 if (context.getResourceNames().contains(up[up.length - 2]) && context.getResourceNames().contains(rp[0])) { 766 StringBuilder b = new StringBuilder(); 767 for (int i = 0; i < up.length - 2; i++) { 768 b.append(up[i]); 769 b.append("/"); 770 } 771 b.append(ref); 772 return b.toString(); 773 } else 774 return ref; 775 } 776 777 protected String describeReference(String reference) { 778 if (reference == null) 779 return "null"; 780 return reference; 781 } 782 783 protected Base resolveInBundle(String url, Element bnd) { 784 if (bnd == null) 785 return null; 786 if (bnd.fhirType().equals(BUNDLE)) { 787 for (Element be : bnd.getChildrenByName(ENTRY)) { 788 Element res = be.getNamedChild(RESOURCE); 789 if (res != null) { 790 String fullUrl = be.getChildValue(FULL_URL); 791 String rt = res.fhirType(); 792 String id = res.getChildValue(ID); 793 if (url.equals(fullUrl)) 794 return res; 795 if (url.equals(rt + "/" + id)) 796 return res; 797 } 798 } 799 } 800 return null; 801 } 802 803 protected Element resolveInBundle(List<Element> entries, String ref, String fullUrl, String type, String id) { 804 if (Utilities.isAbsoluteUrl(ref)) { 805 // if the reference is absolute, then you resolve by fullUrl. No other thinking is required. 806 for (Element entry : entries) { 807 String fu = entry.getNamedChildValue(FULL_URL); 808 if (ref.equals(fu)) 809 return entry; 810 } 811 return null; 812 } else { 813 // split into base, type, and id 814 String u = null; 815 if (fullUrl != null && fullUrl.endsWith(type + "/" + id)) 816 // fullUrl = complex 817 u = fullUrl.substring(0, fullUrl.length() - (type + "/" + id).length()) + ref; 818// u = fullUrl.substring((type+"/"+id).length())+ref; 819 String[] parts = ref.split("\\/"); 820 if (parts.length >= 2) { 821 String t = parts[0]; 822 String i = parts[1]; 823 for (Element entry : entries) { 824 String fu = entry.getNamedChildValue(FULL_URL); 825 if (fu != null && fu.equals(u)) 826 return entry; 827 if (u == null) { 828 Element resource = entry.getNamedChild(RESOURCE); 829 if (resource != null) { 830 String et = resource.getType(); 831 String eid = resource.getNamedChildValue(ID); 832 if (t.equals(et) && i.equals(eid)) 833 return entry; 834 } 835 } 836 } 837 } 838 return null; 839 } 840 } 841 842 843 protected IndexedElement getFromBundle(Element bundle, String ref, String fullUrl, List<ValidationMessage> errors, String path, String type, boolean isTransaction) { 844 String targetUrl = null; 845 String version = ""; 846 String resourceType = null; 847 if (ref.startsWith("http:") || ref.startsWith("urn:") || Utilities.isAbsoluteUrl(ref)) { 848 // We've got an absolute reference, no need to calculate 849 if (ref.contains("/_history/")) { 850 targetUrl = ref.substring(0, ref.indexOf("/_history/") - 1); 851 version = ref.substring(ref.indexOf("/_history/") + 10); 852 } else 853 targetUrl = ref; 854 855 } else if (fullUrl == null) { 856 //This isn't a problem for signatures - if it's a signature, we won't have a resolution for a relative reference. For anything else, this is an error 857 // but this rule doesn't apply for batches or transactions 858 rule(errors, IssueType.REQUIRED, -1, -1, path, Utilities.existsInList(type, "batch-response", "transaction-response") || path.startsWith("Bundle.signature"), I18nConstants.BUNDLE_BUNDLE_FULLURL_MISSING); 859 return null; 860 861 } else if (ref.split("/").length != 2 && ref.split("/").length != 4) { 862 if (isTransaction) { 863 rule(errors, IssueType.INVALID, -1, -1, path, isSearchUrl(ref), I18nConstants.REFERENCE_REF_FORMAT1, ref); 864 } else { 865 rule(errors, IssueType.INVALID, -1, -1, path, false, I18nConstants.REFERENCE_REF_FORMAT2, ref); 866 } 867 return null; 868 869 } else { 870 String base = ""; 871 if (fullUrl.startsWith("urn")) { 872 String[] parts = fullUrl.split("\\:"); 873 for (int i = 0; i < parts.length - 1; i++) { 874 base = base + parts[i] + ":"; 875 } 876 } else { 877 String[] parts; 878 parts = fullUrl.split("/"); 879 for (int i = 0; i < parts.length - 2; i++) { 880 base = base + parts[i] + "/"; 881 } 882 } 883 884 String id = null; 885 if (ref.contains("/_history/")) { 886 version = ref.substring(ref.indexOf("/_history/") + 10); 887 String[] refBaseParts = ref.substring(0, ref.indexOf("/_history/")).split("/"); 888 resourceType = refBaseParts[0]; 889 id = refBaseParts[1]; 890 } else if (base.startsWith("urn")) { 891 resourceType = ref.split("/")[0]; 892 id = ref.split("/")[1]; 893 } else 894 id = ref; 895 896 targetUrl = base + id; 897 } 898 899 List<Element> entries = new ArrayList<Element>(); 900 bundle.getNamedChildren(ENTRY, entries); 901 Element match = null; 902 int matchIndex = -1; 903 for (int i = 0; i < entries.size(); i++) { 904 Element we = entries.get(i); 905 if (targetUrl.equals(we.getChildValue(FULL_URL))) { 906 Element r = we.getNamedChild(RESOURCE); 907 if (version.isEmpty()) { 908 rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); 909 match = r; 910 matchIndex = i; 911 } else { 912 try { 913 if (version.equals(r.getChildren(META).get(0).getChildValue("versionId"))) { 914 rule(errors, IssueType.FORBIDDEN, -1, -1, path, match == null, I18nConstants.BUNDLE_BUNDLE_MULTIPLEMATCHES, ref); 915 match = r; 916 matchIndex = i; 917 } 918 } catch (Exception e) { 919 warning(errors, IssueType.REQUIRED, -1, -1, path, r.getChildren(META).size() == 1 && r.getChildren(META).get(0).getChildValue("versionId") != null, I18nConstants.BUNDLE_BUNDLE_FULLURL_NEEDVERSION, targetUrl); 920 // If one of these things is null 921 } 922 } 923 } 924 } 925 926 if (match != null && resourceType != null) 927 rule(errors, IssueType.REQUIRED, -1, -1, path, match.getType().equals(resourceType), I18nConstants.REFERENCE_REF_RESOURCETYPE, ref, match.getType()); 928 if (match == null) 929 warning(errors, IssueType.REQUIRED, -1, -1, path, !ref.startsWith("urn"), I18nConstants.BUNDLE_BUNDLE_NOT_LOCAL, ref); 930 return match == null ? null : new IndexedElement(matchIndex, match, entries.get(matchIndex)); 931 } 932 933 private boolean isSearchUrl(String ref) { 934 if (Utilities.noString(ref) || !ref.contains("?")) { 935 return false; 936 } 937 String tn = ref.substring(0, ref.indexOf("?")); 938 String q = ref.substring(ref.indexOf("?") + 1); 939 if (!context.getResourceNames().contains(tn)) { 940 return false; 941 } else { 942 return q.matches("([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+)(&([_a-zA-Z][_a-zA-Z0-9]*=[^=&]+))*"); 943 } 944 } 945 946 public Map<String, ValidationControl> getValidationControl() { 947 return validationControl; 948 } 949 950 public XVerExtensionStatus xverStatus(String url) { 951 return xverManager.status(url); 952 } 953 954 public boolean isXverUrl(String url) { 955 return xverManager.matchingUrl(url); 956 } 957 958 public StructureDefinition xverDefn(String url) { 959 return xverManager.makeDefinition(url); 960 } 961 962 public String xverVersion(String url) { 963 return xverManager.getVersion(url); 964 } 965 966 public String xverElementId(String url) { 967 return xverManager.getElementId(url); 968 } 969 970 public StructureDefinition getXverExt(StructureDefinition profile, List<ValidationMessage> errors, String url) { 971 if (isXverUrl(url)) { 972 switch (xverStatus(url)) { 973 case BadVersion: 974 rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INVALID, url, xverVersion(url)); 975 return null; 976 case Unknown: 977 rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INVALIDID, url, xverElementId(url)); 978 return null; 979 case Invalid: 980 rule(errors, IssueType.BUSINESSRULE, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_NOCHANGE, url, xverElementId(url)); 981 return null; 982 case Valid: 983 StructureDefinition defn = xverDefn(url); 984 context.generateSnapshot(defn); 985 context.cacheResource(defn); 986 return defn; 987 default: 988 rule(errors, IssueType.INVALID, profile.getId(), false, I18nConstants.EXTENSION_EXT_VERSION_INTERNAL, url); 989 return null; 990 } 991 } else { 992 return null; 993 } 994 } 995 996 public StructureDefinition getXverExt(List<ValidationMessage> errors, String path, Element element, String url) { 997 if (isXverUrl(url)) { 998 switch (xverStatus(url)) { 999 case BadVersion: 1000 rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALID, url, xverVersion(url)); 1001 break; 1002 case Unknown: 1003 rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INVALIDID, url, xverElementId(url)); 1004 break; 1005 case Invalid: 1006 rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_NOCHANGE, url, xverElementId(url)); 1007 break; 1008 case Valid: 1009 StructureDefinition ex = xverDefn(url); 1010 context.generateSnapshot(ex); 1011 context.cacheResource(ex); 1012 return ex; 1013 default: 1014 rule(errors, IssueType.INVALID, element.line(), element.col(), path + "[url='" + url + "']", false, I18nConstants.EXTENSION_EXT_VERSION_INTERNAL, url); 1015 break; 1016 } 1017 } 1018 return null; 1019 } 1020 1021 protected String versionFromCanonical(String system) { 1022 if (system == null) { 1023 return null; 1024 } else if (system.contains("|")) { 1025 return system.substring(0, system.indexOf("|")); 1026 } else { 1027 return system; 1028 } 1029 } 1030 1031 protected String systemFromCanonical(String system) { 1032 if (system == null) { 1033 return null; 1034 } else if (system.contains("|")) { 1035 return system.substring(system.indexOf("|")+1); 1036 } else { 1037 return system; 1038 } 1039 } 1040 1041 @Override 1042 public Resource loadContainedResource(List<ValidationMessage> errors, String path, Element resource, String id, Class<? extends Resource> class1) throws FHIRException { 1043 for (Element contained : resource.getChildren("contained")) { 1044 if (contained.getIdBase().equals(id)) { 1045 return loadFoundResource(errors, path, contained, class1); 1046 } 1047 } 1048 return null; 1049 } 1050 1051 protected Resource loadFoundResource(List<ValidationMessage> errors, String path, Element resource, Class<? extends Resource> class1) throws FHIRException { 1052 try { 1053 FhirPublication v = FhirPublication.fromCode(context.getVersion()); 1054 ByteArrayOutputStream bs = new ByteArrayOutputStream(); 1055 new JsonParser(context).compose(resource, bs, OutputStyle.NORMAL, resource.getIdBase()); 1056 byte[] json = bs.toByteArray(); 1057 Resource r5 = null; 1058 switch (v) { 1059 case DSTU1: 1060 rule(errors, IssueType.INVALID, resource.line(), resource.col(), path, false, I18nConstants.UNSUPPORTED_VERSION_R1, resource.getIdBase()); 1061 return null; // this can't happen 1062 case DSTU2: 1063 org.hl7.fhir.dstu2.model.Resource r2 = new org.hl7.fhir.dstu2.formats.JsonParser().parse(json); 1064 r5 = VersionConvertorFactory_10_50.convertResource(r2); 1065 break; 1066 case DSTU2016May: 1067 org.hl7.fhir.dstu2016may.model.Resource r2a = new org.hl7.fhir.dstu2016may.formats.JsonParser().parse(json); 1068 r5 = VersionConvertorFactory_14_50.convertResource(r2a); 1069 break; 1070 case STU3: 1071 org.hl7.fhir.dstu3.model.Resource r3 = new org.hl7.fhir.dstu3.formats.JsonParser().parse(json); 1072 r5 = VersionConvertorFactory_30_50.convertResource(r3); 1073 break; 1074 case R4: 1075 org.hl7.fhir.r4.model.Resource r4 = new org.hl7.fhir.r4.formats.JsonParser().parse(json); 1076 r5 = VersionConvertorFactory_40_50.convertResource(r4); 1077 break; 1078 case R5: 1079 r5 = new org.hl7.fhir.r5.formats.JsonParser().parse(json); 1080 break; 1081 default: 1082 return null; // this can't happen 1083 } 1084 if (class1.isInstance(r5)) 1085 return (Resource) r5; 1086 else { 1087 rule(errors, IssueType.INVALID, resource.line(), resource.col(), path, false, I18nConstants.REFERENCE_REF_WRONGTARGET_LOAD, resource.getIdBase(), class1.toString(), r5.fhirType()); 1088 return null; 1089 } 1090 1091 } catch (IOException e) { 1092 throw new FHIRException(e); 1093 } 1094 } 1095 1096 public void setLevel(ValidationLevel level) { 1097 this.level = level; 1098 } 1099 1100 public ValidationLevel getLevel() { 1101 return level; 1102 } 1103 1104}