
001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.FhirContext; 004import ca.uhn.fhir.context.FhirVersionEnum; 005import ca.uhn.fhir.context.support.ConceptValidationOptions; 006import ca.uhn.fhir.context.support.IValidationSupport; 007import ca.uhn.fhir.context.support.LookupCodeRequest; 008import ca.uhn.fhir.context.support.ValidationSupportContext; 009import ca.uhn.fhir.context.support.ValueSetExpansionOptions; 010import ca.uhn.fhir.i18n.Msg; 011import ca.uhn.fhir.util.FhirVersionIndependentConcept; 012import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; 013import com.google.common.annotations.VisibleForTesting; 014import jakarta.annotation.Nonnull; 015import jakarta.annotation.Nullable; 016import org.apache.commons.lang3.Validate; 017import org.hl7.fhir.dstu2.model.ValueSet; 018import org.hl7.fhir.instance.model.api.IBaseResource; 019import org.hl7.fhir.instance.model.api.IPrimitiveType; 020import org.hl7.fhir.r5.model.CanonicalType; 021import org.hl7.fhir.r5.model.CodeSystem; 022import org.hl7.fhir.r5.model.Enumerations; 023 024import java.util.ArrayList; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.Set; 031import java.util.function.Consumer; 032import java.util.function.Function; 033import java.util.stream.Collectors; 034 035import static org.apache.commons.lang3.StringUtils.contains; 036import static org.apache.commons.lang3.StringUtils.defaultString; 037import static org.apache.commons.lang3.StringUtils.isBlank; 038import static org.apache.commons.lang3.StringUtils.isNotBlank; 039import static org.apache.commons.lang3.StringUtils.substringAfter; 040import static org.apache.commons.lang3.StringUtils.substringBefore; 041import static org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService.getFhirVersionEnum; 042 043/** 044 * This class is a basic in-memory terminology service, designed to expand ValueSets and validate codes 045 * completely in-memory. It is suitable for runtime validation purposes where no dedicated terminology 046 * service exists (either an internal one such as the HAPI FHIR JPA terminology service, or an 047 * external term service API) 048 */ 049@SuppressWarnings("EnhancedSwitchMigration") 050public class InMemoryTerminologyServerValidationSupport implements IValidationSupport { 051 private static final String OUR_PIPE_CHARACTER = "|"; 052 private final FhirContext myCtx; 053 private VersionCanonicalizer myVersionCanonicalizer; 054 private IssueSeverity myIssueSeverityForCodeDisplayMismatch = IssueSeverity.WARNING; 055 056 /** 057 * Constructor 058 * 059 * @param theCtx A FhirContext for the FHIR version being validated 060 */ 061 public InMemoryTerminologyServerValidationSupport(FhirContext theCtx) { 062 Validate.notNull(theCtx, "theCtx must not be null"); 063 myCtx = theCtx; 064 myVersionCanonicalizer = new VersionCanonicalizer(theCtx); 065 } 066 067 @VisibleForTesting 068 public void setVersionCanonicalizer(VersionCanonicalizer theVersionCanonicalizer) { 069 myVersionCanonicalizer = theVersionCanonicalizer; 070 } 071 072 @Override 073 public String getName() { 074 return myCtx.getVersion().getVersion() + " In-Memory Validation Support"; 075 } 076 077 /** 078 * This setting controls the validation issue severity to report when a code validation 079 * finds that the code is present in the given CodeSystem, but the display name being 080 * validated doesn't match the expected value(s). Defaults to 081 * {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this 082 * value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION} 083 * if you don't want to see display name validation issues at all in resource validation 084 * outcomes. 085 * 086 * @since 7.0.0 087 */ 088 public IssueSeverity getIssueSeverityForCodeDisplayMismatch() { 089 return myIssueSeverityForCodeDisplayMismatch; 090 } 091 092 /** 093 * This setting controls the validation issue severity to report when a code validation 094 * finds that the code is present in the given CodeSystem, but the display name being 095 * validated doesn't match the expected value(s). Defaults to 096 * {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#WARNING}. Set this 097 * value to {@link ca.uhn.fhir.context.support.IValidationSupport.IssueSeverity#INFORMATION} 098 * if you don't want to see display name validation issues at all in resource validation 099 * outcomes. 100 * 101 * @param theIssueSeverityForCodeDisplayMismatch The severity. Must not be {@literal null}. 102 * @since 7.0.0 103 */ 104 public void setIssueSeverityForCodeDisplayMismatch(@Nonnull IssueSeverity theIssueSeverityForCodeDisplayMismatch) { 105 Validate.notNull( 106 theIssueSeverityForCodeDisplayMismatch, "theIssueSeverityForCodeDisplayMismatch must not be null"); 107 myIssueSeverityForCodeDisplayMismatch = theIssueSeverityForCodeDisplayMismatch; 108 } 109 110 @Override 111 public FhirContext getFhirContext() { 112 return myCtx; 113 } 114 115 @Override 116 public ValueSetExpansionOutcome expandValueSet( 117 ValidationSupportContext theValidationSupportContext, 118 ValueSetExpansionOptions theExpansionOptions, 119 @Nonnull IBaseResource theValueSetToExpand) { 120 return expandValueSet(theValidationSupportContext, theValueSetToExpand, null, null); 121 } 122 123 private ValueSetExpansionOutcome expandValueSet( 124 ValidationSupportContext theValidationSupportContext, 125 IBaseResource theValueSetToExpand, 126 String theWantSystemAndVersion, 127 String theWantCode) { 128 org.hl7.fhir.r5.model.ValueSet expansionR5; 129 try { 130 expansionR5 = expandValueSetToCanonical( 131 theValidationSupportContext, theValueSetToExpand, theWantSystemAndVersion, theWantCode) 132 .getValueSet(); 133 } catch (ExpansionCouldNotBeCompletedInternallyException e) { 134 return new ValueSetExpansionOutcome(e.getMessage(), false); 135 } 136 if (expansionR5 == null) { 137 return null; 138 } 139 140 IBaseResource expansion = myVersionCanonicalizer.valueSetFromValidatorCanonical(expansionR5); 141 return new ValueSetExpansionOutcome(expansion); 142 } 143 144 private ValueSetAndMessages expandValueSetToCanonical( 145 ValidationSupportContext theValidationSupportContext, 146 IBaseResource theValueSetToExpand, 147 @Nullable String theWantSystemUrlAndVersion, 148 @Nullable String theWantCode) 149 throws ExpansionCouldNotBeCompletedInternallyException { 150 org.hl7.fhir.r5.model.ValueSet input = myVersionCanonicalizer.valueSetToValidatorCanonical(theValueSetToExpand); 151 return expandValueSetR5(theValidationSupportContext, input, theWantSystemUrlAndVersion, theWantCode); 152 } 153 154 @Override 155 public CodeValidationResult validateCodeInValueSet( 156 ValidationSupportContext theValidationSupportContext, 157 ConceptValidationOptions theOptions, 158 String theCodeSystemUrlAndVersion, 159 String theCode, 160 String theDisplay, 161 @Nonnull IBaseResource theValueSet) { 162 ValueSetAndMessages expansion; 163 String vsUrl = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); 164 try { 165 expansion = expandValueSetToCanonical( 166 theValidationSupportContext, theValueSet, theCodeSystemUrlAndVersion, theCode); 167 } catch (ExpansionCouldNotBeCompletedInternallyException e) { 168 CodeValidationResult codeValidationResult = new CodeValidationResult(); 169 codeValidationResult.setSeverity(IssueSeverity.ERROR); 170 171 String msg = "Failed to expand ValueSet '" + vsUrl + "' (in-memory). Could not validate code " 172 + theCodeSystemUrlAndVersion + "#" + theCode; 173 if (e.getMessage() != null) { 174 msg += ". Error was: " + e.getMessage(); 175 } 176 177 codeValidationResult.setMessage(msg); 178 codeValidationResult.addIssue(e.getCodeValidationIssue()); 179 return codeValidationResult; 180 } 181 182 if (expansion == null || expansion.getValueSet() == null) { 183 return null; 184 } 185 186 if (expansion.getValueSet().getExpansion().getContains().isEmpty()) { 187 IssueSeverity severity = IssueSeverity.ERROR; 188 String message = "Unknown code '" 189 + getFormattedCodeSystemAndCodeForMessage(theCodeSystemUrlAndVersion, theCode) 190 + "'" 191 + createInMemoryExpansionMessageSuffix(vsUrl) 192 + (expansion.getMessages().isEmpty() ? "" : " Expansion result: " + expansion.getMessages()); 193 CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.NOT_IN_VS; 194 CodeValidationIssueCode notFound = CodeValidationIssueCode.NOT_FOUND; 195 CodeValidationResult codeValidationResult = new CodeValidationResult() 196 .setSeverity(severity) 197 .setMessage(message) 198 .setSourceDetails(null) 199 .addIssue(new CodeValidationIssue(message, severity, notFound, issueCoding)); 200 return codeValidationResult; 201 } 202 203 return validateCodeInExpandedValueSet( 204 theValidationSupportContext, 205 theOptions, 206 theCodeSystemUrlAndVersion, 207 theCode, 208 theDisplay, 209 expansion.getValueSet(), 210 vsUrl); 211 } 212 213 @Override 214 @Nullable 215 public CodeValidationResult validateCode( 216 @Nonnull ValidationSupportContext theValidationSupportContext, 217 @Nonnull ConceptValidationOptions theOptions, 218 String theCodeSystem, 219 String theCode, 220 String theDisplay, 221 String theValueSetUrl) { 222 IBaseResource vs; 223 if (isNotBlank(theValueSetUrl)) { 224 vs = theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl); 225 if (vs == null) { 226 return null; 227 } 228 } else { 229 String codeSystemUrl; 230 String codeSystemVersion = null; 231 int codeSystemVersionIndex = theCodeSystem.indexOf("|"); 232 if (codeSystemVersionIndex > -1) { 233 codeSystemUrl = theCodeSystem.substring(0, codeSystemVersionIndex); 234 codeSystemVersion = theCodeSystem.substring(codeSystemVersionIndex + 1); 235 } else { 236 codeSystemUrl = theCodeSystem; 237 } 238 switch (myCtx.getVersion().getVersion()) { 239 case DSTU2: 240 case DSTU2_HL7ORG: 241 vs = new org.hl7.fhir.dstu2.model.ValueSet() 242 .setCompose(new org.hl7.fhir.dstu2.model.ValueSet.ValueSetComposeComponent() 243 .addInclude(new org.hl7.fhir.dstu2.model.ValueSet.ConceptSetComponent() 244 .setSystem(theCodeSystem))); 245 break; 246 case DSTU3: 247 if (codeSystemVersion != null) { 248 vs = new org.hl7.fhir.dstu3.model.ValueSet() 249 .setCompose(new org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent() 250 .addInclude(new org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent() 251 .setSystem(codeSystemUrl) 252 .setVersion(codeSystemVersion))); 253 } else { 254 vs = new org.hl7.fhir.dstu3.model.ValueSet() 255 .setCompose(new org.hl7.fhir.dstu3.model.ValueSet.ValueSetComposeComponent() 256 .addInclude(new org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent() 257 .setSystem(theCodeSystem))); 258 } 259 break; 260 case R4: 261 if (codeSystemVersion != null) { 262 vs = new org.hl7.fhir.r4.model.ValueSet() 263 .setCompose(new org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent() 264 .addInclude(new org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent() 265 .setSystem(codeSystemUrl) 266 .setVersion(codeSystemVersion))); 267 } else { 268 vs = new org.hl7.fhir.r4.model.ValueSet() 269 .setCompose(new org.hl7.fhir.r4.model.ValueSet.ValueSetComposeComponent() 270 .addInclude(new org.hl7.fhir.r4.model.ValueSet.ConceptSetComponent() 271 .setSystem(theCodeSystem))); 272 } 273 break; 274 case R4B: 275 if (codeSystemVersion != null) { 276 vs = new org.hl7.fhir.r4b.model.ValueSet() 277 .setCompose(new org.hl7.fhir.r4b.model.ValueSet.ValueSetComposeComponent() 278 .addInclude(new org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent() 279 .setSystem(codeSystemUrl) 280 .setVersion(codeSystemVersion))); 281 } else { 282 vs = new org.hl7.fhir.r4b.model.ValueSet() 283 .setCompose(new org.hl7.fhir.r4b.model.ValueSet.ValueSetComposeComponent() 284 .addInclude(new org.hl7.fhir.r4b.model.ValueSet.ConceptSetComponent() 285 .setSystem(theCodeSystem))); 286 } 287 break; 288 case R5: 289 if (codeSystemVersion != null) { 290 vs = new org.hl7.fhir.r5.model.ValueSet() 291 .setCompose(new org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent() 292 .addInclude(new org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent() 293 .setSystem(codeSystemUrl) 294 .setVersion(codeSystemVersion))); 295 } else { 296 vs = new org.hl7.fhir.r5.model.ValueSet() 297 .setCompose(new org.hl7.fhir.r5.model.ValueSet.ValueSetComposeComponent() 298 .addInclude(new org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent() 299 .setSystem(theCodeSystem))); 300 } 301 break; 302 case DSTU2_1: 303 default: 304 throw new IllegalArgumentException(Msg.code(699) + "Can not handle version: " 305 + myCtx.getVersion().getVersion()); 306 } 307 } 308 309 ValueSetExpansionOutcome valueSetExpansionOutcome = 310 expandValueSet(theValidationSupportContext, vs, theCodeSystem, theCode); 311 if (valueSetExpansionOutcome == null) { 312 return null; 313 } 314 315 if (valueSetExpansionOutcome.getError() != null) { 316 return new CodeValidationResult() 317 .setSeverity(IssueSeverity.ERROR) 318 .setMessage(valueSetExpansionOutcome.getError()); 319 } 320 321 IBaseResource expansion = valueSetExpansionOutcome.getValueSet(); 322 return validateCodeInExpandedValueSet( 323 theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, expansion, theValueSetUrl); 324 } 325 326 private CodeValidationResult validateCodeInExpandedValueSet( 327 ValidationSupportContext theValidationSupportContext, 328 ConceptValidationOptions theOptions, 329 String theCodeSystemUrlAndVersionToValidate, 330 String theCodeToValidate, 331 String theDisplayToValidate, 332 IBaseResource theExpansion, 333 String theValueSetUrl) { 334 assert theExpansion != null; 335 336 final CodeValidationResult codeValidationResult; 337 338 boolean caseSensitive = true; 339 IBaseResource codeSystemToValidateResource = null; 340 if (!theOptions.isInferSystem() && isNotBlank(theCodeSystemUrlAndVersionToValidate)) { 341 codeSystemToValidateResource = theValidationSupportContext 342 .getRootValidationSupport() 343 .fetchCodeSystem(theCodeSystemUrlAndVersionToValidate); 344 } 345 346 List<FhirVersionIndependentConcept> codes = new ArrayList<>(); 347 switch (getFhirVersionEnum( 348 theValidationSupportContext.getRootValidationSupport().getFhirContext(), theExpansion)) { 349 case DSTU2: { 350 ca.uhn.fhir.model.dstu2.resource.ValueSet expansionVs = 351 (ca.uhn.fhir.model.dstu2.resource.ValueSet) theExpansion; 352 List<ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains> contains = 353 expansionVs.getExpansion().getContains(); 354 flattenAndConvertCodesDstu2(contains, codes); 355 break; 356 } 357 case DSTU2_HL7ORG: { 358 ValueSet expansionVs = (ValueSet) theExpansion; 359 List<ValueSet.ValueSetExpansionContainsComponent> contains = 360 expansionVs.getExpansion().getContains(); 361 flattenAndConvertCodesDstu2Hl7Org(contains, codes); 362 break; 363 } 364 case DSTU3: { 365 org.hl7.fhir.dstu3.model.ValueSet expansionVs = (org.hl7.fhir.dstu3.model.ValueSet) theExpansion; 366 List<org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent> contains = 367 expansionVs.getExpansion().getContains(); 368 flattenAndConvertCodesDstu3(contains, codes); 369 break; 370 } 371 case R4: { 372 org.hl7.fhir.r4.model.ValueSet expansionVs = (org.hl7.fhir.r4.model.ValueSet) theExpansion; 373 List<org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent> contains = 374 expansionVs.getExpansion().getContains(); 375 flattenAndConvertCodesR4(contains, codes); 376 break; 377 } 378 case R4B: { 379 org.hl7.fhir.r4b.model.ValueSet expansionVs = (org.hl7.fhir.r4b.model.ValueSet) theExpansion; 380 List<org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent> contains = 381 expansionVs.getExpansion().getContains(); 382 flattenAndConvertCodesR4B(contains, codes); 383 break; 384 } 385 case R5: { 386 org.hl7.fhir.r5.model.ValueSet expansionVs = (org.hl7.fhir.r5.model.ValueSet) theExpansion; 387 List<org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent> contains = 388 expansionVs.getExpansion().getContains(); 389 flattenAndConvertCodesR5(contains, codes); 390 break; 391 } 392 case DSTU2_1: 393 default: 394 throw new IllegalArgumentException(Msg.code(700) + "Can not handle version: " 395 + myCtx.getVersion().getVersion()); 396 } 397 398 String codeSystemResourceName = null; 399 String codeSystemResourceVersion = null; 400 String codeSystemResourceContentMode = null; 401 if (codeSystemToValidateResource != null) { 402 switch (getFhirVersionEnum( 403 theValidationSupportContext.getRootValidationSupport().getFhirContext(), 404 codeSystemToValidateResource)) { 405 case DSTU2: 406 case DSTU2_HL7ORG: { 407 caseSensitive = true; 408 break; 409 } 410 case DSTU3: { 411 org.hl7.fhir.dstu3.model.CodeSystem systemDstu3 = 412 (org.hl7.fhir.dstu3.model.CodeSystem) codeSystemToValidateResource; 413 caseSensitive = systemDstu3.getCaseSensitive(); 414 codeSystemResourceName = systemDstu3.getName(); 415 codeSystemResourceVersion = systemDstu3.getVersion(); 416 codeSystemResourceContentMode = 417 systemDstu3.getContentElement().getValueAsString(); 418 break; 419 } 420 case R4: { 421 org.hl7.fhir.r4.model.CodeSystem systemR4 = 422 (org.hl7.fhir.r4.model.CodeSystem) codeSystemToValidateResource; 423 caseSensitive = systemR4.getCaseSensitive(); 424 codeSystemResourceName = systemR4.getName(); 425 codeSystemResourceVersion = systemR4.getVersion(); 426 codeSystemResourceContentMode = systemR4.getContentElement().getValueAsString(); 427 break; 428 } 429 case R4B: { 430 org.hl7.fhir.r4b.model.CodeSystem systemR4B = 431 (org.hl7.fhir.r4b.model.CodeSystem) codeSystemToValidateResource; 432 caseSensitive = systemR4B.getCaseSensitive(); 433 codeSystemResourceName = systemR4B.getName(); 434 codeSystemResourceVersion = systemR4B.getVersion(); 435 codeSystemResourceContentMode = 436 systemR4B.getContentElement().getValueAsString(); 437 break; 438 } 439 case R5: { 440 CodeSystem systemR5 = (CodeSystem) codeSystemToValidateResource; 441 caseSensitive = systemR5.getCaseSensitive(); 442 codeSystemResourceName = systemR5.getName(); 443 codeSystemResourceVersion = systemR5.getVersion(); 444 codeSystemResourceContentMode = systemR5.getContentElement().getValueAsString(); 445 break; 446 } 447 case DSTU2_1: 448 default: 449 throw new IllegalArgumentException(Msg.code(701) + "Can not handle version: " 450 + myCtx.getVersion().getVersion()); 451 } 452 } 453 454 String codeSystemUrlToValidate = null; 455 String codeSystemVersionToValidate = null; 456 if (theCodeSystemUrlAndVersionToValidate != null) { 457 int versionIndex = theCodeSystemUrlAndVersionToValidate.indexOf("|"); 458 if (versionIndex > -1) { 459 codeSystemUrlToValidate = theCodeSystemUrlAndVersionToValidate.substring(0, versionIndex); 460 codeSystemVersionToValidate = theCodeSystemUrlAndVersionToValidate.substring(versionIndex + 1); 461 } else { 462 codeSystemUrlToValidate = theCodeSystemUrlAndVersionToValidate; 463 } 464 } 465 CodeValidationResult valueSetResult = findCodeInExpansion( 466 theCodeToValidate, 467 theDisplayToValidate, 468 theValueSetUrl, 469 codeSystemUrlToValidate, 470 codeSystemVersionToValidate, 471 codeSystemResourceName, 472 codeSystemResourceVersion, 473 codes, 474 theOptions, 475 caseSensitive); 476 if (valueSetResult != null) { 477 codeValidationResult = valueSetResult; 478 } else { 479 IValidationSupport.IssueSeverity severity; 480 String message; 481 CodeValidationIssueCode issueCode = CodeValidationIssueCode.CODE_INVALID; 482 CodeValidationIssueCoding issueCoding = CodeValidationIssueCoding.INVALID_CODE; 483 if ("fragment".equals(codeSystemResourceContentMode)) { 484 severity = IValidationSupport.IssueSeverity.WARNING; 485 message = "Unknown code in fragment CodeSystem '" 486 + getFormattedCodeSystemAndCodeForMessage( 487 theCodeSystemUrlAndVersionToValidate, theCodeToValidate) 488 + "'"; 489 } else { 490 severity = IValidationSupport.IssueSeverity.ERROR; 491 message = "Unknown code '" 492 + getFormattedCodeSystemAndCodeForMessage( 493 theCodeSystemUrlAndVersionToValidate, theCodeToValidate) 494 + "'"; 495 } 496 if (isNotBlank(theValueSetUrl)) { 497 message += createInMemoryExpansionMessageSuffix(theValueSetUrl); 498 issueCoding = CodeValidationIssueCoding.NOT_IN_VS; 499 } 500 501 String sourceDetails = "In-memory expansion containing " + codes.size() + " codes"; 502 if (!codes.isEmpty() && codes.size() < 10) { 503 sourceDetails += ": " 504 + codes.stream() 505 .map(t -> t.getSystem() + "#" + t.getCode()) 506 .collect(Collectors.joining(", ")); 507 } 508 509 codeValidationResult = new CodeValidationResult() 510 .setSeverity(severity) 511 .setMessage(message) 512 .setSourceDetails(sourceDetails) 513 .addIssue(new CodeValidationIssue(message, severity, issueCode, issueCoding)); 514 } 515 516 return codeValidationResult; 517 } 518 519 @Nonnull 520 private static String createInMemoryExpansionMessageSuffix(String theValueSetUrl) { 521 return " for in-memory expansion of ValueSet '" + theValueSetUrl + "'"; 522 } 523 524 private static String getFormattedCodeSystemAndCodeForMessage( 525 String theCodeSystemUrlAndVersionToValidate, String theCodeToValidate) { 526 return (isNotBlank(theCodeSystemUrlAndVersionToValidate) ? theCodeSystemUrlAndVersionToValidate + "#" : "") 527 + theCodeToValidate; 528 } 529 530 private CodeValidationResult findCodeInExpansion( 531 String theCodeToValidate, 532 String theDisplayToValidate, 533 String theValueSetUrl, 534 String codeSystemUrlToValidate, 535 String codeSystemVersionToValidate, 536 String codeSystemResourceName, 537 String codeSystemResourceVersion, 538 List<FhirVersionIndependentConcept> expansionCodes, 539 ConceptValidationOptions theOptions, 540 boolean caseSensitive) { 541 for (FhirVersionIndependentConcept nextExpansionCode : expansionCodes) { 542 543 boolean codeMatches; 544 if (caseSensitive) { 545 codeMatches = defaultString(theCodeToValidate).equals(nextExpansionCode.getCode()); 546 } else { 547 codeMatches = defaultString(theCodeToValidate).equalsIgnoreCase(nextExpansionCode.getCode()); 548 } 549 if (codeMatches) { 550 if (theOptions.isInferSystem() 551 || (nextExpansionCode.getSystem().equals(codeSystemUrlToValidate) 552 && (codeSystemVersionToValidate == null 553 || codeSystemVersionToValidate.equals(nextExpansionCode.getSystemVersion())))) { 554 String csVersion = codeSystemResourceVersion; 555 if (isNotBlank(nextExpansionCode.getSystemVersion())) { 556 csVersion = nextExpansionCode.getSystemVersion(); 557 } 558 if (!theOptions.isValidateDisplay() 559 || (isBlank(nextExpansionCode.getDisplay()) 560 || isBlank(theDisplayToValidate) 561 || nextExpansionCode.getDisplay().equals(theDisplayToValidate))) { 562 CodeValidationResult codeValidationResult = new CodeValidationResult() 563 .setCode(theCodeToValidate) 564 .setDisplay(nextExpansionCode.getDisplay()) 565 .setCodeSystemName(codeSystemResourceName) 566 .setCodeSystemVersion(csVersion); 567 if (isNotBlank(theValueSetUrl)) { 568 populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult); 569 } 570 return codeValidationResult; 571 } else { 572 String messageAppend = ""; 573 if (isNotBlank(theValueSetUrl)) { 574 messageAppend = createInMemoryExpansionMessageSuffix(theValueSetUrl); 575 } 576 CodeValidationResult codeValidationResult = createResultForDisplayMismatch( 577 myCtx, 578 theCodeToValidate, 579 theDisplayToValidate, 580 nextExpansionCode.getDisplay(), 581 codeSystemUrlToValidate, 582 csVersion, 583 messageAppend, 584 getIssueSeverityForCodeDisplayMismatch()); 585 if (isNotBlank(theValueSetUrl)) { 586 populateSourceDetailsForInMemoryExpansion(theValueSetUrl, codeValidationResult); 587 } 588 return codeValidationResult; 589 } 590 } 591 } 592 } 593 return null; 594 } 595 596 @Override 597 public LookupCodeResult lookupCode( 598 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 599 final String code = theLookupCodeRequest.getCode(); 600 final String system = theLookupCodeRequest.getSystem(); 601 CodeValidationResult codeValidationResult = validateCode( 602 theValidationSupportContext, 603 new ConceptValidationOptions(), 604 system, 605 code, 606 theLookupCodeRequest.getDisplayLanguage(), 607 null); 608 if (codeValidationResult == null) { 609 return null; 610 } 611 return codeValidationResult.asLookupCodeResult(system, code); 612 } 613 614 @Override 615 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 616 if (isBlank(theSystem)) { 617 return false; 618 } 619 620 IBaseResource cs = 621 theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(theSystem); 622 623 if (!myCtx.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU2_1)) { 624 return cs != null; 625 } 626 627 if (cs != null) { 628 IPrimitiveType<?> content = 629 getFhirContext().newTerser().getSingleValueOrNull(cs, "content", IPrimitiveType.class); 630 return !"not-present".equals(content.getValueAsString()); 631 } 632 633 return false; 634 } 635 636 @Override 637 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 638 return isNotBlank(theValueSetUrl) 639 && theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl) != null; 640 } 641 642 private void addCodesDstu2Hl7Org( 643 List<ValueSet.ConceptDefinitionComponent> theSourceList, 644 List<CodeSystem.ConceptDefinitionComponent> theTargetList) { 645 for (ValueSet.ConceptDefinitionComponent nextSource : theSourceList) { 646 CodeSystem.ConceptDefinitionComponent targetConcept = new CodeSystem.ConceptDefinitionComponent() 647 .setCode(nextSource.getCode()) 648 .setDisplay(nextSource.getDisplay()); 649 theTargetList.add(targetConcept); 650 addCodesDstu2Hl7Org(nextSource.getConcept(), targetConcept.getConcept()); 651 } 652 } 653 654 private void addCodesDstu2( 655 List<ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept> theSourceList, 656 List<CodeSystem.ConceptDefinitionComponent> theTargetList) { 657 for (ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept nextSource : theSourceList) { 658 CodeSystem.ConceptDefinitionComponent targetConcept = new CodeSystem.ConceptDefinitionComponent() 659 .setCode(nextSource.getCode()) 660 .setDisplay(nextSource.getDisplay()); 661 theTargetList.add(targetConcept); 662 addCodesDstu2(nextSource.getConcept(), targetConcept.getConcept()); 663 } 664 } 665 666 @Nullable 667 private ValueSetAndMessages expandValueSetR5( 668 ValidationSupportContext theValidationSupportContext, 669 org.hl7.fhir.r5.model.ValueSet theInput, 670 @Nullable String theWantSystemUrlAndVersion, 671 @Nullable String theWantCode) 672 throws ExpansionCouldNotBeCompletedInternallyException { 673 674 ValueSetAndMessages retVal = new ValueSetAndMessages(); 675 Set<FhirVersionIndependentConcept> concepts = new HashSet<>(); 676 677 expandValueSetR5IncludeOrExcludes( 678 theValidationSupportContext, 679 concepts, 680 theInput.getCompose().getInclude(), 681 true, 682 theWantSystemUrlAndVersion, 683 theWantCode, 684 retVal); 685 expandValueSetR5IncludeOrExcludes( 686 theValidationSupportContext, 687 concepts, 688 theInput.getCompose().getExclude(), 689 false, 690 theWantSystemUrlAndVersion, 691 theWantCode, 692 retVal); 693 694 org.hl7.fhir.r5.model.ValueSet vs = new org.hl7.fhir.r5.model.ValueSet(); 695 retVal.setValueSet(vs); 696 for (FhirVersionIndependentConcept next : concepts) { 697 org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent contains = 698 vs.getExpansion().addContains(); 699 contains.setSystem(next.getSystem()); 700 contains.setCode(next.getCode()); 701 contains.setDisplay(next.getDisplay()); 702 contains.setVersion(next.getSystemVersion()); 703 } 704 705 return retVal; 706 } 707 708 /** 709 * Use with caution - this is not a stable API 710 * 711 * @since 5.6.0 712 */ 713 public void expandValueSetIncludeOrExclude( 714 ValidationSupportContext theValidationSupportContext, 715 Consumer<FhirVersionIndependentConcept> theConsumer, 716 org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent theIncludeOrExclude) 717 throws ExpansionCouldNotBeCompletedInternallyException { 718 expandValueSetR5IncludeOrExclude( 719 theValidationSupportContext, theConsumer, null, null, theIncludeOrExclude, new ValueSetAndMessages()); 720 } 721 722 private void expandValueSetR5IncludeOrExcludes( 723 ValidationSupportContext theValidationSupportContext, 724 Set<FhirVersionIndependentConcept> theConcepts, 725 List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList, 726 boolean theComposeListIsInclude, 727 @Nullable String theWantSystemUrlAndVersion, 728 @Nullable String theWantCode, 729 ValueSetAndMessages theResponseBuilder) 730 throws ExpansionCouldNotBeCompletedInternallyException { 731 Consumer<FhirVersionIndependentConcept> consumer = c -> { 732 if (theComposeListIsInclude) { 733 theConcepts.add(c); 734 } else { 735 theConcepts.remove(c); 736 } 737 }; 738 expandValueSetR5IncludeOrExcludes( 739 theComposeListIsInclude, 740 theValidationSupportContext, 741 consumer, 742 theComposeList, 743 theWantSystemUrlAndVersion, 744 theWantCode, 745 theResponseBuilder); 746 } 747 748 private void expandValueSetR5IncludeOrExcludes( 749 boolean theComposeListIsInclude, 750 ValidationSupportContext theValidationSupportContext, 751 Consumer<FhirVersionIndependentConcept> theConsumer, 752 List<org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent> theComposeList, 753 @Nullable String theWantSystemUrlAndVersion, 754 @Nullable String theWantCode, 755 ValueSetAndMessages theResponseBuilder) 756 throws ExpansionCouldNotBeCompletedInternallyException { 757 ExpansionCouldNotBeCompletedInternallyException caughtException = null; 758 if (theComposeList.isEmpty()) { 759 if (theComposeListIsInclude) { 760 theResponseBuilder.addMessage("Empty compose list for includes"); 761 } 762 return; 763 } 764 for (org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent nextInclude : theComposeList) { 765 try { 766 boolean outcome = expandValueSetR5IncludeOrExclude( 767 theValidationSupportContext, 768 theConsumer, 769 theWantSystemUrlAndVersion, 770 theWantCode, 771 nextInclude, 772 theResponseBuilder); 773 if (isNotBlank(theWantCode)) { 774 if (outcome) { 775 return; 776 } 777 } 778 } catch (ExpansionCouldNotBeCompletedInternallyException e) { 779 if (isBlank(theWantCode)) { 780 throw e; 781 } else { 782 caughtException = e; 783 } 784 } 785 } 786 if (caughtException != null) { 787 throw caughtException; 788 } 789 } 790 791 /** 792 * Returns <code>true</code> if at least one code was added 793 */ 794 private boolean expandValueSetR5IncludeOrExclude( 795 ValidationSupportContext theValidationSupportContext, 796 Consumer<FhirVersionIndependentConcept> theConsumer, 797 @Nullable String theWantSystemUrlAndVersion, 798 @Nullable String theWantCode, 799 org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent theInclude, 800 ValueSetAndMessages theResponseBuilder) 801 throws ExpansionCouldNotBeCompletedInternallyException { 802 803 String wantSystemUrl = null; 804 String wantSystemVersion = null; 805 806 if (theWantSystemUrlAndVersion != null) { 807 int versionIndex = theWantSystemUrlAndVersion.indexOf(OUR_PIPE_CHARACTER); 808 if (versionIndex > -1) { 809 wantSystemUrl = theWantSystemUrlAndVersion.substring(0, versionIndex); 810 wantSystemVersion = theWantSystemUrlAndVersion.substring(versionIndex + 1); 811 } else { 812 wantSystemUrl = theWantSystemUrlAndVersion; 813 } 814 } 815 816 String includeOrExcludeConceptSystemUrl = theInclude.getSystem(); 817 String includeOrExcludeConceptSystemVersion = theInclude.getVersion(); 818 819 Function<String, CodeSystem> codeSystemLoader = newCodeSystemLoader(theValidationSupportContext); 820 Function<String, org.hl7.fhir.r5.model.ValueSet> valueSetLoader = 821 newValueSetLoader(theValidationSupportContext); 822 823 List<FhirVersionIndependentConcept> nextCodeList = new ArrayList<>(); 824 CodeSystem includeOrExcludeSystemResource = null; 825 826 if (isNotBlank(includeOrExcludeConceptSystemUrl)) { 827 828 includeOrExcludeConceptSystemVersion = optionallyPopulateVersionFromUrl( 829 includeOrExcludeConceptSystemUrl, includeOrExcludeConceptSystemVersion); 830 includeOrExcludeConceptSystemUrl = substringBefore(includeOrExcludeConceptSystemUrl, OUR_PIPE_CHARACTER); 831 832 if (wantSystemUrl != null && !wantSystemUrl.equals(includeOrExcludeConceptSystemUrl)) { 833 return false; 834 } 835 836 if (wantSystemVersion != null && !wantSystemVersion.equals(includeOrExcludeConceptSystemVersion)) { 837 return false; 838 } 839 840 String loadedCodeSystemUrl; 841 if (includeOrExcludeConceptSystemVersion != null) { 842 loadedCodeSystemUrl = 843 includeOrExcludeConceptSystemUrl + OUR_PIPE_CHARACTER + includeOrExcludeConceptSystemVersion; 844 } else { 845 loadedCodeSystemUrl = includeOrExcludeConceptSystemUrl; 846 } 847 848 includeOrExcludeSystemResource = codeSystemLoader.apply(loadedCodeSystemUrl); 849 850 boolean isIncludeWithDeclaredConcepts = !theInclude.getConcept().isEmpty(); 851 852 final Set<String> wantCodes; 853 if (isIncludeWithDeclaredConcepts) { 854 wantCodes = theInclude.getConcept().stream() 855 .map(org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent::getCode) 856 .collect(Collectors.toSet()); 857 } else { 858 wantCodes = null; 859 } 860 861 boolean ableToHandleCode = false; 862 String failureMessage = null; 863 864 boolean isIncludeCodeSystemIgnored = includeOrExcludeSystemResource != null 865 && includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT; 866 867 boolean isIncludeFromSystem = isNotBlank(theInclude.getSystem()) 868 && theInclude.getValueSet().isEmpty(); 869 boolean isIncludeWithFilter = !theInclude.getFilter().isEmpty(); 870 871 // if we can?t load the CS and we?re configured to ignore it... 872 if (isIncludeCodeSystemIgnored && !theInclude.getFilter().isEmpty()) { 873 // We can?t apply any structural (ISA/DESCENDENT-OF/etc..) filters if the CS is absent ? fail 874 String msg = "Unable to expand ValueSet: cannot apply filters '" + theInclude.getFilter() 875 + "' because CodeSystem '" + theInclude.getSystem() 876 + "' is ignored/not-present"; 877 878 throw new ExpansionCouldNotBeCompletedInternallyException( 879 Msg.code(2646) + msg, 880 new CodeValidationIssue( 881 msg, 882 IssueSeverity.ERROR, 883 CodeValidationIssueCode.NOT_FOUND, 884 CodeValidationIssueCoding.NOT_FOUND)); 885 } 886 887 if (includeOrExcludeSystemResource == null || isIncludeCodeSystemIgnored) { 888 889 if (theWantCode != null) { 890 if (theValidationSupportContext 891 .getRootValidationSupport() 892 .isCodeSystemSupported(theValidationSupportContext, includeOrExcludeConceptSystemUrl)) { 893 LookupCodeResult lookup = theValidationSupportContext 894 .getRootValidationSupport() 895 .lookupCode( 896 theValidationSupportContext, 897 new LookupCodeRequest(includeOrExcludeConceptSystemUrl, theWantCode)); 898 if (lookup != null) { 899 ableToHandleCode = true; 900 if (lookup.isFound()) { 901 CodeSystem.ConceptDefinitionComponent conceptDefinition = 902 new CodeSystem.ConceptDefinitionComponent() 903 .addConcept() 904 .setCode(theWantCode) 905 .setDisplay(lookup.getCodeDisplay()); 906 List<CodeSystem.ConceptDefinitionComponent> codesList = 907 Collections.singletonList(conceptDefinition); 908 addCodes( 909 includeOrExcludeConceptSystemUrl, 910 includeOrExcludeConceptSystemVersion, 911 codesList, 912 nextCodeList, 913 wantCodes); 914 } 915 } 916 } else { 917 918 /* 919 * If we're doing an expansion specifically looking for a single code, that means we're validating that code. 920 * In the case where we have a ValueSet that explicitly enumerates a collection of codes 921 * (via ValueSet.compose.include.code) in a code system that is unknown we'll assume the code is valid 922 * even if we can't find the CodeSystem. This is a compromise obviously, since it would be ideal for 923 * CodeSystems to always be known, but realistically there are always going to be CodeSystems that 924 * can't be supplied because of copyright issues, or because they are grammar based. Allowing a VS to 925 * enumerate a set of good codes for them is a nice compromise there. 926 */ 927 if (Objects.equals(theInclude.getSystem(), theWantSystemUrlAndVersion)) { 928 Optional<org.hl7.fhir.r5.model.ValueSet.ConceptReferenceComponent> 929 matchingEnumeratedConcept = theInclude.getConcept().stream() 930 .filter(t -> Objects.equals(t.getCode(), theWantCode)) 931 .findFirst(); 932 933 // If the ValueSet.compose.include has no individual concepts in it, and 934 // we can't find the actual referenced CodeSystem, we have no choice 935 // but to fail 936 if (isIncludeWithDeclaredConcepts) { 937 ableToHandleCode = true; 938 } else { 939 failureMessage = getFailureMessageForMissingOrUnusableCodeSystem( 940 includeOrExcludeSystemResource, loadedCodeSystemUrl); 941 } 942 943 if (matchingEnumeratedConcept.isPresent()) { 944 CodeSystem.ConceptDefinitionComponent conceptDefinition = 945 new CodeSystem.ConceptDefinitionComponent() 946 .addConcept() 947 .setCode(theWantCode) 948 .setDisplay(matchingEnumeratedConcept 949 .get() 950 .getDisplay()); 951 List<CodeSystem.ConceptDefinitionComponent> codesList = 952 Collections.singletonList(conceptDefinition); 953 addCodes( 954 includeOrExcludeConceptSystemUrl, 955 includeOrExcludeConceptSystemVersion, 956 codesList, 957 nextCodeList, 958 wantCodes); 959 } 960 } 961 } 962 } else { 963 if (isIncludeFromSystem && !isIncludeWithFilter) { 964 if (isIncludeWithDeclaredConcepts) { 965 theInclude.getConcept().stream() 966 .map(t -> new FhirVersionIndependentConcept( 967 theInclude.getSystem(), 968 t.getCode(), 969 t.getDisplay(), 970 theInclude.getVersion())) 971 .forEach(nextCodeList::add); 972 ableToHandleCode = true; 973 } else if (isIncludeCodeSystemIgnored) { 974 ableToHandleCode = true; 975 } 976 } 977 978 if (!ableToHandleCode) { 979 failureMessage = getFailureMessageForMissingOrUnusableCodeSystem( 980 includeOrExcludeSystemResource, loadedCodeSystemUrl); 981 } 982 } 983 984 } else { 985 ableToHandleCode = true; 986 } 987 988 if (!ableToHandleCode) { 989 if (failureMessage == null) { 990 if (includeOrExcludeSystemResource == null) { 991 failureMessage = getFailureMessageForMissingOrUnusableCodeSystem( 992 includeOrExcludeSystemResource, loadedCodeSystemUrl); 993 } else { 994 failureMessage = "Unable to expand value set"; 995 } 996 } 997 998 throw new ExpansionCouldNotBeCompletedInternallyException( 999 Msg.code(702) + failureMessage, 1000 new CodeValidationIssue( 1001 failureMessage, 1002 IssueSeverity.ERROR, 1003 CodeValidationIssueCode.NOT_FOUND, 1004 CodeValidationIssueCoding.NOT_FOUND)); 1005 } 1006 1007 if (includeOrExcludeSystemResource != null 1008 && includeOrExcludeSystemResource.getContent() != Enumerations.CodeSystemContentMode.NOTPRESENT) { 1009 addCodes( 1010 includeOrExcludeConceptSystemUrl, 1011 includeOrExcludeConceptSystemVersion, 1012 includeOrExcludeSystemResource.getConcept(), 1013 nextCodeList, 1014 wantCodes); 1015 } 1016 } 1017 1018 for (CanonicalType nextValueSetInclude : theInclude.getValueSet()) { 1019 org.hl7.fhir.r5.model.ValueSet vs = valueSetLoader.apply(nextValueSetInclude.getValueAsString()); 1020 if (vs != null) { 1021 org.hl7.fhir.r5.model.ValueSet subExpansion = expandValueSetR5( 1022 theValidationSupportContext, vs, theWantSystemUrlAndVersion, theWantCode) 1023 .getValueSet(); 1024 if (subExpansion == null) { 1025 String theMessage = "Failed to expand ValueSet: " + nextValueSetInclude.getValueAsString(); 1026 throw new ExpansionCouldNotBeCompletedInternallyException( 1027 Msg.code(703) + theMessage, 1028 new CodeValidationIssue( 1029 theMessage, 1030 IssueSeverity.ERROR, 1031 CodeValidationIssueCode.INVALID, 1032 CodeValidationIssueCoding.VS_INVALID)); 1033 } 1034 for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next : 1035 subExpansion.getExpansion().getContains()) { 1036 nextCodeList.add(new FhirVersionIndependentConcept( 1037 next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion())); 1038 } 1039 } 1040 } 1041 1042 boolean retVal = false; 1043 ValueSetExpansionFilterContext valueSetExpansionFilterContext = 1044 new ValueSetExpansionFilterContext(includeOrExcludeSystemResource, theInclude.getFilter()); 1045 1046 for (FhirVersionIndependentConcept next : nextCodeList) { 1047 if (includeOrExcludeSystemResource != null && theWantCode != null) { 1048 boolean matches = includeOrExcludeSystemResource.getCaseSensitive() 1049 ? theWantCode.equals(next.getCode()) 1050 : theWantCode.equalsIgnoreCase(next.getCode()); 1051 1052 if (!matches) { 1053 continue; 1054 } 1055 } 1056 1057 if (!valueSetExpansionFilterContext.isFiltered(next)) { 1058 theConsumer.accept(next); 1059 retVal = true; 1060 } 1061 } 1062 1063 return retVal; 1064 } 1065 1066 private Function<String, org.hl7.fhir.r5.model.ValueSet> newValueSetLoader( 1067 ValidationSupportContext theValidationSupportContext) { 1068 return t -> { 1069 IBaseResource valueSet = 1070 theValidationSupportContext.getRootValidationSupport().fetchValueSet(t); 1071 return myVersionCanonicalizer.valueSetToValidatorCanonical(valueSet); 1072 }; 1073 } 1074 1075 private Function<String, CodeSystem> newCodeSystemLoader(ValidationSupportContext theValidationSupportContext) { 1076 FhirVersionEnum version = myCtx.getVersion().getVersion(); 1077 if (FhirVersionEnum.DSTU2.equals(version) || FhirVersionEnum.DSTU2_HL7ORG.equals(version)) { 1078 return t -> { 1079 IBaseResource codeSystem = 1080 theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(t); 1081 CodeSystem retVal = null; 1082 if (codeSystem != null) { 1083 retVal = new CodeSystem(); 1084 if (codeSystem instanceof ca.uhn.fhir.model.dstu2.resource.ValueSet codeSystemCasted) { 1085 retVal.setUrl(codeSystemCasted.getUrl()); 1086 addCodesDstu2(codeSystemCasted.getCodeSystem().getConcept(), retVal.getConcept()); 1087 } else { 1088 org.hl7.fhir.dstu2.model.ValueSet codeSystemCasted = 1089 (org.hl7.fhir.dstu2.model.ValueSet) codeSystem; 1090 retVal.setUrl(codeSystemCasted.getUrl()); 1091 addCodesDstu2Hl7Org(codeSystemCasted.getCodeSystem().getConcept(), retVal.getConcept()); 1092 } 1093 } 1094 return retVal; 1095 }; 1096 } else { 1097 return t -> { 1098 IBaseResource codeSystem = 1099 theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(t); 1100 return myVersionCanonicalizer.codeSystemToValidatorCanonical(codeSystem); 1101 }; 1102 } 1103 } 1104 1105 private String getFailureMessageForMissingOrUnusableCodeSystem( 1106 CodeSystem includeOrExcludeSystemResource, String loadedCodeSystemUrl) { 1107 String failureMessage; 1108 if (includeOrExcludeSystemResource == null) { 1109 failureMessage = "Unable to expand ValueSet because CodeSystem could not be found: " + loadedCodeSystemUrl; 1110 } else { 1111 assert includeOrExcludeSystemResource.getContent() == Enumerations.CodeSystemContentMode.NOTPRESENT; 1112 failureMessage = 1113 "Unable to expand ValueSet because CodeSystem has CodeSystem.content=not-present but contents were not found: " 1114 + loadedCodeSystemUrl; 1115 } 1116 return failureMessage; 1117 } 1118 1119 private void addCodes( 1120 String theCodeSystemUrl, 1121 String theCodeSystemVersion, 1122 List<CodeSystem.ConceptDefinitionComponent> theSource, 1123 List<FhirVersionIndependentConcept> theTarget, 1124 Set<String> theCodeFilter) { 1125 for (CodeSystem.ConceptDefinitionComponent next : theSource) { 1126 if (isNotBlank(next.getCode())) { 1127 if (theCodeFilter == null || theCodeFilter.contains(next.getCode())) { 1128 theTarget.add(new FhirVersionIndependentConcept( 1129 theCodeSystemUrl, next.getCode(), next.getDisplay(), theCodeSystemVersion)); 1130 } 1131 } 1132 addCodes(theCodeSystemUrl, theCodeSystemVersion, next.getConcept(), theTarget, theCodeFilter); 1133 } 1134 } 1135 1136 private String optionallyPopulateVersionFromUrl(String theSystemUrl, String theVersion) { 1137 if (contains(theSystemUrl, OUR_PIPE_CHARACTER) && isBlank(theVersion)) { 1138 theVersion = substringAfter(theSystemUrl, OUR_PIPE_CHARACTER); 1139 } 1140 return theVersion; 1141 } 1142 1143 private static void populateSourceDetailsForInMemoryExpansion( 1144 String theValueSetUrl, CodeValidationResult codeValidationResult) { 1145 codeValidationResult.setSourceDetails( 1146 "Code was validated against in-memory expansion of ValueSet: " + theValueSetUrl); 1147 } 1148 1149 public static CodeValidationResult createResultForDisplayMismatch( 1150 FhirContext theFhirContext, 1151 String theCode, 1152 String theDisplay, 1153 String theExpectedDisplay, 1154 String theCodeSystem, 1155 String theCodeSystemVersion, 1156 IssueSeverity theIssueSeverityForCodeDisplayMismatch) { 1157 return createResultForDisplayMismatch( 1158 theFhirContext, 1159 theCode, 1160 theDisplay, 1161 theExpectedDisplay, 1162 theCodeSystem, 1163 theCodeSystemVersion, 1164 "", 1165 theIssueSeverityForCodeDisplayMismatch); 1166 } 1167 1168 private static CodeValidationResult createResultForDisplayMismatch( 1169 FhirContext theFhirContext, 1170 String theCode, 1171 String theDisplay, 1172 String theExpectedDisplay, 1173 String theCodeSystem, 1174 String theCodeSystemVersion, 1175 String theMessageAppend, 1176 IssueSeverity theIssueSeverityForCodeDisplayMismatch) { 1177 1178 String message; 1179 IssueSeverity issueSeverity = theIssueSeverityForCodeDisplayMismatch; 1180 if (issueSeverity == IssueSeverity.INFORMATION) { 1181 message = null; 1182 issueSeverity = null; 1183 } else { 1184 message = theFhirContext 1185 .getLocalizer() 1186 .getMessage( 1187 InMemoryTerminologyServerValidationSupport.class, 1188 "displayMismatch", 1189 theDisplay, 1190 theExpectedDisplay, 1191 theCodeSystem, 1192 theCode) 1193 + theMessageAppend; 1194 } 1195 CodeValidationResult codeValidationResult = new CodeValidationResult() 1196 .setSeverity(issueSeverity) 1197 .setMessage(message) 1198 .setCode(theCode) 1199 .setCodeSystemVersion(theCodeSystemVersion) 1200 .setDisplay(theExpectedDisplay); 1201 if (issueSeverity != null) { 1202 codeValidationResult.setIssues(Collections.singletonList(new CodeValidationIssue( 1203 message, 1204 theIssueSeverityForCodeDisplayMismatch, 1205 CodeValidationIssueCode.INVALID, 1206 CodeValidationIssueCoding.INVALID_DISPLAY))); 1207 } 1208 1209 return codeValidationResult; 1210 } 1211 1212 private static void flattenAndConvertCodesDstu2( 1213 List<ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains> theInput, 1214 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1215 for (ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains next : theInput) { 1216 theFhirVersionIndependentConcepts.add( 1217 new FhirVersionIndependentConcept(next.getSystem(), next.getCode(), next.getDisplay())); 1218 flattenAndConvertCodesDstu2(next.getContains(), theFhirVersionIndependentConcepts); 1219 } 1220 } 1221 1222 private static void flattenAndConvertCodesDstu2Hl7Org( 1223 List<org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent> theInput, 1224 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1225 for (org.hl7.fhir.dstu2.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) { 1226 theFhirVersionIndependentConcepts.add( 1227 new FhirVersionIndependentConcept(next.getSystem(), next.getCode(), next.getDisplay())); 1228 flattenAndConvertCodesDstu2Hl7Org(next.getContains(), theFhirVersionIndependentConcepts); 1229 } 1230 } 1231 1232 private static void flattenAndConvertCodesDstu3( 1233 List<org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent> theInput, 1234 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1235 for (org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) { 1236 theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept( 1237 next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion())); 1238 flattenAndConvertCodesDstu3(next.getContains(), theFhirVersionIndependentConcepts); 1239 } 1240 } 1241 1242 private static void flattenAndConvertCodesR4( 1243 List<org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent> theInput, 1244 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1245 for (org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) { 1246 theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept( 1247 next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion())); 1248 flattenAndConvertCodesR4(next.getContains(), theFhirVersionIndependentConcepts); 1249 } 1250 } 1251 1252 private static void flattenAndConvertCodesR4B( 1253 List<org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent> theInput, 1254 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1255 for (org.hl7.fhir.r4b.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) { 1256 theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept( 1257 next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion())); 1258 flattenAndConvertCodesR4B(next.getContains(), theFhirVersionIndependentConcepts); 1259 } 1260 } 1261 1262 private static void flattenAndConvertCodesR5( 1263 List<org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent> theInput, 1264 List<FhirVersionIndependentConcept> theFhirVersionIndependentConcepts) { 1265 for (org.hl7.fhir.r5.model.ValueSet.ValueSetExpansionContainsComponent next : theInput) { 1266 theFhirVersionIndependentConcepts.add(new FhirVersionIndependentConcept( 1267 next.getSystem(), next.getCode(), next.getDisplay(), next.getVersion())); 1268 flattenAndConvertCodesR5(next.getContains(), theFhirVersionIndependentConcepts); 1269 } 1270 } 1271 1272 public static class ExpansionCouldNotBeCompletedInternallyException extends Exception { 1273 1274 private static final long serialVersionUID = -2226561628771483085L; 1275 private final CodeValidationIssue myCodeValidationIssue; 1276 1277 public ExpansionCouldNotBeCompletedInternallyException( 1278 String theMessage, CodeValidationIssue theCodeValidationIssue) { 1279 super(theMessage); 1280 myCodeValidationIssue = theCodeValidationIssue; 1281 } 1282 1283 public CodeValidationIssue getCodeValidationIssue() { 1284 return myCodeValidationIssue; 1285 } 1286 } 1287 1288 private static class ValueSetAndMessages { 1289 1290 private org.hl7.fhir.r5.model.ValueSet myValueSet; 1291 private List<String> myMessages = new ArrayList<>(); 1292 1293 public void setValueSet(org.hl7.fhir.r5.model.ValueSet theValueSet) { 1294 myValueSet = theValueSet; 1295 } 1296 1297 public void addMessage(String theMessage) { 1298 myMessages.add(theMessage); 1299 } 1300 1301 public org.hl7.fhir.r5.model.ValueSet getValueSet() { 1302 return myValueSet; 1303 } 1304 1305 public List<String> getMessages() { 1306 return myMessages; 1307 } 1308 } 1309}