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        @Override
113        public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() {
114                String key = "fetchAllNonBaseStructureDefinitions";
115                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
116        }
117
118        @Override
119        public IBaseResource fetchCodeSystem(String theSystem) {
120                return loadFromCache(myCache, "fetchCodeSystem " + theSystem, t -> super.fetchCodeSystem(theSystem));
121        }
122
123        @Override
124        public IBaseResource fetchValueSet(String theUri) {
125                return loadFromCache(myCache, "fetchValueSet " + theUri, t -> super.fetchValueSet(theUri));
126        }
127
128        @Override
129        public IBaseResource fetchStructureDefinition(String theUrl) {
130                return loadFromCache(myCache, "fetchStructureDefinition " + theUrl, t -> super.fetchStructureDefinition(theUrl));
131        }
132
133        @Override
134        public byte[] fetchBinary(String theBinaryKey) {
135                return loadFromCache(myCache, "fetchBinary " + theBinaryKey, t -> super.fetchBinary(theBinaryKey));
136        }
137
138        @Override
139        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
140                return loadFromCache(myCache, "fetchResource " + theClass + " " + theUri,
141                        t -> super.fetchResource(theClass, theUri));
142        }
143
144        @Override
145        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
146                String key = "isCodeSystemSupported " + theSystem;
147                Boolean retVal = loadFromCacheReentrantSafe(myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem));
148                assert retVal != null;
149                return retVal;
150        }
151
152        @Override
153        public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
154                if (!theValueSetToExpand.getIdElement().hasIdPart()) {
155                        return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
156                }
157
158                ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS);
159                String key = "expandValueSet " +
160                        theValueSetToExpand.getIdElement().getValue() + " " +
161                        expansionOptions.isIncludeHierarchy() + " " +
162                        expansionOptions.getFilter() + " " +
163                        expansionOptions.getOffset() + " " +
164                        expansionOptions.getCount();
165                return loadFromCache(myExpandValueSetCache, key, t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand));
166        }
167
168        @Override
169        public CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
170                String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultString(theDisplay) + " " + defaultIfBlank(theValueSetUrl, "NO_VS");
171                return loadFromCache(myValidateCodeCache, key, t -> super.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl));
172        }
173
174        @Override
175        public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
176                String key = "lookupCode " + theSystem + " " + theCode + " " + defaultIfBlank(theDisplayLanguage, "NO_LANG");
177                return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode, theDisplayLanguage));
178        }
179
180        @Override
181        public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
182
183                BaseRuntimeChildDefinition urlChild = myCtx.getResourceDefinition(theValueSet).getChildByName("url");
184                Optional<String> valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream().map(t -> ((IPrimitiveType<?>) t).getValueAsString()).filter(t -> isNotBlank(t)).findFirst();
185                if (valueSetUrl.isPresent()) {
186                        String key = "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem) + " " + defaultString(theCode) + " " + defaultString(theDisplay) + " " + valueSetUrl.get();
187                        return loadFromCache(myValidateCodeCache, key, t -> super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet));
188                }
189
190                return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
191        }
192
193        @Override
194        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
195                return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest));
196        }
197
198        @SuppressWarnings("OptionalAssignedToNull")
199        @Nullable
200        private <S, T> T loadFromCache(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
201                ourLog.trace("Fetching from cache: {}", theKey);
202
203                Function<S, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
204                Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
205                assert result != null;
206
207                return result.orElse(null);
208        }
209
210        /**
211         * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable
212         * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock.  Use this
213         * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda
214         */
215        @Nullable
216        private <S, T> T loadFromCacheReentrantSafe(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
217                ourLog.trace("Reentrant fetch from cache: {}", theKey);
218
219                Optional<T> result = (Optional<T>) theCache.getIfPresent(theKey);
220                if (result != null && result.isPresent()) {
221                        return result.get();
222                }
223                T value = theLoader.apply(theKey);
224                assert value != null;
225
226                theCache.put(theKey, Optional.of(value));
227
228                return value;
229        }
230
231        private <S, T> T loadFromCacheWithAsyncRefresh(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
232                T retVal = (T) theCache.getIfPresent(theKey);
233                if (retVal == null) {
234                        retVal = (T) myNonExpiringCache.get(theKey);
235                        if (retVal != null) {
236
237                                Runnable loaderTask = () -> {
238                                        T loadedItem = loadFromCache(theCache, theKey, theLoader);
239                                        myNonExpiringCache.put(theKey, loadedItem);
240                                };
241                                myBackgroundExecutor.execute(loaderTask);
242
243                                return retVal;
244                        }
245                }
246
247                retVal = loadFromCache(theCache, theKey, theLoader);
248                myNonExpiringCache.put(theKey, retVal);
249                return retVal;
250        }
251
252
253        @Override
254        public void invalidateCaches() {
255                myExpandValueSetCache.invalidateAll();
256                myLookupCodeCache.invalidateAll();
257                myCache.invalidateAll();
258                myValidateCodeCache.invalidateAll();
259                myNonExpiringCache.clear();
260        }
261
262        /**
263         * @since 5.4.0
264         */
265        public static class CacheTimeouts {
266
267                private long myTranslateCodeMillis;
268                private long myLookupCodeMillis;
269                private long myValidateCodeMillis;
270                private long myMiscMillis;
271                private long myExpandValueSetMillis;
272
273                public long getExpandValueSetMillis() {
274                        return myExpandValueSetMillis;
275                }
276
277                public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) {
278                        myExpandValueSetMillis = theExpandValueSetMillis;
279                        return this;
280                }
281
282                public long getTranslateCodeMillis() {
283                        return myTranslateCodeMillis;
284                }
285
286                public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) {
287                        myTranslateCodeMillis = theTranslateCodeMillis;
288                        return this;
289                }
290
291                public long getLookupCodeMillis() {
292                        return myLookupCodeMillis;
293                }
294
295                public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) {
296                        myLookupCodeMillis = theLookupCodeMillis;
297                        return this;
298                }
299
300                public long getValidateCodeMillis() {
301                        return myValidateCodeMillis;
302                }
303
304                public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) {
305                        myValidateCodeMillis = theValidateCodeMillis;
306                        return this;
307                }
308
309                public long getMiscMillis() {
310                        return myMiscMillis;
311                }
312
313                public CacheTimeouts setMiscMillis(long theMiscMillis) {
314                        myMiscMillis = theMiscMillis;
315                        return this;
316                }
317
318                public static CacheTimeouts defaultValues() {
319                        return new CacheTimeouts()
320                                .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
321                                .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE)
322                                .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
323                                .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
324                                .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE);
325                }
326        }
327
328        public boolean isEnabledValidationForCodingsLogicalAnd() {
329                return myIsEnabledValidationForCodingsLogicalAnd;
330        }
331}