
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}