ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl Maven / Gradle / Ivy
/*
* #%L
* HAPI FHIR JPA - Search Parameters
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* 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.
* #L%
*/
package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.ComboSearchParamType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.search.ISearchParamHashIdentityRegistry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.IndexedSearchParam;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.SearchParameterUtil;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Sets;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchParamRegistryImpl
implements ISearchParamRegistry,
IResourceChangeListener,
ISearchParamRegistryController,
ISearchParamHashIdentityRegistry {
public static final Set NON_DISABLEABLE_SEARCH_PARAMS =
Collections.unmodifiableSet(Sets.newHashSet("*:url", "Subscription:*", "SearchParameter:*"));
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
private static final long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE;
private final JpaSearchParamCache myJpaSearchParamCache = new JpaSearchParamCache();
@Autowired
private StorageSettings myStorageSettings;
@Autowired
private ISearchParamProvider mySearchParamProvider;
@Autowired
private FhirContext myFhirContext;
@Autowired
private SearchParameterCanonicalizer mySearchParameterCanonicalizer;
@Autowired
private IInterceptorService myInterceptorBroadcaster;
@Autowired
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
private IResourceChangeListenerCache myResourceChangeListenerCache;
private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
private volatile IPhoneticEncoder myPhoneticEncoder;
private volatile RuntimeSearchParamCache myActiveSearchParams;
/**
* Constructor
*/
public SearchParamRegistryImpl() {
super();
}
@Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
requiresActiveSearchParams();
// Can still be null in unit test scenarios
if (myActiveSearchParams != null) {
return myActiveSearchParams.get(theResourceName, theParamName);
} else {
return null;
}
}
@Nonnull
@Override
public ResourceSearchParams getActiveSearchParams(String theResourceName) {
requiresActiveSearchParams();
return getActiveSearchParams().getSearchParamMap(theResourceName);
}
private void requiresActiveSearchParams() {
if (myActiveSearchParams == null) {
// forced refreshes should not use a cache - we're forcibly refrsching it, after all
myResourceChangeListenerCache.forceRefresh();
}
}
@Override
public List getActiveComboSearchParams(String theResourceName) {
return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName);
}
@Override
public List getActiveComboSearchParams(
String theResourceName, ComboSearchParamType theParamType) {
return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamType);
}
@Override
public List getActiveComboSearchParams(String theResourceName, Set theParamNames) {
return myJpaSearchParamCache.getActiveComboSearchParams(theResourceName, theParamNames);
}
@Override
public Optional getIndexedSearchParamByHashIdentity(Long theHashIdentity) {
return myJpaSearchParamCache.getIndexedSearchParamByHashIdentity(theHashIdentity);
}
@Nullable
@Override
public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
if (myActiveSearchParams != null) {
return myActiveSearchParams.getByUrl(theUrl);
} else {
return null;
}
}
@Override
public Optional getActiveComboSearchParamById(String theResourceName, IIdType theId) {
return myJpaSearchParamCache.getActiveComboSearchParamById(theResourceName, theId);
}
private void rebuildActiveSearchParams() {
ourLog.info("Rebuilding SearchParamRegistry");
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
params.setCount(MAX_MANAGED_PARAM_COUNT);
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
List allSearchParams = allSearchParamsBp.getResources(0, MAX_MANAGED_PARAM_COUNT);
Integer size = allSearchParamsBp.size();
ourLog.trace("Loaded {} search params from the DB", allSearchParams.size());
if (size == null) {
ourLog.error(
"Only {} search parameters have been loaded, but there are more than that in the repository. Is offset search configured on this server?",
allSearchParams.size());
} else if (size >= MAX_MANAGED_PARAM_COUNT) {
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
}
initializeActiveSearchParams(allSearchParams);
}
private void initializeActiveSearchParams(Collection theJpaSearchParams) {
StopWatch sw = new StopWatch();
ReadOnlySearchParamCache builtInSearchParams = getBuiltInSearchParams();
RuntimeSearchParamCache searchParams =
RuntimeSearchParamCache.fromReadOnlySearchParamCache(builtInSearchParams);
long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
removeInactiveSearchParams(searchParams);
/*
* The _language SearchParameter is a weird exception - It is actually just a normal
* token SP, but we explcitly ban SPs from registering themselves with a prefix
* of "_" since that's system reserved so we put this one behind a settings toggle
*/
if (myStorageSettings.isLanguageSearchParameterEnabled()) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setValue("SearchParameter/Resource-language");
RuntimeSearchParam sp = new RuntimeSearchParam(
id,
"http://hl7.org/fhir/SearchParameter/Resource-language",
Constants.PARAM_LANGUAGE,
"Language of the resource content",
"language",
RestSearchParameterTypeEnum.TOKEN,
Collections.emptySet(),
Collections.emptySet(),
RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE,
myFhirContext.getResourceTypes());
for (String baseResourceType : sp.getBase()) {
searchParams.add(baseResourceType, sp.getName(), sp);
}
}
setActiveSearchParams(searchParams);
myJpaSearchParamCache.populateActiveSearchParams(
myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
}
@VisibleForTesting
public void setFhirContext(FhirContext theFhirContext) {
myFhirContext = theFhirContext;
}
private ReadOnlySearchParamCache getBuiltInSearchParams() {
if (myBuiltInSearchParams == null) {
if (myStorageSettings.isAutoSupportDefaultSearchParams()) {
myBuiltInSearchParams =
ReadOnlySearchParamCache.fromFhirContext(myFhirContext, mySearchParameterCanonicalizer);
} else {
// Only the built-in search params that can not be disabled will be supported automatically
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(
myFhirContext, mySearchParameterCanonicalizer, NON_DISABLEABLE_SEARCH_PARAMS);
}
}
return myBuiltInSearchParams;
}
private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) {
for (String resourceName : theSearchParams.getResourceNameKeys()) {
ResourceSearchParams resourceSearchParams = theSearchParams.getSearchParamMap(resourceName);
resourceSearchParams.removeInactive();
}
}
@VisibleForTesting
public void setStorageSettings(StorageSettings theStorageSettings) {
myStorageSettings = theStorageSettings;
}
private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(
RuntimeSearchParamCache theSearchParamCache, Collection theSearchParams) {
if (!myStorageSettings.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) {
return 0;
}
long retval = 0;
for (IBaseResource searchParam : theSearchParams) {
retval += overrideSearchParam(theSearchParamCache, searchParam);
}
return retval;
}
private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) {
if (theSearchParameter == null) {
return 0;
}
RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter);
if (runtimeSp == null) {
return 0;
}
if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
return 0;
}
long retval = 0;
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) {
if (isBlank(nextBaseName)) {
continue;
}
String name = runtimeSp.getName();
theSearchParams.add(nextBaseName, name, runtimeSp);
ourLog.debug(
"Adding search parameter {}.{} to SearchParamRegistry",
nextBaseName,
StringUtils.defaultString(name, "[composite]"));
retval++;
}
return retval;
}
@Override
public void requestRefresh() {
myResourceChangeListenerCache.requestRefresh();
}
@Override
public void forceRefresh() {
RuntimeSearchParamCache activeSearchParams = myActiveSearchParams;
myResourceChangeListenerCache.forceRefresh();
// If the refresh didn't trigger a change, proceed with one anyway
if (myActiveSearchParams == activeSearchParams) {
rebuildActiveSearchParams();
}
}
@Override
public ResourceChangeResult refreshCacheIfNecessary() {
return myResourceChangeListenerCache.refreshCacheIfNecessary();
}
@VisibleForTesting
public void setResourceChangeListenerRegistry(IResourceChangeListenerRegistry theResourceChangeListenerRegistry) {
myResourceChangeListenerRegistry = theResourceChangeListenerRegistry;
}
/**
* There is a circular reference between this class and the ResourceChangeListenerRegistry:
* SearchParamRegistryImpl -> ResourceChangeListenerRegistry -> InMemoryResourceMatcher -> SearchParamRegistryImpl. Since we only need this once on boot-up, we delay
* until ContextRefreshedEvent.
*/
@PostConstruct
public void registerListener() {
SearchParameterMap spMap = SearchParameterMap.newSynchronous();
spMap.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
"SearchParameter", spMap, this, REFRESH_INTERVAL);
}
@PreDestroy
public void unregisterListener() {
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
}
public ReadOnlySearchParamCache getActiveSearchParams() {
requiresActiveSearchParams();
if (myActiveSearchParams == null) {
throw new IllegalStateException(Msg.code(511) + "SearchParamRegistry has not been initialized");
}
return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams);
}
/**
* All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder.
*
* @since 5.1.0
*/
@Override
public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
myPhoneticEncoder = thePhoneticEncoder;
if (myActiveSearchParams == null) {
return;
}
myActiveSearchParams
.getSearchParamStream()
.forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam));
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
if (theResourceChangeEvent.isEmpty()) {
return;
}
ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent);
if (result.created > 0) {
ourLog.info(
"Adding {} search parameters to SearchParamRegistry: {}",
result.created,
unqualified(theResourceChangeEvent.getCreatedResourceIds()));
}
if (result.updated > 0) {
ourLog.info(
"Updating {} search parameters in SearchParamRegistry: {}",
result.updated,
unqualified(theResourceChangeEvent.getUpdatedResourceIds()));
}
if (result.deleted > 0) {
ourLog.info(
"Deleting {} search parameters from SearchParamRegistry: {}",
result.deleted,
unqualified(theResourceChangeEvent.getDeletedResourceIds()));
}
rebuildActiveSearchParams();
}
private String unqualified(List theIds) {
Iterator unqualifiedIds = theIds.stream()
.map(IIdType::toUnqualifiedVersionless)
.map(IIdType::getValue)
.iterator();
return StringUtils.join(unqualifiedIds, ", ");
}
@Override
public void handleInit(Collection theResourceIds) {
List searchParams = new ArrayList<>();
for (IIdType id : theResourceIds) {
try {
IBaseResource searchParam = mySearchParamProvider.read(id);
searchParams.add(searchParam);
} catch (ResourceNotFoundException e) {
ourLog.warn("SearchParameter {} not found. Excluding from list of active search params.", id);
}
}
initializeActiveSearchParams(searchParams);
}
@Override
public boolean isInitialized() {
return myActiveSearchParams != null;
}
@VisibleForTesting
public void resetForUnitTest() {
myBuiltInSearchParams = null;
setActiveSearchParams(null);
handleInit(Collections.emptyList());
}
@VisibleForTesting
public void setSearchParameterCanonicalizerForUnitTest(
SearchParameterCanonicalizer theSearchParameterCanonicalizerForUnitTest) {
mySearchParameterCanonicalizer = theSearchParameterCanonicalizerForUnitTest;
}
@VisibleForTesting
public int getMaxManagedParamCountForUnitTests() {
return MAX_MANAGED_PARAM_COUNT;
}
@VisibleForTesting
public void setActiveSearchParams(RuntimeSearchParamCache theSearchParams) {
myActiveSearchParams = theSearchParams;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy