001/*-
002 * #%L
003 * HAPI FHIR - Server Framework
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.messaging;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.IDefaultPartitionSettings;
024import ca.uhn.fhir.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.rest.api.Constants;
026import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
027import jakarta.annotation.Nonnull;
028import jakarta.annotation.Nullable;
029
030import java.util.ArrayList;
031import java.util.List;
032import java.util.Optional;
033
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;
036
037/**
038 * Utility class for parsing and validating partition information from HTTP request headers.
039 * <p>
040 * This class provides methods to convert the X-Request-Partition-IDs header value into
041 * a {@link ca.uhn.fhir.interceptor.model.RequestPartitionId} object that can be used
042 * by the application to determine which partition(s) to operate on.
043 * </p>
044 */
045public final class RequestPartitionHeaderUtil {
046
047        public static final String HTTP_HEADER_SOURCE_NAME = "header: " + Constants.HEADER_X_REQUEST_PARTITION_IDS;
048
049        private RequestPartitionHeaderUtil() {}
050
051        /**
052         * Parses the X-Request-Partition-IDs header value and converts it to a {@link RequestPartitionId} object.
053         *
054         * @param theSourceName               The name of the source of the value, used for error messages
055         * @param thePartitionHeaderValue     The value of the X-Request-Partition-IDs header, may be null
056         * @param theDefaultPartitionSettings Settings that provide the default partition ID
057         * @return A {@link RequestPartitionId} object representing the partition(s) specified in the header, or null if the header is null
058         * @throws InvalidRequestException If the header value is invalid
059         */
060        @Nullable
061        public static RequestPartitionId fromHeader(
062                        @Nonnull String theSourceName,
063                        @Nullable String thePartitionHeaderValue,
064                        @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
065                return fromHeader(theSourceName, thePartitionHeaderValue, false, theDefaultPartitionSettings);
066        }
067
068        @Nullable
069        public static RequestPartitionId fromHeader(
070                        @Nullable String thePartitionHeaderValue, @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
071                return fromHeader(HTTP_HEADER_SOURCE_NAME, thePartitionHeaderValue, false, theDefaultPartitionSettings);
072        }
073
074        /**
075         * Parses the X-Request-Partition-IDs header value and converts it to a {@link RequestPartitionId} object,
076         * including only the first partition ID from the header. This useful when using the RequestPartitionId for
077         * a write operation.
078         *
079         * @param theSourceName               The name of the source of the value, used for error messages
080         * @param thePartitionHeaderValue     The value of the X-Request-Partition-IDs header, may be null
081         * @param theDefaultPartitionSettings Settings that provide the default partition ID
082         * @return A {@link RequestPartitionId} object representing the first partition specified in the header, or null if the header is null
083         * @throws InvalidRequestException If the header value is invalid
084         */
085        @SuppressWarnings("unused")
086        @Nullable
087        public static RequestPartitionId fromHeaderFirstPartitionOnly(
088                        @Nonnull String theSourceName,
089                        @Nullable String thePartitionHeaderValue,
090                        @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
091                return fromHeader(theSourceName, thePartitionHeaderValue, true, theDefaultPartitionSettings);
092        }
093
094        @Nullable
095        public static RequestPartitionId fromHeaderFirstPartitionOnly(
096                        @Nullable String thePartitionHeaderValue, @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
097                return fromHeader(HTTP_HEADER_SOURCE_NAME, thePartitionHeaderValue, true, theDefaultPartitionSettings);
098        }
099
100        /**
101         * Validates the syntax of the X-Request-Partition-IDs header value.
102         * <p>
103         * This method checks if the header value can be successfully parsed into a {@link RequestPartitionId} object.
104         * It does not validate whether the partition IDs actually exist in the system.
105         * </p>
106         *
107         * @param theSourceName           The name of the source of the value, used for error messages
108         * @param thePartitionHeaderValue The value of the X-Request-Partition-IDs header to validate
109         * @throws InvalidRequestException If the header value is invalid
110         */
111        public static void validateHeader(String theSourceName, String thePartitionHeaderValue) {
112                // We're only validating syntax, so it doesn't matter what the default partition id is
113                fromHeader(theSourceName, thePartitionHeaderValue, new IDefaultPartitionSettings() {});
114        }
115
116        public static void validateHeader(String thePartitionHeaderValue) {
117                validateHeader(HTTP_HEADER_SOURCE_NAME, thePartitionHeaderValue);
118        }
119
120        /**
121         * Parses the X-Request-Partition-IDs header value and converts it to a {@link RequestPartitionId} object.
122         * <p>
123         * The header value can be:
124         * <ul>
125         *   <li>A single partition ID (e.g., "123")</li>
126         *   <li>Multiple partition IDs separated by commas (e.g., "123,456")</li>
127         *   <li>The special value "DEFAULT" to indicate the default partition</li>
128         *   <li>The special value "_ALL" to indicate all partitions</li>
129         * </ul>
130         * </p>
131         *
132         * @param theSourceName               The name of the source of the value, used for error messages
133         * @param thePartitionHeaderValue     The value of the X-Request-Partition-IDs header, may be null
134         * @param theIncludeOnlyTheFirst      If true, only the first partition ID in the header will be included in the result
135         * @param theDefaultPartitionSettings Settings that provide the default partition ID
136         * @return A {@link RequestPartitionId} object representing the partition(s) specified in the header, or null if the header is null
137         * @throws InvalidRequestException If the header value is invalid
138         */
139        @Nullable
140        private static RequestPartitionId fromHeader(
141                        @Nonnull String theSourceName,
142                        @Nullable String thePartitionHeaderValue,
143                        boolean theIncludeOnlyTheFirst,
144                        @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
145                if (thePartitionHeaderValue == null) {
146                        return null;
147                }
148                String[] partitionIdStrings = thePartitionHeaderValue.split(",");
149                List<Integer> partitionIds = new ArrayList<>();
150
151                for (String partitionIdString : partitionIdStrings) {
152
153                        String trimmedPartitionId = partitionIdString.trim();
154
155                        if (trimmedPartitionId.equals(ALL_PARTITIONS_TENANT_NAME)) {
156                                return RequestPartitionId.allPartitions();
157                        }
158
159                        @Nullable
160                        Integer partitionId = getPartitionId(theSourceName, theDefaultPartitionSettings, trimmedPartitionId);
161
162                        // return early if we only need the first partition ID
163                        if (theIncludeOnlyTheFirst) {
164                                return RequestPartitionId.fromPartitionId(partitionId);
165                        }
166                        partitionIds.add(partitionId);
167                }
168
169                if (partitionIds.isEmpty()) {
170                        // this case happens only when the header contains nothing but commas
171                        // since we already checked for blank header before calling this function
172                        String msg = String.format("No partition IDs provided in %s", theSourceName);
173                        throw new InvalidRequestException(Msg.code(2645) + msg);
174                }
175
176                return RequestPartitionId.fromPartitionIds(partitionIds);
177        }
178
179        /**
180         * Converts a partition ID string to an Integer.
181         * <p>
182         * If the string is "DEFAULT", returns the default partition ID from the settings.
183         * Otherwise, attempts to parse the string as an integer.
184         * </p>
185         *
186         * @param theSourceName               The name of the source of the value, used for error messages
187         * @param theDefaultPartitionSettings Settings that provide the default partition ID
188         * @param trimmedPartitionId          The partition ID string to convert, already trimmed of whitespace
189         * @return The partition ID as an Integer, or null if the default partition ID is null
190         * @throws InvalidRequestException If the partition ID string is not "DEFAULT" and cannot be parsed as an integer
191         */
192        @Nullable
193        private static Integer getPartitionId(
194                        @Nonnull String theSourceName,
195                        @Nullable IDefaultPartitionSettings theDefaultPartitionSettings,
196                        String trimmedPartitionId) {
197                Integer partitionId;
198
199                if (trimmedPartitionId.equals(DEFAULT_PARTITION_NAME)) {
200                        if (theDefaultPartitionSettings == null) {
201                                throw new InvalidRequestException(Msg.code(2722)
202                                                + "Can only use DEFAULT partitionId in contexts where the default partition ID is defined.");
203                        } else {
204                                partitionId = theDefaultPartitionSettings.getDefaultPartitionId();
205                        }
206                } else {
207                        try {
208                                partitionId = Integer.parseInt(trimmedPartitionId);
209                        } catch (NumberFormatException e) {
210                                String msg =
211                                                String.format("Invalid partition ID: '%s' provided in %s", trimmedPartitionId, theSourceName);
212                                throw new InvalidRequestException(Msg.code(2643) + msg);
213                        }
214                }
215                return partitionId;
216        }
217
218        /**
219         * Sets the partition ID on a message payload from the X-Request-Partition-IDs header if it's not already set.
220         *
221         * @param <T>                         The type of the payload, which must have methods for getting and setting a partition ID
222         * @param theMessage                  The message containing the payload and headers
223         * @param theDefaultPartitionSettings Settings that provide the default partition ID
224         */
225        public static <T> void setRequestPartitionIdFromHeaderIfNotAlreadySet(
226                        @Nonnull IMessage<T> theMessage, @Nullable IDefaultPartitionSettings theDefaultPartitionSettings) {
227                if (theMessage.getPayload() instanceof BaseResourceMessage baseResourceMessage) {
228                        if (baseResourceMessage.getPartitionId() != null) {
229                                // TODO KHS suggestion from MB: if partitions are also set in the header, log a warning if they don't
230                                // match.
231                                return;
232                        }
233
234                        Optional<Object> oHeader = theMessage.getHeader(Constants.HEADER_X_REQUEST_PARTITION_IDS);
235                        if (oHeader.isEmpty()) {
236                                return;
237                        }
238
239                        RequestPartitionId headerPartitionId =
240                                        RequestPartitionHeaderUtil.fromHeader((String) oHeader.get(), theDefaultPartitionSettings);
241                        baseResourceMessage.setPartitionId(headerPartitionId);
242                }
243        }
244}