001package org.hl7.fhir.common.hapi.validation.support; 002 003import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 004import ca.uhn.fhir.context.ConfigurationException; 005import ca.uhn.fhir.context.FhirContext; 006import ca.uhn.fhir.context.FhirVersionEnum; 007import ca.uhn.fhir.context.support.ConceptValidationOptions; 008import ca.uhn.fhir.context.support.IValidationSupport; 009import ca.uhn.fhir.context.support.LookupCodeRequest; 010import ca.uhn.fhir.context.support.TranslateConceptResults; 011import ca.uhn.fhir.context.support.ValidationSupportContext; 012import ca.uhn.fhir.context.support.ValueSetExpansionOptions; 013import ca.uhn.fhir.i18n.Msg; 014import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 015import ca.uhn.fhir.sl.cache.Cache; 016import ca.uhn.fhir.sl.cache.CacheFactory; 017import ca.uhn.fhir.util.FhirTerser; 018import ca.uhn.fhir.util.Logs; 019import ca.uhn.fhir.util.StopWatch; 020import jakarta.annotation.Nonnull; 021import jakarta.annotation.Nullable; 022import jakarta.annotation.PostConstruct; 023import jakarta.annotation.PreDestroy; 024import org.apache.commons.lang3.Validate; 025import org.apache.commons.lang3.concurrent.BasicThreadFactory; 026import org.hl7.fhir.instance.model.api.IBaseResource; 027import org.hl7.fhir.instance.model.api.IPrimitiveType; 028import org.slf4j.Logger; 029 030import java.time.Duration; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.HashSet; 036import java.util.List; 037import java.util.Map; 038import java.util.Objects; 039import java.util.Set; 040import java.util.UUID; 041import java.util.concurrent.LinkedBlockingQueue; 042import java.util.concurrent.ThreadPoolExecutor; 043import java.util.concurrent.TimeUnit; 044import java.util.function.Function; 045import java.util.function.Supplier; 046 047import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 048import static org.apache.commons.lang3.StringUtils.defaultIfBlank; 049import static org.apache.commons.lang3.StringUtils.isBlank; 050import static org.apache.commons.lang3.StringUtils.isNotBlank; 051 052/** 053 * This validation support module has two primary purposes: It can be used to 054 * chain multiple backing modules together, and it can optionally cache the 055 * results. 056 * <p> 057 * The following chaining logic is used: 058 * <ul> 059 * <li> 060 * Calls to {@literal fetchAll...} methods such as {@link #fetchAllConformanceResources()} 061 * and {@link #fetchAllStructureDefinitions()} will call every method in the chain in 062 * order, and aggregate the results into a single list to return. 063 * </li> 064 * <li> 065 * Calls to fetch or validate codes, such as {@link #validateCode(ValidationSupportContext, ConceptValidationOptions, String, String, String, String)} 066 * and {@link #lookupCode(ValidationSupportContext, LookupCodeRequest)} will first test 067 * each module in the chain using the {@link #isCodeSystemSupported(ValidationSupportContext, String)} 068 * or {@link #isValueSetSupported(ValidationSupportContext, String)} 069 * methods (depending on whether a ValueSet URL is present in the method parameters) 070 * and will invoke any methods in the chain which return that they can handle the given 071 * CodeSystem/ValueSet URL. The first non-null value returned by a method in the chain 072 * that can support the URL will be returned to the caller. 073 * </li> 074 * <li> 075 * All other methods will invoke each method in the chain in order, and will stop processing and return 076 * immediately as soon as the first non-null value is returned. 077 * </li> 078 * </ul> 079 * </p> 080 * <p> 081 * The following caching logic is used if caching is enabled using {@link CacheConfiguration}. 082 * You can use {@link CacheConfiguration#disabled()} if you want to disable caching. 083 * <ul> 084 * <li> 085 * Calls to fetch StructureDefinitions including {@link #fetchAllStructureDefinitions()} 086 * and {@link #fetchStructureDefinition(String)} are cached in a non-expiring cache. 087 * This is because the {@link org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator} 088 * module makes assumptions that these objects will not change for the lifetime 089 * of the validator for performance reasons. 090 * </li> 091 * <li> 092 * Calls to all other {@literal fetchAll...} methods including 093 * {@link #fetchAllConformanceResources()} and {@link #fetchAllSearchParameters()} 094 * cache their results in an expiring cache, but will refresh that cache asynchronously. 095 * </li> 096 * <li> 097 * Results of {@link #generateSnapshot(ValidationSupportContext, IBaseResource, String, String, String)} 098 * are not cached, since this method is generally called in contexts where the results 099 * are cached. 100 * </li> 101 * <li> 102 * Results of all other methods are stored in an expiring cache. 103 * </li> 104 * </ul> 105 * </p> 106 * <p> 107 * Note that caching functionality used to be provided by a separate provider 108 * called {@literal CachingValidationSupport} but that functionality has been 109 * moved into this class as of HAPI FHIR 8.0.0, because it is possible to 110 * provide a more efficient chain when these functions are combined. 111 * </p> 112 */ 113public class ValidationSupportChain implements IValidationSupport { 114 public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions(); 115 static Logger ourLog = Logs.getTerminologyTroubleshootingLog(); 116 private final List<IValidationSupport> myChain = new ArrayList<>(); 117 118 @Nullable 119 private final Cache<BaseKey<?>, Object> myExpiringCache; 120 121 @Nullable 122 private final Map<BaseKey<?>, Object> myNonExpiringCache; 123 124 /** 125 * See class documentation for an explanation of why this is separate 126 * and non-expiring. Note that this field is non-synchronized. If you 127 * access it, you should first wrap the call in 128 * <code>synchronized(myStructureDefinitionsByUrl)</code>. 129 */ 130 @Nonnull 131 private final Map<String, IBaseResource> myStructureDefinitionsByUrl = new HashMap<>(); 132 /** 133 * See class documentation for an explanation of why this is separate 134 * and non-expiring. Note that this field is non-synchronized. If you 135 * access it, you should first wrap the call in 136 * <code>synchronized(myStructureDefinitionsByUrl)</code> (synchronize on 137 * the other field because both collections are expected to be modified 138 * at the same time). 139 */ 140 @Nonnull 141 private final List<IBaseResource> myStructureDefinitionsAsList = new ArrayList<>(); 142 143 private final ThreadPoolExecutor myBackgroundExecutor; 144 private final CacheConfiguration myCacheConfiguration; 145 private boolean myEnabledValidationForCodingsLogicalAnd; 146 private String myName = getClass().getSimpleName(); 147 private ValidationSupportChainMetrics myMetrics; 148 private volatile boolean myHaveFetchedAllStructureDefinitions = false; 149 150 /** 151 * Constructor which initializes the chain with no modules (modules 152 * must subsequently be registered using {@link #addValidationSupport(IValidationSupport)}). 153 * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. 154 */ 155 public ValidationSupportChain() { 156 /* 157 * Note, this constructor is called by default when 158 * FhirContext#getValidationSupport() is called, so it should 159 * provide sensible defaults. 160 */ 161 this(Collections.emptyList()); 162 } 163 164 /** 165 * Constructor which initializes the chain with the given modules. 166 * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. 167 */ 168 public ValidationSupportChain(IValidationSupport... theValidationSupportModules) { 169 this( 170 theValidationSupportModules != null 171 ? Arrays.asList(theValidationSupportModules) 172 : Collections.emptyList()); 173 } 174 175 /** 176 * Constructor which initializes the chain with the given modules. 177 * The cache will be enabled using {@link CacheConfiguration#defaultValues()}. 178 */ 179 public ValidationSupportChain(List<IValidationSupport> theValidationSupportModules) { 180 this(CacheConfiguration.defaultValues(), theValidationSupportModules); 181 } 182 183 /** 184 * Constructor 185 * 186 * @param theCacheConfiguration The caching configuration 187 * @param theValidationSupportModules The initial modules to add to the chain 188 */ 189 public ValidationSupportChain( 190 @Nonnull CacheConfiguration theCacheConfiguration, IValidationSupport... theValidationSupportModules) { 191 this( 192 theCacheConfiguration, 193 theValidationSupportModules != null 194 ? Arrays.asList(theValidationSupportModules) 195 : Collections.emptyList()); 196 } 197 198 /** 199 * Constructor 200 * 201 * @param theCacheConfiguration The caching configuration 202 * @param theValidationSupportModules The initial modules to add to the chain 203 */ 204 public ValidationSupportChain( 205 @Nonnull CacheConfiguration theCacheConfiguration, 206 @Nonnull List<IValidationSupport> theValidationSupportModules) { 207 208 Validate.notNull(theCacheConfiguration, "theCacheConfiguration must not be null"); 209 Validate.notNull(theValidationSupportModules, "theValidationSupportModules must not be null"); 210 211 myCacheConfiguration = theCacheConfiguration; 212 if (theCacheConfiguration.getCacheSize() == 0 || theCacheConfiguration.getCacheTimeout() == 0) { 213 myExpiringCache = null; 214 myNonExpiringCache = null; 215 myBackgroundExecutor = null; 216 } else { 217 myExpiringCache = 218 CacheFactory.build(theCacheConfiguration.getCacheTimeout(), theCacheConfiguration.getCacheSize()); 219 myNonExpiringCache = Collections.synchronizedMap(new HashMap<>()); 220 221 LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000); 222 BasicThreadFactory threadFactory = new BasicThreadFactory.Builder() 223 .namingPattern("CachingValidationSupport-%d") 224 .daemon(false) 225 .priority(Thread.NORM_PRIORITY) 226 .build(); 227 228 // NOTE: We're not using ThreadPoolUtil here, because that class depends on Spring and 229 // we want the validator infrastructure to not require spring dependencies. 230 myBackgroundExecutor = new ThreadPoolExecutor( 231 1, 232 1, 233 0L, 234 TimeUnit.MILLISECONDS, 235 executorQueue, 236 threadFactory, 237 new ThreadPoolExecutor.DiscardPolicy()); 238 } 239 240 for (IValidationSupport next : theValidationSupportModules) { 241 if (next != null) { 242 addValidationSupport(next); 243 } 244 } 245 } 246 247 @Override 248 public String getName() { 249 return myName; 250 } 251 252 /** 253 * Sets a name for this chain. This name will be returned by 254 * {@link #getName()} and used by OpenTelemetry. 255 */ 256 public void setName(String theName) { 257 Validate.notBlank(theName, "theName must not be blank"); 258 myName = theName; 259 } 260 261 @PostConstruct 262 public void start() { 263 if (myMetrics == null) { 264 myMetrics = new ValidationSupportChainMetrics(this); 265 myMetrics.start(); 266 } 267 } 268 269 @PreDestroy 270 public void stop() { 271 if (myMetrics != null) { 272 myMetrics.stop(); 273 myMetrics = null; 274 } 275 } 276 277 @Override 278 public boolean isCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid() { 279 return myEnabledValidationForCodingsLogicalAnd; 280 } 281 282 /** 283 * When validating a CodeableConcept containing multiple codings, this method can be used to control whether 284 * the validator requires all codings in the CodeableConcept to be valid in order to consider the 285 * CodeableConcept valid. 286 * <p> 287 * See VersionSpecificWorkerContextWrapper#validateCode in hapi-fhir-validation, and the refer to the values below 288 * for the behaviour associated with each value. 289 * </p> 290 * <p> 291 * <ul> 292 * <li>If <code>false</code> (default setting) the validation for codings will return a positive result only if 293 * ALL codings are valid.</li> 294 * <li>If <code>true</code> the validation for codings will return a positive result if ANY codings are valid. 295 * </li> 296 * </ul> 297 * </p> 298 * 299 * @return true or false depending on the desired coding validation behaviour. 300 */ 301 public ValidationSupportChain setCodeableConceptValidationSuccessfulIfNotAllCodingsAreValid( 302 boolean theEnabledValidationForCodingsLogicalAnd) { 303 myEnabledValidationForCodingsLogicalAnd = theEnabledValidationForCodingsLogicalAnd; 304 return this; 305 } 306 307 @Override 308 public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) { 309 TranslateConceptKey key = new TranslateConceptKey(theRequest); 310 CacheValue<TranslateConceptResults> retVal = getFromCache(key); 311 if (retVal == null) { 312 313 /* 314 * The chain behaviour for this method is to call every element in the 315 * chain and aggregate the results (as opposed to just using the first 316 * module which provides a response). 317 */ 318 retVal = CacheValue.empty(); 319 320 TranslateConceptResults outcome = null; 321 for (IValidationSupport next : myChain) { 322 TranslateConceptResults translations = next.translateConcept(theRequest); 323 if (translations != null) { 324 if (outcome == null) { 325 outcome = new TranslateConceptResults(); 326 } 327 328 if (outcome.getMessage() == null) { 329 outcome.setMessage(translations.getMessage()); 330 } 331 332 if (translations.getResult() && !outcome.getResult()) { 333 outcome.setResult(translations.getResult()); 334 outcome.setMessage(translations.getMessage()); 335 } 336 337 if (!translations.isEmpty()) { 338 ourLog.debug( 339 "{} found {} concept translation{} for {}", 340 next.getName(), 341 translations.size(), 342 translations.size() > 1 ? "s" : "", 343 theRequest); 344 outcome.getResults().addAll(translations.getResults()); 345 } 346 } 347 } 348 349 if (outcome != null) { 350 retVal = new CacheValue<>(outcome); 351 } 352 353 putInCache(key, retVal); 354 } 355 356 return retVal.getValue(); 357 } 358 359 @Override 360 public void invalidateCaches() { 361 ourLog.debug("Invalidating caches in {} validation support modules", myChain.size()); 362 myHaveFetchedAllStructureDefinitions = false; 363 for (IValidationSupport next : myChain) { 364 next.invalidateCaches(); 365 } 366 if (myNonExpiringCache != null) { 367 myNonExpiringCache.clear(); 368 } 369 if (myExpiringCache != null) { 370 myExpiringCache.invalidateAll(); 371 } 372 synchronized (myStructureDefinitionsByUrl) { 373 myStructureDefinitionsByUrl.clear(); 374 myStructureDefinitionsAsList.clear(); 375 } 376 } 377 378 /** 379 * Invalidate the expiring cache, but not the permanent StructureDefinition cache 380 * 381 * @since 8.0.0 382 */ 383 public void invalidateExpiringCaches() { 384 if (myExpiringCache != null) { 385 myExpiringCache.invalidateAll(); 386 } 387 } 388 389 @Override 390 public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { 391 for (IValidationSupport next : myChain) { 392 boolean retVal = isValueSetSupported(theValidationSupportContext, next, theValueSetUrl); 393 if (retVal) { 394 ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName()); 395 return true; 396 } 397 } 398 return false; 399 } 400 401 private boolean isValueSetSupported( 402 ValidationSupportContext theValidationSupportContext, 403 IValidationSupport theValidationSupport, 404 String theValueSetUrl) { 405 IsValueSetSupportedKey key = new IsValueSetSupportedKey(theValidationSupport, theValueSetUrl); 406 CacheValue<Boolean> value = getFromCache(key); 407 if (value == null) { 408 value = new CacheValue<>( 409 theValidationSupport.isValueSetSupported(theValidationSupportContext, theValueSetUrl)); 410 putInCache(key, value); 411 } 412 return value.getValue(); 413 } 414 415 @Override 416 public IBaseResource generateSnapshot( 417 ValidationSupportContext theValidationSupportContext, 418 IBaseResource theInput, 419 String theUrl, 420 String theWebUrl, 421 String theProfileName) { 422 423 /* 424 * No caching for this method because we typically cache the results anyhow. 425 * If this ever changes, make sure to update the class javadocs and the 426 * HAPI FHIR documentation which indicate that this isn't cached. 427 */ 428 429 for (IValidationSupport next : myChain) { 430 IBaseResource retVal = 431 next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName); 432 if (retVal != null) { 433 ourLog.atDebug() 434 .setMessage("Profile snapshot for {} generated by {}") 435 .addArgument(theInput::getIdElement) 436 .addArgument(next::getName) 437 .log(); 438 return retVal; 439 } 440 } 441 return null; 442 } 443 444 @Override 445 public FhirContext getFhirContext() { 446 if (myChain.isEmpty()) { 447 return null; 448 } 449 return myChain.get(0).getFhirContext(); 450 } 451 452 /** 453 * Add a validation support module to the chain. 454 * <p> 455 * Note that this method is not thread-safe. All validation support modules should be added prior to use. 456 * </p> 457 * 458 * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain. 459 */ 460 public void addValidationSupport(IValidationSupport theValidationSupport) { 461 int index = myChain.size(); 462 addValidationSupport(index, theValidationSupport); 463 } 464 465 /** 466 * Add a validation support module to the chain at the given index. 467 * <p> 468 * Note that this method is not thread-safe. All validation support modules should be added prior to use. 469 * </p> 470 * 471 * @param theIndex The index to add to 472 * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain. 473 */ 474 public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) { 475 Validate.notNull(theValidationSupport, "theValidationSupport must not be null"); 476 invalidateCaches(); 477 478 if (theValidationSupport.getFhirContext() == null) { 479 String message = "Can not add validation support: getFhirContext() returns null"; 480 throw new ConfigurationException(Msg.code(708) + message); 481 } 482 483 FhirContext existingFhirContext = getFhirContext(); 484 if (existingFhirContext != null) { 485 FhirVersionEnum newVersion = 486 theValidationSupport.getFhirContext().getVersion().getVersion(); 487 FhirVersionEnum existingVersion = existingFhirContext.getVersion().getVersion(); 488 if (!existingVersion.equals(newVersion)) { 489 String message = "Trying to add validation support of version " + newVersion + " to chain with " 490 + myChain.size() + " entries of version " + existingVersion; 491 throw new ConfigurationException(Msg.code(709) + message); 492 } 493 } 494 495 myChain.add(theIndex, theValidationSupport); 496 } 497 498 /** 499 * Removes an item from the chain. Note that this method is mostly intended for testing. Removing items from the chain while validation is 500 * actually occurring is not an expected use case for this class. 501 */ 502 public void removeValidationSupport(IValidationSupport theValidationSupport) { 503 myChain.remove(theValidationSupport); 504 } 505 506 @Nullable 507 @Override 508 public ValueSetExpansionOutcome expandValueSet( 509 ValidationSupportContext theValidationSupportContext, 510 @Nullable ValueSetExpansionOptions theExpansionOptions, 511 @Nonnull String theValueSetUrlToExpand) 512 throws ResourceNotFoundException { 513 ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); 514 ExpandValueSetKey key = new ExpandValueSetKey(expansionOptions, null, theValueSetUrlToExpand); 515 CacheValue<ValueSetExpansionOutcome> retVal = getFromCache(key); 516 517 if (retVal == null) { 518 retVal = CacheValue.empty(); 519 for (IValidationSupport next : myChain) { 520 if (isValueSetSupported(theValidationSupportContext, next, theValueSetUrlToExpand)) { 521 ValueSetExpansionOutcome expanded = 522 next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetUrlToExpand); 523 if (expanded != null) { 524 ourLog.debug("ValueSet {} expanded by URL by {}", theValueSetUrlToExpand, next.getName()); 525 retVal = new CacheValue<>(expanded); 526 break; 527 } 528 } 529 } 530 531 putInCache(key, retVal); 532 } 533 534 return retVal.getValue(); 535 } 536 537 @Override 538 public ValueSetExpansionOutcome expandValueSet( 539 ValidationSupportContext theValidationSupportContext, 540 ValueSetExpansionOptions theExpansionOptions, 541 @Nonnull IBaseResource theValueSetToExpand) { 542 543 ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS); 544 String id = theValueSetToExpand.getIdElement().getValue(); 545 ExpandValueSetKey key = null; 546 CacheValue<ValueSetExpansionOutcome> retVal = null; 547 if (isNotBlank(id)) { 548 key = new ExpandValueSetKey(expansionOptions, id, null); 549 retVal = getFromCache(key); 550 } 551 if (retVal == null) { 552 retVal = CacheValue.empty(); 553 for (IValidationSupport next : myChain) { 554 ValueSetExpansionOutcome expanded = 555 next.expandValueSet(theValidationSupportContext, expansionOptions, theValueSetToExpand); 556 if (expanded != null) { 557 ourLog.debug("ValueSet {} expanded by {}", theValueSetToExpand.getIdElement(), next.getName()); 558 retVal = new CacheValue<>(expanded); 559 break; 560 } 561 } 562 563 if (key != null) { 564 putInCache(key, retVal); 565 } 566 } 567 568 return retVal.getValue(); 569 } 570 571 @Override 572 public boolean isRemoteTerminologyServiceConfigured() { 573 return myChain.stream().anyMatch(RemoteTerminologyServiceValidationSupport.class::isInstance); 574 } 575 576 @Override 577 public List<IBaseResource> fetchAllConformanceResources() { 578 FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL); 579 Supplier<List<IBaseResource>> loader = () -> { 580 List<IBaseResource> allCandidates = new ArrayList<>(); 581 for (IValidationSupport next : myChain) { 582 List<IBaseResource> candidates = next.fetchAllConformanceResources(); 583 if (candidates != null) { 584 allCandidates.addAll(candidates); 585 } 586 } 587 return allCandidates; 588 }; 589 590 return getFromCacheWithAsyncRefresh(key, loader); 591 } 592 593 @SuppressWarnings("unchecked") 594 @Override 595 @Nonnull 596 public List<IBaseResource> fetchAllStructureDefinitions() { 597 if (!myHaveFetchedAllStructureDefinitions) { 598 FhirTerser terser = getFhirContext().newTerser(); 599 List<IBaseResource> allStructureDefinitions = 600 doFetchStructureDefinitions(IValidationSupport::fetchAllStructureDefinitions); 601 if (myExpiringCache != null) { 602 synchronized (myStructureDefinitionsByUrl) { 603 for (IBaseResource structureDefinition : allStructureDefinitions) { 604 String url = terser.getSinglePrimitiveValueOrNull(structureDefinition, "url"); 605 url = defaultIfBlank(url, UUID.randomUUID().toString()); 606 if (myStructureDefinitionsByUrl.putIfAbsent(url, structureDefinition) == null) { 607 myStructureDefinitionsAsList.add(structureDefinition); 608 } 609 } 610 } 611 } 612 myHaveFetchedAllStructureDefinitions = true; 613 } 614 return Collections.unmodifiableList(new ArrayList<>(myStructureDefinitionsAsList)); 615 } 616 617 @SuppressWarnings("unchecked") 618 @Override 619 public List<IBaseResource> fetchAllNonBaseStructureDefinitions() { 620 FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_NON_BASE_STRUCTUREDEFINITIONS); 621 Supplier<List<IBaseResource>> loader = 622 () -> doFetchStructureDefinitions(IValidationSupport::fetchAllNonBaseStructureDefinitions); 623 return getFromCacheWithAsyncRefresh(key, loader); 624 } 625 626 @SuppressWarnings("unchecked") 627 @Nullable 628 @Override 629 public <T extends IBaseResource> List<T> fetchAllSearchParameters() { 630 FetchAllKey key = new FetchAllKey(FetchAllKey.TypeEnum.ALL_SEARCHPARAMETERS); 631 Supplier<List<IBaseResource>> loader = 632 () -> doFetchStructureDefinitions(IValidationSupport::fetchAllSearchParameters); 633 return (List<T>) getFromCacheWithAsyncRefresh(key, loader); 634 } 635 636 private List<IBaseResource> doFetchStructureDefinitions( 637 Function<IValidationSupport, List<IBaseResource>> theFunction) { 638 ArrayList<IBaseResource> retVal = new ArrayList<>(); 639 Set<String> urls = new HashSet<>(); 640 for (IValidationSupport nextSupport : myChain) { 641 List<IBaseResource> allStructureDefinitions = theFunction.apply(nextSupport); 642 if (allStructureDefinitions != null) { 643 for (IBaseResource next : allStructureDefinitions) { 644 645 IPrimitiveType<?> urlType = 646 getFhirContext().newTerser().getSingleValueOrNull(next, "url", IPrimitiveType.class); 647 if (urlType == null 648 || isBlank(urlType.getValueAsString()) 649 || urls.add(urlType.getValueAsString())) { 650 retVal.add(next); 651 } 652 } 653 } 654 } 655 return retVal; 656 } 657 658 @Override 659 public IBaseResource fetchCodeSystem(String theSystem) { 660 Function<IValidationSupport, IBaseResource> invoker = v -> v.fetchCodeSystem(theSystem); 661 ResourceByUrlKey<IBaseResource> key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.CODESYSTEM, theSystem); 662 return fetchValue(key, invoker, theSystem); 663 } 664 665 private <T> T fetchValue(ResourceByUrlKey<T> theKey, Function<IValidationSupport, T> theInvoker, String theUrl) { 666 CacheValue<T> retVal = getFromCache(theKey); 667 668 if (retVal == null) { 669 retVal = CacheValue.empty(); 670 for (IValidationSupport next : myChain) { 671 T outcome = theInvoker.apply(next); 672 if (outcome != null) { 673 ourLog.debug("{} {} with URL {} fetched by {}", theKey.myType, outcome, theUrl, next.getName()); 674 retVal = new CacheValue<>(outcome); 675 break; 676 } 677 } 678 putInCache(theKey, retVal); 679 } 680 681 return retVal.getValue(); 682 } 683 684 @Override 685 public IBaseResource fetchValueSet(String theUrl) { 686 Function<IValidationSupport, IBaseResource> invoker = v -> v.fetchValueSet(theUrl); 687 ResourceByUrlKey<IBaseResource> key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.VALUESET, theUrl); 688 return fetchValue(key, invoker, theUrl); 689 } 690 691 @SuppressWarnings("unchecked") 692 @Override 693 public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) { 694 695 /* 696 * If we're looking for a common type with a dedicated fetch method, use that 697 * so that we can use a common cache location for lookups wanting a given 698 * URL on both methods (the validator will call both paths when looking for a 699 * specific URL so this improves cache efficiency). 700 */ 701 if (theClass != null) { 702 BaseRuntimeElementDefinition<?> elementDefinition = getFhirContext().getElementDefinition(theClass); 703 if (elementDefinition != null) { 704 switch (elementDefinition.getName()) { 705 case "ValueSet": 706 return (T) fetchValueSet(theUri); 707 case "CodeSystem": 708 return (T) fetchCodeSystem(theUri); 709 case "StructureDefinition": 710 return (T) fetchStructureDefinition(theUri); 711 } 712 } 713 } 714 715 Function<IValidationSupport, T> invoker = v -> v.fetchResource(theClass, theUri); 716 TypedResourceByUrlKey<T> key = new TypedResourceByUrlKey<>(theClass, theUri); 717 return fetchValue(key, invoker, theUri); 718 } 719 720 @Override 721 public byte[] fetchBinary(String theKey) { 722 Function<IValidationSupport, byte[]> invoker = v -> v.fetchBinary(theKey); 723 ResourceByUrlKey<byte[]> key = new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.BINARY, theKey); 724 return fetchValue(key, invoker, theKey); 725 } 726 727 @Override 728 public IBaseResource fetchStructureDefinition(String theUrl) { 729 synchronized (myStructureDefinitionsByUrl) { 730 IBaseResource candidate = myStructureDefinitionsByUrl.get(theUrl); 731 if (candidate == null) { 732 Function<IValidationSupport, IBaseResource> invoker = v -> v.fetchStructureDefinition(theUrl); 733 ResourceByUrlKey<IBaseResource> key = 734 new ResourceByUrlKey<>(ResourceByUrlKey.TypeEnum.STRUCTUREDEFINITION, theUrl); 735 candidate = fetchValue(key, invoker, theUrl); 736 if (myExpiringCache != null) { 737 if (candidate != null) { 738 if (myStructureDefinitionsByUrl.putIfAbsent(theUrl, candidate) == null) { 739 myStructureDefinitionsAsList.add(candidate); 740 } 741 } 742 } 743 } 744 return candidate; 745 } 746 } 747 748 @Override 749 public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { 750 for (IValidationSupport next : myChain) { 751 if (isCodeSystemSupported(theValidationSupportContext, next, theSystem)) { 752 if (ourLog.isDebugEnabled()) { 753 ourLog.debug("CodeSystem with System {} is supported by {}", theSystem, next.getName()); 754 } 755 return true; 756 } 757 } 758 return false; 759 } 760 761 private boolean isCodeSystemSupported( 762 ValidationSupportContext theValidationSupportContext, 763 IValidationSupport theValidationSupport, 764 String theCodeSystemUrl) { 765 IsCodeSystemSupportedKey key = new IsCodeSystemSupportedKey(theValidationSupport, theCodeSystemUrl); 766 CacheValue<Boolean> value = getFromCache(key); 767 if (value == null) { 768 value = new CacheValue<>( 769 theValidationSupport.isCodeSystemSupported(theValidationSupportContext, theCodeSystemUrl)); 770 putInCache(key, value); 771 } 772 return value.getValue(); 773 } 774 775 @Override 776 public CodeValidationResult validateCode( 777 @Nonnull ValidationSupportContext theValidationSupportContext, 778 @Nonnull ConceptValidationOptions theOptions, 779 String theCodeSystem, 780 String theCode, 781 String theDisplay, 782 String theValueSetUrl) { 783 784 ValidateCodeKey key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl); 785 CacheValue<CodeValidationResult> retVal = getFromCache(key); 786 if (retVal == null) { 787 retVal = CacheValue.empty(); 788 789 for (IValidationSupport next : myChain) { 790 if ((isBlank(theValueSetUrl) && isCodeSystemSupported(theValidationSupportContext, next, theCodeSystem)) 791 || (isNotBlank(theValueSetUrl) 792 && isValueSetSupported(theValidationSupportContext, next, theValueSetUrl))) { 793 CodeValidationResult outcome = next.validateCode( 794 theValidationSupportContext, 795 theOptions, 796 theCodeSystem, 797 theCode, 798 theDisplay, 799 theValueSetUrl); 800 if (outcome != null) { 801 ourLog.debug( 802 "Code {}|{} '{}' in ValueSet {} validated by {}", 803 theCodeSystem, 804 theCode, 805 theDisplay, 806 theValueSetUrl, 807 next.getName()); 808 retVal = new CacheValue<>(outcome); 809 break; 810 } 811 } 812 } 813 814 putInCache(key, retVal); 815 } 816 817 return retVal.getValue(); 818 } 819 820 @Override 821 public CodeValidationResult validateCodeInValueSet( 822 ValidationSupportContext theValidationSupportContext, 823 ConceptValidationOptions theOptions, 824 String theCodeSystem, 825 String theCode, 826 String theDisplay, 827 @Nonnull IBaseResource theValueSet) { 828 String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet); 829 830 ValidateCodeKey key = null; 831 CacheValue<CodeValidationResult> retVal = null; 832 if (isNotBlank(url)) { 833 key = new ValidateCodeKey(theOptions, theCodeSystem, theCode, theDisplay, url); 834 retVal = getFromCache(key); 835 } 836 if (retVal != null) { 837 return retVal.getValue(); 838 } 839 840 retVal = CacheValue.empty(); 841 for (IValidationSupport next : myChain) { 842 if (isBlank(url) || isValueSetSupported(theValidationSupportContext, next, url)) { 843 CodeValidationResult outcome = next.validateCodeInValueSet( 844 theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet); 845 if (outcome != null) { 846 ourLog.debug( 847 "Code {}|{} '{}' in ValueSet {} validated by {}", 848 theCodeSystem, 849 theCode, 850 theDisplay, 851 theValueSet.getIdElement(), 852 next.getName()); 853 retVal = new CacheValue<>(outcome); 854 break; 855 } 856 } 857 } 858 859 if (key != null) { 860 putInCache(key, retVal); 861 } 862 863 return retVal.getValue(); 864 } 865 866 @Override 867 public LookupCodeResult lookupCode( 868 ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) { 869 870 LookupCodeKey key = new LookupCodeKey(theLookupCodeRequest); 871 CacheValue<LookupCodeResult> retVal = getFromCache(key); 872 if (retVal == null) { 873 874 retVal = CacheValue.empty(); 875 for (IValidationSupport next : myChain) { 876 final String system = theLookupCodeRequest.getSystem(); 877 final String code = theLookupCodeRequest.getCode(); 878 final String displayLanguage = theLookupCodeRequest.getDisplayLanguage(); 879 if (isCodeSystemSupported(theValidationSupportContext, next, system)) { 880 LookupCodeResult lookupCodeResult = 881 next.lookupCode(theValidationSupportContext, theLookupCodeRequest); 882 if (lookupCodeResult == null) { 883 /* 884 This branch has been added as a fall-back mechanism for supporting lookupCode 885 methods marked as deprecated in interface IValidationSupport. 886 */ 887 //noinspection deprecation 888 lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage); 889 } 890 if (lookupCodeResult != null) { 891 ourLog.debug( 892 "Code {}|{}{} {} by {}", 893 system, 894 code, 895 isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")", 896 lookupCodeResult.isFound() ? "found" : "not found", 897 next.getName()); 898 retVal = new CacheValue<>(lookupCodeResult); 899 break; 900 } 901 } 902 } 903 904 putInCache(key, retVal); 905 } 906 907 return retVal.getValue(); 908 } 909 910 /** 911 * Returns a view of the {@link IValidationSupport} modules within 912 * this chain. The returned collection is unmodifiable and will reflect 913 * changes to the underlying list. 914 * 915 * @since 8.0.0 916 */ 917 public List<IValidationSupport> getValidationSupports() { 918 return Collections.unmodifiableList(myChain); 919 } 920 921 private <T> void putInCache(BaseKey<T> key, CacheValue<T> theValue) { 922 if (myExpiringCache != null) { 923 myExpiringCache.put(key, theValue); 924 } 925 } 926 927 @SuppressWarnings("unchecked") 928 private <T> CacheValue<T> getFromCache(BaseKey<T> key) { 929 if (myExpiringCache != null) { 930 return (CacheValue<T>) myExpiringCache.getIfPresent(key); 931 } else { 932 return null; 933 } 934 } 935 936 @SuppressWarnings("unchecked") 937 private List<IBaseResource> getFromCacheWithAsyncRefresh( 938 FetchAllKey theKey, Supplier<List<IBaseResource>> theLoader) { 939 if (myExpiringCache == null || myNonExpiringCache == null) { 940 return theLoader.get(); 941 } 942 943 CacheValue<List<IBaseResource>> retVal = getFromCache(theKey); 944 if (retVal == null) { 945 retVal = (CacheValue<List<IBaseResource>>) myNonExpiringCache.get(theKey); 946 if (retVal != null) { 947 Runnable loaderTask = () -> { 948 List<IBaseResource> loadedItem = theLoader.get(); 949 CacheValue<List<IBaseResource>> value = new CacheValue<>(loadedItem); 950 myNonExpiringCache.put(theKey, value); 951 putInCache(theKey, value); 952 }; 953 List<IBaseResource> returnValue = retVal.getValue(); 954 955 myBackgroundExecutor.execute(loaderTask); 956 957 return returnValue; 958 } else { 959 // Avoid flooding the validation support modules tons of concurrent 960 // requests for the same thing 961 synchronized (this) { 962 retVal = getFromCache(theKey); 963 if (retVal == null) { 964 StopWatch sw = new StopWatch(); 965 ourLog.info("Performing initial retrieval for non-expiring cache: {}", theKey); 966 retVal = new CacheValue<>(theLoader.get()); 967 ourLog.info("Initial retrieval for non-expiring cache {} succeeded in {}", theKey, sw); 968 myNonExpiringCache.put(theKey, retVal); 969 putInCache(theKey, retVal); 970 } 971 } 972 } 973 } 974 975 return retVal.getValue(); 976 } 977 978 public void logCacheSizes() { 979 String b = "Cache sizes:" + "\n * Expiring: " 980 + (myExpiringCache != null ? myExpiringCache.estimatedSize() : "(disabled)") 981 + "\n * Non-Expiring: " 982 + (myNonExpiringCache != null ? myNonExpiringCache.size() : "(disabled)"); 983 ourLog.info(b); 984 } 985 986 long getMetricExpiringCacheEntries() { 987 if (myExpiringCache != null) { 988 return myExpiringCache.estimatedSize(); 989 } else { 990 return 0; 991 } 992 } 993 994 int getMetricNonExpiringCacheEntries() { 995 synchronized (myStructureDefinitionsByUrl) { 996 int size = myNonExpiringCache != null ? myNonExpiringCache.size() : 0; 997 return size + myStructureDefinitionsAsList.size(); 998 } 999 } 1000 1001 int getMetricExpiringCacheMaxSize() { 1002 return myCacheConfiguration.getCacheSize(); 1003 } 1004 1005 /** 1006 * @since 5.4.0 1007 */ 1008 public static class CacheConfiguration { 1009 1010 private long myCacheTimeout; 1011 private int myCacheSize; 1012 1013 /** 1014 * Non-instantiable. Use the factory methods. 1015 */ 1016 private CacheConfiguration() { 1017 super(); 1018 } 1019 1020 public long getCacheTimeout() { 1021 return myCacheTimeout; 1022 } 1023 1024 public CacheConfiguration setCacheTimeout(Duration theCacheTimeout) { 1025 Validate.isTrue(theCacheTimeout.toMillis() >= 0, "Cache timeout must not be negative"); 1026 myCacheTimeout = theCacheTimeout.toMillis(); 1027 return this; 1028 } 1029 1030 public int getCacheSize() { 1031 return myCacheSize; 1032 } 1033 1034 public CacheConfiguration setCacheSize(int theCacheSize) { 1035 Validate.isTrue(theCacheSize >= 0, "Cache size must not be negative"); 1036 myCacheSize = theCacheSize; 1037 return this; 1038 } 1039 1040 /** 1041 * Creates a cache configuration with sensible default values: 1042 * 10 minutes expiry, and 5000 cache entries. 1043 */ 1044 public static CacheConfiguration defaultValues() { 1045 return new CacheConfiguration() 1046 .setCacheTimeout(Duration.ofMinutes(10)) 1047 .setCacheSize(5000); 1048 } 1049 1050 public static CacheConfiguration disabled() { 1051 return new CacheConfiguration().setCacheSize(0).setCacheTimeout(Duration.ofMillis(0)); 1052 } 1053 } 1054 1055 /** 1056 * @param <V> The value type associated with this key 1057 */ 1058 @SuppressWarnings("unused") 1059 abstract static class BaseKey<V> { 1060 1061 @Override 1062 public abstract boolean equals(Object theO); 1063 1064 @Override 1065 public abstract int hashCode(); 1066 } 1067 1068 static class ExpandValueSetKey extends BaseKey<ValueSetExpansionOutcome> { 1069 1070 private final ValueSetExpansionOptions myOptions; 1071 private final String myId; 1072 private final String myUrl; 1073 private final int myHashCode; 1074 1075 private ExpandValueSetKey(ValueSetExpansionOptions theOptions, String theId, String theUrl) { 1076 myOptions = theOptions; 1077 myId = theId; 1078 myUrl = theUrl; 1079 myHashCode = Objects.hash(myOptions, myId, myUrl); 1080 } 1081 1082 @Override 1083 public boolean equals(Object theO) { 1084 if (this == theO) return true; 1085 if (!(theO instanceof ExpandValueSetKey)) return false; 1086 ExpandValueSetKey that = (ExpandValueSetKey) theO; 1087 return Objects.equals(myOptions, that.myOptions) 1088 && Objects.equals(myId, that.myId) 1089 && Objects.equals(myUrl, that.myUrl); 1090 } 1091 1092 @Override 1093 public int hashCode() { 1094 return myHashCode; 1095 } 1096 } 1097 1098 static class FetchAllKey extends BaseKey<List<IBaseResource>> { 1099 1100 private final TypeEnum myType; 1101 private final int myHashCode; 1102 1103 private FetchAllKey(TypeEnum theType) { 1104 myType = theType; 1105 myHashCode = Objects.hash(myType); 1106 } 1107 1108 @Override 1109 public boolean equals(Object theO) { 1110 if (this == theO) return true; 1111 if (!(theO instanceof FetchAllKey)) return false; 1112 FetchAllKey that = (FetchAllKey) theO; 1113 return myType == that.myType; 1114 } 1115 1116 @Override 1117 public int hashCode() { 1118 return myHashCode; 1119 } 1120 1121 private enum TypeEnum { 1122 ALL, 1123 ALL_STRUCTUREDEFINITIONS, 1124 ALL_NON_BASE_STRUCTUREDEFINITIONS, 1125 ALL_SEARCHPARAMETERS 1126 } 1127 } 1128 1129 static class ResourceByUrlKey<T> extends BaseKey<T> { 1130 1131 private final TypeEnum myType; 1132 private final String myUrl; 1133 private final int myHashCode; 1134 1135 private ResourceByUrlKey(TypeEnum theType, String theUrl) { 1136 this(theType, theUrl, Objects.hash("ResourceByUrl", theType, theUrl)); 1137 } 1138 1139 private ResourceByUrlKey(TypeEnum theType, String theUrl, int theHashCode) { 1140 myType = theType; 1141 myUrl = theUrl; 1142 myHashCode = theHashCode; 1143 } 1144 1145 @Override 1146 public boolean equals(Object theO) { 1147 if (this == theO) return true; 1148 if (!(theO instanceof ResourceByUrlKey)) return false; 1149 ResourceByUrlKey<?> that = (ResourceByUrlKey<?>) theO; 1150 return myType == that.myType && Objects.equals(myUrl, that.myUrl); 1151 } 1152 1153 @Override 1154 public int hashCode() { 1155 return myHashCode; 1156 } 1157 1158 private enum TypeEnum { 1159 CODESYSTEM, 1160 VALUESET, 1161 RESOURCE, 1162 BINARY, 1163 STRUCTUREDEFINITION 1164 } 1165 } 1166 1167 static class TypedResourceByUrlKey<T> extends ResourceByUrlKey<T> { 1168 1169 private final Class<?> myType; 1170 1171 private TypedResourceByUrlKey(Class<?> theType, String theUrl) { 1172 super(ResourceByUrlKey.TypeEnum.RESOURCE, theUrl, Objects.hash("TypedResourceByUrl", theType, theUrl)); 1173 myType = theType; 1174 } 1175 1176 @Override 1177 public boolean equals(Object theO) { 1178 if (this == theO) return true; 1179 if (!(theO instanceof TypedResourceByUrlKey)) return false; 1180 if (!super.equals(theO)) return false; 1181 TypedResourceByUrlKey<?> that = (TypedResourceByUrlKey<?>) theO; 1182 return Objects.equals(myType, that.myType); 1183 } 1184 1185 @Override 1186 public int hashCode() { 1187 return Objects.hash(super.hashCode(), myType); 1188 } 1189 } 1190 1191 static class IsValueSetSupportedKey extends BaseKey<Boolean> { 1192 1193 private final String myValueSetUrl; 1194 private final IValidationSupport myValidationSupport; 1195 private final int myHashCode; 1196 1197 private IsValueSetSupportedKey(IValidationSupport theValidationSupport, String theValueSetUrl) { 1198 myValidationSupport = theValidationSupport; 1199 myValueSetUrl = theValueSetUrl; 1200 myHashCode = Objects.hash("IsValueSetSupported", theValidationSupport, myValueSetUrl); 1201 } 1202 1203 @Override 1204 public boolean equals(Object theO) { 1205 if (this == theO) return true; 1206 if (!(theO instanceof IsValueSetSupportedKey)) return false; 1207 IsValueSetSupportedKey that = (IsValueSetSupportedKey) theO; 1208 return myValidationSupport == that.myValidationSupport && Objects.equals(myValueSetUrl, that.myValueSetUrl); 1209 } 1210 1211 @Override 1212 public int hashCode() { 1213 return myHashCode; 1214 } 1215 } 1216 1217 static class IsCodeSystemSupportedKey extends BaseKey<Boolean> { 1218 1219 private final String myCodeSystemUrl; 1220 private final IValidationSupport myValidationSupport; 1221 private final int myHashCode; 1222 1223 private IsCodeSystemSupportedKey(IValidationSupport theValidationSupport, String theCodeSystemUrl) { 1224 myValidationSupport = theValidationSupport; 1225 myCodeSystemUrl = theCodeSystemUrl; 1226 myHashCode = Objects.hash("IsCodeSystemSupported", theValidationSupport, myCodeSystemUrl); 1227 } 1228 1229 @Override 1230 public boolean equals(Object theO) { 1231 if (this == theO) return true; 1232 if (!(theO instanceof IsCodeSystemSupportedKey)) return false; 1233 IsCodeSystemSupportedKey that = (IsCodeSystemSupportedKey) theO; 1234 return myValidationSupport == that.myValidationSupport 1235 && Objects.equals(myCodeSystemUrl, that.myCodeSystemUrl); 1236 } 1237 1238 @Override 1239 public int hashCode() { 1240 return myHashCode; 1241 } 1242 } 1243 1244 static class LookupCodeKey extends BaseKey<LookupCodeResult> { 1245 1246 private final LookupCodeRequest myRequest; 1247 private final int myHashCode; 1248 1249 private LookupCodeKey(LookupCodeRequest theRequest) { 1250 myRequest = theRequest; 1251 myHashCode = Objects.hash("LookupCode", myRequest); 1252 } 1253 1254 @Override 1255 public boolean equals(Object theO) { 1256 if (this == theO) return true; 1257 if (!(theO instanceof LookupCodeKey)) return false; 1258 LookupCodeKey that = (LookupCodeKey) theO; 1259 return Objects.equals(myRequest, that.myRequest); 1260 } 1261 1262 @Override 1263 public int hashCode() { 1264 return myHashCode; 1265 } 1266 } 1267 1268 static class TranslateConceptKey extends BaseKey<TranslateConceptResults> { 1269 1270 private final TranslateCodeRequest myRequest; 1271 private final int myHashCode; 1272 1273 private TranslateConceptKey(TranslateCodeRequest theRequest) { 1274 myRequest = theRequest; 1275 myHashCode = Objects.hash("TranslateConcept", myRequest); 1276 } 1277 1278 @Override 1279 public boolean equals(Object theO) { 1280 if (this == theO) return true; 1281 if (!(theO instanceof TranslateConceptKey)) return false; 1282 TranslateConceptKey that = (TranslateConceptKey) theO; 1283 return Objects.equals(myRequest, that.myRequest); 1284 } 1285 1286 @Override 1287 public int hashCode() { 1288 return myHashCode; 1289 } 1290 } 1291 1292 static class ValidateCodeKey extends BaseKey<CodeValidationResult> { 1293 private final String mySystem; 1294 private final String myCode; 1295 private final String myDisplay; 1296 private final String myValueSetUrl; 1297 private final int myHashCode; 1298 private final ConceptValidationOptions myOptions; 1299 1300 private ValidateCodeKey( 1301 ConceptValidationOptions theOptions, 1302 String theSystem, 1303 String theCode, 1304 String theDisplay, 1305 String theValueSetUrl) { 1306 myOptions = theOptions; 1307 mySystem = theSystem; 1308 myCode = theCode; 1309 myDisplay = theDisplay; 1310 myValueSetUrl = theValueSetUrl; 1311 myHashCode = Objects.hash("ValidateCodeKey", myOptions, mySystem, myCode, myDisplay, myValueSetUrl); 1312 } 1313 1314 @Override 1315 public boolean equals(Object theO) { 1316 if (this == theO) return true; 1317 if (!(theO instanceof ValidateCodeKey)) return false; 1318 ValidateCodeKey that = (ValidateCodeKey) theO; 1319 return Objects.equals(myOptions, that.myOptions) 1320 && Objects.equals(mySystem, that.mySystem) 1321 && Objects.equals(myCode, that.myCode) 1322 && Objects.equals(myDisplay, that.myDisplay) 1323 && Objects.equals(myValueSetUrl, that.myValueSetUrl); 1324 } 1325 1326 @Override 1327 public int hashCode() { 1328 return myHashCode; 1329 } 1330 } 1331 1332 /** 1333 * This class is basically the same thing as Optional, but is a distinct thing 1334 * because we want to use it as a method parameter value, and compare instances of 1335 * it with null. Both of these things generate warnings in various linters. 1336 */ 1337 private static class CacheValue<T> { 1338 1339 private static final CacheValue<CodeValidationResult> EMPTY = new CacheValue<>(null); 1340 1341 private final T myValue; 1342 1343 private CacheValue(T theValue) { 1344 myValue = theValue; 1345 } 1346 1347 public T getValue() { 1348 return myValue; 1349 } 1350 1351 @SuppressWarnings("unchecked") 1352 public static <T> CacheValue<T> empty() { 1353 return (CacheValue<T>) EMPTY; 1354 } 1355 } 1356}