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}