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