
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}