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

ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorService 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.extractor;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
import ca.uhn.fhir.jpa.model.dao.JpaPid;
import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.IResourceIndexComboSearchParameter;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboTokenNonUnique;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString;
import ca.uhn.fhir.jpa.model.entity.ResourceLink;
import ca.uhn.fhir.jpa.model.entity.ResourceLink.ResourceLinkForLocalReferenceParams;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.entity.SearchParamPresentEntity;
import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
import ca.uhn.fhir.util.FhirTerser;
import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseReference;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.IdType;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static ca.uhn.fhir.jpa.model.config.PartitionSettings.CrossPartitionReferenceMode.ALLOWED_UNQUALIFIED;
import static ca.uhn.fhir.jpa.model.entity.ResourceLink.forLocalReference;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

public class SearchParamExtractorService {
	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchParamExtractorService.class);

	@Autowired
	private ISearchParamExtractor mySearchParamExtractor;

	@Autowired
	private IInterceptorBroadcaster myInterceptorBroadcaster;

	@Autowired
	private StorageSettings myStorageSettings;

	@Autowired
	private FhirContext myContext;

	@Autowired
	private ISearchParamRegistry mySearchParamRegistry;

	@Autowired
	private PartitionSettings myPartitionSettings;

	@Autowired(required = false)
	private IResourceLinkResolver myResourceLinkResolver;

	private SearchParamExtractionUtil mySearchParamExtractionUtil;

	@VisibleForTesting
	public void setSearchParamExtractor(ISearchParamExtractor theSearchParamExtractor) {
		mySearchParamExtractor = theSearchParamExtractor;
	}

	/**
	 * This method is responsible for scanning a resource for all of the search parameter instances.
	 * I.e. for all search parameters defined for
	 * a given resource type, it extracts the associated indexes and populates
	 * {@literal theParams}.
	 */
	public void extractFromResource(
			RequestPartitionId theRequestPartitionId,
			RequestDetails theRequestDetails,
			ResourceIndexedSearchParams theNewParams,
			ResourceIndexedSearchParams theExistingParams,
			ResourceTable theEntity,
			IBaseResource theResource,
			TransactionDetails theTransactionDetails,
			boolean theFailOnInvalidReference,
			@Nonnull ISearchParamExtractor.ISearchParamFilter theSearchParamFilter) {
		// All search parameter types except Reference
		ResourceIndexedSearchParams normalParams = ResourceIndexedSearchParams.withSets();
		getExtractionUtil()
				.extractSearchIndexParameters(theRequestDetails, normalParams, theResource, theSearchParamFilter);
		mergeParams(normalParams, theNewParams);

		boolean indexOnContainedResources = myStorageSettings.isIndexOnContainedResources();
		ISearchParamExtractor.SearchParamSet indexedReferences =
				mySearchParamExtractor.extractResourceLinks(theResource, indexOnContainedResources);
		SearchParamExtractorService.handleWarnings(theRequestDetails, myInterceptorBroadcaster, indexedReferences);

		if (indexOnContainedResources) {
			ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets();
			extractSearchIndexParametersForContainedResources(
					theRequestDetails, containedParams, theResource, theEntity, indexedReferences);
			mergeParams(containedParams, theNewParams);
		}

		if (myStorageSettings.isIndexOnUpliftedRefchains()) {
			ResourceIndexedSearchParams containedParams = ResourceIndexedSearchParams.withSets();
			extractSearchIndexParametersForUpliftedRefchains(
					theRequestDetails,
					containedParams,
					theEntity,
					theRequestPartitionId,
					theTransactionDetails,
					indexedReferences);
			mergeParams(containedParams, theNewParams);
		}

		// Do this after, because we add to strings during both string and token processing, and contained resource if
		// any
		populateResourceTables(theNewParams, theEntity);

		// Reference search parameters
		extractResourceLinks(
				theRequestPartitionId,
				theExistingParams,
				theNewParams,
				theEntity,
				theResource,
				theTransactionDetails,
				theFailOnInvalidReference,
				theRequestDetails,
				indexedReferences);

		if (indexOnContainedResources) {
			extractResourceLinksForContainedResources(
					theRequestPartitionId,
					theNewParams,
					theEntity,
					theResource,
					theTransactionDetails,
					theFailOnInvalidReference,
					theRequestDetails);
		}

		// Missing (:missing) Indexes - These are indexes to satisfy the :missing
		// modifier
		if (myStorageSettings.getIndexMissingFields() == StorageSettings.IndexEnabledEnum.ENABLED) {

			// References
			Map presenceMap = getReferenceSearchParamPresenceMap(theEntity, theNewParams);
			presenceMap.forEach((key, value) -> {
				SearchParamPresentEntity present = new SearchParamPresentEntity();
				present.setPartitionSettings(myPartitionSettings);
				present.setResource(theEntity);
				present.setParamName(key);
				present.setPresent(value);
				present.setPartitionId(theEntity.getPartitionId());
				present.calculateHashes();
				theNewParams.mySearchParamPresentEntities.add(present);
			});

			// Everything else
			ResourceSearchParams activeSearchParams =
					mySearchParamRegistry.getActiveSearchParams(theEntity.getResourceType());
			theNewParams.findMissingSearchParams(myPartitionSettings, myStorageSettings, theEntity, activeSearchParams);
		}

		extractSearchParamComboUnique(theEntity, theNewParams);

		extractSearchParamComboNonUnique(theEntity, theNewParams);

		theNewParams.setUpdatedTime(theTransactionDetails.getTransactionDate());
	}

	private SearchParamExtractionUtil getExtractionUtil() {
		if (mySearchParamExtractionUtil == null) {
			mySearchParamExtractionUtil = new SearchParamExtractionUtil(
					myContext, myStorageSettings, mySearchParamExtractor, myInterceptorBroadcaster);
		}
		return mySearchParamExtractionUtil;
	}

	@Nonnull
	private Map getReferenceSearchParamPresenceMap(
			ResourceTable entity, ResourceIndexedSearchParams newParams) {
		Map retval = new HashMap<>();

		for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
			retval.put(nextKey, Boolean.TRUE);
		}

		ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(entity.getResourceType());
		activeSearchParams.getReferenceSearchParamNames().forEach(key -> retval.putIfAbsent(key, Boolean.FALSE));
		return retval;
	}

	@VisibleForTesting
	public void setStorageSettings(StorageSettings theStorageSettings) {
		myStorageSettings = theStorageSettings;
	}

	/**
	 * Extract search parameter indexes for contained resources. E.g. if we
	 * are storing a Patient with a contained Organization, we might extract
	 * a String index on the Patient with paramName="organization.name" and
	 * value="Org Name"
	 */
	private void extractSearchIndexParametersForContainedResources(
			RequestDetails theRequestDetails,
			ResourceIndexedSearchParams theParams,
			IBaseResource theResource,
			ResourceTable theEntity,
			ISearchParamExtractor.SearchParamSet theIndexedReferences) {

		FhirTerser terser = myContext.newTerser();

		// 1. get all contained resources
		Collection containedResources = terser.getAllEmbeddedResources(theResource, false);

		// Extract search parameters
		IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {
			@Nonnull
			@Override
			public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) {
				// Currently for contained resources we always index all search parameters
				// on all contained resources. A potential nice future optimization would
				// be to make this configurable, perhaps with an optional extension you could
				// add to a SearchParameter?
				return ISearchParamExtractor.ALL_PARAMS;
			}

			@Override
			public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) {
				if (thePathAndRef.getRef() == null) {
					return null;
				}
				return findContainedResource(containedResources, thePathAndRef.getRef());
			}
		};
		boolean recurse = myStorageSettings.isIndexOnContainedResourcesRecursively();
		extractSearchIndexParametersForTargetResources(
				theRequestDetails,
				theParams,
				theEntity,
				new HashSet<>(),
				strategy,
				theIndexedReferences,
				recurse,
				true);
	}

	/**
	 * Extract search parameter indexes for uplifted refchains. E.g. if we
	 * are storing a Patient with reference to an Organization and the
	 * "Patient:organization" SearchParameter declares an uplifted refchain
	 * on the "name" SearchParameter, we might extract a String index
	 * on the Patient with paramName="organization.name" and value="Org Name"
	 */
	private void extractSearchIndexParametersForUpliftedRefchains(
			RequestDetails theRequestDetails,
			ResourceIndexedSearchParams theParams,
			ResourceTable theEntity,
			RequestPartitionId theRequestPartitionId,
			TransactionDetails theTransactionDetails,
			ISearchParamExtractor.SearchParamSet theIndexedReferences) {
		IChainedSearchParameterExtractionStrategy strategy = new IChainedSearchParameterExtractionStrategy() {

			@Nonnull
			@Override
			public ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef) {
				String searchParamName = thePathAndRef.getSearchParamName();
				RuntimeSearchParam searchParam =
						mySearchParamRegistry.getActiveSearchParam(theEntity.getResourceType(), searchParamName);
				Set upliftRefchainCodes = searchParam.getUpliftRefchainCodes();
				if (upliftRefchainCodes.isEmpty()) {
					return ISearchParamExtractor.NO_PARAMS;
				}
				return sp -> sp.stream()
						.filter(t -> upliftRefchainCodes.contains(t.getName()))
						.collect(Collectors.toList());
			}

			@Override
			public IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef) {
				// The PathAndRef will contain a resource if the SP path was inside a Bundle
				// and pointed to a resource (e.g. Bundle.entry.resource) as opposed to
				// pointing to a reference (e.g. Observation.subject)
				if (thePathAndRef.getResource() != null) {
					return thePathAndRef.getResource();
				}

				// Ok, it's a normal reference
				IIdType reference = thePathAndRef.getRef().getReferenceElement();

				// If we're processing a FHIR transaction, we store the resources
				// mapped by their resolved resource IDs in theTransactionDetails
				IBaseResource resolvedResource = theTransactionDetails.getResolvedResource(reference);

				// And the usual case is that the reference points to a resource
				// elsewhere in the repository, so we load it
				if (resolvedResource == null
						&& myResourceLinkResolver != null
						&& !reference.getValue().startsWith("urn:uuid:")) {
					RequestPartitionId targetRequestPartitionId = determineResolverPartitionId(theRequestPartitionId);
					resolvedResource = myResourceLinkResolver.loadTargetResource(
							targetRequestPartitionId,
							theEntity.getResourceType(),
							thePathAndRef,
							theRequestDetails,
							theTransactionDetails);
					if (resolvedResource != null) {
						ourLog.trace("Found target: {}", resolvedResource.getIdElement());
						theTransactionDetails.addResolvedResource(
								thePathAndRef.getRef().getReferenceElement(), resolvedResource);
					}
				}

				return resolvedResource;
			}
		};
		extractSearchIndexParametersForTargetResources(
				theRequestDetails, theParams, theEntity, new HashSet<>(), strategy, theIndexedReferences, false, false);
	}

	/**
	 * Extract indexes for contained references as well as for uplifted refchains.
	 * These two types of indexes are both similar special cases. Normally we handle
	 * chained searches ("Patient?organization.name=Foo") using a join from the
	 * {@link ResourceLink} table (for the "organization" part) to the
	 * {@link ResourceIndexedSearchParamString} table (for the "name" part). But
	 * for both contained resource indexes and uplifted refchains we use only the
	 * {@link ResourceIndexedSearchParamString} table to handle the entire
	 * "organization.name" part, or the other similar tables for token, number, etc.
	 *
	 * @see #extractSearchIndexParametersForContainedResources(RequestDetails, ResourceIndexedSearchParams, IBaseResource, ResourceTable, ISearchParamExtractor.SearchParamSet)
	 * @see #extractSearchIndexParametersForUpliftedRefchains(RequestDetails, ResourceIndexedSearchParams, ResourceTable, RequestPartitionId, TransactionDetails, ISearchParamExtractor.SearchParamSet)
	 */
	private void extractSearchIndexParametersForTargetResources(
			RequestDetails theRequestDetails,
			ResourceIndexedSearchParams theParams,
			ResourceTable theEntity,
			Collection theAlreadySeenResources,
			IChainedSearchParameterExtractionStrategy theTargetIndexingStrategy,
			ISearchParamExtractor.SearchParamSet theIndexedReferences,
			boolean theRecurse,
			boolean theIndexOnContainedResources) {
		// 2. Find referenced search parameters

		String spnamePrefix;
		// 3. for each referenced search parameter, create an index
		for (PathAndRef nextPathAndRef : theIndexedReferences) {

			// 3.1 get the search parameter name as spname prefix
			spnamePrefix = nextPathAndRef.getSearchParamName();

			if (spnamePrefix == null || (nextPathAndRef.getRef() == null && nextPathAndRef.getResource() == null))
				continue;

			// 3.1.2 check if this ref actually applies here
			ISearchParamExtractor.ISearchParamFilter searchParamsToIndex =
					theTargetIndexingStrategy.getSearchParamFilter(nextPathAndRef);
			if (searchParamsToIndex == ISearchParamExtractor.NO_PARAMS) {
				continue;
			}

			// 3.2 find the target resource
			IBaseResource targetResource = theTargetIndexingStrategy.fetchResourceAtPath(nextPathAndRef);
			if (targetResource == null) continue;

			// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite
			// loops
			if (theAlreadySeenResources.contains(targetResource)) {
				continue;
			}

			ResourceIndexedSearchParams currParams = ResourceIndexedSearchParams.withSets();

			// 3.3 create indexes for the current contained resource
			getExtractionUtil()
					.extractSearchIndexParameters(theRequestDetails, currParams, targetResource, searchParamsToIndex);

			// 3.4 recurse to process any other contained resources referenced by this one
			// Recursing is currently only allowed for contained resources and not
			// uplifted refchains because the latter could potentially kill performance
			// with the number of resource resolutions needed in order to handle
			// a single write. Maybe in the future we could add caching to improve
			// this
			if (theRecurse) {
				HashSet nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
				nextAlreadySeenResources.add(targetResource);

				ISearchParamExtractor.SearchParamSet indexedReferences =
						mySearchParamExtractor.extractResourceLinks(targetResource, theIndexOnContainedResources);
				SearchParamExtractorService.handleWarnings(
						theRequestDetails, myInterceptorBroadcaster, indexedReferences);

				extractSearchIndexParametersForTargetResources(
						theRequestDetails,
						currParams,
						theEntity,
						nextAlreadySeenResources,
						theTargetIndexingStrategy,
						indexedReferences,
						true,
						theIndexOnContainedResources);
			}

			// 3.5 added reference name as a prefix for the contained resource if any
			// e.g. for Observation.subject contained reference
			// the SP_NAME = subject.family
			currParams.updateSpnamePrefixForIndexOnUpliftedChain(
					theEntity.getResourceType(), nextPathAndRef.getSearchParamName());

			// 3.6 merge to the mainParams
			// NOTE: the spname prefix is different
			mergeParams(currParams, theParams);
		}
	}

	private IBaseResource findContainedResource(Collection resources, IBaseReference reference) {
		for (IBaseResource resource : resources) {
			if (resource.getIdElement().equals(reference.getReferenceElement())) return resource;
		}
		return null;
	}

	private void mergeParams(ResourceIndexedSearchParams theSrcParams, ResourceIndexedSearchParams theTargetParams) {

		theTargetParams.myNumberParams.addAll(theSrcParams.myNumberParams);
		theTargetParams.myQuantityParams.addAll(theSrcParams.myQuantityParams);
		theTargetParams.myQuantityNormalizedParams.addAll(theSrcParams.myQuantityNormalizedParams);
		theTargetParams.myDateParams.addAll(theSrcParams.myDateParams);
		theTargetParams.myUriParams.addAll(theSrcParams.myUriParams);
		theTargetParams.myTokenParams.addAll(theSrcParams.myTokenParams);
		theTargetParams.myStringParams.addAll(theSrcParams.myStringParams);
		theTargetParams.myCoordsParams.addAll(theSrcParams.myCoordsParams);
		theTargetParams.myCompositeParams.addAll(theSrcParams.myCompositeParams);
	}

	private void populateResourceTables(ResourceIndexedSearchParams theParams, ResourceTable theEntity) {

		populateResourceTable(theParams.myNumberParams, theEntity);
		populateResourceTable(theParams.myQuantityParams, theEntity);
		populateResourceTable(theParams.myQuantityNormalizedParams, theEntity);
		populateResourceTable(theParams.myDateParams, theEntity);
		populateResourceTable(theParams.myUriParams, theEntity);
		populateResourceTable(theParams.myTokenParams, theEntity);
		populateResourceTable(theParams.myStringParams, theEntity);
		populateResourceTable(theParams.myCoordsParams, theEntity);
	}

	@VisibleForTesting
	public void setContext(FhirContext theContext) {
		myContext = theContext;
	}

	private void extractResourceLinks(
			RequestPartitionId theRequestPartitionId,
			ResourceIndexedSearchParams theParams,
			ResourceTable theEntity,
			IBaseResource theResource,
			TransactionDetails theTransactionDetails,
			boolean theFailOnInvalidReference,
			RequestDetails theRequest,
			ISearchParamExtractor.SearchParamSet theIndexedReferences) {
		extractResourceLinks(
				theRequestPartitionId,
				ResourceIndexedSearchParams.withSets(),
				theParams,
				theEntity,
				theResource,
				theTransactionDetails,
				theFailOnInvalidReference,
				theRequest,
				theIndexedReferences);
	}

	private void extractResourceLinks(
			RequestPartitionId theRequestPartitionId,
			ResourceIndexedSearchParams theExistingParams,
			ResourceIndexedSearchParams theNewParams,
			ResourceTable theEntity,
			IBaseResource theResource,
			TransactionDetails theTransactionDetails,
			boolean theFailOnInvalidReference,
			RequestDetails theRequest,
			ISearchParamExtractor.SearchParamSet theIndexedReferences) {
		String sourceResourceName = myContext.getResourceType(theResource);

		for (PathAndRef nextPathAndRef : theIndexedReferences) {
			if (nextPathAndRef.getRef() != null) {
				if (nextPathAndRef.getRef().getReferenceElement().isLocal()) {
					continue;
				}

				RuntimeSearchParam searchParam = mySearchParamRegistry.getActiveSearchParam(
						sourceResourceName, nextPathAndRef.getSearchParamName());
				extractResourceLinks(
						theRequestPartitionId,
						theExistingParams,
						theNewParams,
						theEntity,
						theTransactionDetails,
						sourceResourceName,
						searchParam,
						nextPathAndRef,
						theFailOnInvalidReference,
						theRequest);
			}
		}

		theEntity.setHasLinks(!theNewParams.myLinks.isEmpty());
	}

	private void extractResourceLinks(
			@Nonnull RequestPartitionId theRequestPartitionId,
			ResourceIndexedSearchParams theExistingParams,
			ResourceIndexedSearchParams theNewParams,
			ResourceTable theEntity,
			TransactionDetails theTransactionDetails,
			String theSourceResourceName,
			RuntimeSearchParam theRuntimeSearchParam,
			PathAndRef thePathAndRef,
			boolean theFailOnInvalidReference,
			RequestDetails theRequest) {
		IBaseReference nextReference = thePathAndRef.getRef();
		IIdType nextId = nextReference.getReferenceElement();
		String path = thePathAndRef.getPath();
		Date transactionDate = theTransactionDetails.getTransactionDate();

		/*
		 * This can only really happen if the DAO is being called
		 * programmatically with a Bundle (not through the FHIR REST API)
		 * but Smile does this
		 */
		if (nextId.isEmpty() && nextReference.getResource() != null) {
			nextId = nextReference.getResource().getIdElement();
		}

		if (myContext.getParserOptions().isStripVersionsFromReferences()
				&& !myContext
						.getParserOptions()
						.getDontStripVersionsFromReferencesAtPaths()
						.contains(thePathAndRef.getPath())
				&& nextId.hasVersionIdPart()) {
			nextId = nextId.toVersionless();
		}

		theNewParams.myPopulatedResourceLinkParameters.add(thePathAndRef.getSearchParamName());

		boolean canonical = thePathAndRef.isCanonical();
		if (LogicalReferenceHelper.isLogicalReference(myStorageSettings, nextId) || canonical) {
			String value = nextId.getValue();
			ResourceLink resourceLink =
					ResourceLink.forLogicalReference(thePathAndRef.getPath(), theEntity, value, transactionDate);
			if (theNewParams.myLinks.add(resourceLink)) {
				ourLog.debug("Indexing remote resource reference URL: {}", nextId);
			}
			return;
		}

		String baseUrl = nextId.getBaseUrl();

		// If this is a conditional URL, the part after the question mark
		// can include URLs (e.g. token system URLs) and these really confuse
		// the IdType parser because a conditional URL isn't actually a valid
		// FHIR ID. So in order to truly determine whether we're dealing with
		// an absolute reference, we strip the query part and reparse
		// the reference.
		int questionMarkIndex = nextId.getValue().indexOf('?');
		if (questionMarkIndex != -1) {
			IdType preQueryId = new IdType(nextId.getValue().substring(0, questionMarkIndex - 1));
			baseUrl = preQueryId.getBaseUrl();
		}

		String typeString = nextId.getResourceType();
		if (isBlank(typeString)) {
			String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource type - "
					+ nextId.getValue();
			if (theFailOnInvalidReference) {
				throw new InvalidRequestException(Msg.code(505) + msg);
			} else {
				ourLog.debug(msg);
				return;
			}
		}
		RuntimeResourceDefinition resourceDefinition;
		try {
			resourceDefinition = myContext.getResourceDefinition(typeString);
		} catch (DataFormatException e) {
			String msg = "Invalid resource reference found at path[" + path
					+ "] - Resource type is unknown or not supported on this server - " + nextId.getValue();
			if (theFailOnInvalidReference) {
				throw new InvalidRequestException(Msg.code(506) + msg);
			} else {
				ourLog.debug(msg);
				return;
			}
		}

		if (theRuntimeSearchParam.hasTargets()) {
			if (!theRuntimeSearchParam.getTargets().contains(typeString)) {
				return;
			}
		}

		if (isNotBlank(baseUrl)) {
			if (!myStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl)
					&& !myStorageSettings.isAllowExternalReferences()) {
				String msg = myContext
						.getLocalizer()
						.getMessage(BaseSearchParamExtractor.class, "externalReferenceNotAllowed", nextId.getValue());
				throw new InvalidRequestException(Msg.code(507) + msg);
			} else {
				ResourceLink resourceLink =
						ResourceLink.forAbsoluteReference(thePathAndRef.getPath(), theEntity, nextId, transactionDate);
				if (theNewParams.myLinks.add(resourceLink)) {
					ourLog.debug("Indexing remote resource reference URL: {}", nextId);
				}
				return;
			}
		}

		Class type = resourceDefinition.getImplementingClass();
		String targetId = nextId.getIdPart();
		if (StringUtils.isBlank(targetId)) {
			String msg = "Invalid resource reference found at path[" + path + "] - Does not contain resource ID - "
					+ nextId.getValue();
			if (theFailOnInvalidReference) {
				throw new InvalidRequestException(Msg.code(508) + msg);
			} else {
				ourLog.debug(msg);
				return;
			}
		}

		IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
		JpaPid resolvedTargetId = (JpaPid) theTransactionDetails.getResolvedResourceId(referenceElement);
		ResourceLink resourceLink;

		Long targetVersionId = nextId.getVersionIdPartAsLong();
		if (resolvedTargetId != null) {

			/*
			 * If we have already resolved the given reference within this transaction, we don't
			 * need to resolve it again
			 */
			myResourceLinkResolver.validateTypeOrThrowException(type);

			ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
					.setSourcePath(thePathAndRef.getPath())
					.setSourceResource(theEntity)
					.setTargetResourceType(typeString)
					.setTargetResourcePid(resolvedTargetId.getId())
					.setTargetResourceId(targetId)
					.setUpdated(transactionDate)
					.setTargetResourceVersion(targetVersionId)
					.setTargetResourcePartitionablePartitionId(resolvedTargetId.getPartitionablePartitionId());

			resourceLink = forLocalReference(params);

		} else if (theFailOnInvalidReference) {

			/*
			 * The reference points to another resource, so let's look it up. We need to do this
			 * since the target may be a forced ID, but also so that we can throw an exception
			 * if the reference is invalid
			 */
			myResourceLinkResolver.validateTypeOrThrowException(type);

			/*
			 * We need to obtain a resourceLink out of the provided {@literal thePathAndRef}.  In the case
			 * where we are updating a resource that already has resourceLinks (stored in {@literal theExistingParams.getResourceLinks()}),
			 * let's try to match thePathAndRef to an already existing resourceLink to avoid the
			 * very expensive operation of creating a resourceLink that would end up being exactly the same
			 * one we already have.
			 */
			Optional optionalResourceLink =
					findMatchingResourceLink(thePathAndRef, theExistingParams.getResourceLinks());
			if (optionalResourceLink.isPresent()) {
				resourceLink = optionalResourceLink.get();
			} else {
				resourceLink = resolveTargetAndCreateResourceLinkOrReturnNull(
						theRequestPartitionId,
						theSourceResourceName,
						thePathAndRef,
						theEntity,
						transactionDate,
						nextId,
						theRequest,
						theTransactionDetails);
			}

			if (resourceLink == null) {
				return;
			} else {
				// Cache the outcome in the current transaction in case there are more references
				JpaPid persistentId = JpaPid.fromId(resourceLink.getTargetResourcePid());
				persistentId.setPartitionablePartitionId(resourceLink.getTargetResourcePartitionId());
				theTransactionDetails.addResolvedResourceId(referenceElement, persistentId);
			}

		} else {

			/*
			 * Just assume the reference is valid. This is used for in-memory matching since there
			 * is no expectation of a database in this situation
			 */
			ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
					.setSourcePath(thePathAndRef.getPath())
					.setSourceResource(theEntity)
					.setTargetResourceType(typeString)
					.setTargetResourceId(targetId)
					.setUpdated(transactionDate)
					.setTargetResourceVersion(targetVersionId);

			resourceLink = forLocalReference(params);
		}

		theNewParams.myLinks.add(resourceLink);
	}

	private Optional findMatchingResourceLink(
			PathAndRef thePathAndRef, Collection theResourceLinks) {
		IIdType referenceElement = thePathAndRef.getRef().getReferenceElement();
		List resourceLinks = new ArrayList<>(theResourceLinks);
		for (ResourceLink resourceLink : resourceLinks) {

			// comparing the searchParam path ex: Group.member.entity
			boolean hasMatchingSearchParamPath =
					StringUtils.equals(resourceLink.getSourcePath(), thePathAndRef.getPath());

			boolean hasMatchingResourceType =
					StringUtils.equals(resourceLink.getTargetResourceType(), referenceElement.getResourceType());

			boolean hasMatchingResourceId =
					StringUtils.equals(resourceLink.getTargetResourceId(), referenceElement.getIdPart());

			boolean hasMatchingResourceVersion = myContext.getParserOptions().isStripVersionsFromReferences()
					|| referenceElement.getVersionIdPartAsLong() == null
					|| referenceElement.getVersionIdPartAsLong().equals(resourceLink.getTargetResourceVersion());

			if (hasMatchingSearchParamPath
					&& hasMatchingResourceType
					&& hasMatchingResourceId
					&& hasMatchingResourceVersion) {
				return Optional.of(resourceLink);
			}
		}

		return Optional.empty();
	}

	private void extractResourceLinksForContainedResources(
			RequestPartitionId theRequestPartitionId,
			ResourceIndexedSearchParams theParams,
			ResourceTable theEntity,
			IBaseResource theResource,
			TransactionDetails theTransactionDetails,
			boolean theFailOnInvalidReference,
			RequestDetails theRequest) {

		FhirTerser terser = myContext.newTerser();

		// 1. get all contained resources
		Collection containedResources = terser.getAllEmbeddedResources(theResource, false);

		extractResourceLinksForContainedResources(
				theRequestPartitionId,
				theParams,
				theEntity,
				theResource,
				theTransactionDetails,
				theFailOnInvalidReference,
				theRequest,
				containedResources,
				new HashSet<>());
	}

	private void extractResourceLinksForContainedResources(
			RequestPartitionId theRequestPartitionId,
			ResourceIndexedSearchParams theParams,
			ResourceTable theEntity,
			IBaseResource theResource,
			TransactionDetails theTransactionDetails,
			boolean theFailOnInvalidReference,
			RequestDetails theRequest,
			Collection theContainedResources,
			Collection theAlreadySeenResources) {

		// 2. Find referenced search parameters
		ISearchParamExtractor.SearchParamSet referencedSearchParamSet =
				mySearchParamExtractor.extractResourceLinks(theResource, true);

		String spNamePrefix;
		ResourceIndexedSearchParams currParams;
		// 3. for each referenced search parameter, create an index
		for (PathAndRef nextPathAndRef : referencedSearchParamSet) {

			// 3.1 get the search parameter name as spname prefix
			spNamePrefix = nextPathAndRef.getSearchParamName();

			if (spNamePrefix == null || nextPathAndRef.getRef() == null) continue;

			// 3.2 find the contained resource
			IBaseResource containedResource = findContainedResource(theContainedResources, nextPathAndRef.getRef());
			if (containedResource == null) continue;

			// 3.2.1 if we've already processed this resource upstream, do not process it again, to prevent infinite
			// loops
			if (theAlreadySeenResources.contains(containedResource)) {
				continue;
			}

			currParams = ResourceIndexedSearchParams.withSets();

			// 3.3 create indexes for the current contained resource
			ISearchParamExtractor.SearchParamSet indexedReferences =
					mySearchParamExtractor.extractResourceLinks(containedResource, true);
			extractResourceLinks(
					theRequestPartitionId,
					currParams,
					theEntity,
					containedResource,
					theTransactionDetails,
					theFailOnInvalidReference,
					theRequest,
					indexedReferences);

			// 3.4 recurse to process any other contained resources referenced by this one
			if (myStorageSettings.isIndexOnContainedResourcesRecursively()) {
				HashSet nextAlreadySeenResources = new HashSet<>(theAlreadySeenResources);
				nextAlreadySeenResources.add(containedResource);
				extractResourceLinksForContainedResources(
						theRequestPartitionId,
						currParams,
						theEntity,
						containedResource,
						theTransactionDetails,
						theFailOnInvalidReference,
						theRequest,
						theContainedResources,
						nextAlreadySeenResources);
			}

			// 3.4 added reference name as a prefix for the contained resource if any
			// e.g. for Observation.subject contained reference
			// the SP_NAME = subject.family
			currParams.updateSpnamePrefixForLinksOnContainedResource(nextPathAndRef.getPath());

			// 3.5 merge to the mainParams
			// NOTE: the spname prefix is different
			theParams.getResourceLinks().addAll(currParams.getResourceLinks());
		}
	}

	@SuppressWarnings("unchecked")
	private ResourceLink resolveTargetAndCreateResourceLinkOrReturnNull(
			@Nonnull RequestPartitionId theRequestPartitionId,
			String theSourceResourceName,
			PathAndRef thePathAndRef,
			ResourceTable theEntity,
			Date theUpdateTime,
			IIdType theNextId,
			RequestDetails theRequest,
			TransactionDetails theTransactionDetails) {
		JpaPid resolvedResourceId = (JpaPid) theTransactionDetails.getResolvedResourceId(theNextId);

		if (resolvedResourceId != null) {
			String targetResourceType = theNextId.getResourceType();
			Long targetResourcePid = resolvedResourceId.getId();
			String targetResourceIdPart = theNextId.getIdPart();
			Long targetVersion = theNextId.getVersionIdPartAsLong();

			ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
					.setSourcePath(thePathAndRef.getPath())
					.setSourceResource(theEntity)
					.setTargetResourceType(targetResourceType)
					.setTargetResourcePid(targetResourcePid)
					.setTargetResourceId(targetResourceIdPart)
					.setUpdated(theUpdateTime)
					.setTargetResourceVersion(targetVersion)
					.setTargetResourcePartitionablePartitionId(resolvedResourceId.getPartitionablePartitionId());

			return ResourceLink.forLocalReference(params);
		}

		/*
		 * We keep a cache of resolved target resources. This is good since for some resource types, there
		 * are multiple search parameters that map to the same element path within a resource (e.g.
		 * Observation:patient and Observation.subject and we don't want to force a resolution of the
		 * target any more times than we have to.
		 */

		IResourceLookup targetResource;
		if (myPartitionSettings.isPartitioningEnabled()) {
			if (myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) {

				// Interceptor: Pointcut.JPA_CROSS_PARTITION_REFERENCE_DETECTED
				if (CompositeInterceptorBroadcaster.hasHooks(
						Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE, myInterceptorBroadcaster, theRequest)) {
					CrossPartitionReferenceDetails referenceDetails = new CrossPartitionReferenceDetails(
							theRequestPartitionId,
							theSourceResourceName,
							thePathAndRef,
							theRequest,
							theTransactionDetails);
					HookParams params = new HookParams(referenceDetails);
					targetResource =
							(IResourceLookup) CompositeInterceptorBroadcaster.doCallHooksAndReturnObject(
									myInterceptorBroadcaster,
									theRequest,
									Pointcut.JPA_RESOLVE_CROSS_PARTITION_REFERENCE,
									params);
				} else {
					targetResource = myResourceLinkResolver.findTargetResource(
							RequestPartitionId.allPartitions(),
							theSourceResourceName,
							thePathAndRef,
							theRequest,
							theTransactionDetails);
				}

			} else {
				targetResource = myResourceLinkResolver.findTargetResource(
						theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
			}
		} else {
			targetResource = myResourceLinkResolver.findTargetResource(
					theRequestPartitionId, theSourceResourceName, thePathAndRef, theRequest, theTransactionDetails);
		}

		if (targetResource == null) {
			return null;
		}

		String targetResourceType = targetResource.getResourceType();
		Long targetResourcePid = targetResource.getPersistentId().getId();
		String targetResourceIdPart = theNextId.getIdPart();
		Long targetVersion = theNextId.getVersionIdPartAsLong();

		ResourceLinkForLocalReferenceParams params = ResourceLinkForLocalReferenceParams.instance()
				.setSourcePath(thePathAndRef.getPath())
				.setSourceResource(theEntity)
				.setTargetResourceType(targetResourceType)
				.setTargetResourcePid(targetResourcePid)
				.setTargetResourceId(targetResourceIdPart)
				.setUpdated(theUpdateTime)
				.setTargetResourceVersion(targetVersion)
				.setTargetResourcePartitionablePartitionId(
						targetResource.getPersistentId().getPartitionablePartitionId());

		return forLocalReference(params);
	}

	private RequestPartitionId determineResolverPartitionId(@Nonnull RequestPartitionId theRequestPartitionId) {
		RequestPartitionId targetRequestPartitionId = theRequestPartitionId;
		if (myPartitionSettings.isPartitioningEnabled()
				&& myPartitionSettings.getAllowReferencesAcrossPartitions() == ALLOWED_UNQUALIFIED) {
			targetRequestPartitionId = RequestPartitionId.allPartitions();
		}
		return targetRequestPartitionId;
	}

	private void populateResourceTable(
			Collection theParams, ResourceTable theResourceTable) {
		for (BaseResourceIndexedSearchParam next : theParams) {
			if (next.getResourcePid() == null) {
				next.setResource(theResourceTable);
			}
		}
	}

	private void populateResourceTableForComboParams(
			Collection theParams, ResourceTable theResourceTable) {
		for (IResourceIndexComboSearchParameter next : theParams) {
			if (next.getResource() == null) {
				next.setResource(theResourceTable);
				if (next instanceof BasePartitionable) {
					((BasePartitionable) next).setPartitionId(theResourceTable.getPartitionId());
				}
			}
		}
	}

	@VisibleForTesting
	void setInterceptorBroadcasterForUnitTest(IInterceptorBroadcaster theInterceptorBroadcaster) {
		myInterceptorBroadcaster = theInterceptorBroadcaster;
	}

	@Nonnull
	public List extractParamValuesAsStrings(
			RuntimeSearchParam theActiveSearchParam, IBaseResource theResource) {
		return mySearchParamExtractor.extractParamValuesAsStrings(theActiveSearchParam, theResource);
	}

	public void extractSearchParamComboUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
		String resourceType = theEntity.getResourceType();
		Set comboUniques =
				mySearchParamExtractor.extractSearchParamComboUnique(resourceType, theParams);
		theParams.myComboStringUniques.addAll(comboUniques);
		populateResourceTableForComboParams(theParams.myComboStringUniques, theEntity);
	}

	public void extractSearchParamComboNonUnique(ResourceTable theEntity, ResourceIndexedSearchParams theParams) {
		String resourceType = theEntity.getResourceType();
		Set comboNonUniques =
				mySearchParamExtractor.extractSearchParamComboNonUnique(resourceType, theParams);
		theParams.myComboTokenNonUnique.addAll(comboNonUniques);
		populateResourceTableForComboParams(theParams.myComboTokenNonUnique, theEntity);
	}

	/**
	 * This interface is used by {@link #extractSearchIndexParametersForTargetResources(RequestDetails, ResourceIndexedSearchParams, ResourceTable, Collection, IChainedSearchParameterExtractionStrategy, ISearchParamExtractor.SearchParamSet, boolean, boolean)}
	 * in order to use that method for extracting chained search parameter indexes both
	 * from contained resources and from uplifted refchains.
	 */
	private interface IChainedSearchParameterExtractionStrategy {

		/**
		 * Which search parameters should be indexed for the resource target
		 * at the given path. In other words if thePathAndRef contains
		 * "Patient/123", then we could return a filter that only lets the
		 * "name" and "gender" search params through  if we only want those
		 * two parameters to be indexed for the resolved Patient resource
		 * with that ID.
		 */
		@Nonnull
		ISearchParamExtractor.ISearchParamFilter getSearchParamFilter(@Nonnull PathAndRef thePathAndRef);

		/**
		 * Actually fetch the resource at the given path, or return
		 * {@literal null} if none can be found.
		 */
		@Nullable
		IBaseResource fetchResourceAtPath(@Nonnull PathAndRef thePathAndRef);
	}

	static void handleWarnings(
			RequestDetails theRequestDetails,
			IInterceptorBroadcaster theInterceptorBroadcaster,
			ISearchParamExtractor.SearchParamSet theSearchParamSet) {
		if (theSearchParamSet.getWarnings().isEmpty()) {
			return;
		}

		// If extraction generated any warnings, broadcast an error
		if (CompositeInterceptorBroadcaster.hasHooks(
				Pointcut.JPA_PERFTRACE_WARNING, theInterceptorBroadcaster, theRequestDetails)) {
			for (String next : theSearchParamSet.getWarnings()) {
				StorageProcessingMessage messageHolder = new StorageProcessingMessage();
				messageHolder.setMessage(next);
				HookParams params = new HookParams()
						.add(RequestDetails.class, theRequestDetails)
						.addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
						.add(StorageProcessingMessage.class, messageHolder);
				CompositeInterceptorBroadcaster.doCallHooks(
						theInterceptorBroadcaster, theRequestDetails, Pointcut.JPA_PERFTRACE_WARNING, params);
			}
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy