001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
004import ca.uhn.fhir.context.support.ConceptValidationOptions;
005import ca.uhn.fhir.context.support.IValidationSupport;
006import ca.uhn.fhir.context.support.LookupCodeRequest;
007import ca.uhn.fhir.context.support.TranslateConceptResults;
008import ca.uhn.fhir.context.support.ValidationSupportContext;
009import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
010import ca.uhn.fhir.sl.cache.Cache;
011import ca.uhn.fhir.sl.cache.CacheFactory;
012import jakarta.annotation.Nonnull;
013import jakarta.annotation.Nullable;
014import org.apache.commons.lang3.concurrent.BasicThreadFactory;
015import org.apache.commons.lang3.time.DateUtils;
016import org.hl7.fhir.instance.model.api.IBaseResource;
017import org.hl7.fhir.instance.model.api.IPrimitiveType;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Optional;
026import java.util.concurrent.LinkedBlockingQueue;
027import java.util.concurrent.ThreadPoolExecutor;
028import java.util.concurrent.TimeUnit;
029import java.util.function.Function;
030
031import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
032import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
033import static org.apache.commons.lang3.StringUtils.defaultString;
034import static org.apache.commons.lang3.StringUtils.isNotBlank;
035
036@SuppressWarnings("unchecked")
037public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport {
038
039        private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class);
040        public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions();
041
042        private final Cache<String, Object> myCache;
043        private final Cache<String, Object> myValidateCodeCache;
044        private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache;
045        private final Cache<String, Object> myLookupCodeCache;
046        private final ThreadPoolExecutor myBackgroundExecutor;
047        private final Map<Object, Object> myNonExpiringCache;
048        private final Cache<String, Object> myExpandValueSetCache;
049        private final boolean myIsEnabledValidationForCodingsLogicalAnd;
050
051        /**
052         * Constructor with default timeouts
053         *
054         * @param theWrap The validation support module to wrap
055         */
056        public CachingValidationSupport(IValidationSupport theWrap) {
057                this(theWrap, CacheTimeouts.defaultValues(), false);
058        }
059
060        public CachingValidationSupport(IValidationSupport theWrap, boolean theIsEnabledValidationForCodingsLogicalAnd) {
061                this(theWrap, CacheTimeouts.defaultValues(), theIsEnabledValidationForCodingsLogicalAnd);
062        }
063
064        public CachingValidationSupport(IValidationSupport theWrap, CacheTimeouts theCacheTimeouts) {
065                this(theWrap, theCacheTimeouts, false);
066        }
067
068        /**
069         * Constructor with configurable timeouts
070         *
071         * @param theWrap          The validation support module to wrap
072         * @param theCacheTimeouts The timeouts to use
073         */
074        public CachingValidationSupport(
075                        IValidationSupport theWrap,
076                        CacheTimeouts theCacheTimeouts,
077                        boolean theIsEnabledValidationForCodingsLogicalAnd) {
078                super(theWrap.getFhirContext(), theWrap);
079                myExpandValueSetCache = CacheFactory.build(theCacheTimeouts.getExpandValueSetMillis(), 100);
080                myValidateCodeCache = CacheFactory.build(theCacheTimeouts.getValidateCodeMillis(), 5000);
081                myLookupCodeCache = CacheFactory.build(theCacheTimeouts.getLookupCodeMillis(), 5000);
082                myTranslateCodeCache = CacheFactory.build(theCacheTimeouts.getTranslateCodeMillis(), 5000);
083                myCache = CacheFactory.build(theCacheTimeouts.getMiscMillis(), 5000);
084                myNonExpiringCache = Collections.synchronizedMap(new HashMap<>());
085
086                LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
087                BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
088                                .namingPattern("CachingValidationSupport-%d")
089                                .daemon(false)
090                                .priority(Thread.NORM_PRIORITY)
091                                .build();
092                myBackgroundExecutor = new ThreadPoolExecutor(
093                                1, 1, 0L, TimeUnit.MILLISECONDS, executorQueue, threadFactory, new ThreadPoolExecutor.DiscardPolicy());
094
095                myIsEnabledValidationForCodingsLogicalAnd = theIsEnabledValidationForCodingsLogicalAnd;
096        }
097
098        @Override
099        public List<IBaseResource> fetchAllConformanceResources() {
100                String key = "fetchAllConformanceResources";
101                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources());
102        }
103
104        @Override
105        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
106                String key = "fetchAllStructureDefinitions";
107                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions());
108        }
109
110        @Nullable
111        @Override
112        public <T extends IBaseResource> List<T> fetchAllSearchParameters() {
113                String key = "fetchAllSearchParameters";
114                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllSearchParameters());
115        }
116
117        @Override
118        public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() {
119                String key = "fetchAllNonBaseStructureDefinitions";
120                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
121        }
122
123        @Override
124        public IBaseResource fetchCodeSystem(String theSystem) {
125                return loadFromCache(myCache, "fetchCodeSystem " + theSystem, t -> super.fetchCodeSystem(theSystem));
126        }
127
128        @Override
129        public IBaseResource fetchValueSet(String theUri) {
130                return loadFromCache(myCache, "fetchValueSet " + theUri, t -> super.fetchValueSet(theUri));
131        }
132
133        @Override
134        public IBaseResource fetchStructureDefinition(String theUrl) {
135                return loadFromCache(
136                                myCache, "fetchStructureDefinition " + theUrl, t -> super.fetchStructureDefinition(theUrl));
137        }
138
139        @Override
140        public byte[] fetchBinary(String theBinaryKey) {
141                return loadFromCache(myCache, "fetchBinary " + theBinaryKey, t -> super.fetchBinary(theBinaryKey));
142        }
143
144        @Override
145        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
146                return loadFromCache(
147                                myCache, "fetchResource " + theClass + " " + theUri, t -> super.fetchResource(theClass, theUri));
148        }
149
150        @Override
151        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
152                String key = "isCodeSystemSupported " + theSystem;
153                Boolean retVal = loadFromCacheReentrantSafe(
154                                myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem));
155                assert retVal != null;
156                return retVal;
157        }
158
159        @Override
160        public ValueSetExpansionOutcome expandValueSet(
161                        ValidationSupportContext theValidationSupportContext,
162                        ValueSetExpansionOptions theExpansionOptions,
163                        @Nonnull IBaseResource theValueSetToExpand) {
164                if (!theValueSetToExpand.getIdElement().hasIdPart()) {
165                        return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
166                }
167
168                ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS);
169                String key = "expandValueSet " + theValueSetToExpand.getIdElement().getValue()
170                                + " " + expansionOptions.isIncludeHierarchy()
171                                + " " + expansionOptions.getFilter()
172                                + " " + expansionOptions.getOffset()
173                                + " " + expansionOptions.getCount();
174                return loadFromCache(
175                                myExpandValueSetCache,
176                                key,
177                                t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand));
178        }
179
180        @Override
181        public CodeValidationResult validateCode(
182                        @Nonnull ValidationSupportContext theValidationSupportContext,
183                        @Nonnull ConceptValidationOptions theOptions,
184                        String theCodeSystem,
185                        String theCode,
186                        String theDisplay,
187                        String theValueSetUrl) {
188                String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultString(theDisplay) + " "
189                                + defaultIfBlank(theValueSetUrl, "NO_VS");
190                return loadFromCache(
191                                myValidateCodeCache,
192                                key,
193                                t -> super.validateCode(
194                                                theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl));
195        }
196
197        @Override
198        public LookupCodeResult lookupCode(
199                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
200                String key = "lookupCode " + theLookupCodeRequest.getSystem() + " "
201                                + theLookupCodeRequest.getCode()
202                                + " " + defaultIfBlank(theLookupCodeRequest.getDisplayLanguage(), "NO_LANG")
203                                + " " + theLookupCodeRequest.getPropertyNames().toString();
204                return loadFromCache(
205                                myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theLookupCodeRequest));
206        }
207
208        @Override
209        public IValidationSupport.CodeValidationResult validateCodeInValueSet(
210                        ValidationSupportContext theValidationSupportContext,
211                        ConceptValidationOptions theValidationOptions,
212                        String theCodeSystem,
213                        String theCode,
214                        String theDisplay,
215                        @Nonnull IBaseResource theValueSet) {
216
217                BaseRuntimeChildDefinition urlChild =
218                                myCtx.getResourceDefinition(theValueSet).getChildByName("url");
219                Optional<String> valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream()
220                                .map(t -> ((IPrimitiveType<?>) t).getValueAsString())
221                                .filter(t -> isNotBlank(t))
222                                .findFirst();
223                if (valueSetUrl.isPresent()) {
224                        String key =
225                                        "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem)
226                                                        + " " + defaultString(theCode) + " " + defaultString(theDisplay) + " " + valueSetUrl.get();
227                        return loadFromCache(
228                                        myValidateCodeCache,
229                                        key,
230                                        t -> super.validateCodeInValueSet(
231                                                        theValidationSupportContext,
232                                                        theValidationOptions,
233                                                        theCodeSystem,
234                                                        theCode,
235                                                        theDisplay,
236                                                        theValueSet));
237                }
238
239                return super.validateCodeInValueSet(
240                                theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
241        }
242
243        @Override
244        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
245                return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest));
246        }
247
248        @SuppressWarnings("OptionalAssignedToNull")
249        @Nullable
250        private <S, T> T loadFromCache(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
251                ourLog.trace("Fetching from cache: {}", theKey);
252
253                Function<S, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
254                Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
255                assert result != null;
256
257                // UGH!  Animal sniffer :(
258                if (!result.isPresent()) {
259                        ourLog.debug(
260                                        "Invalidating cache entry for key: {} since the result of the underlying query is empty", theKey);
261                        theCache.invalidate(theKey);
262                }
263
264                return result.orElse(null);
265        }
266
267        /**
268         * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable
269         * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock.  Use this
270         * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda
271         */
272        @Nullable
273        private <S, T> T loadFromCacheReentrantSafe(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
274                ourLog.trace("Reentrant fetch from cache: {}", theKey);
275
276                Optional<T> result = (Optional<T>) theCache.getIfPresent(theKey);
277                if (result != null && result.isPresent()) {
278                        return result.get();
279                }
280                T value = theLoader.apply(theKey);
281                assert value != null;
282
283                theCache.put(theKey, Optional.of(value));
284
285                return value;
286        }
287
288        private <S, T> T loadFromCacheWithAsyncRefresh(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
289                T retVal = (T) theCache.getIfPresent(theKey);
290                if (retVal == null) {
291                        retVal = (T) myNonExpiringCache.get(theKey);
292                        if (retVal != null) {
293
294                                Runnable loaderTask = () -> {
295                                        T loadedItem = loadFromCache(theCache, theKey, theLoader);
296                                        myNonExpiringCache.put(theKey, loadedItem);
297                                };
298                                myBackgroundExecutor.execute(loaderTask);
299
300                                return retVal;
301                        }
302                }
303
304                retVal = loadFromCache(theCache, theKey, theLoader);
305                myNonExpiringCache.put(theKey, retVal);
306                return retVal;
307        }
308
309        @Override
310        public void invalidateCaches() {
311                myExpandValueSetCache.invalidateAll();
312                myLookupCodeCache.invalidateAll();
313                myCache.invalidateAll();
314                myValidateCodeCache.invalidateAll();
315                myNonExpiringCache.clear();
316        }
317
318        /**
319         * @since 5.4.0
320         */
321        public static class CacheTimeouts {
322
323                private long myTranslateCodeMillis;
324                private long myLookupCodeMillis;
325                private long myValidateCodeMillis;
326                private long myMiscMillis;
327                private long myExpandValueSetMillis;
328
329                public long getExpandValueSetMillis() {
330                        return myExpandValueSetMillis;
331                }
332
333                public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) {
334                        myExpandValueSetMillis = theExpandValueSetMillis;
335                        return this;
336                }
337
338                public long getTranslateCodeMillis() {
339                        return myTranslateCodeMillis;
340                }
341
342                public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) {
343                        myTranslateCodeMillis = theTranslateCodeMillis;
344                        return this;
345                }
346
347                public long getLookupCodeMillis() {
348                        return myLookupCodeMillis;
349                }
350
351                public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) {
352                        myLookupCodeMillis = theLookupCodeMillis;
353                        return this;
354                }
355
356                public long getValidateCodeMillis() {
357                        return myValidateCodeMillis;
358                }
359
360                public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) {
361                        myValidateCodeMillis = theValidateCodeMillis;
362                        return this;
363                }
364
365                public long getMiscMillis() {
366                        return myMiscMillis;
367                }
368
369                public CacheTimeouts setMiscMillis(long theMiscMillis) {
370                        myMiscMillis = theMiscMillis;
371                        return this;
372                }
373
374                public static CacheTimeouts defaultValues() {
375                        return new CacheTimeouts()
376                                        .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
377                                        .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE)
378                                        .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
379                                        .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
380                                        .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE);
381                }
382        }
383
384        public boolean isEnabledValidationForCodingsLogicalAnd() {
385                return myIsEnabledValidationForCodingsLogicalAnd;
386        }
387}