au.csiro.pathling.terminology.caching.CachingTerminologyService Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of terminology Show documentation
Show all versions of terminology Show documentation
Interact with a FHIR terminology server from Spark.
/*
* Copyright 2023 Commonwealth Scientific and Industrial Research
* Organisation (CSIRO) ABN 41 687 119 230.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package au.csiro.pathling.terminology.caching;
import static au.csiro.pathling.utilities.Preconditions.checkPresent;
import static java.util.Objects.requireNonNull;
import au.csiro.pathling.config.HttpClientCachingConfiguration;
import au.csiro.pathling.fhir.TerminologyClient;
import au.csiro.pathling.fhirpath.encoding.ImmutableCoding;
import au.csiro.pathling.terminology.BaseTerminologyService;
import au.csiro.pathling.terminology.TerminologyOperation;
import au.csiro.pathling.terminology.TerminologyParameters;
import au.csiro.pathling.terminology.TerminologyResult;
import au.csiro.pathling.terminology.lookup.LookupExecutor;
import au.csiro.pathling.terminology.lookup.LookupParameters;
import au.csiro.pathling.terminology.subsumes.SubsumesExecutor;
import au.csiro.pathling.terminology.subsumes.SubsumesParameters;
import au.csiro.pathling.terminology.translate.TranslateExecutor;
import au.csiro.pathling.terminology.translate.TranslateParameters;
import au.csiro.pathling.terminology.validatecode.ValidateCodeExecutor;
import au.csiro.pathling.terminology.validatecode.ValidateCodeParameters;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import java.io.Closeable;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.ws.rs.core.CacheControl;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome;
import org.infinispan.Cache;
import org.infinispan.manager.EmbeddedCacheManager;
/**
* A terminology service that uses embedded Infinispan to cache the results of the underlying
* terminology service operations.
*
* @author John Grimes
*/
public abstract class CachingTerminologyService extends BaseTerminologyService {
public static final String VALIDATE_CODE_CACHE_NAME = "validate-code";
public static final String SUBSUMES_CACHE_NAME = "subsumes";
public static final String TRANSLATE_CACHE_NAME = "translate";
public static final String LOOKUP_CACHE_NAME = "lookup";
public static final String ETAG_HEADER_NAME = "etag";
public static final String IF_NONE_MATCH_HEADER_NAME = "if-none-match";
public static final String CACHE_CONTROL_HEADER_NAME = "cache-control";
@Nonnull
protected final HttpClientCachingConfiguration configuration;
@Nonnull
protected final EmbeddedCacheManager cacheManager;
@Nonnull
protected final Cache> validateCodeCache;
@Nonnull
protected final Cache> subsumesCache;
@Nonnull
protected final Cache>> translateCache;
@Nonnull
protected final Cache>> lookupCache;
@SuppressWarnings("unchecked")
public CachingTerminologyService(@Nonnull final TerminologyClient terminologyClient,
@Nonnull final HttpClientCachingConfiguration configuration,
@Nonnull final Closeable... resourcesToClose) {
super(terminologyClient, resourcesToClose);
this.configuration = configuration;
// register manager as a closeable resource
cacheManager = registerResource(buildCacheManager());
validateCodeCache = (Cache>) buildCache(cacheManager,
VALIDATE_CODE_CACHE_NAME);
subsumesCache = (Cache>) buildCache(
cacheManager, SUBSUMES_CACHE_NAME);
translateCache = (Cache>>) buildCache(
cacheManager, TRANSLATE_CACHE_NAME);
lookupCache = (Cache>>) buildCache(
cacheManager, LOOKUP_CACHE_NAME);
}
@Override
public boolean validateCode(@Nonnull final String valueSetUrl, @Nonnull final Coding coding) {
final ValidateCodeParameters parameters = new ValidateCodeParameters(valueSetUrl,
ImmutableCoding.of(coding));
final ValidateCodeExecutor executor = new ValidateCodeExecutor(terminologyClient, parameters);
return getFromCache(validateCodeCache, parameters, executor);
}
@Nonnull
@Override
public List translate(@Nonnull final Coding coding,
@Nonnull final String conceptMapUrl,
final boolean reverse, @Nullable final String target) {
final TranslateParameters parameters = new TranslateParameters(ImmutableCoding.of(coding),
conceptMapUrl, reverse, target);
final TranslateExecutor executor = new TranslateExecutor(terminologyClient, parameters);
return getFromCache(translateCache, parameters, executor);
}
@Nonnull
@Override
public ConceptSubsumptionOutcome subsumes(@Nonnull final Coding codingA,
@Nonnull final Coding codingB) {
final SubsumesParameters parameters = new SubsumesParameters(
ImmutableCoding.of(codingA), ImmutableCoding.of(codingB));
final SubsumesExecutor executor = new SubsumesExecutor(terminologyClient, parameters);
return getFromCache(subsumesCache, parameters, executor);
}
@Nonnull
@Override
public List lookup(@Nonnull final Coding coding,
@Nullable final String property, @Nullable final String acceptLanguage) {
final LookupParameters parameters = new LookupParameters(ImmutableCoding.of(coding), property,
acceptLanguage);
final LookupExecutor executor = new LookupExecutor(terminologyClient, parameters);
return getFromCache(lookupCache, parameters, executor);
}
/**
* Gets the result of an operation from the cache, or fetches a new result if the cache is empty
* or expired.
*
* @param cache The cache being used for this operation
* @param parameters The parameters for the operation
* @param operation A {@link TerminologyOperation} that provides the behavior specific to the type
* of operation
* @param The type of the parameters that are input to the operation
* @param The type of the response returned by the terminology client
* @param The type of the final result that is extracted from the response
* @return The operation result
*/
private ResultType getFromCache(
@Nonnull final Cache> cache,
@Nonnull final ParametersType parameters,
@Nonnull final TerminologyOperation operation
) {
final int key = parameters.hashCode();
final TerminologyResult cached = cache.get(key);
if (cached == null) {
// Cache miss.
final TerminologyResult result = fetch(operation, Optional.empty());
cache.put(key, result);
return result.getData();
} else {
final boolean expired =
cached.getExpires() != null && System.currentTimeMillis() > cached.getExpires();
return requireNonNull(cache.compute(key,
(k, v) -> expired
// Cache hit, but the entry is expired and needs to be revalidated.
? fetch(operation, Optional.ofNullable(v))
// Cache hit, and the entry is still valid.
: v)).getData();
}
}
/**
* Fetches an operation result, or revalidates it if the cached result is still valid.
*
* @param operation A {@link TerminologyOperation} that provides the behavior specific to the type
* of operation
* @param cached A previously cached value
* @param The type of the response returned by the terminology client
* @param The type of the final result that is extracted from the response
* @return The operation result
*/
private TerminologyResult fetch(
@Nonnull final TerminologyOperation operation,
@Nonnull final Optional> cached) {
final Optional invalidResult = operation.validate();
if (invalidResult.isPresent()) {
// If the parameters fail validation, cache the invalid result forever.
return new TerminologyResult<>(invalidResult.get(), null, null, false);
}
final IOperationUntypedWithInput request = operation.buildRequest();
// Add an If-None-Match header if the cached result is accompanied by an ETag.
cached.flatMap(c -> Optional.ofNullable(c.getETag()))
.ifPresent(eTag -> request.withAdditionalHeader(IF_NONE_MATCH_HEADER_NAME, eTag));
// We use a default expiry if the server does not provide one.
final Optional defaultExpires = Optional.of(
secondsFromNow(configuration.getDefaultExpiry()));
// There may be a configured override expiry.
final Optional overrideExpires = Optional.ofNullable(
configuration.getOverrideExpiry()).map(CachingTerminologyService::secondsFromNow);
try {
final MethodOutcome outcome = request.returnMethodOutcome().execute();
// If the response was 200 OK, use the data from the fresh response.
@SuppressWarnings("unchecked") final ResultType result = operation.extractResult(
(ResponseType) outcome.getResource());
final Optional newETag = getSingularHeader(outcome, ETAG_HEADER_NAME);
final Optional serverExpires = getExpires(outcome);
return new TerminologyResult<>(
result,
newETag.orElse(null),
// Expiry values are used in this order:
// 1. The override expiry, if present;
// 2. The expiry provided by the server, if present, then;
// 3. The default expiry.
resolveExpires(overrideExpires, serverExpires, defaultExpires),
false);
} catch (final NotModifiedException e) {
// If the response was 304 Not Modified, use the data from the cached response and update
// the ETag and expiry.
final Optional newETag = getSingularHeader(e.getResponseHeaders(), ETAG_HEADER_NAME);
final TerminologyResult previous = checkPresent(cached);
final Optional serverExpires = getExpires(e.getResponseHeaders());
final Optional previousExpiry = Optional.ofNullable(previous.getExpires());
return new TerminologyResult<>(previous.getData(),
newETag.orElse(previous.getETag()),
// Expiry values are used in this order:
// 1. The override expiry, if present;
// 2. The expiry from the 304 response, if present;
// 3. The expiry from the cached response, if present, then;
// 4. The default expiry.
resolveExpires(overrideExpires, serverExpires, previousExpiry, defaultExpires),
false);
} catch (final BaseServerResponseException e) {
// If the terminology server rejects the request as invalid, cache the invalid result for the
// amount of time instructed by the server. If there is no such instruction, cache it for the
// configured default expiry.
final Optional serverExpires = getExpires(e.getResponseHeaders());
final long expires = resolveExpires(serverExpires, defaultExpires);
final TerminologyResult fallback = new TerminologyResult<>(
operation.invalidRequestFallback(), null, expires, false);
return handleError(e, fallback);
}
}
/**
* @return a new {@link EmbeddedCacheManager} instance appropriate for the specific implementation
*/
protected abstract EmbeddedCacheManager buildCacheManager();
/**
* @param cacheManager the {@link EmbeddedCacheManager} to use to construct the cache
* @param cacheName a name for the cache
* @return a new {@link Cache} instance appropriate for the specific implementation
*/
protected abstract Cache buildCache(@Nonnull final EmbeddedCacheManager cacheManager,
@Nonnull final String cacheName);
@SuppressWarnings("SameParameterValue")
@Nonnull
private static Optional getSingularHeader(@Nonnull final MethodOutcome outcome,
@Nonnull final String headerName) {
return getSingularHeader(outcome.getResponseHeaders(), headerName);
}
@Nonnull
private static Optional getSingularHeader(
@Nullable final Map> headers,
@Nonnull final String headerName) {
return Optional.ofNullable(headers)
.map(rh -> rh.get(headerName))
.flatMap(h -> h.stream().findFirst());
}
@Nonnull
private static Optional getExpires(@Nonnull final MethodOutcome outcome) {
return getExpires(outcome.getResponseHeaders());
}
@Nonnull
private static Optional getExpires(@Nullable final Map> headers) {
final Optional maxAge = getSingularHeader(headers, CACHE_CONTROL_HEADER_NAME)
.map(CacheControl::valueOf)
.map(CacheControl::getMaxAge);
return maxAge.map(CachingTerminologyService::secondsFromNow);
}
private static long secondsFromNow(final int seconds) {
return Instant.now().plusSeconds(seconds).toEpochMilli();
}
@SafeVarargs
private static Long resolveExpires(final Optional... orderedExpiryValues) {
// Go through each of the expiry values and combine them using OR logic. If the final value is
// not present, throw an error.
return Arrays.stream(orderedExpiryValues)
.reduce((acc, v) -> acc.or(() -> v))
.orElseThrow()
.orElseThrow();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy