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}