
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}