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}