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