All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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