001/*-
002 * #%L
003 * HAPI FHIR Storage api
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.jpa.interceptor;
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.model.RequestPartitionId;
026import ca.uhn.fhir.rest.api.server.RequestDetails;
027import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
028
029import java.util.ArrayList;
030import java.util.List;
031
032import static ca.uhn.fhir.interceptor.api.Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE;
033import static ca.uhn.fhir.interceptor.api.Pointcut.STORAGE_PARTITION_IDENTIFY_READ;
034import static ca.uhn.fhir.rest.server.provider.ProviderConstants.ALL_PARTITIONS_TENANT_NAME;
035import static ca.uhn.fhir.rest.server.provider.ProviderConstants.DEFAULT_PARTITION_NAME;
036import static org.apache.commons.lang3.StringUtils.isBlank;
037
038/**
039 * This is an interceptor to identify the partition ID from a request header.
040 * It reads the value of the X-Request-Partition-IDs header, which is expected to be a comma separated partition ids.
041 * For the read operations it uses all the partitions specified in the header.
042 * The create operations it uses the first partition ID from the header.
043 *
044 * The tests for the functionality of this interceptor can be found in the
045 * ca.uhn.fhir.jpa.interceptor.RequestHeaderPartitionTest class.
046 */
047@Interceptor
048public class RequestHeaderPartitionInterceptor {
049
050        public static final String PARTITIONS_HEADER = "X-Request-Partition-IDs";
051
052        /**
053         * This method is called to identify the partition ID for create operations.
054         * It reads the value of the X-Request-Partition-IDs header, and parses and returns the first partition ID
055         * from the header value.
056         */
057        @Hook(STORAGE_PARTITION_IDENTIFY_CREATE)
058        public RequestPartitionId identifyPartitionForCreate(RequestDetails theRequestDetails) {
059                String partitionHeader = getPartitionHeaderOrThrowIfBlank(theRequestDetails);
060                return parseRequestPartitionIdsFromCommaSeparatedString(partitionHeader, true);
061        }
062
063        /**
064         * This method is called to identify the partition ID for read operations.
065         * Parses all the partition IDs from the header into a RequestPartitionId object.
066         */
067        @Hook(STORAGE_PARTITION_IDENTIFY_READ)
068        public RequestPartitionId identifyPartitionForRead(RequestDetails theRequestDetails) {
069                String partitionHeader = getPartitionHeaderOrThrowIfBlank(theRequestDetails);
070                return parseRequestPartitionIdsFromCommaSeparatedString(partitionHeader, false);
071        }
072
073        private String getPartitionHeaderOrThrowIfBlank(RequestDetails theRequestDetails) {
074                String partitionHeader = theRequestDetails.getHeader(PARTITIONS_HEADER);
075                if (isBlank(partitionHeader)) {
076                        String msg = String.format(
077                                        "%s header is missing or blank, it is required to identify the storage partition",
078                                        PARTITIONS_HEADER);
079                        throw new InvalidRequestException(Msg.code(2642) + msg);
080                }
081                return partitionHeader;
082        }
083
084        private RequestPartitionId parseRequestPartitionIdsFromCommaSeparatedString(
085                        String thePartitionIds, boolean theIncludeOnlyTheFirst) {
086                String[] partitionIdStrings = thePartitionIds.split(",");
087                List<Integer> partitionIds = new ArrayList<>();
088
089                for (String partitionIdString : partitionIdStrings) {
090
091                        String trimmedPartitionId = partitionIdString.trim();
092
093                        if (trimmedPartitionId.equals(ALL_PARTITIONS_TENANT_NAME)) {
094                                return RequestPartitionId.allPartitions();
095                        }
096
097                        if (trimmedPartitionId.equals(DEFAULT_PARTITION_NAME)) {
098                                partitionIds.add(RequestPartitionId.defaultPartition().getFirstPartitionIdOrNull());
099                        } else {
100                                try {
101                                        int partitionId = Integer.parseInt(trimmedPartitionId);
102                                        partitionIds.add(partitionId);
103                                } catch (NumberFormatException e) {
104                                        String msg = String.format(
105                                                        "Invalid partition ID: '%s' provided in header: %s", trimmedPartitionId, PARTITIONS_HEADER);
106                                        throw new InvalidRequestException(Msg.code(2643) + msg);
107                                }
108                        }
109
110                        // return early if we only need the first partition ID
111                        if (theIncludeOnlyTheFirst) {
112                                return RequestPartitionId.fromPartitionIds(partitionIds);
113                        }
114                }
115
116                if (partitionIds.isEmpty()) {
117                        // this case happens only when the header contains nothing but commas
118                        // since we already checked for blank header before calling this function
119                        String msg = String.format("No partition IDs provided in header: %s", PARTITIONS_HEADER);
120                        throw new InvalidRequestException(Msg.code(2645) + msg);
121                }
122
123                return RequestPartitionId.fromPartitionIds(partitionIds);
124        }
125}