001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.rest.server.interceptor.partition;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.api.Hook;
024import ca.uhn.fhir.interceptor.api.Interceptor;
025import ca.uhn.fhir.interceptor.api.Pointcut;
026import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
027import ca.uhn.fhir.interceptor.model.RequestPartitionId;
028import ca.uhn.fhir.jpa.model.config.PartitionSettings;
029import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
030import ca.uhn.fhir.rest.api.server.RequestDetails;
031import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
032import ca.uhn.fhir.rest.param.ReferenceParam;
033import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
034import ca.uhn.fhir.rest.server.provider.ProviderConstants;
035import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
036import jakarta.annotation.Nonnull;
037import org.springframework.beans.factory.annotation.Autowired;
038
039import java.util.Collection;
040
041import static org.apache.commons.lang3.StringUtils.isBlank;
042
043/**
044 * This interceptor uses the request tenant ID (as supplied to the server using
045 * {@link ca.uhn.fhir.rest.server.RestfulServer#setTenantIdentificationStrategy(ITenantIdentificationStrategy)})
046 * to indicate the partition ID. With this interceptor registered, The server treats the tenant name
047 * supplied by the {@link ITenantIdentificationStrategy tenant identification strategy} as a partition name.
048 * <p>
049 * Partition names (aka tenant IDs) must be registered in advance using the partition management operations.
050 * </p>
051 *
052 * @since 5.0.0
053 */
054@Interceptor
055public class RequestTenantPartitionInterceptor {
056
057        @Autowired
058        private PartitionSettings myPartitionSettings;
059
060        public void setPartitionSettings(PartitionSettings thePartitionSettings) {
061                myPartitionSettings = thePartitionSettings;
062        }
063
064        @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_READ)
065        public RequestPartitionId partitionIdentifyRead(
066                        RequestDetails theRequestDetails, ReadPartitionIdRequestDetails theReadDetails) {
067                /*
068                 * If we're configured to allow cross-partition references, and we're performing a search with references,
069                 * we'll switch the search to being performed across all partitions.
070                 *
071                 * A potential future enhancement could be to look at the actual references and figure out which tenants they
072                 * refer to. We don't currently allow the tenant to actually be stored in the reference currently, so this
073                 * might or might not be a good idea. More thought is needed.
074                 */
075                if (myPartitionSettings.isAllowUnqualifiedCrossPartitionReference()) {
076                        if (theRequestDetails.getRestOperationType() == RestOperationTypeEnum.SEARCH_TYPE
077                                        && theReadDetails.getSearchParams() != null) {
078                                boolean haveReferences = theReadDetails.getSearchParams().entrySet().stream()
079                                                .flatMap(t -> t.getValue().stream())
080                                                .flatMap(Collection::stream)
081                                                .filter(t -> t instanceof ReferenceParam)
082                                                .map(t -> (ReferenceParam) t)
083                                                .anyMatch(t -> t.getChain() == null);
084                                if (haveReferences) {
085                                        return RequestPartitionId.allPartitions();
086                                }
087                        }
088                }
089
090                return extractPartitionIdFromRequest(theRequestDetails);
091        }
092
093        @Hook(Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE)
094        public RequestPartitionId partitionIdentifyCreate(RequestDetails theRequestDetails) {
095                return extractPartitionIdFromRequest(theRequestDetails);
096        }
097
098        @Nonnull
099        protected RequestPartitionId extractPartitionIdFromRequest(RequestDetails theRequestDetails) {
100
101                // We will use the tenant ID that came from the request as the partition name
102                String tenantId = theRequestDetails.getTenantId();
103                if (isBlank(tenantId)) {
104                        // this branch is no-op happen when "partitioning.tenant_identification_strategy" is set to URL_BASED
105                        if (theRequestDetails instanceof SystemRequestDetails requestDetails) {
106                                if (requestDetails.getRequestPartitionId() != null) {
107                                        return requestDetails.getRequestPartitionId();
108                                }
109                                return RequestPartitionId.defaultPartition(myPartitionSettings);
110                        }
111                        throw new InternalErrorException(Msg.code(343) + "No partition ID has been specified");
112                }
113
114                // for REQUEST_TENANT partition selection mode, allPartitions is supported when URL includes _ALL as the tenant
115                // else if no tenant is provided in the URL, DEFAULT will be used as per UrlBaseTenantIdentificationStrategy
116                if (tenantId.equals(ProviderConstants.ALL_PARTITIONS_TENANT_NAME)) {
117                        return RequestPartitionId.allPartitions();
118                }
119                return RequestPartitionId.fromPartitionName(tenantId);
120        }
121}