
001/* 002 * #%L 003 * HAPI FHIR JPA Server 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.dao; 021 022import ca.uhn.fhir.batch2.api.IJobCoordinator; 023import ca.uhn.fhir.batch2.api.IJobPartitionProvider; 024import ca.uhn.fhir.batch2.jobs.reindex.ReindexJobParameters; 025import ca.uhn.fhir.batch2.model.JobInstanceStartRequest; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.RuntimeResourceDefinition; 028import ca.uhn.fhir.i18n.Msg; 029import ca.uhn.fhir.interceptor.api.HookParams; 030import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 031import ca.uhn.fhir.interceptor.api.Pointcut; 032import ca.uhn.fhir.interceptor.executor.InterceptorService; 033import ca.uhn.fhir.interceptor.model.RequestPartitionId; 034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 035import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 036import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 037import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 038import ca.uhn.fhir.jpa.api.dao.ReindexOutcome; 039import ca.uhn.fhir.jpa.api.dao.ReindexParameters; 040import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 041import ca.uhn.fhir.jpa.api.model.DeleteConflictList; 042import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; 043import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 044import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; 045import ca.uhn.fhir.jpa.api.model.LazyDaoMethodOutcome; 046import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 047import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 048import ca.uhn.fhir.jpa.dao.data.IResourceHistoryProvenanceDao; 049import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; 050import ca.uhn.fhir.jpa.delete.DeleteConflictUtil; 051import ca.uhn.fhir.jpa.interceptor.PatientCompartmentEnforcingInterceptor; 052import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; 053import ca.uhn.fhir.jpa.model.dao.JpaPid; 054import ca.uhn.fhir.jpa.model.dao.JpaPidFk; 055import ca.uhn.fhir.jpa.model.entity.BaseHasResource; 056import ca.uhn.fhir.jpa.model.entity.BaseTag; 057import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum; 058import ca.uhn.fhir.jpa.model.entity.IdAndPartitionId; 059import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 060import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum; 061import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity; 062import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable; 063import ca.uhn.fhir.jpa.model.entity.ResourceTable; 064import ca.uhn.fhir.jpa.model.entity.TagDefinition; 065import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 066import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; 067import ca.uhn.fhir.jpa.model.util.JpaConstants; 068import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 069import ca.uhn.fhir.jpa.search.PersistedJpaBundleProvider; 070import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory; 071import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc; 072import ca.uhn.fhir.jpa.search.builder.SearchBuilder; 073import ca.uhn.fhir.jpa.search.builder.StorageInterceptorHooksFacade; 074import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum; 075import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 076import ca.uhn.fhir.jpa.searchparam.ResourceSearch; 077import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 078import ca.uhn.fhir.jpa.update.UpdateParameters; 079import ca.uhn.fhir.jpa.util.MemoryCacheService; 080import ca.uhn.fhir.jpa.util.QueryChunker; 081import ca.uhn.fhir.model.api.IQueryParameterType; 082import ca.uhn.fhir.model.api.StorageResponseCodeEnum; 083import ca.uhn.fhir.model.dstu2.resource.BaseResource; 084import ca.uhn.fhir.model.dstu2.resource.ListResource; 085import ca.uhn.fhir.model.primitive.IdDt; 086import ca.uhn.fhir.rest.api.CacheControlDirective; 087import ca.uhn.fhir.rest.api.Constants; 088import ca.uhn.fhir.rest.api.EncodingEnum; 089import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum; 090import ca.uhn.fhir.rest.api.MethodOutcome; 091import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 092import ca.uhn.fhir.rest.api.SearchContainedModeEnum; 093import ca.uhn.fhir.rest.api.ValidationModeEnum; 094import ca.uhn.fhir.rest.api.server.IBundleProvider; 095import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails; 096import ca.uhn.fhir.rest.api.server.IPreResourceShowDetails; 097import ca.uhn.fhir.rest.api.server.RequestDetails; 098import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails; 099import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails; 100import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 101import ca.uhn.fhir.rest.api.server.storage.IDeleteExpungeJobSubmitter; 102import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 103import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 104import ca.uhn.fhir.rest.param.HasParam; 105import ca.uhn.fhir.rest.param.HistorySearchDateRangeParam; 106import ca.uhn.fhir.rest.server.IPagingProvider; 107import ca.uhn.fhir.rest.server.IRestfulServerDefaults; 108import ca.uhn.fhir.rest.server.RestfulServerUtils; 109import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 110import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 111import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; 112import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; 113import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 114import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 115import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; 116import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 117import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; 118import ca.uhn.fhir.util.ReflectionUtil; 119import ca.uhn.fhir.util.StopWatch; 120import ca.uhn.fhir.util.UrlUtil; 121import ca.uhn.fhir.validation.FhirValidator; 122import ca.uhn.fhir.validation.IInstanceValidatorModule; 123import ca.uhn.fhir.validation.IValidationContext; 124import ca.uhn.fhir.validation.IValidatorModule; 125import ca.uhn.fhir.validation.ValidationOptions; 126import ca.uhn.fhir.validation.ValidationResult; 127import com.google.common.annotations.VisibleForTesting; 128import jakarta.annotation.Nonnull; 129import jakarta.annotation.Nullable; 130import jakarta.annotation.PostConstruct; 131import jakarta.persistence.LockModeType; 132import jakarta.persistence.NoResultException; 133import jakarta.persistence.TypedQuery; 134import jakarta.servlet.http.HttpServletResponse; 135import org.apache.commons.lang3.Validate; 136import org.apache.commons.lang3.function.TriFunction; 137import org.hl7.fhir.instance.model.api.IBaseCoding; 138import org.hl7.fhir.instance.model.api.IBaseMetaType; 139import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; 140import org.hl7.fhir.instance.model.api.IBaseResource; 141import org.hl7.fhir.instance.model.api.IIdType; 142import org.hl7.fhir.instance.model.api.IPrimitiveType; 143import org.hl7.fhir.r4.model.Parameters; 144import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; 145import org.springframework.beans.factory.annotation.Autowired; 146import org.springframework.data.domain.PageRequest; 147import org.springframework.data.domain.Pageable; 148import org.springframework.data.domain.Slice; 149import org.springframework.data.domain.Sort; 150import org.springframework.transaction.PlatformTransactionManager; 151import org.springframework.transaction.annotation.Propagation; 152import org.springframework.transaction.annotation.Transactional; 153import org.springframework.transaction.support.TransactionSynchronization; 154import org.springframework.transaction.support.TransactionSynchronizationManager; 155import org.springframework.transaction.support.TransactionTemplate; 156 157import java.io.IOException; 158import java.util.ArrayList; 159import java.util.Collection; 160import java.util.Date; 161import java.util.HashSet; 162import java.util.List; 163import java.util.Map; 164import java.util.Objects; 165import java.util.Optional; 166import java.util.Set; 167import java.util.UUID; 168import java.util.concurrent.Callable; 169import java.util.function.Supplier; 170import java.util.stream.Collectors; 171import java.util.stream.Stream; 172 173import static ca.uhn.fhir.batch2.jobs.reindex.ReindexUtils.JOB_REINDEX; 174import static ca.uhn.fhir.jpa.model.util.JpaConstants.SINGLE_RESULT; 175import static java.util.Objects.isNull; 176import static org.apache.commons.lang3.StringUtils.isBlank; 177import static org.apache.commons.lang3.StringUtils.isNotBlank; 178 179public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> extends BaseHapiFhirDao<T> 180 implements IFhirResourceDao<T> { 181 182 public static final String BASE_RESOURCE_NAME = "resource"; 183 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirResourceDao.class); 184 185 @Autowired 186 protected IInterceptorBroadcaster myInterceptorBroadcaster; 187 188 @Autowired 189 protected PlatformTransactionManager myPlatformTransactionManager; 190 191 @Autowired(required = false) 192 protected IFulltextSearchSvc mySearchDao; 193 194 @Autowired 195 protected HapiTransactionService myTransactionService; 196 197 @Autowired 198 private MatchResourceUrlService<JpaPid> myMatchResourceUrlService; 199 200 @Autowired 201 private SearchBuilderFactory<JpaPid> mySearchBuilderFactory; 202 203 @Autowired 204 private DaoRegistry myDaoRegistry; 205 206 @Autowired 207 private IRequestPartitionHelperSvc myRequestPartitionHelperService; 208 209 @Autowired 210 private IJobPartitionProvider myJobPartitionProvider; 211 212 @Autowired 213 private MatchUrlService myMatchUrlService; 214 215 @Autowired 216 private IDeleteExpungeJobSubmitter myDeleteExpungeJobSubmitter; 217 218 @Autowired 219 private IJobCoordinator myJobCoordinator; 220 221 @Autowired 222 private IResourceHistoryProvenanceDao myResourceHistoryProvenanceDao; 223 224 private IInstanceValidatorModule myInstanceValidator; 225 private String myResourceName; 226 private Class<T> myResourceType; 227 228 @Autowired 229 private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory; 230 231 @Autowired 232 private MemoryCacheService myMemoryCacheService; 233 234 private TransactionTemplate myTxTemplate; 235 236 @Autowired 237 private ResourceSearchUrlSvc myResourceSearchUrlSvc; 238 239 @Autowired 240 private IFhirSystemDao<?, ?> mySystemDao; 241 242 private StorageInterceptorHooksFacade myStorageInterceptorHooks; 243 244 @Nullable 245 public static <T extends IBaseResource> T invokeStoragePreShowResources( 246 IInterceptorBroadcaster theInterceptorBroadcaster, RequestDetails theRequest, T retVal) { 247 IInterceptorBroadcaster compositeBroadcaster = 248 CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest); 249 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PRESHOW_RESOURCES)) { 250 SimplePreResourceShowDetails showDetails = new SimplePreResourceShowDetails(retVal); 251 HookParams params = new HookParams() 252 .add(IPreResourceShowDetails.class, showDetails) 253 .add(RequestDetails.class, theRequest) 254 .addIfMatchesType(ServletRequestDetails.class, theRequest); 255 compositeBroadcaster.callHooks(Pointcut.STORAGE_PRESHOW_RESOURCES, params); 256 //noinspection unchecked 257 retVal = (T) showDetails.getResource( 258 0); // TODO GGG/JA : getting resource 0 is interesting. We apparently allow null values in the list. 259 // Should we? 260 return retVal; 261 } else { 262 return retVal; 263 } 264 } 265 266 public static void invokeStoragePreAccessResources( 267 IInterceptorBroadcaster theInterceptorBroadcaster, 268 RequestDetails theRequest, 269 IIdType theId, 270 IBaseResource theResource) { 271 IInterceptorBroadcaster compositeBroadcaster = 272 CompositeInterceptorBroadcaster.newCompositeBroadcaster(theInterceptorBroadcaster, theRequest); 273 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 274 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource); 275 HookParams params = new HookParams() 276 .add(IPreResourceAccessDetails.class, accessDetails) 277 .add(RequestDetails.class, theRequest) 278 .addIfMatchesType(ServletRequestDetails.class, theRequest); 279 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 280 if (accessDetails.isDontReturnResourceAtIndex(0)) { 281 throw new ResourceNotFoundException(Msg.code(1995) + "Resource " + theId + " is not known"); 282 } 283 } 284 } 285 286 @Override 287 protected HapiTransactionService getTransactionService() { 288 return myTransactionService; 289 } 290 291 @VisibleForTesting 292 public void setTransactionService(HapiTransactionService theTransactionService) { 293 myTransactionService = theTransactionService; 294 } 295 296 @Override 297 protected MatchResourceUrlService getMatchResourceUrlService() { 298 return myMatchResourceUrlService; 299 } 300 301 @Override 302 protected IStorageResourceParser getStorageResourceParser() { 303 return myJpaStorageResourceParser; 304 } 305 306 @Override 307 protected IDeleteExpungeJobSubmitter getDeleteExpungeJobSubmitter() { 308 return myDeleteExpungeJobSubmitter; 309 } 310 311 @Override 312 protected IRequestPartitionHelperSvc getRequestPartitionHelperService() { 313 return myRequestPartitionHelperService; 314 } 315 316 @Override 317 protected MatchUrlService getMatchUrlService() { 318 return myMatchUrlService; 319 } 320 321 /** 322 * @deprecated Use {@link #create(T, RequestDetails)} instead 323 */ 324 @Override 325 public DaoMethodOutcome create(final T theResource) { 326 return create(theResource, null, true, null, new TransactionDetails()); 327 } 328 329 @Override 330 public DaoMethodOutcome create(final T theResource, RequestDetails theRequestDetails) { 331 return create(theResource, null, true, theRequestDetails, new TransactionDetails()); 332 } 333 334 /** 335 * @deprecated Use {@link #create(T, String, RequestDetails)} instead 336 */ 337 @Override 338 public DaoMethodOutcome create(final T theResource, String theIfNoneExist) { 339 return create(theResource, theIfNoneExist, null); 340 } 341 342 @Override 343 public DaoMethodOutcome create(final T theResource, String theIfNoneExist, RequestDetails theRequestDetails) { 344 return create(theResource, theIfNoneExist, true, theRequestDetails, new TransactionDetails()); 345 } 346 347 @Override 348 public DaoMethodOutcome create( 349 T theResource, 350 String theIfNoneExist, 351 boolean thePerformIndexing, 352 RequestDetails theRequestDetails, 353 @Nonnull TransactionDetails theTransactionDetails) { 354 355 validateResourceIsNotNullForClientRequest(theResource); 356 357 assignServerAssignedIdFromUserDataIfRequired(theResource); 358 assignServerAssignedUuidIfRequired(theResource); 359 360 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest( 361 theRequestDetails, theResource, getResourceName()); 362 363 return myTransactionService 364 .withRequest(theRequestDetails) 365 .withTransactionDetails(theTransactionDetails) 366 .withRequestPartitionId(requestPartitionId) 367 .execute(tx -> doCreateForPost( 368 theResource, 369 theIfNoneExist, 370 thePerformIndexing, 371 theTransactionDetails, 372 theRequestDetails, 373 requestPartitionId)); 374 } 375 376 @VisibleForTesting 377 public void setRequestPartitionHelperService(IRequestPartitionHelperSvc theRequestPartitionHelperService) { 378 myRequestPartitionHelperService = theRequestPartitionHelperService; 379 } 380 381 /** 382 * Called for FHIR create (POST) operations 383 */ 384 protected DaoMethodOutcome doCreateForPost( 385 T theResource, 386 String theIfNoneExist, 387 boolean thePerformIndexing, 388 TransactionDetails theTransactionDetails, 389 RequestDetails theRequestDetails, 390 RequestPartitionId theRequestPartitionId) { 391 validateResourceIsNotNullForClientRequest(theResource); 392 393 if (isNotBlank(theResource.getIdElement().getIdPart()) && !isResourceIdServerAssigned(theResource)) { 394 if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 395 String message = getMessageSanitized( 396 "failedToCreateWithClientAssignedId", 397 theResource.getIdElement().getIdPart()); 398 throw new InvalidRequestException( 399 Msg.code(957) + message, createErrorOperationOutcome(message, "processing")); 400 } else { 401 // As of DSTU3, ID and version in the body should be ignored for a create/update 402 theResource.setId(""); 403 } 404 } 405 406 assignServerAssignedUuidIfRequired(theResource); 407 408 return doCreateForPostOrPut( 409 theRequestDetails, 410 theResource, 411 theIfNoneExist, 412 true, 413 thePerformIndexing, 414 theRequestPartitionId, 415 RestOperationTypeEnum.CREATE, 416 theTransactionDetails); 417 } 418 419 /** 420 * Tests whether a resource is non-null, and throws a {@link InvalidRequestException} 421 * with a message explaining that the body must not be missing on a create/update 422 * request. 423 */ 424 private void validateResourceIsNotNullForClientRequest(T theResource) { 425 if (theResource == null) { 426 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody"); 427 throw new InvalidRequestException(Msg.code(956) + msg); 428 } 429 } 430 431 /** 432 * If the resource being created has a UserData value with key 433 * {@link JpaConstants#RESOURCE_ID_SERVER_ASSIGNED_VALUE}, this value will be 434 * used as the resource's server-assigned FHIR ID. No duplication checking is 435 * performed on the ID, so this feature should only be used if you are certain 436 * that the ID is unique and not previously used for any other resource. 437 * This feature is not exposed to the FHIR REST client API at all, and is 438 * only intended for interceptor code that carefully considers the implications 439 * of its use. 440 * 441 * @since 8.6.0 442 */ 443 private void assignServerAssignedIdFromUserDataIfRequired(T theResource) { 444 String serverAssignedIdFromMetadata = 445 (String) theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED_VALUE); 446 if (serverAssignedIdFromMetadata != null) { 447 theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); 448 theResource.setId(serverAssignedIdFromMetadata); 449 verifyResourceIdIsValid(theResource); 450 } 451 } 452 453 private void assignServerAssignedUuidIfRequired(T theResource) { 454 if (getStorageSettings().getResourceServerIdStrategy() == JpaStorageSettings.IdStrategyEnum.UUID) { 455 if (!isResourceIdServerAssigned(theResource)) { 456 theResource.setId(UUID.randomUUID().toString()); 457 theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); 458 } 459 } 460 } 461 462 /** 463 * Called both for FHIR create (POST) operations (via {@link #doCreateForPost(IBaseResource, String, boolean, TransactionDetails, RequestDetails, RequestPartitionId)} 464 * as well as for FHIR update (PUT) where we're doing a create-with-client-assigned-ID (via {@link #doUpdate(IBaseResource, String, boolean, boolean, RequestDetails, TransactionDetails, RequestPartitionId)}. 465 */ 466 private DaoMethodOutcome doCreateForPostOrPut( 467 RequestDetails theRequest, 468 T theResource, 469 String theMatchUrl, 470 boolean theProcessMatchUrl, 471 boolean thePerformIndexing, 472 RequestPartitionId theRequestPartitionId, 473 RestOperationTypeEnum theOperationType, 474 TransactionDetails theTransactionDetails) { 475 StopWatch w = new StopWatch(); 476 477 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); 478 479 ResourceTable entity = new ResourceTable(); 480 entity.setResourceType(toResourceName(theResource)); 481 entity.setResourceTypeId(myResourceTypeCacheSvc.getResourceTypeId(toResourceName(theResource))); 482 483 RequestPartitionId targetWritePartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest( 484 theRequest, theResource, entity.getResourceType()); 485 if (!theRequestPartitionId.contains(targetWritePartitionId)) { 486 throw new InternalErrorException(Msg.code(2636) + "Active partition " + theRequestPartitionId 487 + " does not contain target write partition " + targetWritePartitionId); 488 } 489 490 entity.setPartitionId(PartitionablePartitionId.toStoragePartition(targetWritePartitionId, myPartitionSettings)); 491 entity.setCreatedByMatchUrl(theMatchUrl); 492 entity.initializeVersion(); 493 494 if (isNotBlank(theMatchUrl) && theProcessMatchUrl) { 495 Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl( 496 theMatchUrl, myResourceType, theTransactionDetails, theRequest, theRequestPartitionId); 497 ourLog.trace("Resolving match URL {} found: {}", theMatchUrl, match); 498 if (match.size() > 1) { 499 String msg = getContext() 500 .getLocalizer() 501 .getMessageSanitized( 502 BaseStorageDao.class, 503 "transactionOperationWithMultipleMatchFailure", 504 "CREATE", 505 myResourceName, 506 theMatchUrl, 507 match.size()); 508 throw new PreconditionFailedException(Msg.code(958) + msg); 509 } else if (match.size() == 1) { 510 511 /* 512 * Ok, so we've found a single PID that matches the conditional URL. 513 * That's good, there are two possibilities below. 514 */ 515 516 JpaPid pid = match.iterator().next(); 517 if (theTransactionDetails.getDeletedResourceIds().contains(pid)) { 518 519 /* 520 * If the resource matching the given match URL has already been 521 * deleted within this transaction. This is a really rare case, since 522 * it means the client has performed a FHIR transaction with both 523 * a delete and a create on the same conditional URL. This is rare 524 * but allowed, and means that it's now ok to create a new one resource 525 * matching the conditional URL since we'll be deleting any existing 526 * index rows on the existing resource as a part of this transaction. 527 * We can also un-resolve the previous match URL in the TransactionDetails 528 * since we'll resolve it to the new resource ID below 529 */ 530 531 myMatchResourceUrlService.unresolveMatchUrl(theTransactionDetails, getResourceName(), theMatchUrl); 532 533 } else { 534 535 /* 536 * This is the normal path where the conditional URL matched exactly 537 * one resource, so we won't be creating anything but instead 538 * just returning the existing ID. We now have a PID for the matching 539 * resource, but we haven't loaded anything else (e.g. the forced ID 540 * or the resource body aren't yet loaded from the DB). We're going to 541 * return a LazyDaoOutcome with two lazy loaded providers for loading the 542 * entity and the forced ID since we can avoid these extra SQL loads 543 * unless we know we're actually going to use them. For example, if 544 * the client has specified "Prefer: return=minimal" then we won't be 545 * needing the load the body. 546 */ 547 548 Supplier<LazyDaoMethodOutcome.EntityAndResource> entitySupplier = () -> myTxTemplate.execute(tx -> { 549 ResourceTable foundEntity = myEntityManager.find(ResourceTable.class, pid); 550 IBaseResource resource = myJpaStorageResourceParser.toResource(foundEntity, false); 551 theResource.setId(resource.getIdElement().getValue()); 552 return new LazyDaoMethodOutcome.EntityAndResource(foundEntity, resource); 553 }); 554 Supplier<IIdType> idSupplier = () -> myTxTemplate.execute(tx -> { 555 IIdType retVal = myIdHelperService.translatePidIdToForcedId(myFhirContext, myResourceName, pid); 556 if (!retVal.hasVersionIdPart()) { 557 Long version = pid.getVersion(); 558 if (version == null) { 559 version = myMemoryCacheService.getIfPresent( 560 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid); 561 } 562 if (version == null) { 563 version = myResourceTableDao.findCurrentVersionByPid(pid); 564 if (version != null) { 565 myMemoryCacheService.putAfterCommit( 566 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, 567 pid, 568 version); 569 } 570 } 571 if (version != null) { 572 retVal = myFhirContext 573 .getVersion() 574 .newIdType() 575 .setParts( 576 retVal.getBaseUrl(), 577 retVal.getResourceType(), 578 retVal.getIdPart(), 579 Long.toString(version)); 580 } 581 } 582 return retVal; 583 }); 584 585 DaoMethodOutcome outcome = toMethodOutcomeLazy(theRequest, pid, entitySupplier, idSupplier) 586 .setCreated(false) 587 .setNop(true); 588 StorageResponseCodeEnum responseCode = 589 StorageResponseCodeEnum.SUCCESSFUL_CREATE_WITH_CONDITIONAL_MATCH; 590 String msg = getContext() 591 .getLocalizer() 592 .getMessageSanitized( 593 BaseStorageDao.class, 594 "successfulCreateConditionalWithMatch", 595 w.getMillisAndRestart(), 596 UrlUtil.sanitizeUrlPart(theMatchUrl)); 597 outcome.setOperationOutcome(createInfoOperationOutcome(msg, responseCode)); 598 return outcome; 599 } 600 } 601 } 602 603 boolean isClientAssignedId = storeNonPidResourceId(theResource, entity); 604 605 HookParams hookParams; 606 607 // Notify interceptor for accepting/rejecting client assigned ids 608 if (isClientAssignedId) { 609 hookParams = new HookParams().add(IBaseResource.class, theResource).add(RequestDetails.class, theRequest); 610 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_CLIENT_ASSIGNED_ID, hookParams); 611 } 612 613 // Interceptor call: STORAGE_PRESTORAGE_RESOURCE_CREATED 614 hookParams = new HookParams() 615 .add(IBaseResource.class, theResource) 616 .add(RequestDetails.class, theRequest) 617 .addIfMatchesType(ServletRequestDetails.class, theRequest) 618 .add(RequestPartitionId.class, theRequestPartitionId) 619 .add(TransactionDetails.class, theTransactionDetails); 620 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRESTORAGE_RESOURCE_CREATED, hookParams); 621 622 if (isClientAssignedId) { 623 validateResourceIdCreation(theResource, theRequest); 624 } 625 626 if (theMatchUrl != null) { 627 // Note: We actually create the search URL below by calling enforceMatchUrlResourceUniqueness 628 // since we can't do that until we know the assigned PID, but we set this flag up here 629 // because we need to set it before we persist the ResourceTable entity in order to 630 // avoid triggering an extra DB update 631 entity.setSearchUrlPresent(true); 632 } 633 634 // Perform actual DB update 635 // this call will also update the metadata 636 ResourceTable updatedEntity = updateEntity( 637 theRequest, 638 theResource, 639 entity, 640 null, 641 thePerformIndexing, 642 false, 643 theTransactionDetails, 644 false, 645 thePerformIndexing); 646 647 // Store the resource forced ID if necessary 648 JpaPid jpaPid = updatedEntity.getPersistentId(); 649 650 // Populate the resource with its actual final stored ID from the entity 651 theResource.setId(entity.getIdDt()); 652 653 // Pre-cache the resource ID 654 jpaPid.setAssociatedResourceId(entity.getIdType(myFhirContext)); 655 String fhirId = entity.getFhirId(); 656 assert fhirId != null; 657 myIdHelperService.addResolvedPidToFhirIdAfterCommit( 658 jpaPid, theRequestPartitionId, getResourceName(), fhirId, null); 659 theTransactionDetails.addResolvedResourceId(jpaPid.getAssociatedResourceId(), jpaPid); 660 theTransactionDetails.addResolvedResource(jpaPid.getAssociatedResourceId(), theResource); 661 662 // Pre-cache the match URL, and create an entry in the HFJ_RES_SEARCH_URL table to 663 // protect against concurrent writes to the same conditional URL 664 if (theMatchUrl != null) { 665 myResourceSearchUrlSvc.enforceMatchUrlResourceUniqueness(getResourceName(), theMatchUrl, updatedEntity); 666 myMatchResourceUrlService.matchUrlResolved(theTransactionDetails, getResourceName(), theMatchUrl, jpaPid); 667 } 668 669 // Update the version/last updated in the resource so that interceptors get 670 // the correct version 671 // TODO - the above updateEntity calls updateResourceMetadata 672 // Maybe we don't need this call here? 673 myJpaStorageResourceParser.updateResourceMetadata(entity, theResource); 674 675 // Populate the PID in the resource so it is available to hooks 676 addPidToResource(entity, theResource); 677 678 // Notify JPA interceptors 679 if (!updatedEntity.isUnchangedInCurrentOperation()) { 680 hookParams = new HookParams() 681 .add(IBaseResource.class, theResource) 682 .add(RequestDetails.class, theRequest) 683 .addIfMatchesType(ServletRequestDetails.class, theRequest) 684 .add(TransactionDetails.class, theTransactionDetails) 685 .add( 686 InterceptorInvocationTimingEnum.class, 687 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)); 688 doCallHooks(theTransactionDetails, theRequest, Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, hookParams); 689 } 690 691 DaoMethodOutcome outcome = toMethodOutcome(theRequest, entity, theResource, theMatchUrl, theOperationType) 692 .setCreated(true); 693 outcome.setResponseStatusCode(Constants.STATUS_HTTP_201_CREATED); 694 695 if (!thePerformIndexing) { 696 outcome.setId(theResource.getIdElement()); 697 } 698 699 populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, theOperationType, theTransactionDetails); 700 701 return outcome; 702 } 703 704 /** 705 * Check for an id on the resource and if so, 706 * store it in ResourceTable. 707 * 708 * The fhirId property is either set here with the resource id 709 * OR by hibernate once the PK is generated for a server-assigned id. 710 * 711 * Used for both client-assigned id and for server-assigned UUIDs. 712 * 713 * @return true if this is a client-assigned id 714 * 715 * @see ca.uhn.fhir.jpa.model.entity.ResourceTable.FhirIdGenerator 716 */ 717 private boolean storeNonPidResourceId(T theResource, ResourceTable entity) { 718 String resourceIdBeforeStorage = theResource.getIdElement().getIdPart(); 719 boolean resourceHadIdBeforeStorage = isNotBlank(resourceIdBeforeStorage); 720 boolean resourceIdWasServerAssigned = 721 theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED) == Boolean.TRUE; 722 723 // We distinguish actual client-assigned ids from UUIDs which the server assigned. 724 boolean isClientAssigned = resourceHadIdBeforeStorage && !resourceIdWasServerAssigned; 725 726 // But both need to be set on the entity fhirId field. 727 if (resourceHadIdBeforeStorage) { 728 entity.setFhirId(resourceIdBeforeStorage); 729 } 730 731 return isClientAssigned; 732 } 733 734 void validateResourceIdCreation(T theResource, RequestDetails theRequest) { 735 JpaStorageSettings.ClientIdStrategyEnum strategy = getStorageSettings().getResourceClientIdStrategy(); 736 737 if (strategy == JpaStorageSettings.ClientIdStrategyEnum.NOT_ALLOWED) { 738 if (!isSystemRequest(theRequest)) { 739 throw new ResourceNotFoundException(Msg.code(959) 740 + getMessageSanitized( 741 "failedToCreateWithClientAssignedIdNotAllowed", 742 theResource.getIdElement().getIdPart())); 743 } 744 } 745 746 if (strategy == JpaStorageSettings.ClientIdStrategyEnum.ALPHANUMERIC) { 747 if (theResource.getIdElement().isIdPartValidLong()) { 748 throw new InvalidRequestException(Msg.code(960) 749 + getMessageSanitized( 750 "failedToCreateWithClientAssignedNumericId", 751 theResource.getIdElement().getIdPart())); 752 } 753 } 754 } 755 756 protected String getMessageSanitized(String theKey, String theIdPart) { 757 return getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, theKey, theIdPart); 758 } 759 760 private boolean isSystemRequest(RequestDetails theRequest) { 761 return theRequest instanceof SystemRequestDetails; 762 } 763 764 private IInstanceValidatorModule getInstanceValidator() { 765 return myInstanceValidator; 766 } 767 768 /** 769 * @deprecated Use {@link #delete(IIdType, RequestDetails)} instead 770 */ 771 @Override 772 public DaoMethodOutcome delete(IIdType theId) { 773 return delete(theId, null); 774 } 775 776 @Override 777 public DaoMethodOutcome delete(IIdType theId, RequestDetails theRequestDetails) { 778 TransactionDetails transactionDetails = new TransactionDetails(); 779 780 validateIdPresentForDelete(theId); 781 validateDeleteEnabled(); 782 783 return myTransactionService.execute(theRequestDetails, transactionDetails, tx -> { 784 DeleteConflictList deleteConflicts = new DeleteConflictList(); 785 if (isNotBlank(theId.getValue())) { 786 deleteConflicts.setResourceIdMarkedForDeletion(theId); 787 } 788 789 StopWatch w = new StopWatch(); 790 791 DaoMethodOutcome retVal = delete(theId, deleteConflicts, theRequestDetails, transactionDetails); 792 793 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 794 795 ourLog.debug("Processed delete on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); 796 return retVal; 797 }); 798 } 799 800 @Override 801 public DaoMethodOutcome delete( 802 IIdType theId, 803 DeleteConflictList theDeleteConflicts, 804 RequestDetails theRequestDetails, 805 @Nonnull TransactionDetails theTransactionDetails) { 806 validateIdPresentForDelete(theId); 807 validateDeleteEnabled(); 808 809 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 810 theRequestDetails, getResourceName(), theId); 811 812 final ResourceTable entity; 813 try { 814 entity = readEntityLatestVersion(theRequestDetails, theId, requestPartitionId, theTransactionDetails); 815 } catch (ResourceNotFoundException ex) { 816 // we don't want to throw 404s. 817 // if not found, return an outcome anyway. 818 // Because no object actually existed, we'll 819 // just set the id and nothing else 820 return createMethodOutcomeForResourceId( 821 theId.getValue(), 822 MESSAGE_KEY_DELETE_RESOURCE_NOT_EXISTING, 823 StorageResponseCodeEnum.SUCCESSFUL_DELETE_NOT_FOUND); 824 } 825 826 if (theId.hasVersionIdPart() && Long.parseLong(theId.getVersionIdPart()) != entity.getVersion()) { 827 throw new ResourceVersionConflictException( 828 Msg.code(961) + "Trying to delete " + theId + " but this is not the current version"); 829 } 830 831 JpaPid persistentId = entity.getId(); 832 theTransactionDetails.addDeletedResourceId(persistentId); 833 834 // Don't delete again if it's already deleted 835 if (isDeleted(entity)) { 836 DaoMethodOutcome outcome = createMethodOutcomeForResourceId( 837 entity.getIdDt().getValue(), 838 MESSAGE_KEY_DELETE_RESOURCE_ALREADY_DELETED, 839 StorageResponseCodeEnum.SUCCESSFUL_DELETE_ALREADY_DELETED); 840 841 // used to exist, so we'll set the persistent id 842 outcome.setPersistentId(persistentId); 843 outcome.setEntity(entity); 844 845 return outcome; 846 } 847 848 StopWatch w = new StopWatch(); 849 850 T resourceToDelete = 851 myJpaStorageResourceParser.toResource(theRequestDetails, myResourceType, entity, null, false); 852 theDeleteConflicts.setResourceIdMarkedForDeletion(theId); 853 854 // Notify IServerOperationInterceptors about pre-action call 855 HookParams hook = new HookParams() 856 .add(IBaseResource.class, resourceToDelete) 857 .add(RequestDetails.class, theRequestDetails) 858 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 859 .add(TransactionDetails.class, theTransactionDetails); 860 doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hook); 861 862 myDeleteConflictService.validateOkToDelete( 863 theDeleteConflicts, entity, false, theRequestDetails, theTransactionDetails); 864 865 preDelete(resourceToDelete, entity, theRequestDetails); 866 867 ResourceTable savedEntity = updateEntityForDelete(theRequestDetails, theTransactionDetails, entity); 868 resourceToDelete.setId(entity.getIdDt()); 869 870 // Notify JPA interceptors 871 HookParams hookParams = new HookParams() 872 .add(IBaseResource.class, resourceToDelete) 873 .add(RequestDetails.class, theRequestDetails) 874 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 875 .add(TransactionDetails.class, theTransactionDetails) 876 .add( 877 InterceptorInvocationTimingEnum.class, 878 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)); 879 880 doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, hookParams); 881 882 DaoMethodOutcome outcome = toMethodOutcome( 883 theRequestDetails, savedEntity, resourceToDelete, null, RestOperationTypeEnum.DELETE) 884 .setCreated(true); 885 886 String msg = getContext().getLocalizer().getMessageSanitized(BaseStorageDao.class, "successfulDeletes", 1); 887 msg += " " 888 + getContext() 889 .getLocalizer() 890 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", w.getMillis()); 891 outcome.setOperationOutcome(createInfoOperationOutcome(msg, StorageResponseCodeEnum.SUCCESSFUL_DELETE)); 892 893 myIdHelperService.addResolvedPidToFhirIdAfterCommit( 894 entity.getPersistentId(), 895 requestPartitionId, 896 entity.getResourceType(), 897 entity.getFhirId(), 898 entity.getDeleted()); 899 900 return outcome; 901 } 902 903 @Override 904 public DeleteMethodOutcome deleteByUrl(String theUrl, RequestDetails theRequest) { 905 validateDeleteEnabled(); 906 907 TransactionDetails transactionDetails = new TransactionDetails(); 908 ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); 909 910 if (resourceSearch.isDeleteExpunge()) { 911 return deleteExpunge(theUrl, theRequest); 912 } 913 914 return myTransactionService 915 .withRequest(theRequest) 916 .withTransactionDetails(transactionDetails) 917 .execute(tx -> { 918 DeleteConflictList deleteConflicts = new DeleteConflictList(); 919 DeleteMethodOutcome outcome = deleteByUrl(theUrl, deleteConflicts, theRequest, transactionDetails); 920 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 921 return outcome; 922 }); 923 } 924 925 /** 926 * This method gets called by {@link #deleteByUrl(String, RequestDetails)} as well as by 927 * transaction processors 928 */ 929 @Override 930 public DeleteMethodOutcome deleteByUrl( 931 String theUrl, 932 DeleteConflictList deleteConflicts, 933 RequestDetails theRequestDetails, 934 @Nonnull TransactionDetails theTransactionDetails) { 935 validateDeleteEnabled(); 936 937 return myTransactionService 938 .withRequest(theRequestDetails) 939 .withTransactionDetails(theTransactionDetails) 940 .execute(tx -> doDeleteByUrl(theUrl, deleteConflicts, theTransactionDetails, theRequestDetails)); 941 } 942 943 @Nonnull 944 private DeleteMethodOutcome doDeleteByUrl( 945 String theUrl, 946 DeleteConflictList deleteConflicts, 947 TransactionDetails theTransactionDetails, 948 RequestDetails theRequestDetails) { 949 ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); 950 SearchParameterMap paramMap = resourceSearch.getSearchParameterMap(); 951 paramMap.setLoadSynchronous(true); 952 953 Set<JpaPid> resourceIds = myMatchResourceUrlService.search(paramMap, myResourceType, theRequestDetails, null); 954 955 if (resourceIds.size() > 1) { 956 if (!getStorageSettings().isAllowMultipleDelete()) { 957 throw new PreconditionFailedException(Msg.code(962) 958 + getContext() 959 .getLocalizer() 960 .getMessageSanitized( 961 BaseStorageDao.class, 962 "transactionOperationWithMultipleMatchFailure", 963 "DELETE", 964 myResourceName, 965 theUrl, 966 resourceIds.size())); 967 } 968 // TODO: LD: There is a still a bug on slow deletes: https://github.com/hapifhir/hapi-fhir/issues/5675 969 final long threshold = getStorageSettings().getRestDeleteByUrlResourceIdThreshold(); 970 if (resourceIds.size() > threshold) { 971 throw new PreconditionFailedException(Msg.code(2496) 972 + getContext() 973 .getLocalizer() 974 .getMessageSanitized( 975 BaseStorageDao.class, 976 "deleteByUrlThresholdExceeded", 977 theUrl, 978 resourceIds.size(), 979 threshold)); 980 } 981 } 982 983 return deletePidList(theUrl, resourceIds, deleteConflicts, theRequestDetails, theTransactionDetails); 984 } 985 986 @Override 987 public <P extends IResourcePersistentId> void expunge(Collection<P> theResourceIds, RequestDetails theRequest) { 988 ExpungeOptions options = new ExpungeOptions(); 989 options.setExpungeDeletedResources(true); 990 for (P pid : theResourceIds) { 991 if (pid instanceof JpaPid) { 992 ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId()); 993 994 forceExpungeInExistingTransaction(entity.getIdDt().toVersionless(), options, theRequest); 995 } else { 996 ourLog.warn("Unable to process expunge on resource {}", pid); 997 return; 998 } 999 } 1000 } 1001 1002 @Nonnull 1003 @Override 1004 public <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList( 1005 String theUrl, 1006 Collection<P> theResourceIds, 1007 DeleteConflictList theDeleteConflicts, 1008 RequestDetails theRequestDetails, 1009 TransactionDetails theTransactionDetails) { 1010 StopWatch w = new StopWatch(); 1011 TransactionDetails transactionDetails = 1012 theTransactionDetails != null ? theTransactionDetails : new TransactionDetails(); 1013 List<ResourceTable> deletedResources = new ArrayList<>(); 1014 1015 List<IResourcePersistentId<?>> resolvedIds = 1016 theResourceIds.stream().map(t -> (IResourcePersistentId<?>) t).collect(Collectors.toList()); 1017 mySystemDao.preFetchResources(resolvedIds, false); 1018 1019 for (P pid : theResourceIds) { 1020 JpaPid jpaPid = (JpaPid) pid; 1021 1022 // This shouldn't actually need to hit the DB because we pre-fetch above 1023 ResourceTable entity = myEntityManager.find(ResourceTable.class, jpaPid); 1024 deletedResources.add(entity); 1025 1026 T resourceToDelete = 1027 myJpaStorageResourceParser.toResource(theRequestDetails, myResourceType, entity, null, false); 1028 1029 transactionDetails.addDeletedResourceId(pid); 1030 1031 // Notify IServerOperationInterceptors about pre-action call 1032 HookParams hooks = new HookParams() 1033 .add(IBaseResource.class, resourceToDelete) 1034 .add(RequestDetails.class, theRequestDetails) 1035 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1036 .add(TransactionDetails.class, transactionDetails); 1037 doCallHooks(transactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); 1038 1039 myDeleteConflictService.validateOkToDelete( 1040 theDeleteConflicts, entity, false, theRequestDetails, transactionDetails); 1041 1042 // Perform delete 1043 1044 preDelete(resourceToDelete, entity, theRequestDetails); 1045 1046 updateEntityForDelete(theRequestDetails, transactionDetails, entity); 1047 resourceToDelete.setId(entity.getIdDt()); 1048 1049 // Notify JPA interceptors 1050 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 1051 @Override 1052 public void beforeCommit(boolean readOnly) { 1053 HookParams hookParams = new HookParams() 1054 .add(IBaseResource.class, resourceToDelete) 1055 .add(RequestDetails.class, theRequestDetails) 1056 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1057 .add(TransactionDetails.class, transactionDetails) 1058 .add( 1059 InterceptorInvocationTimingEnum.class, 1060 transactionDetails.getInvocationTiming( 1061 Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)); 1062 doCallHooks( 1063 transactionDetails, 1064 theRequestDetails, 1065 Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, 1066 hookParams); 1067 } 1068 }); 1069 } 1070 1071 IBaseOperationOutcome oo; 1072 if (deletedResources.isEmpty()) { 1073 String msg = getContext() 1074 .getLocalizer() 1075 .getMessageSanitized(BaseStorageDao.class, "unableToDeleteNotFound", theUrl); 1076 oo = createWarnOperationOutcome(msg, "not-found", StorageResponseCodeEnum.SUCCESSFUL_DELETE_NOT_FOUND); 1077 } else { 1078 String msg = getContext() 1079 .getLocalizer() 1080 .getMessageSanitized(BaseStorageDao.class, "successfulDeletes", deletedResources.size()); 1081 msg += " " 1082 + getContext() 1083 .getLocalizer() 1084 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", w.getMillis()); 1085 oo = createInfoOperationOutcome(msg, StorageResponseCodeEnum.SUCCESSFUL_DELETE); 1086 } 1087 1088 ourLog.debug( 1089 "Processed delete on {} (matched {} resource(s)) in {}ms", 1090 theUrl, 1091 deletedResources.size(), 1092 w.getMillis()); 1093 1094 DeleteMethodOutcome retVal = new DeleteMethodOutcome(); 1095 retVal.setDeletedEntities(deletedResources); 1096 retVal.setOperationOutcome(oo); 1097 return retVal; 1098 } 1099 1100 protected ResourceTable updateEntityForDelete( 1101 RequestDetails theRequest, TransactionDetails theTransactionDetails, ResourceTable theEntity) { 1102 myResourceSearchUrlSvc.deleteByResId(theEntity.getPersistentId()); 1103 Date updateTime = new Date(); 1104 return updateEntity(theRequest, null, theEntity, updateTime, true, true, theTransactionDetails, false, true); 1105 } 1106 1107 private void validateDeleteEnabled() { 1108 if (!getStorageSettings().isDeleteEnabled()) { 1109 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "deleteBlockedBecauseDisabled"); 1110 throw new PreconditionFailedException(Msg.code(966) + msg); 1111 } 1112 } 1113 1114 private void validateIdPresentForDelete(IIdType theId) { 1115 if (theId == null || !theId.hasIdPart()) { 1116 throw new InvalidRequestException(Msg.code(967) + "Can not perform delete, no ID provided"); 1117 } 1118 } 1119 1120 private <MT extends IBaseMetaType> void doMetaAdd( 1121 MT theMetaAdd, 1122 BaseHasResource<?> theEntity, 1123 RequestDetails theRequestDetails, 1124 TransactionDetails theTransactionDetails) { 1125 IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1126 1127 List<TagDefinition> tags = toTagList(theMetaAdd); 1128 for (TagDefinition nextDef : tags) { 1129 1130 boolean entityHasTag = false; 1131 for (BaseTag next : theEntity.getTags()) { 1132 if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType()) 1133 && Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) 1134 && Objects.equals(next.getTag().getCode(), nextDef.getCode()) 1135 && Objects.equals(next.getTag().getVersion(), nextDef.getVersion()) 1136 && Objects.equals(next.getTag().getUserSelected(), nextDef.getUserSelected())) { 1137 entityHasTag = true; 1138 break; 1139 } 1140 } 1141 1142 if (!entityHasTag) { 1143 theEntity.setHasTags(true); 1144 1145 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 1146 theTransactionDetails, 1147 nextDef.getTagType(), 1148 nextDef.getSystem(), 1149 nextDef.getCode(), 1150 nextDef.getDisplay(), 1151 nextDef.getVersion(), 1152 nextDef.getUserSelected()); 1153 if (def != null) { 1154 BaseTag newEntity = theEntity.addTag(def); 1155 if (newEntity.getTagId() == null) { 1156 myEntityManager.persist(newEntity); 1157 } 1158 } 1159 } 1160 } 1161 1162 validateMetaCount(theEntity.getTags().size()); 1163 1164 myEntityManager.merge(theEntity); 1165 1166 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1167 IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1168 HookParams preStorageParams = new HookParams() 1169 .add(IBaseResource.class, oldVersion) 1170 .add(IBaseResource.class, newVersion) 1171 .add(RequestDetails.class, theRequestDetails) 1172 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1173 .add(TransactionDetails.class, theTransactionDetails); 1174 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 1175 1176 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1177 HookParams preCommitParams = new HookParams() 1178 .add(IBaseResource.class, oldVersion) 1179 .add(IBaseResource.class, newVersion) 1180 .add(RequestDetails.class, theRequestDetails) 1181 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1182 .add(TransactionDetails.class, theTransactionDetails) 1183 .add( 1184 InterceptorInvocationTimingEnum.class, 1185 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 1186 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 1187 } 1188 1189 private <MT extends IBaseMetaType> void doMetaDelete( 1190 MT theMetaDel, 1191 BaseHasResource theEntity, 1192 RequestDetails theRequestDetails, 1193 TransactionDetails theTransactionDetails) { 1194 1195 // todo mb update hibernate search index if we are storing resources - it assumes inline tags. 1196 IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1197 1198 List<TagDefinition> tags = toTagList(theMetaDel); 1199 1200 for (TagDefinition nextDef : tags) { 1201 for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) { 1202 if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType()) 1203 && Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) 1204 && Objects.equals(next.getTag().getCode(), nextDef.getCode())) { 1205 myEntityManager.remove(next); 1206 theEntity.getTags().remove(next); 1207 } 1208 } 1209 } 1210 1211 if (theEntity.getTags().isEmpty()) { 1212 theEntity.setHasTags(false); 1213 } 1214 1215 theEntity = myEntityManager.merge(theEntity); 1216 1217 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1218 IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1219 HookParams preStorageParams = new HookParams() 1220 .add(IBaseResource.class, oldVersion) 1221 .add(IBaseResource.class, newVersion) 1222 .add(RequestDetails.class, theRequestDetails) 1223 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1224 .add(TransactionDetails.class, theTransactionDetails); 1225 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 1226 1227 HookParams preCommitParams = new HookParams() 1228 .add(IBaseResource.class, oldVersion) 1229 .add(IBaseResource.class, newVersion) 1230 .add(RequestDetails.class, theRequestDetails) 1231 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1232 .add(TransactionDetails.class, theTransactionDetails) 1233 .add( 1234 InterceptorInvocationTimingEnum.class, 1235 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 1236 1237 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 1238 } 1239 1240 @Override 1241 public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 1242 HapiTransactionService.noTransactionAllowed(); 1243 validateExpungeEnabled(); 1244 return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest); 1245 } 1246 1247 @Override 1248 public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) { 1249 HapiTransactionService.noTransactionAllowed(); 1250 ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName()); 1251 validateExpungeEnabled(); 1252 return myExpungeService.expunge(getResourceName(), null, theExpungeOptions, theRequestDetails); 1253 } 1254 1255 private void validateExpungeEnabled() { 1256 if (!getStorageSettings().isExpungeEnabled()) { 1257 throw new MethodNotAllowedException(Msg.code(968) + "$expunge is not enabled on this server"); 1258 } 1259 } 1260 1261 @Override 1262 public Stream<IResourcePersistentId> fetchAllVersionsOfResources( 1263 RequestDetails theRequestDetails, Collection<IResourcePersistentId> theIds) { 1264 HapiTransactionService.requireTransaction(); 1265 1266 List<JpaPidFk> ids = theIds.stream().map(t -> ((JpaPid) t).toFk()).toList(); 1267 1268 return myResourceHistoryTableDao 1269 .findVersionPidsForResources(Pageable.unpaged(), ids) 1270 .map(t -> (IResourcePersistentId) t); 1271 } 1272 1273 @Override 1274 public ExpungeOutcome forceExpungeInExistingTransaction( 1275 IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 1276 TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); 1277 1278 BaseHasResource<?> entity = txTemplate.execute(t -> readEntity(theId, theRequest)); 1279 Validate.notNull(entity, "Resource with ID %s not found in database", theId); 1280 1281 if (theId.hasVersionIdPart()) { 1282 BaseHasResource<?> currentVersion; 1283 currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless(), theRequest)); 1284 Validate.notNull( 1285 currentVersion, 1286 "Current version of resource with ID %s not found in database", 1287 theId.toVersionless()); 1288 1289 if (entity.getVersion() == currentVersion.getVersion()) { 1290 throw new PreconditionFailedException( 1291 Msg.code(969) + "Can not perform version-specific expunge of resource " 1292 + theId.toUnqualified().getValue() + " as this is the current version"); 1293 } 1294 1295 return myExpungeService.expunge(getResourceName(), entity.getResourceId(), theExpungeOptions, theRequest); 1296 } 1297 1298 // Downstream code expects the version to be null in the JpaPid if we're not 1299 // doing a version specific expunge 1300 JpaPid resourceId = entity.getResourceId(); 1301 resourceId = JpaPid.fromId(resourceId.getId(), resourceId.getPartitionId()); 1302 1303 return myExpungeService.expunge(getResourceName(), resourceId, theExpungeOptions, theRequest); 1304 } 1305 1306 @Override 1307 @Nonnull 1308 public String getResourceName() { 1309 return myResourceName; 1310 } 1311 1312 @Override 1313 public Class<T> getResourceType() { 1314 return myResourceType; 1315 } 1316 1317 @SuppressWarnings("unchecked") 1318 public void setResourceType(Class<? extends IBaseResource> theTableType) { 1319 myResourceType = (Class<T>) theTableType; 1320 } 1321 1322 @Override 1323 public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { 1324 StopWatch w = new StopWatch(); 1325 RequestPartitionId requestPartitionId = 1326 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1327 theRequestDetails, myResourceName, null); 1328 IBundleProvider retVal = myTransactionService 1329 .withRequest(theRequestDetails) 1330 .withRequestPartitionId(requestPartitionId) 1331 .execute(() -> myPersistedJpaBundleProviderFactory.history( 1332 theRequestDetails, myResourceName, null, theSince, theUntil, theOffset, requestPartitionId)); 1333 1334 ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart()); 1335 return retVal; 1336 } 1337 1338 /** 1339 * @deprecated Use {@link #history(IIdType, HistorySearchDateRangeParam, RequestDetails)} instead 1340 */ 1341 @Override 1342 public IBundleProvider history( 1343 final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) { 1344 StopWatch w = new StopWatch(); 1345 1346 RequestPartitionId requestPartitionId = 1347 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1348 theRequest, myResourceName, theId); 1349 IBundleProvider retVal = myTransactionService 1350 .withRequest(theRequest) 1351 .withRequestPartitionId(requestPartitionId) 1352 .execute(() -> { 1353 IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); 1354 BaseHasResource entity = readEntity(id, requestPartitionId); 1355 1356 return myPersistedJpaBundleProviderFactory.history( 1357 theRequest, 1358 myResourceName, 1359 entity.getResourceId(), 1360 theSince, 1361 theUntil, 1362 theOffset, 1363 requestPartitionId); 1364 }); 1365 1366 ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart()); 1367 return retVal; 1368 } 1369 1370 @Override 1371 public IBundleProvider history( 1372 final IIdType theId, 1373 final HistorySearchDateRangeParam theHistorySearchDateRangeParam, 1374 RequestDetails theRequest) { 1375 StopWatch w = new StopWatch(); 1376 RequestPartitionId requestPartitionId = 1377 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1378 theRequest, myResourceName, theId); 1379 IBundleProvider retVal = myTransactionService 1380 .withRequest(theRequest) 1381 .withRequestPartitionId(requestPartitionId) 1382 .execute(() -> { 1383 IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); 1384 BaseHasResource entity = readEntity(id, requestPartitionId); 1385 1386 return myPersistedJpaBundleProviderFactory.history( 1387 theRequest, 1388 myResourceName, 1389 entity.getResourceId(), 1390 theHistorySearchDateRangeParam.getLowerBoundAsInstant(), 1391 theHistorySearchDateRangeParam.getUpperBoundAsInstant(), 1392 theHistorySearchDateRangeParam.getOffset(), 1393 theHistorySearchDateRangeParam.getHistorySearchType(), 1394 requestPartitionId); 1395 }); 1396 1397 ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart()); 1398 return retVal; 1399 } 1400 1401 protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) { 1402 if (theRequestDetails == null || theRequestDetails.getServer() == null) { 1403 return false; 1404 } 1405 IRestfulServerDefaults server = theRequestDetails.getServer(); 1406 IPagingProvider pagingProvider = server.getPagingProvider(); 1407 return pagingProvider != null; 1408 } 1409 1410 protected void requestReindexForRelatedResources( 1411 Boolean theCurrentlyReindexing, List<String> theBase, RequestDetails theRequestDetails) { 1412 // Avoid endless loops 1413 if (Boolean.TRUE.equals(theCurrentlyReindexing) || shouldSkipReindex(theRequestDetails)) { 1414 return; 1415 } 1416 1417 if (getStorageSettings().isMarkResourcesForReindexingUponSearchParameterChange()) { 1418 1419 ReindexJobParameters params = new ReindexJobParameters(); 1420 1421 List<String> urls = List.of(); 1422 if (!isCommonSearchParam(theBase)) { 1423 urls = theBase.stream().map(t -> t + "?").collect(Collectors.toList()); 1424 } 1425 1426 myJobPartitionProvider.getPartitionedUrls(theRequestDetails, urls).forEach(params::addPartitionedUrl); 1427 1428 JobInstanceStartRequest request = new JobInstanceStartRequest(); 1429 request.setJobDefinitionId(JOB_REINDEX); 1430 request.setParameters(params); 1431 myJobCoordinator.startInstance(theRequestDetails, request); 1432 1433 ourLog.debug("Started reindex job with parameters {}", params); 1434 } 1435 1436 mySearchParamRegistry.requestRefresh(); 1437 } 1438 1439 protected final boolean shouldSkipReindex(RequestDetails theRequestDetails) { 1440 if (theRequestDetails == null) { 1441 return false; 1442 } 1443 Object shouldSkip = theRequestDetails.getUserData().getOrDefault(JpaConstants.SKIP_REINDEX_ON_UPDATE, false); 1444 return Boolean.parseBoolean(shouldSkip.toString()); 1445 } 1446 1447 private boolean isCommonSearchParam(List<String> theBase) { 1448 // If the base contains the special resource "Resource", this is a common SP that applies to all resources 1449 return theBase.stream().map(String::toLowerCase).anyMatch(BASE_RESOURCE_NAME::equals); 1450 } 1451 1452 @Override 1453 @Transactional 1454 public <MT extends IBaseMetaType> MT metaAddOperation( 1455 IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) { 1456 1457 RequestPartitionId requestPartitionId = 1458 myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( 1459 theRequest, JpaConstants.OPERATION_META_ADD); 1460 1461 myTransactionService 1462 .withRequest(theRequest) 1463 .withRequestPartitionId(requestPartitionId) 1464 .execute(() -> doMetaAddOperation(theResourceId, theMetaAdd, theRequest, requestPartitionId)); 1465 1466 @SuppressWarnings("unchecked") 1467 MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest); 1468 return retVal; 1469 } 1470 1471 protected <MT extends IBaseMetaType> void doMetaAddOperation( 1472 IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 1473 TransactionDetails transactionDetails = new TransactionDetails(); 1474 1475 StopWatch w = new StopWatch(); 1476 BaseHasResource entity = readEntity(theResourceId, theRequestPartitionId); 1477 1478 if (isNull(entity)) { 1479 throw new ResourceNotFoundException(Msg.code(1993) + theResourceId); 1480 } 1481 1482 ResourceTable latestVersion = 1483 readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails); 1484 if (latestVersion.getVersion() != entity.getVersion()) { 1485 doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails); 1486 } else { 1487 doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails); 1488 1489 // Also update history entry 1490 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion( 1491 entity.getResourceId().toFk(), entity.getVersion()); 1492 doMetaAdd(theMetaAdd, history, theRequest, transactionDetails); 1493 } 1494 1495 ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart()); 1496 } 1497 1498 @Override 1499 @Transactional 1500 public <MT extends IBaseMetaType> MT metaDeleteOperation( 1501 IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) { 1502 1503 RequestPartitionId requestPartitionId = 1504 myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( 1505 theRequest, JpaConstants.OPERATION_META_DELETE); 1506 1507 myTransactionService 1508 .withRequest(theRequest) 1509 .withRequestPartitionId(requestPartitionId) 1510 .execute(() -> doMetaDeleteOperation(theResourceId, theMetaDel, theRequest, requestPartitionId)); 1511 1512 @SuppressWarnings("unchecked") 1513 MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest); 1514 return retVal; 1515 } 1516 1517 @Transactional 1518 public <MT extends IBaseMetaType> void doMetaDeleteOperation( 1519 IIdType theResourceId, MT theMetaDel, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 1520 TransactionDetails transactionDetails = new TransactionDetails(); 1521 StopWatch w = new StopWatch(); 1522 1523 BaseHasResource entity = readEntity(theResourceId, theRequestPartitionId); 1524 1525 if (isNull(entity)) { 1526 throw new ResourceNotFoundException(Msg.code(1994) + theResourceId); 1527 } 1528 1529 ResourceTable latestVersion = 1530 readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails); 1531 boolean nonVersionedTags = 1532 myStorageSettings.getTagStorageMode() != JpaStorageSettings.TagStorageModeEnum.VERSIONED; 1533 1534 if (latestVersion.getVersion() != entity.getVersion() || nonVersionedTags) { 1535 doMetaDelete(theMetaDel, entity, theRequest, transactionDetails); 1536 } else { 1537 doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails); 1538 // Also update history entry 1539 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion( 1540 entity.getResourceId().toFk(), entity.getVersion()); 1541 doMetaDelete(theMetaDel, history, theRequest, transactionDetails); 1542 } 1543 1544 ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart()); 1545 } 1546 1547 @Override 1548 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequest) { 1549 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1550 theRequest, myResourceName, theId); 1551 1552 return myTransactionService 1553 .withRequest(theRequest) 1554 .withRequestPartitionId(requestPartitionId) 1555 .execute(() -> { 1556 Set<TagDefinition> tagDefs = new HashSet<>(); 1557 BaseHasResource<?> entity = readEntity(theId, requestPartitionId); 1558 for (BaseTag next : entity.getTags()) { 1559 tagDefs.add(next.getTag()); 1560 } 1561 MT retVal = toMetaDt(theType, tagDefs); 1562 1563 retVal.setLastUpdated(entity.getUpdatedDate()); 1564 retVal.setVersionId(Long.toString(entity.getVersion())); 1565 1566 return retVal; 1567 }); 1568 } 1569 1570 @Override 1571 @Transactional 1572 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { 1573 String sql = 1574 "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)"; 1575 TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class); 1576 q.setParameter("res_type", myResourceName); 1577 List<TagDefinition> tagDefinitions = q.getResultList(); 1578 1579 return toMetaDt(theType, tagDefinitions); 1580 } 1581 1582 private boolean isDeleted(BaseHasResource entityToUpdate) { 1583 return entityToUpdate.getDeleted() != null; 1584 } 1585 1586 @PostConstruct 1587 @Override 1588 public void start() { 1589 assert getStorageSettings() != null; 1590 1591 RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType); 1592 myResourceName = def.getName(); 1593 1594 if (mySearchDao != null && mySearchDao.isDisabled()) { 1595 mySearchDao = null; 1596 } 1597 1598 ourLog.debug("Starting resource DAO for type: {}", getResourceName()); 1599 myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class); 1600 myTxTemplate = new TransactionTemplate(myPlatformTransactionManager); 1601 1602 myStorageInterceptorHooks = new StorageInterceptorHooksFacade(myInterceptorBroadcaster); 1603 1604 super.start(); 1605 } 1606 1607 /** 1608 * Subclasses may override to provide behaviour. Invoked within a delete 1609 * transaction with the resource that is about to be deleted. 1610 */ 1611 protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) { 1612 // nothing by default 1613 } 1614 1615 @Override 1616 @Transactional 1617 public T readByPid(IResourcePersistentId thePid) { 1618 return readByPid(thePid, false); 1619 } 1620 1621 @Override 1622 @Transactional 1623 public T readByPid(IResourcePersistentId thePid, boolean theDeletedOk) { 1624 StopWatch w = new StopWatch(); 1625 JpaPid jpaPid = (JpaPid) thePid; 1626 1627 Optional<ResourceTable> entity = myResourceTableDao.findById(jpaPid); 1628 if (entity.isEmpty()) { 1629 throw new ResourceNotFoundException(Msg.code(975) + "No resource found with PID " + jpaPid); 1630 } 1631 if (isDeleted(entity.get()) && !theDeletedOk) { 1632 throw createResourceGoneException(entity.get()); 1633 } 1634 1635 T retVal = myJpaStorageResourceParser.toResource(null, myResourceType, entity.get(), null, false); 1636 1637 ourLog.debug("Processed read on {} in {}ms", jpaPid, w.getMillis()); 1638 return retVal; 1639 } 1640 1641 /** 1642 * @deprecated Use {@link #read(IIdType, RequestDetails)} instead 1643 */ 1644 @Override 1645 public T read(IIdType theId) { 1646 return read(theId, null); 1647 } 1648 1649 @Override 1650 public T read(IIdType theId, RequestDetails theRequestDetails) { 1651 return read(theId, theRequestDetails, false); 1652 } 1653 1654 @Override 1655 public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) { 1656 validateResourceTypeAndThrowInvalidRequestException(theId); 1657 TransactionDetails transactionDetails = new TransactionDetails(); 1658 1659 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1660 theRequest, myResourceName, theId); 1661 1662 return myTransactionService 1663 .withRequest(theRequest) 1664 .withTransactionDetails(transactionDetails) 1665 .withRequestPartitionId(requestPartitionId) 1666 .read(() -> doReadInTransaction(theId, theRequest, theDeletedOk, requestPartitionId)); 1667 } 1668 1669 private T doReadInTransaction( 1670 IIdType theId, RequestDetails theRequest, boolean theDeletedOk, RequestPartitionId theRequestPartitionId) { 1671 assert TransactionSynchronizationManager.isActualTransactionActive(); 1672 1673 StopWatch w = new StopWatch(); 1674 BaseHasResource<?> entity = readEntity(theId, theRequestPartitionId); 1675 validateResourceType(entity); 1676 1677 T retVal = myJpaStorageResourceParser.toResource(theRequest, myResourceType, entity, null, false); 1678 1679 if (!theDeletedOk) { 1680 if (isDeleted(entity)) { 1681 throw createResourceGoneException(entity); 1682 } 1683 } 1684 // If the resolved fhir model is null, we don't need to run pre-access over or pre-show over it. 1685 if (retVal != null) { 1686 invokeStoragePreAccessResources(theId, theRequest, retVal); 1687 retVal = invokeStoragePreShowResources(theRequest, retVal); 1688 } 1689 1690 ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); 1691 return retVal; 1692 } 1693 1694 @Nullable 1695 private T invokeStoragePreShowResources(RequestDetails theRequest, T retVal) { 1696 retVal = invokeStoragePreShowResources(myInterceptorBroadcaster, theRequest, retVal); 1697 return retVal; 1698 } 1699 1700 private void invokeStoragePreAccessResources(IIdType theId, RequestDetails theRequest, T theResource) { 1701 invokeStoragePreAccessResources(myInterceptorBroadcaster, theRequest, theId, theResource); 1702 } 1703 1704 private Optional<T> invokeStoragePreAccessResources(RequestDetails theRequest, T theResource) { 1705 IInterceptorBroadcaster compositeBroadcaster = 1706 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 1707 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 1708 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource); 1709 HookParams params = new HookParams() 1710 .add(IPreResourceAccessDetails.class, accessDetails) 1711 .add(RequestDetails.class, theRequest) 1712 .addIfMatchesType(ServletRequestDetails.class, theRequest); 1713 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 1714 if (accessDetails.isDontReturnResourceAtIndex(0)) { 1715 return Optional.empty(); 1716 } 1717 } 1718 return Optional.of(theResource); 1719 } 1720 1721 @Override 1722 public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) { 1723 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1724 theRequest, myResourceName, theId); 1725 return myTransactionService 1726 .withRequest(theRequest) 1727 .withRequestPartitionId(requestPartitionId) 1728 .execute(() -> readEntity(theId, requestPartitionId)); 1729 } 1730 1731 @Override 1732 public ReindexOutcome reindex( 1733 IResourcePersistentId thePid, 1734 ReindexParameters theReindexParameters, 1735 RequestDetails theRequest, 1736 TransactionDetails theTransactionDetails) { 1737 ReindexOutcome retVal = new ReindexOutcome(); 1738 1739 JpaPid jpaPid = (JpaPid) thePid; 1740 1741 // Careful! Reindex only reads ResourceTable, but we tell Hibernate to check version 1742 // to ensure Hibernate will catch concurrent updates (PUT/DELETE) elsewhere. 1743 // Otherwise, we may index stale data. See #4584 1744 // We use the main entity as the lock object since all the index rows hang off it. 1745 ResourceTable entity; 1746 if (theReindexParameters.isOptimisticLock()) { 1747 entity = myEntityManager.find(ResourceTable.class, jpaPid, LockModeType.OPTIMISTIC); 1748 } else { 1749 entity = myEntityManager.find(ResourceTable.class, jpaPid); 1750 } 1751 1752 if (entity == null) { 1753 retVal.addWarning("Unable to find entity with PID: " + jpaPid.getId()); 1754 return retVal; 1755 } 1756 1757 boolean reindexSearchParameters = 1758 theReindexParameters.getReindexSearchParameters() != ReindexParameters.ReindexSearchParametersEnum.NONE; 1759 boolean correctCurrentVersion = 1760 theReindexParameters.getCorrectCurrentVersion() != ReindexParameters.CorrectCurrentVersionModeEnum.NONE; 1761 boolean optimizeStorage = 1762 theReindexParameters.getOptimizeStorage() != ReindexParameters.OptimizeStorageModeEnum.NONE; 1763 1764 if (correctCurrentVersion) { 1765 CorrectCurrentVersionResponse outcome = 1766 reindexCorrectCurrentVersion(entity, theReindexParameters.getReindexSearchParameters()); 1767 if (outcome.forceReindexSps()) { 1768 reindexSearchParameters = true; 1769 } 1770 if (outcome.forceReloadResourceEntity() && (reindexSearchParameters || optimizeStorage)) { 1771 entity = myEntityManager.find(ResourceTable.class, jpaPid); 1772 if (outcome.currentVersion() != null) { 1773 entity.setCurrentVersionEntity(outcome.currentVersion()); 1774 } 1775 } 1776 } 1777 1778 if (reindexSearchParameters) { 1779 reindexSearchParameters(entity, retVal, theTransactionDetails); 1780 } 1781 1782 if (optimizeStorage) { 1783 reindexOptimizeStorage(entity, theReindexParameters.getOptimizeStorage()); 1784 } 1785 1786 return retVal; 1787 } 1788 1789 /** 1790 * @return Returns an updated copy of the entity if it was modified (returns null otherwise) 1791 */ 1792 private CorrectCurrentVersionResponse reindexCorrectCurrentVersion( 1793 ResourceTable theEntity, ReindexParameters.ReindexSearchParametersEnum theReindexSearchParameters) { 1794 ResourceHistoryTable version = theEntity.getCurrentVersionEntity(); 1795 1796 if (version == null) { 1797 Optional<ResourceHistoryTable> currentVersionOpt = myResourceHistoryTableDao 1798 .findVersionsForResource( 1799 SINGLE_RESULT, theEntity.getResourceId().toFk()) 1800 .findFirst(); 1801 1802 if (currentVersionOpt.isPresent()) { 1803 Long newVersion = currentVersionOpt.get().getVersion(); 1804 if (newVersion == theEntity.getVersion()) { 1805 // Already have the correct version 1806 return null; 1807 } 1808 1809 ourLog.info( 1810 "Correcting current version for {}/{} (PID {}) from version {} to version {}", 1811 theEntity.getResourceType(), 1812 theEntity.getFhirId(), 1813 theEntity.getResourceId(), 1814 theEntity.getVersion(), 1815 newVersion); 1816 1817 /* 1818 * We are going to manually change (i.e. correct) the version number on the HFJ_RESOURCE table. The 1819 * version number is used as the optimistic locking key, so we need to detach the entity first, or 1820 * we'll get an optimistic lock failure later. 1821 */ 1822 myEntityManager.detach(theEntity); 1823 1824 myResourceTableDao.updateVersionAndLastUpdated(theEntity.getId(), newVersion, new Date()); 1825 1826 return new CorrectCurrentVersionResponse(false, true, currentVersionOpt.get()); 1827 1828 } else { 1829 ourLog.info( 1830 "No versions exist for {}/{} (PID {}), marking resource as deleted", 1831 theEntity.getResourceType(), 1832 theEntity.getFhirId(), 1833 theEntity.getResourceId()); 1834 theEntity.setDeleted(new Date()); 1835 return new CorrectCurrentVersionResponse(true, false, null); 1836 } 1837 } 1838 1839 return new CorrectCurrentVersionResponse(false, false, null); 1840 } 1841 1842 @SuppressWarnings("unchecked") 1843 private void reindexSearchParameters( 1844 ResourceTable entity, ReindexOutcome theReindexOutcome, TransactionDetails theTransactionDetails) { 1845 try { 1846 T resource = (T) myJpaStorageResourceParser.toResource(entity, false); 1847 reindexSearchParameters(resource, entity, theTransactionDetails); 1848 } catch (Exception e) { 1849 ourLog.warn("Failure during reindex: {}", e.toString()); 1850 theReindexOutcome.addWarning("Failed to reindex resource " + entity.getIdDt() + ": " + e); 1851 myResourceTableDao.updateIndexStatus(entity.getId(), EntityIndexStatusEnum.INDEXING_FAILED); 1852 } 1853 } 1854 1855 /** 1856 * @deprecated Use {@link #reindex(IResourcePersistentId, ReindexParameters, RequestDetails, TransactionDetails)} 1857 */ 1858 @Deprecated 1859 @Override 1860 public void reindex(T theResource, IBasePersistedResource theEntity) { 1861 assert TransactionSynchronizationManager.isActualTransactionActive(); 1862 ResourceTable entity = (ResourceTable) theEntity; 1863 TransactionDetails transactionDetails = new TransactionDetails(entity.getUpdatedDate()); 1864 1865 reindexSearchParameters(theResource, theEntity, transactionDetails); 1866 } 1867 1868 private void reindexSearchParameters( 1869 T theResource, IBasePersistedResource theEntity, TransactionDetails transactionDetails) { 1870 ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getPersistentId()); 1871 if (theResource != null) { 1872 CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); 1873 } 1874 1875 SystemRequestDetails request = new SystemRequestDetails(); 1876 request.getUserData().put(JpaConstants.SKIP_REINDEX_ON_UPDATE, Boolean.TRUE); 1877 1878 updateEntity( 1879 request, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false); 1880 if (theResource != null) { 1881 CURRENTLY_REINDEXING.put(theResource, null); 1882 } 1883 } 1884 1885 private void reindexOptimizeStorage( 1886 ResourceTable entity, ReindexParameters.OptimizeStorageModeEnum theOptimizeStorageMode) { 1887 ResourceHistoryTable historyEntity = entity.getCurrentVersionEntity(); 1888 if (historyEntity != null) { 1889 reindexOptimizeStorageHistoryEntity(entity, historyEntity); 1890 if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) { 1891 int pageSize = 100; 1892 for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) { 1893 1894 // We need to sort the pages, because we are updating the same data we are paging through. 1895 // If not sorted explicitly, a database like Postgres returns the same data multiple times on 1896 // different pages as the underlying data gets updated. 1897 PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("myId")); 1898 Slice<ResourceHistoryTable> historyEntities = 1899 myResourceHistoryTableDao.findAllVersionsExceptSpecificForResourcePid( 1900 pageRequest, entity.getId().toFk(), historyEntity.getVersion()); 1901 1902 for (ResourceHistoryTable next : historyEntities) { 1903 reindexOptimizeStorageHistoryEntity(entity, next); 1904 } 1905 } 1906 } 1907 } 1908 } 1909 1910 /** 1911 * Note that the entity will be detached after being saved if it has changed 1912 * in order to avoid growing the number of resources in memory to be too big 1913 */ 1914 private void reindexOptimizeStorageHistoryEntity(ResourceTable entity, ResourceHistoryTable historyEntity) { 1915 if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC 1916 || historyEntity.getEncoding() == ResourceEncodingEnum.JSON) { 1917 byte[] resourceBytes = historyEntity.getResource(); 1918 if (resourceBytes != null) { 1919 String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding()); 1920 myResourceHistoryCalculator.conditionallyAlterHistoryEntity(entity, historyEntity, resourceText); 1921 } 1922 } 1923 if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) { 1924 if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) { 1925 IdAndPartitionId id = historyEntity.getId().asIdAndPartitionId(); 1926 Optional<ResourceHistoryProvenanceEntity> provenanceEntityOpt = 1927 myResourceHistoryProvenanceDao.findById(id); 1928 if (provenanceEntityOpt.isPresent()) { 1929 ResourceHistoryProvenanceEntity provenanceEntity = provenanceEntityOpt.get(); 1930 historyEntity.setSourceUri(provenanceEntity.getSourceUri()); 1931 historyEntity.setRequestId(provenanceEntity.getRequestId()); 1932 myResourceHistoryProvenanceDao.delete(provenanceEntity); 1933 } 1934 } 1935 } 1936 } 1937 1938 private BaseHasResource<?> readEntity(IIdType theId, RequestPartitionId requestPartitionId) { 1939 validateResourceTypeAndThrowInvalidRequestException(theId); 1940 1941 BaseHasResource entity; 1942 JpaPid pid = myIdHelperService 1943 .resolveResourceIdentity( 1944 requestPartitionId, 1945 getResourceName(), 1946 theId.getIdPart(), 1947 ResolveIdentityMode.includeDeleted().cacheOk()) 1948 .getPersistentId(); 1949 Set<Integer> readPartitions = null; 1950 if (requestPartitionId.isAllPartitions()) { 1951 entity = myEntityManager.find(ResourceTable.class, pid); 1952 } else { 1953 readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId); 1954 if (readPartitions.size() == 1) { 1955 if (readPartitions.contains(null)) { 1956 entity = myResourceTableDao 1957 .readByPartitionIdNull(pid.getId()) 1958 .orElse(null); 1959 } else { 1960 entity = myResourceTableDao 1961 .readByPartitionId(readPartitions.iterator().next(), pid.getId()) 1962 .orElse(null); 1963 } 1964 } else { 1965 if (readPartitions.contains(null)) { 1966 List<Integer> readPartitionsWithoutNull = 1967 readPartitions.stream().filter(Objects::nonNull).collect(Collectors.toList()); 1968 entity = myResourceTableDao 1969 .readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getId()) 1970 .orElse(null); 1971 } else { 1972 entity = myResourceTableDao 1973 .readByPartitionIds(readPartitions, pid.getId()) 1974 .orElse(null); 1975 } 1976 } 1977 } 1978 1979 // Verify that the resource is for the correct partition 1980 if (entity != null && readPartitions != null && entity.getPartitionId() != null) { 1981 if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) { 1982 ourLog.debug( 1983 "Performing a read for PartitionId={} but entity has partition: {}", 1984 requestPartitionId, 1985 entity.getPartitionId()); 1986 entity = null; 1987 } 1988 } 1989 1990 if (theId.hasVersionIdPart()) { 1991 if (!theId.isVersionIdPartValidLong()) { 1992 throw new ResourceNotFoundException(Msg.code(978) 1993 + getContext() 1994 .getLocalizer() 1995 .getMessageSanitized( 1996 BaseStorageDao.class, 1997 "invalidVersion", 1998 theId.getVersionIdPart(), 1999 theId.toUnqualifiedVersionless())); 2000 } 2001 if (entity.getVersion() != theId.getVersionIdPartAsLong()) { 2002 entity = null; 2003 } 2004 } 2005 2006 if (entity == null) { 2007 if (theId.hasVersionIdPart()) { 2008 TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery( 2009 "SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", 2010 ResourceHistoryTable.class); 2011 q.setParameter("RID", pid.getId()); 2012 q.setParameter("RTYP", myResourceName); 2013 q.setParameter("RVER", theId.getVersionIdPartAsLong()); 2014 try { 2015 entity = q.getSingleResult(); 2016 } catch (NoResultException e) { 2017 throw new ResourceNotFoundException(Msg.code(979) 2018 + getContext() 2019 .getLocalizer() 2020 .getMessageSanitized( 2021 BaseStorageDao.class, 2022 "invalidVersion", 2023 theId.getVersionIdPart(), 2024 theId.toUnqualifiedVersionless())); 2025 } 2026 } 2027 } 2028 2029 if (entity == null) { 2030 throw new ResourceNotFoundException(Msg.code(1996) + "Resource " + theId + " is not known"); 2031 } 2032 2033 validateResourceType(entity); 2034 validateGivenIdIsAppropriateToRetrieveResource(theId, entity); 2035 2036 return entity; 2037 } 2038 2039 @Override 2040 protected IBasePersistedResource readEntityLatestVersion( 2041 IResourcePersistentId thePersistentId, 2042 RequestDetails theRequestDetails, 2043 TransactionDetails theTransactionDetails) { 2044 JpaPid jpaPid = (JpaPid) thePersistentId; 2045 return myEntityManager.find(ResourceTable.class, jpaPid); 2046 } 2047 2048 @Override 2049 @Nonnull 2050 protected ResourceTable readEntityLatestVersion( 2051 IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 2052 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 2053 theRequestDetails, getResourceName(), theId); 2054 2055 return myTransactionService 2056 .withRequest(theRequestDetails) 2057 .withRequestPartitionId(requestPartitionId) 2058 .execute(() -> 2059 readEntityLatestVersion(theRequestDetails, theId, requestPartitionId, theTransactionDetails)); 2060 } 2061 2062 @Nonnull 2063 private ResourceTable readEntityLatestVersion( 2064 RequestDetails theRequestDetails, 2065 IIdType theId, 2066 @Nonnull RequestPartitionId theRequestPartitionId, 2067 TransactionDetails theTransactionDetails) { 2068 HapiTransactionService.requireTransaction(); 2069 2070 IIdType id = theId; 2071 validateResourceTypeAndThrowInvalidRequestException(id); 2072 if (!id.hasResourceType()) { 2073 id = id.withResourceType(getResourceName()); 2074 } 2075 2076 JpaPid persistentId = null; 2077 if (theTransactionDetails != null) { 2078 if (theTransactionDetails.isResolvedResourceIdEmpty(id.toUnqualifiedVersionless())) { 2079 throw new ResourceNotFoundException(Msg.code(1997) + id); 2080 } 2081 if (theTransactionDetails.hasResolvedResourceIds()) { 2082 persistentId = (JpaPid) theTransactionDetails.getResolvedResourceId(id); 2083 } 2084 } 2085 2086 if (persistentId == null) { 2087 persistentId = myIdHelperService.resolveResourceIdentityPid( 2088 theRequestPartitionId, 2089 id.getResourceType(), 2090 id.getIdPart(), 2091 ResolveIdentityMode.includeDeleted().cacheOk()); 2092 } 2093 2094 ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId); 2095 if (entity == null) { 2096 throw new ResourceNotFoundException(Msg.code(1998) + id); 2097 } 2098 validateGivenIdIsAppropriateToRetrieveResource(id, entity); 2099 return entity; 2100 } 2101 2102 @Transactional 2103 @Override 2104 public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { 2105 removeTag(theId, theTagType, theScheme, theTerm, null); 2106 } 2107 2108 @Transactional 2109 @Override 2110 public void removeTag( 2111 IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) { 2112 StopWatch w = new StopWatch(); 2113 BaseHasResource<?> entity = readEntity(theId, theRequest); 2114 if (entity == null) { 2115 throw new ResourceNotFoundException(Msg.code(1999) + theId); 2116 } 2117 2118 for (BaseTag next : new ArrayList<>(entity.getTags())) { 2119 if (Objects.equals(next.getTag().getTagType(), theTagType) 2120 && Objects.equals(next.getTag().getSystem(), theScheme) 2121 && Objects.equals(next.getTag().getCode(), theTerm)) { 2122 myEntityManager.remove(next); 2123 entity.getTags().remove(next); 2124 } 2125 } 2126 2127 if (entity.getTags().isEmpty()) { 2128 entity.setHasTags(false); 2129 } 2130 2131 myEntityManager.merge(entity); 2132 2133 ourLog.debug( 2134 "Processed remove tag {}/{} on {} in {}ms", 2135 theScheme, 2136 theTerm, 2137 theId.getValue(), 2138 w.getMillisAndRestart()); 2139 } 2140 2141 /** 2142 * @deprecated Use {@link #search(SearchParameterMap, RequestDetails)} instead 2143 */ 2144 @Override 2145 public IBundleProvider search(final SearchParameterMap theParams) { 2146 return search(theParams, null); 2147 } 2148 2149 @Override 2150 public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest) { 2151 return search(theParams, theRequest, null); 2152 } 2153 2154 @Override 2155 public IBundleProvider search( 2156 final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) { 2157 2158 if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) { 2159 throw new MethodNotAllowedException(Msg.code(983) + "Contained mode 'both' is not currently supported"); 2160 } 2161 if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE 2162 && !myStorageSettings.isIndexOnContainedResources()) { 2163 throw new MethodNotAllowedException( 2164 Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server"); 2165 } 2166 2167 translateListSearchParams(theParams); 2168 2169 setOffsetAndCount(theParams, theRequest); 2170 2171 CacheControlDirective cacheControlDirective = new CacheControlDirective(); 2172 if (theRequest != null) { 2173 cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)); 2174 } 2175 2176 IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch( 2177 this, theParams, getResourceName(), cacheControlDirective, theRequest); 2178 2179 if (retVal instanceof PersistedJpaBundleProvider provider) { 2180 // Note: we calculate the partition -after- calling registerSearch, since that 2181 // method invokes several interceptors that can affect the partition selection. 2182 RequestPartitionId requestPartitionId = 2183 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2184 theRequest, getResourceName(), theParams); 2185 2186 provider.setRequestPartitionId(requestPartitionId); 2187 if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) { 2188 if (theServletResponse != null && theRequest != null) { 2189 String value = "HIT from " + theRequest.getFhirServerBase(); 2190 theServletResponse.addHeader(Constants.HEADER_X_CACHE, value); 2191 } 2192 } 2193 } 2194 2195 return retVal; 2196 } 2197 2198 private void translateListSearchParams(SearchParameterMap theParams) { 2199 2200 Set<Map.Entry<String, List<List<IQueryParameterType>>>> entryHashSet = new HashSet<>(theParams.entrySet()); 2201 2202 // Translate _list=42 to _has=List:item:_id=42 2203 for (Map.Entry<String, List<List<IQueryParameterType>>> stringListEntry : entryHashSet) { 2204 String key = stringListEntry.getKey(); 2205 if (Constants.PARAM_LIST.equals((key))) { 2206 List<List<IQueryParameterType>> andOrValues = theParams.get(key); 2207 theParams.remove(key); 2208 List<List<IQueryParameterType>> hasParamValues = new ArrayList<>(); 2209 for (List<IQueryParameterType> orValues : andOrValues) { 2210 List<IQueryParameterType> orList = new ArrayList<>(); 2211 for (IQueryParameterType value : orValues) { 2212 orList.add(new HasParam( 2213 "List", ListResource.SP_ITEM, BaseResource.SP_RES_ID, value.getValueAsQueryToken())); 2214 } 2215 hasParamValues.add(orList); 2216 } 2217 theParams.put(Constants.PARAM_HAS, hasParamValues); 2218 } 2219 } 2220 } 2221 2222 protected void setOffsetAndCount(SearchParameterMap theParams, RequestDetails theRequest) { 2223 if (theRequest != null) { 2224 2225 if (theRequest.isSubRequest()) { 2226 Integer max = getStorageSettings().getMaximumSearchResultCountInTransaction(); 2227 if (max != null) { 2228 Validate.inclusiveBetween( 2229 1, 2230 Integer.MAX_VALUE, 2231 max, 2232 "Maximum search result count in transaction must be a positive integer"); 2233 theParams.setLoadSynchronousUpTo(getStorageSettings().getMaximumSearchResultCountInTransaction()); 2234 } 2235 } 2236 2237 final Integer offset = RestfulServerUtils.extractOffsetParameter(theRequest); 2238 if (offset != null || !isPagingProviderDatabaseBacked(theRequest)) { 2239 theParams.setLoadSynchronous(true); 2240 if (offset != null) { 2241 Validate.inclusiveBetween(0, Integer.MAX_VALUE, offset, "Offset must be a positive integer"); 2242 } 2243 theParams.setOffset(offset); 2244 } 2245 2246 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 2247 if (count != null) { 2248 Integer maxPageSize = theRequest.getServer().getMaximumPageSize(); 2249 if (maxPageSize != null && count > maxPageSize) { 2250 ourLog.info( 2251 "Reducing {} from {} to {} which is the maximum allowable page size.", 2252 Constants.PARAM_COUNT, 2253 count, 2254 maxPageSize); 2255 count = maxPageSize; 2256 } 2257 theParams.setCount(count); 2258 } else if (theRequest.getServer().getDefaultPageSize() != null) { 2259 theParams.setCount(theRequest.getServer().getDefaultPageSize()); 2260 } 2261 } 2262 } 2263 2264 @Override 2265 public List<JpaPid> searchForIds( 2266 SearchParameterMap theParams, 2267 RequestDetails theRequest, 2268 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 2269 TransactionDetails transactionDetails = new TransactionDetails(); 2270 RequestPartitionId requestPartitionId = 2271 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2272 theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull); 2273 2274 myStorageInterceptorHooks.callStoragePresearchRegistered( 2275 theRequest, theParams, new NonPersistedSearch(getResourceName()), requestPartitionId); 2276 2277 return myTransactionService 2278 .withRequest(theRequest) 2279 .withTransactionDetails(transactionDetails) 2280 .withRequestPartitionId(requestPartitionId) 2281 .searchList(() -> { 2282 if (isNull(theParams.getLoadSynchronousUpTo())) { 2283 theParams.setLoadSynchronousUpTo(myStorageSettings.getInternalSynchronousSearchSize()); 2284 } 2285 2286 ISearchBuilder<JpaPid> builder = 2287 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2288 2289 List<JpaPid> ids = new ArrayList<>(); 2290 2291 String uuid = UUID.randomUUID().toString(); 2292 2293 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2294 try (IResultIterator<JpaPid> iter = 2295 builder.createQuery(theParams, searchRuntimeDetails, theRequest, requestPartitionId)) { 2296 while (iter.hasNext()) { 2297 ids.add(iter.next()); 2298 } 2299 } catch (IOException e) { 2300 ourLog.error("IO failure during database access", e); 2301 } 2302 2303 return ids; 2304 }); 2305 } 2306 2307 @Override 2308 public <PID extends IResourcePersistentId<?>> Stream<PID> searchForIdStream( 2309 SearchParameterMap theParams, 2310 RequestDetails theRequest, 2311 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 2312 2313 // the Stream is useless outside the bound connection time, so require our caller to have a session. 2314 HapiTransactionService.requireTransaction(); 2315 2316 RequestPartitionId requestPartitionId = 2317 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2318 theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull); 2319 2320 ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2321 2322 String uuid = UUID.randomUUID().toString(); 2323 2324 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2325 //noinspection unchecked 2326 return (Stream<PID>) myTransactionService 2327 .withRequest(theRequest) 2328 .search(() -> 2329 builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, requestPartitionId)); 2330 } 2331 2332 @Override 2333 public List<T> searchForResources(SearchParameterMap theParams, RequestDetails theRequest) { 2334 return searchForTransformedIds(theParams, theRequest, this::pidsToResource); 2335 } 2336 2337 @Override 2338 public List<IIdType> searchForResourceIds(SearchParameterMap theParams, RequestDetails theRequest) { 2339 return searchForTransformedIds(theParams, theRequest, this::pidsToIds); 2340 } 2341 2342 private <V> List<V> searchForTransformedIds( 2343 SearchParameterMap theParams, 2344 RequestDetails theRequest, 2345 TriFunction<RequestDetails, Stream<JpaPid>, RequestPartitionId, Stream<V>> transform) { 2346 RequestPartitionId requestPartitionId = 2347 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2348 theRequest, myResourceName, theParams); 2349 2350 String uuid = UUID.randomUUID().toString(); 2351 2352 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2353 return myTransactionService 2354 .withRequest(theRequest) 2355 .withPropagation(Propagation.REQUIRED) 2356 .searchList(() -> { 2357 ISearchBuilder<JpaPid> builder = 2358 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2359 Stream<JpaPid> pidStream = 2360 builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, requestPartitionId); 2361 2362 Stream<V> transformedStream = transform.apply(theRequest, pidStream, requestPartitionId); 2363 2364 return transformedStream.collect(Collectors.toList()); 2365 }); 2366 } 2367 2368 /** 2369 * Fetch the resources in chunks and apply PreAccess/PreShow interceptors. 2370 */ 2371 @Nonnull 2372 private Stream<T> pidsToResource( 2373 RequestDetails theRequest, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) { 2374 ISearchBuilder<JpaPid> searchBuilder = 2375 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2376 @SuppressWarnings("unchecked") 2377 Stream<T> resourceStream = (Stream<T>) new QueryChunker<>() 2378 .chunk(thePidStream, SearchBuilder.getMaximumPageSize()) 2379 .flatMap(pidChunk -> searchBuilder.loadResourcesByPid(pidChunk, theRequest).stream()); 2380 // apply interceptors 2381 return resourceStream 2382 .flatMap(resource -> resource == null 2383 ? Stream.empty() 2384 : invokeStoragePreAccessResources(theRequest, resource).stream()) 2385 .flatMap(resource -> Optional.ofNullable(invokeStoragePreShowResources(theRequest, resource)).stream()); 2386 } 2387 2388 /** 2389 * get the Ids from the ResourceTable entities in chunks. 2390 */ 2391 @Nonnull 2392 private Stream<IIdType> pidsToIds( 2393 RequestDetails theRequestDetails, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) { 2394 2395 Stream<JpaPid> pidStream = thePidStream; 2396 2397 if (!theRequestPartitionId.isAllPartitions() 2398 && theRequestPartitionId.getPartitionIds().size() == 1) { 2399 pidStream = pidStream.map(t -> t.setPartitionIdIfNotAlreadySet( 2400 theRequestPartitionId.getPartitionIds().get(0))); 2401 } 2402 2403 return new QueryChunker<>() 2404 .chunk(pidStream, SearchBuilder.getMaximumPageSize()) 2405 .flatMap(ids -> myResourceTableDao.findAllById(ids).stream()) 2406 .map(ResourceTable::getIdDt); 2407 } 2408 2409 protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) { 2410 MT retVal = ReflectionUtil.newInstance(theType); 2411 for (TagDefinition next : tagDefinitions) { 2412 switch (next.getTagType()) { 2413 case PROFILE: 2414 retVal.addProfile(next.getCode()); 2415 break; 2416 case SECURITY_LABEL: 2417 retVal.addSecurity() 2418 .setSystem(next.getSystem()) 2419 .setCode(next.getCode()) 2420 .setDisplay(next.getDisplay()); 2421 break; 2422 case TAG: 2423 retVal.addTag() 2424 .setSystem(next.getSystem()) 2425 .setCode(next.getCode()) 2426 .setDisplay(next.getDisplay()); 2427 break; 2428 } 2429 } 2430 myMetaTagSorter.sort(retVal); 2431 return retVal; 2432 } 2433 2434 private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) { 2435 ArrayList<TagDefinition> retVal = new ArrayList<>(); 2436 2437 for (IBaseCoding next : theMeta.getTag()) { 2438 retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay())); 2439 } 2440 for (IBaseCoding next : theMeta.getSecurity()) { 2441 retVal.add( 2442 new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay())); 2443 } 2444 for (IPrimitiveType<String> next : theMeta.getProfile()) { 2445 retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null)); 2446 } 2447 2448 return retVal; 2449 } 2450 2451 /** 2452 * @deprecated Use {@link #update(T, RequestDetails)} instead 2453 */ 2454 @Override 2455 public DaoMethodOutcome update(T theResource) { 2456 return update(theResource, null, null); 2457 } 2458 2459 @Override 2460 public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) { 2461 return update(theResource, null, theRequestDetails); 2462 } 2463 2464 /** 2465 * @deprecated Use {@link #update(T, String, RequestDetails)} instead 2466 */ 2467 @Override 2468 public DaoMethodOutcome update(T theResource, String theMatchUrl) { 2469 return update(theResource, theMatchUrl, null); 2470 } 2471 2472 @Override 2473 public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { 2474 return update(theResource, theMatchUrl, true, theRequestDetails); 2475 } 2476 2477 @Override 2478 public DaoMethodOutcome update( 2479 T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { 2480 return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails, new TransactionDetails()); 2481 } 2482 2483 @Override 2484 public DaoMethodOutcome update( 2485 T theResource, 2486 String theMatchUrl, 2487 boolean thePerformIndexing, 2488 boolean theForceUpdateVersion, 2489 RequestDetails theRequest, 2490 @Nonnull TransactionDetails theTransactionDetails) { 2491 if (theResource == null) { 2492 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody"); 2493 throw new InvalidRequestException(Msg.code(986) + msg); 2494 } 2495 if (!theResource.getIdElement().hasIdPart() && isBlank(theMatchUrl)) { 2496 String type = myFhirContext.getResourceType(theResource); 2497 String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "updateWithNoId", type); 2498 throw new InvalidRequestException(Msg.code(987) + msg); 2499 } 2500 2501 /* 2502 * Resource updates will modify/update the version of the resource with the new version. This is generally helpful 2503 * but leads to issues if the transaction is rolled back and retried. So if we do a rollback, we reset the resource 2504 * version to what it was. 2505 */ 2506 String id = theResource.getIdElement().getValue(); 2507 Runnable onRollback = () -> { 2508 ourLog.trace("Rolling back from {} to {}", theResource.getIdElement(), id); 2509 theResource.getIdElement().setValue(id); 2510 }; 2511 theTransactionDetails.addRollbackUndoAction(onRollback); 2512 2513 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest( 2514 theRequest, theResource, getResourceName()); 2515 2516 boolean rewriteHistory = theRequest != null && theRequest.isRewriteHistory(); 2517 if (rewriteHistory && !myStorageSettings.isUpdateWithHistoryRewriteEnabled()) { 2518 String msg = 2519 getContext().getLocalizer().getMessage(BaseStorageDao.class, "updateWithHistoryRewriteDisabled"); 2520 throw new InvalidRequestException(Msg.code(2781) + msg); 2521 } 2522 2523 Callable<DaoMethodOutcome> updateCallback; 2524 if (rewriteHistory) { 2525 updateCallback = () -> doUpdateWithHistoryRewrite( 2526 theResource, theRequest, theTransactionDetails, requestPartitionId, RestOperationTypeEnum.UPDATE); 2527 } else { 2528 updateCallback = () -> doUpdate( 2529 theResource, 2530 theMatchUrl, 2531 thePerformIndexing, 2532 theForceUpdateVersion, 2533 theRequest, 2534 theTransactionDetails, 2535 requestPartitionId); 2536 } 2537 2538 // Execute the update in a retryable transaction 2539 return myTransactionService 2540 .withRequest(theRequest) 2541 .withTransactionDetails(theTransactionDetails) 2542 .withRequestPartitionId(requestPartitionId) 2543 .execute(updateCallback); 2544 } 2545 2546 private DaoMethodOutcome doUpdate( 2547 T theResource, 2548 String theMatchUrl, 2549 boolean thePerformIndexing, 2550 boolean theForceUpdateVersion, 2551 RequestDetails theRequest, 2552 TransactionDetails theTransactionDetails, 2553 RequestPartitionId theRequestPartitionId) { 2554 DaoMethodOutcome outcome = null; 2555 preProcessResourceForStorage(theResource); 2556 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); 2557 2558 ResourceTable entity = null; 2559 2560 IIdType resourceId = null; 2561 RestOperationTypeEnum update = RestOperationTypeEnum.UPDATE; 2562 if (isNotBlank(theMatchUrl)) { 2563 // Validate that the supplied resource matches the conditional. 2564 Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl( 2565 theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource, theRequestPartitionId); 2566 if (match.size() > 1) { 2567 String msg = getContext() 2568 .getLocalizer() 2569 .getMessageSanitized( 2570 BaseStorageDao.class, 2571 "transactionOperationWithMultipleMatchFailure", 2572 "UPDATE", 2573 myResourceName, 2574 theMatchUrl, 2575 match.size()); 2576 throw new PreconditionFailedException(Msg.code(988) + msg); 2577 } else if (match.size() == 1) { 2578 JpaPid pid = match.iterator().next(); 2579 entity = myEntityManager.find(ResourceTable.class, pid); 2580 resourceId = entity.getIdDt(); 2581 if (myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4) 2582 && theResource.getIdElement().getIdPart() != null) { 2583 if (!Objects.equals(theResource.getIdElement().getIdPart(), resourceId.getIdPart())) { 2584 String msg = getContext() 2585 .getLocalizer() 2586 .getMessageSanitized( 2587 BaseStorageDao.class, 2588 "transactionOperationWithIdNotMatchFailure", 2589 "UPDATE", 2590 theMatchUrl); 2591 throw new InvalidRequestException(Msg.code(2279) + msg); 2592 } 2593 } 2594 } else { 2595 // assign UUID if no id provided in the request (numeric id mode is handled in doCreateForPostOrPut) 2596 if (!theResource.getIdElement().hasIdPart() 2597 && getStorageSettings().getResourceServerIdStrategy() 2598 == JpaStorageSettings.IdStrategyEnum.UUID) { 2599 theResource.setId(UUID.randomUUID().toString()); 2600 theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); 2601 } 2602 outcome = doCreateForPostOrPut( 2603 theRequest, 2604 theResource, 2605 theMatchUrl, 2606 false, 2607 thePerformIndexing, 2608 theRequestPartitionId, 2609 update, 2610 theTransactionDetails); 2611 2612 // Pre-cache the match URL 2613 if (outcome.getPersistentId() != null) { 2614 myMatchResourceUrlService.matchUrlResolved( 2615 theTransactionDetails, getResourceName(), theMatchUrl, (JpaPid) outcome.getPersistentId()); 2616 } 2617 } 2618 } else { 2619 /* 2620 * Note: resourceId will not be null or empty here, because we 2621 * check it and reject requests in 2622 * BaseOutcomeReturningMethodBindingWithResourceParam 2623 */ 2624 resourceId = theResource.getIdElement(); 2625 assert resourceId != null; 2626 assert resourceId.hasIdPart(); 2627 2628 if (!resourceId.hasResourceType()) { 2629 resourceId = resourceId.withResourceType(getResourceName()); 2630 } 2631 2632 boolean create = false; 2633 2634 if (theRequest != null) { 2635 String existenceCheck = theRequest.getHeader(JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK); 2636 if (JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK_DISABLED.equals(existenceCheck)) { 2637 create = true; 2638 } 2639 } 2640 2641 if (!create) { 2642 if (theTransactionDetails != null && theTransactionDetails.hasNullResolvedResourceId(resourceId)) { 2643 create = true; 2644 } else { 2645 try { 2646 entity = readEntityLatestVersion( 2647 theRequest, resourceId, theRequestPartitionId, theTransactionDetails); 2648 if (myPartitionSettings.isPartitioningEnabled()) { 2649 validatePartitionIdMatch(theResource.getIdElement(), theRequestPartitionId, entity); 2650 } 2651 } catch (ResourceNotFoundException e) { 2652 create = true; 2653 } 2654 } 2655 } 2656 2657 if (create) { 2658 outcome = doCreateForPostOrPut( 2659 theRequest, 2660 theResource, 2661 null, 2662 false, 2663 thePerformIndexing, 2664 theRequestPartitionId, 2665 update, 2666 theTransactionDetails); 2667 } 2668 } 2669 2670 // Start 2671 if (outcome == null) { 2672 UpdateParameters updateParameters = new UpdateParameters<>() 2673 .setRequestDetails(theRequest) 2674 .setResourceIdToUpdate(resourceId) 2675 .setMatchUrl(theMatchUrl) 2676 .setShouldPerformIndexing(thePerformIndexing) 2677 .setShouldForceUpdateVersion(theForceUpdateVersion) 2678 .setResource(theResource) 2679 .setEntity(entity) 2680 .setOperationType(update) 2681 .setTransactionDetails(theTransactionDetails) 2682 .setShouldForcePopulateOldResourceForProcessing(false); 2683 2684 outcome = doUpdateForUpdateOrPatch(updateParameters); 2685 } 2686 2687 postUpdateTransaction(theTransactionDetails); 2688 2689 return outcome; 2690 } 2691 2692 @SuppressWarnings("rawtypes") 2693 protected void postUpdateTransaction(TransactionDetails theTransactionDetails) { 2694 // Transactions will delete these at the end of the entire transaction 2695 if (!theTransactionDetails.isFhirTransaction()) { 2696 Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds(); 2697 if (resourceIds != null && !resourceIds.isEmpty()) { 2698 @Nonnull 2699 List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList()); 2700 myResourceSearchUrlSvc.deleteByResIds(ids); 2701 } 2702 } 2703 } 2704 2705 @Override 2706 protected DaoMethodOutcome doUpdateForUpdateOrPatch(UpdateParameters<T> theUpdateParameters) { 2707 2708 /* 2709 * We stored a resource searchUrl at creation time to prevent resource duplication. 2710 * We'll clear any currently existing urls from the db, otherwise we could hit 2711 * duplicate index violations if we try to add another (after this create/update) 2712 */ 2713 ResourceTable entity = (ResourceTable) theUpdateParameters.getEntity(); 2714 2715 /* 2716 * If we read back the entity from hibernate, and it's already saying 2717 * it's been updated in this transaction, then someone is probably 2718 * doing multiple modifications of the same resource within the same 2719 * database transaction. The only way for this to work cleanly is for 2720 * us to flush hibernate now. That way we can increment the version 2721 * a second time, create multiple history entries, etc. 2722 */ 2723 if (entity != null && entity.isVersionUpdatedInCurrentTransaction()) { 2724 myEntityManager.flush(); 2725 } 2726 2727 if (entity.isSearchUrlPresent()) { 2728 JpaPid persistentId = entity.getResourceId(); 2729 theUpdateParameters.getTransactionDetails().addUpdatedResourceId(persistentId); 2730 2731 entity.setSearchUrlPresent(false); // it will be removed at the end 2732 } 2733 2734 if (entity.isDeleted()) { 2735 // We're un-deleting this entity so let's inform the memory cache service 2736 myIdHelperService.addResolvedPidToFhirIdAfterCommit( 2737 entity.getPersistentId(), 2738 entity.getPartitionId() == null 2739 ? RequestPartitionId.defaultPartition() 2740 : entity.getPartitionId().toPartitionId(), 2741 entity.getResourceType(), 2742 entity.getFhirId(), 2743 null); 2744 } 2745 2746 boolean shouldForcePopulateOldResourceForProcessing = myInterceptorBroadcaster instanceof InterceptorService 2747 && ((InterceptorService) myInterceptorBroadcaster) 2748 .hasRegisteredInterceptor(PatientCompartmentEnforcingInterceptor.class); 2749 theUpdateParameters.setShouldForcePopulateOldResourceForProcessing(shouldForcePopulateOldResourceForProcessing); 2750 return super.doUpdateForUpdateOrPatch(theUpdateParameters); 2751 } 2752 2753 /** 2754 * Method for updating the historical version of the resource when a history version id is included in the request. 2755 * 2756 * @param theResource to be saved 2757 * @param theRequest details of the request 2758 * @param theTransactionDetails details of the transaction 2759 * @param theRequestPartitionId the partition on which to perform the request 2760 * @param theRestOperationType the rest operation type (update or patch) 2761 * @return the outcome of the operation 2762 */ 2763 DaoMethodOutcome doUpdateWithHistoryRewrite( 2764 T theResource, 2765 RequestDetails theRequest, 2766 TransactionDetails theTransactionDetails, 2767 RequestPartitionId theRequestPartitionId, 2768 RestOperationTypeEnum theRestOperationType) { 2769 StopWatch w = new StopWatch(); 2770 2771 // No need for indexing as this will update a non-current version of the resource which will not be searchable 2772 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, false); 2773 2774 BaseHasResource entity; 2775 BaseHasResource currentEntity; 2776 2777 IIdType resourceId; 2778 2779 resourceId = theResource.getIdElement(); 2780 assert resourceId != null; 2781 assert resourceId.hasIdPart(); 2782 2783 try { 2784 currentEntity = readEntityLatestVersion( 2785 theRequest, resourceId.toVersionless(), theRequestPartitionId, theTransactionDetails); 2786 2787 if (!resourceId.hasVersionIdPart()) { 2788 throw new InvalidRequestException( 2789 Msg.code(2093) + "Invalid resource ID, ID must contain a history version"); 2790 } 2791 entity = readEntity(resourceId, theRequest); 2792 validateResourceType(entity); 2793 } catch (ResourceNotFoundException e) { 2794 throw new ResourceNotFoundException( 2795 Msg.code(2087) + "Resource not found [" + resourceId + "] - Doesn't exist"); 2796 } 2797 2798 if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) { 2799 throw new UnprocessableEntityException( 2800 Msg.code(2088) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" 2801 + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]"); 2802 } 2803 assert resourceId.hasVersionIdPart(); 2804 2805 boolean wasDeleted = isDeleted(entity); 2806 entity.setDeleted(null); 2807 boolean isUpdatingCurrent = resourceId.hasVersionIdPart() 2808 && Long.parseLong(resourceId.getVersionIdPart()) == currentEntity.getVersion(); 2809 IBasePersistedResource<?> savedEntity = updateHistoryEntity( 2810 theRequest, theResource, currentEntity, entity, resourceId, theTransactionDetails, isUpdatingCurrent); 2811 DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource, null, theRestOperationType) 2812 .setCreated(wasDeleted); 2813 2814 populateOperationOutcomeForUpdate(w, outcome, null, theRestOperationType, theTransactionDetails); 2815 2816 return outcome; 2817 } 2818 2819 @Override 2820 public MethodOutcome validate( 2821 T theResource, 2822 IIdType theId, 2823 String theRawResource, 2824 EncodingEnum theEncoding, 2825 ValidationModeEnum theMode, 2826 String theProfile, 2827 RequestDetails theRequest) { 2828 TransactionDetails transactionDetails = new TransactionDetails(); 2829 2830 if (theMode == ValidationModeEnum.DELETE) { 2831 if (theId == null || !theId.hasIdPart()) { 2832 throw new InvalidRequestException( 2833 Msg.code(991) + "No ID supplied. ID is required when validating with mode=DELETE"); 2834 } 2835 2836 RequestPartitionId requestPartitionId = 2837 myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 2838 theRequest, getResourceName(), theId); 2839 2840 return myTransactionService 2841 .withRequest(theRequest) 2842 .withRequestPartitionId(requestPartitionId) 2843 .execute(() -> { 2844 final ResourceTable entity = 2845 readEntityLatestVersion(theRequest, theId, requestPartitionId, transactionDetails); 2846 2847 // Validate that there are no resources pointing to the candidate that 2848 // would prevent deletion 2849 DeleteConflictList deleteConflicts = new DeleteConflictList(); 2850 if (getStorageSettings().isEnforceReferentialIntegrityOnDelete()) { 2851 myDeleteConflictService.validateOkToDelete( 2852 deleteConflicts, entity, true, theRequest, new TransactionDetails()); 2853 } 2854 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 2855 2856 IBaseOperationOutcome oo = createInfoOperationOutcome("Ok to delete"); 2857 return new MethodOutcome(new IdDt(theId.getValue()), oo); 2858 }); 2859 } 2860 2861 FhirValidator validator = getContext().newValidator(); 2862 validator.setInterceptorBroadcaster( 2863 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest)); 2864 validator.registerValidatorModule(getInstanceValidator()); 2865 validator.registerValidatorModule(new IdChecker(theMode)); 2866 2867 IBaseResource resourceToValidateById = null; 2868 if (theId != null && theId.hasResourceType() && theId.hasIdPart()) { 2869 Class<? extends IBaseResource> type = 2870 getContext().getResourceDefinition(theId.getResourceType()).getImplementingClass(); 2871 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(type); 2872 resourceToValidateById = dao.read(theId, theRequest); 2873 } 2874 2875 ValidationResult result; 2876 ValidationOptions options = new ValidationOptions().addProfileIfNotBlank(theProfile); 2877 2878 if (theResource == null) { 2879 if (resourceToValidateById != null) { 2880 result = validator.validateWithResult(resourceToValidateById, options); 2881 } else { 2882 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "cantValidateWithNoResource"); 2883 throw new InvalidRequestException(Msg.code(992) + msg); 2884 } 2885 } else if (isNotBlank(theRawResource)) { 2886 result = validator.validateWithResult(theRawResource, options); 2887 } else { 2888 result = validator.validateWithResult(theResource, options); 2889 } 2890 2891 MethodOutcome retVal = new MethodOutcome(); 2892 retVal.setOperationOutcome(result.toOperationOutcome()); 2893 // Note an earlier version of this code returned PreconditionFailedException when the validation 2894 // failed, but we since realized the spec requires we return 200 regardless of the validation result. 2895 return retVal; 2896 } 2897 2898 /** 2899 * Get the resource definition from the criteria which specifies the resource type 2900 */ 2901 @Override 2902 public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { 2903 String resourceName; 2904 if (criteria == null || criteria.trim().isEmpty()) { 2905 throw new IllegalArgumentException(Msg.code(994) + "Criteria cannot be empty"); 2906 } 2907 if (criteria.contains("?")) { 2908 resourceName = criteria.substring(0, criteria.indexOf("?")); 2909 } else { 2910 resourceName = criteria; 2911 } 2912 2913 return getContext().getResourceDefinition(resourceName); 2914 } 2915 2916 private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) { 2917 if (!entity.getIdDt().getIdPart().equals(theId.getIdPart())) { 2918 // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning 2919 // that 2920 // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal 2921 // pointer 2922 // to the 2923 // forced ID) 2924 throw new ResourceNotFoundException(Msg.code(2000) + theId); 2925 } 2926 } 2927 2928 private void validateResourceType(IBasePersistedResource<?> entity) { 2929 validateResourceType(entity, myResourceName); 2930 } 2931 2932 private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) { 2933 if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) { 2934 // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database 2935 // exception 2936 throw new InvalidRequestException(Msg.code(996) + "Incorrect resource type (" + theId.getResourceType() 2937 + ") for this DAO, wanted: " + myResourceName); 2938 } 2939 } 2940 2941 @VisibleForTesting 2942 public void setIdHelperSvcForUnitTest(IIdHelperService theIdHelperService) { 2943 myIdHelperService = theIdHelperService; 2944 } 2945 2946 @SuppressWarnings("BooleanMethodIsAlwaysInverted") 2947 public static <T extends IBaseResource> boolean isResourceIdServerAssigned(IBaseResource theResource) { 2948 return Boolean.TRUE.equals(theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED)); 2949 } 2950 2951 private static class IdChecker implements IValidatorModule { 2952 2953 private final ValidationModeEnum myMode; 2954 2955 IdChecker(ValidationModeEnum theMode) { 2956 myMode = theMode; 2957 } 2958 2959 @Override 2960 public void validateResource(IValidationContext<IBaseResource> theCtx) { 2961 IBaseResource resource = theCtx.getResource(); 2962 if (resource instanceof Parameters) { 2963 List<ParametersParameterComponent> params = ((Parameters) resource).getParameter(); 2964 params = params.stream() 2965 .filter(param -> param.getName().contains("resource")) 2966 .collect(Collectors.toList()); 2967 resource = params.get(0).getResource(); 2968 } 2969 boolean hasId = resource.getIdElement().hasIdPart(); 2970 if (myMode == ValidationModeEnum.CREATE) { 2971 if (hasId) { 2972 throw new UnprocessableEntityException( 2973 Msg.code(997) + "Resource has an ID - ID must not be populated for a FHIR create"); 2974 } 2975 } else if (myMode == ValidationModeEnum.UPDATE) { 2976 if (!hasId) { 2977 throw new UnprocessableEntityException( 2978 Msg.code(998) + "Resource has no ID - ID must be populated for a FHIR update"); 2979 } 2980 } 2981 } 2982 } 2983 2984 /** 2985 * Validates that the partition ID of the request resource matches the partition ID of the entity. 2986 * <p> 2987 * If the request is for all partitions, this method does nothing. Otherwise, it checks that the resource type 2988 * and FHIR ID of the entity match those of the provided resource ID type, and that the partition IDs are equal. 2989 * If there is a mismatch, an {@link InvalidRequestException} is thrown. 2990 * 2991 * @param theResourceIdType The resource ID type being processed 2992 * @param theRequestPartitionId The partition ID from the request 2993 * @param theEntity The entity being updated 2994 * @throws InvalidRequestException if the partition IDs do not match for the same resource type and FHIR ID 2995 */ 2996 @VisibleForTesting 2997 protected void validatePartitionIdMatch( 2998 @Nonnull IIdType theResourceIdType, 2999 @Nonnull RequestPartitionId theRequestPartitionId, 3000 @Nonnull ResourceTable theEntity) { 3001 if (theRequestPartitionId.isAllPartitions()) { 3002 return; 3003 } 3004 3005 boolean sameResType = Objects.equals(theEntity.getResourceType(), theResourceIdType.getResourceType()); 3006 boolean sameFhirId = Objects.equals(theResourceIdType.getIdPart(), theEntity.getFhirId()); 3007 boolean differentPartition = 3008 !theRequestPartitionId.isPartition(theEntity.getPartitionId().getPartitionId()); 3009 3010 if (sameResType && sameFhirId && differentPartition) { 3011 String msg = getContext() 3012 .getLocalizer() 3013 .getMessageSanitized( 3014 BaseHapiFhirDao.class, 3015 "resourceTypeAndFhirIdConflictAcrossPartitions", 3016 theEntity.getResourceType(), 3017 theResourceIdType.getIdPart(), 3018 theRequestPartitionId.getFirstPartitionNameOrNull()); 3019 throw new InvalidRequestException(Msg.code(2733) + msg); 3020 } 3021 } 3022 3023 /** 3024 * Method return type for {@link #reindexCorrectCurrentVersion(ResourceTable, ReindexParameters.ReindexSearchParametersEnum)} 3025 */ 3026 private record CorrectCurrentVersionResponse( 3027 boolean forceReindexSps, boolean forceReloadResourceEntity, ResourceHistoryTable currentVersion) {} 3028}