001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 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.partition; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.i18n.Msg; 025import ca.uhn.fhir.interceptor.api.HookParams; 026import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; 029import ca.uhn.fhir.interceptor.model.RequestPartitionId; 030import ca.uhn.fhir.jpa.model.config.PartitionSettings; 031import ca.uhn.fhir.jpa.model.util.JpaConstants; 032import ca.uhn.fhir.rest.api.server.RequestDetails; 033import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 034import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 035import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 036import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 037import jakarta.annotation.Nonnull; 038import jakarta.annotation.Nullable; 039import org.apache.commons.lang3.StringUtils; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.springframework.beans.factory.annotation.Autowired; 042 043import java.util.HashSet; 044import java.util.List; 045import java.util.Set; 046import java.util.stream.Collectors; 047 048import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.doCallHooks; 049import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.doCallHooksAndReturnObject; 050import static ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster.hasHooks; 051 052public abstract class BaseRequestPartitionHelperSvc implements IRequestPartitionHelperSvc { 053 054 private final HashSet<Object> myNonPartitionableResourceNames; 055 056 @Autowired 057 protected FhirContext myFhirContext; 058 059 @Autowired 060 private IInterceptorBroadcaster myInterceptorBroadcaster; 061 062 @Autowired 063 private PartitionSettings myPartitionSettings; 064 065 public BaseRequestPartitionHelperSvc() { 066 myNonPartitionableResourceNames = new HashSet<>(); 067 068 // Infrastructure 069 myNonPartitionableResourceNames.add("SearchParameter"); 070 071 // Validation and Conformance 072 myNonPartitionableResourceNames.add("StructureDefinition"); 073 myNonPartitionableResourceNames.add("Questionnaire"); 074 myNonPartitionableResourceNames.add("CapabilityStatement"); 075 myNonPartitionableResourceNames.add("CompartmentDefinition"); 076 myNonPartitionableResourceNames.add("OperationDefinition"); 077 078 myNonPartitionableResourceNames.add("Library"); 079 080 // Terminology 081 myNonPartitionableResourceNames.add("ConceptMap"); 082 myNonPartitionableResourceNames.add("CodeSystem"); 083 myNonPartitionableResourceNames.add("ValueSet"); 084 myNonPartitionableResourceNames.add("NamingSystem"); 085 myNonPartitionableResourceNames.add("StructureMap"); 086 } 087 088 /** 089 * Invoke the {@link Pointcut#STORAGE_PARTITION_IDENTIFY_READ} interceptor pointcut to determine the tenant for a read request. 090 * <p> 091 * If no interceptors are registered with a hook for {@link Pointcut#STORAGE_PARTITION_IDENTIFY_READ}, return 092 * {@link RequestPartitionId#allPartitions()} instead. 093 */ 094 @Nonnull 095 @Override 096 public RequestPartitionId determineReadPartitionForRequest( 097 @Nullable RequestDetails theRequest, @Nonnull ReadPartitionIdRequestDetails theDetails) { 098 RequestPartitionId requestPartitionId; 099 100 String resourceType = theDetails.getResourceType(); 101 boolean nonPartitionableResource = !isResourcePartitionable(resourceType); 102 if (myPartitionSettings.isPartitioningEnabled()) { 103 104 RequestDetails requestDetails = theRequest; 105 // TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through 106 // SystemRequestDetails instead. 107 if (requestDetails == null) { 108 requestDetails = new SystemRequestDetails(); 109 } 110 111 // Handle system requests 112 if (requestDetails instanceof SystemRequestDetails 113 && systemRequestHasExplicitPartition((SystemRequestDetails) requestDetails) 114 && !nonPartitionableResource) { 115 requestPartitionId = 116 getSystemRequestPartitionId((SystemRequestDetails) requestDetails, nonPartitionableResource); 117 } else if ((requestDetails instanceof SystemRequestDetails) && nonPartitionableResource) { 118 return RequestPartitionId.fromPartitionId(myPartitionSettings.getDefaultPartitionId()); 119 } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, requestDetails)) { 120 // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY 121 HookParams params = new HookParams() 122 .add(RequestDetails.class, requestDetails) 123 .addIfMatchesType(ServletRequestDetails.class, requestDetails); 124 requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( 125 myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); 126 } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, requestDetails)) { 127 // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ 128 HookParams params = new HookParams() 129 .add(RequestDetails.class, requestDetails) 130 .addIfMatchesType(ServletRequestDetails.class, requestDetails) 131 .add(ReadPartitionIdRequestDetails.class, theDetails); 132 requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( 133 myInterceptorBroadcaster, requestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_READ, params); 134 } else { 135 requestPartitionId = null; 136 } 137 138 validateRequestPartitionNotNull(requestPartitionId, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); 139 140 return validateNormalizeAndNotifyHooksForRead(requestPartitionId, requestDetails, resourceType); 141 } 142 143 return RequestPartitionId.allPartitions(); 144 } 145 146 @Override 147 public RequestPartitionId determineGenericPartitionForRequest(RequestDetails theRequestDetails) { 148 RequestPartitionId retVal = null; 149 150 if (myPartitionSettings.isPartitioningEnabled()) { 151 if (theRequestDetails instanceof SystemRequestDetails) { 152 SystemRequestDetails systemRequestDetails = (SystemRequestDetails) theRequestDetails; 153 retVal = systemRequestDetails.getRequestPartitionId(); 154 } 155 } 156 157 if (retVal == null) { 158 if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, theRequestDetails)) { 159 // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY 160 HookParams params = new HookParams() 161 .add(RequestDetails.class, theRequestDetails) 162 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails); 163 retVal = (RequestPartitionId) doCallHooksAndReturnObject( 164 myInterceptorBroadcaster, theRequestDetails, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); 165 166 if (retVal != null) { 167 retVal = validateNormalizeAndNotifyHooksForRead(retVal, theRequestDetails, null); 168 } 169 } 170 } 171 172 return retVal; 173 } 174 175 /** 176 * For system requests, read partition from tenant ID if present, otherwise set to DEFAULT. If the resource they are attempting to partition 177 * is non-partitionable scream in the logs and set the partition to DEFAULT. 178 */ 179 private RequestPartitionId getSystemRequestPartitionId( 180 SystemRequestDetails theRequest, boolean theNonPartitionableResource) { 181 RequestPartitionId requestPartitionId; 182 requestPartitionId = getSystemRequestPartitionId(theRequest); 183 if (theNonPartitionableResource && !requestPartitionId.isDefaultPartition()) { 184 throw new InternalErrorException(Msg.code(1315) 185 + "System call is attempting to write a non-partitionable resource to a partition! This is a bug!"); 186 } 187 return requestPartitionId; 188 } 189 190 /** 191 * Determine the partition for a System Call (defined by the fact that the request is of type SystemRequestDetails) 192 * <p> 193 * 1. If the tenant ID is set to the constant for all partitions, return all partitions 194 * 2. If there is a tenant ID set in the request, use it. 195 * 3. Otherwise, return the Default Partition. 196 * 197 * @param theRequest The {@link SystemRequestDetails} 198 * @return the {@link RequestPartitionId} to be used for this request. 199 */ 200 @Nonnull 201 private RequestPartitionId getSystemRequestPartitionId(@Nonnull SystemRequestDetails theRequest) { 202 if (theRequest.getRequestPartitionId() != null) { 203 return theRequest.getRequestPartitionId(); 204 } 205 if (theRequest.getTenantId() != null) { 206 // TODO: JA2 we should not be inferring the partition name from the tenant name 207 return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); 208 } else { 209 return RequestPartitionId.defaultPartition(); 210 } 211 } 212 213 /** 214 * Invoke the {@link Pointcut#STORAGE_PARTITION_IDENTIFY_CREATE} interceptor pointcut to determine the tenant for a create request. 215 */ 216 @Nonnull 217 @Override 218 public RequestPartitionId determineCreatePartitionForRequest( 219 @Nullable RequestDetails theRequest, @Nonnull IBaseResource theResource, @Nonnull String theResourceType) { 220 RequestPartitionId requestPartitionId; 221 222 if (!myPartitionSettings.isPartitioningEnabled()) { 223 return RequestPartitionId.allPartitions(); 224 } 225 226 boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType); 227 228 // TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through 229 // SystemRequestDetails instead. 230 if ((theRequest == null || theRequest instanceof SystemRequestDetails) && nonPartitionableResource) { 231 return RequestPartitionId.defaultPartition(); 232 } 233 234 if (theRequest instanceof SystemRequestDetails 235 && systemRequestHasExplicitPartition((SystemRequestDetails) theRequest)) { 236 requestPartitionId = 237 getSystemRequestPartitionId((SystemRequestDetails) theRequest, nonPartitionableResource); 238 } else { 239 if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, myInterceptorBroadcaster, theRequest)) { 240 // Interceptor call: STORAGE_PARTITION_IDENTIFY_ANY 241 HookParams params = new HookParams() 242 .add(RequestDetails.class, theRequest) 243 .addIfMatchesType(ServletRequestDetails.class, theRequest); 244 requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( 245 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_ANY, params); 246 } else { 247 // This is an external Request (e.g. ServletRequestDetails) so we want to figure out the partition 248 // via interceptor. 249 // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE 250 HookParams params = new HookParams() 251 .add(IBaseResource.class, theResource) 252 .add(RequestDetails.class, theRequest) 253 .addIfMatchesType(ServletRequestDetails.class, theRequest); 254 requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject( 255 myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); 256 } 257 258 // If the interceptors haven't selected a partition, and its a non-partitionable resource anyhow, send 259 // to DEFAULT 260 if (nonPartitionableResource && requestPartitionId == null) { 261 requestPartitionId = RequestPartitionId.defaultPartition(); 262 } 263 } 264 265 String resourceName = myFhirContext.getResourceType(theResource); 266 validateSinglePartitionForCreate(requestPartitionId, resourceName, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE); 267 268 return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest, theResourceType); 269 } 270 271 private boolean systemRequestHasExplicitPartition(@Nonnull SystemRequestDetails theRequest) { 272 return theRequest.getRequestPartitionId() != null || theRequest.getTenantId() != null; 273 } 274 275 @Nonnull 276 @Override 277 public Set<Integer> toReadPartitions(@Nonnull RequestPartitionId theRequestPartitionId) { 278 return theRequestPartitionId.getPartitionIds().stream() 279 .map(t -> t == null ? myPartitionSettings.getDefaultPartitionId() : t) 280 .collect(Collectors.toSet()); 281 } 282 283 /** 284 * If the partition only has a name but not an ID, this method resolves the ID. 285 * <p> 286 * If the partition has an ID but not a name, the name is resolved. 287 * <p> 288 * If the partition has both, they are validated to ensure that they correspond. 289 */ 290 @Nonnull 291 private RequestPartitionId validateNormalizeAndNotifyHooksForRead( 292 @Nonnull RequestPartitionId theRequestPartitionId, 293 RequestDetails theRequest, 294 @Nullable String theResourceType) { 295 RequestPartitionId retVal = theRequestPartitionId; 296 297 if (!myPartitionSettings.isUnnamedPartitionMode()) { 298 if (retVal.getPartitionNames() != null) { 299 retVal = validateAndNormalizePartitionNames(retVal); 300 } else if (retVal.hasPartitionIds()) { 301 retVal = validateAndNormalizePartitionIds(retVal); 302 } 303 } 304 305 // Note: It's still possible that the partition only has a date but no name/id 306 307 if (StringUtils.isNotBlank(theResourceType)) { 308 validateHasPartitionPermissions(theRequest, theResourceType, retVal); 309 } 310 311 return retVal; 312 } 313 314 @Override 315 public void validateHasPartitionPermissions( 316 @Nonnull RequestDetails theRequest, String theResourceType, RequestPartitionId theRequestPartitionId) { 317 if (myInterceptorBroadcaster.hasHooks(Pointcut.STORAGE_PARTITION_SELECTED)) { 318 RuntimeResourceDefinition runtimeResourceDefinition = null; 319 if (theResourceType != null) { 320 runtimeResourceDefinition = myFhirContext.getResourceDefinition(theResourceType); 321 } 322 HookParams params = new HookParams() 323 .add(RequestPartitionId.class, theRequestPartitionId) 324 .add(RequestDetails.class, theRequest) 325 .addIfMatchesType(ServletRequestDetails.class, theRequest) 326 .add(RuntimeResourceDefinition.class, runtimeResourceDefinition); 327 doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_SELECTED, params); 328 } 329 } 330 331 @Override 332 public boolean isResourcePartitionable(String theResourceType) { 333 return !myNonPartitionableResourceNames.contains(theResourceType); 334 } 335 336 protected abstract RequestPartitionId validateAndNormalizePartitionIds(RequestPartitionId theRequestPartitionId); 337 338 protected abstract RequestPartitionId validateAndNormalizePartitionNames(RequestPartitionId theRequestPartitionId); 339 340 private void validateSinglePartitionForCreate( 341 RequestPartitionId theRequestPartitionId, @Nonnull String theResourceName, Pointcut thePointcut) { 342 validateRequestPartitionNotNull(theRequestPartitionId, thePointcut); 343 344 if (theRequestPartitionId.hasPartitionIds()) { 345 validateSinglePartitionIdOrNameForCreate(theRequestPartitionId.getPartitionIds()); 346 } 347 validateSinglePartitionIdOrNameForCreate(theRequestPartitionId.getPartitionNames()); 348 349 // Make sure we're not using one of the conformance resources in a non-default partition 350 if ((theRequestPartitionId.hasPartitionIds() 351 && !theRequestPartitionId.getPartitionIds().contains(null)) 352 || (theRequestPartitionId.hasPartitionNames() 353 && !theRequestPartitionId.getPartitionNames().contains(JpaConstants.DEFAULT_PARTITION_NAME))) { 354 355 if (!isResourcePartitionable(theResourceName)) { 356 String msg = myFhirContext 357 .getLocalizer() 358 .getMessageSanitized( 359 BaseRequestPartitionHelperSvc.class, 360 "nonDefaultPartitionSelectedForNonPartitionable", 361 theResourceName); 362 throw new UnprocessableEntityException(Msg.code(1318) + msg); 363 } 364 } 365 } 366 367 private void validateRequestPartitionNotNull(RequestPartitionId theRequestPartitionId, Pointcut theThePointcut) { 368 if (theRequestPartitionId == null) { 369 throw new InternalErrorException( 370 Msg.code(1319) + "No interceptor provided a value for pointcut: " + theThePointcut); 371 } 372 } 373 374 private void validateSinglePartitionIdOrNameForCreate(@Nullable List<?> thePartitionIds) { 375 if (thePartitionIds != null && thePartitionIds.size() != 1) { 376 throw new InternalErrorException( 377 Msg.code(1320) + "RequestPartitionId must contain a single partition for create operations, found: " 378 + thePartitionIds); 379 } 380 } 381}