
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 ResourceSearch resourceSearch = myMatchUrlService.getResourceSearch(theUrl); 938 SearchParameterMap paramMap = resourceSearch.getSearchParameterMap(); 939 940 RequestPartitionId requestPartitionId = 941 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 942 theRequestDetails, myResourceName, paramMap); 943 944 return myTransactionService 945 .withRequest(theRequestDetails) 946 .withRequestPartitionId(requestPartitionId) 947 .withTransactionDetails(theTransactionDetails) 948 .execute(tx -> 949 doDeleteByUrl(theUrl, paramMap, deleteConflicts, theTransactionDetails, theRequestDetails)); 950 } 951 952 @Nonnull 953 private DeleteMethodOutcome doDeleteByUrl( 954 String theUrl, 955 SearchParameterMap theParamMap, 956 DeleteConflictList deleteConflicts, 957 TransactionDetails theTransactionDetails, 958 RequestDetails theRequestDetails) { 959 960 theParamMap.setLoadSynchronous(true); 961 Set<JpaPid> resourceIds = 962 myMatchResourceUrlService.search(theParamMap, myResourceType, theRequestDetails, null); 963 964 if (resourceIds.size() > 1) { 965 if (!getStorageSettings().isAllowMultipleDelete()) { 966 throw new PreconditionFailedException(Msg.code(962) 967 + getContext() 968 .getLocalizer() 969 .getMessageSanitized( 970 BaseStorageDao.class, 971 "transactionOperationWithMultipleMatchFailure", 972 "DELETE", 973 myResourceName, 974 theUrl, 975 resourceIds.size())); 976 } 977 // TODO: LD: There is a still a bug on slow deletes: https://github.com/hapifhir/hapi-fhir/issues/5675 978 final long threshold = getStorageSettings().getRestDeleteByUrlResourceIdThreshold(); 979 if (resourceIds.size() > threshold) { 980 throw new PreconditionFailedException(Msg.code(2496) 981 + getContext() 982 .getLocalizer() 983 .getMessageSanitized( 984 BaseStorageDao.class, 985 "deleteByUrlThresholdExceeded", 986 theUrl, 987 resourceIds.size(), 988 threshold)); 989 } 990 } 991 992 return deletePidList(theUrl, resourceIds, deleteConflicts, theRequestDetails, theTransactionDetails); 993 } 994 995 @Override 996 public <P extends IResourcePersistentId> void expunge(Collection<P> theResourceIds, RequestDetails theRequest) { 997 ExpungeOptions options = new ExpungeOptions(); 998 options.setExpungeDeletedResources(true); 999 for (P pid : theResourceIds) { 1000 if (pid instanceof JpaPid) { 1001 ResourceTable entity = myEntityManager.find(ResourceTable.class, pid.getId()); 1002 1003 forceExpungeInExistingTransaction(entity.getIdDt().toVersionless(), options, theRequest); 1004 } else { 1005 ourLog.warn("Unable to process expunge on resource {}", pid); 1006 return; 1007 } 1008 } 1009 } 1010 1011 @Nonnull 1012 @Override 1013 public <P extends IResourcePersistentId> DeleteMethodOutcome deletePidList( 1014 String theUrl, 1015 Collection<P> theResourceIds, 1016 DeleteConflictList theDeleteConflicts, 1017 RequestDetails theRequestDetails, 1018 TransactionDetails theTransactionDetails) { 1019 StopWatch w = new StopWatch(); 1020 TransactionDetails transactionDetails = 1021 theTransactionDetails != null ? theTransactionDetails : new TransactionDetails(); 1022 List<ResourceTable> deletedResources = new ArrayList<>(); 1023 1024 List<IResourcePersistentId<?>> resolvedIds = 1025 theResourceIds.stream().map(t -> (IResourcePersistentId<?>) t).collect(Collectors.toList()); 1026 mySystemDao.preFetchResources(resolvedIds, false); 1027 1028 for (P pid : theResourceIds) { 1029 JpaPid jpaPid = (JpaPid) pid; 1030 1031 // This shouldn't actually need to hit the DB because we pre-fetch above 1032 ResourceTable entity = myEntityManager.find(ResourceTable.class, jpaPid); 1033 deletedResources.add(entity); 1034 1035 T resourceToDelete = 1036 myJpaStorageResourceParser.toResource(theRequestDetails, myResourceType, entity, null, false); 1037 1038 transactionDetails.addDeletedResourceId(pid); 1039 1040 // Notify IServerOperationInterceptors about pre-action call 1041 HookParams hooks = new HookParams() 1042 .add(IBaseResource.class, resourceToDelete) 1043 .add(RequestDetails.class, theRequestDetails) 1044 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1045 .add(TransactionDetails.class, transactionDetails); 1046 doCallHooks(transactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_DELETED, hooks); 1047 1048 myDeleteConflictService.validateOkToDelete( 1049 theDeleteConflicts, entity, false, theRequestDetails, transactionDetails); 1050 1051 // Perform delete 1052 1053 preDelete(resourceToDelete, entity, theRequestDetails); 1054 1055 updateEntityForDelete(theRequestDetails, transactionDetails, entity); 1056 resourceToDelete.setId(entity.getIdDt()); 1057 1058 // Notify JPA interceptors 1059 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 1060 @Override 1061 public void beforeCommit(boolean readOnly) { 1062 HookParams hookParams = new HookParams() 1063 .add(IBaseResource.class, resourceToDelete) 1064 .add(RequestDetails.class, theRequestDetails) 1065 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1066 .add(TransactionDetails.class, transactionDetails) 1067 .add( 1068 InterceptorInvocationTimingEnum.class, 1069 transactionDetails.getInvocationTiming( 1070 Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)); 1071 doCallHooks( 1072 transactionDetails, 1073 theRequestDetails, 1074 Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, 1075 hookParams); 1076 } 1077 }); 1078 } 1079 1080 IBaseOperationOutcome oo; 1081 if (deletedResources.isEmpty()) { 1082 String msg = getContext() 1083 .getLocalizer() 1084 .getMessageSanitized(BaseStorageDao.class, "unableToDeleteNotFound", theUrl); 1085 oo = createWarnOperationOutcome(msg, "not-found", StorageResponseCodeEnum.SUCCESSFUL_DELETE_NOT_FOUND); 1086 } else { 1087 String msg = getContext() 1088 .getLocalizer() 1089 .getMessageSanitized(BaseStorageDao.class, "successfulDeletes", deletedResources.size()); 1090 msg += " " 1091 + getContext() 1092 .getLocalizer() 1093 .getMessageSanitized(BaseStorageDao.class, "successfulTimingSuffix", w.getMillis()); 1094 oo = createInfoOperationOutcome(msg, StorageResponseCodeEnum.SUCCESSFUL_DELETE); 1095 } 1096 1097 ourLog.debug( 1098 "Processed delete on {} (matched {} resource(s)) in {}ms", 1099 theUrl, 1100 deletedResources.size(), 1101 w.getMillis()); 1102 1103 DeleteMethodOutcome retVal = new DeleteMethodOutcome(); 1104 retVal.setDeletedEntities(deletedResources); 1105 retVal.setOperationOutcome(oo); 1106 return retVal; 1107 } 1108 1109 protected ResourceTable updateEntityForDelete( 1110 RequestDetails theRequest, TransactionDetails theTransactionDetails, ResourceTable theEntity) { 1111 myResourceSearchUrlSvc.deleteByResId(theEntity.getPersistentId()); 1112 Date updateTime = new Date(); 1113 return updateEntity(theRequest, null, theEntity, updateTime, true, true, theTransactionDetails, false, true); 1114 } 1115 1116 private void validateDeleteEnabled() { 1117 if (!getStorageSettings().isDeleteEnabled()) { 1118 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "deleteBlockedBecauseDisabled"); 1119 throw new PreconditionFailedException(Msg.code(966) + msg); 1120 } 1121 } 1122 1123 private void validateIdPresentForDelete(IIdType theId) { 1124 if (theId == null || !theId.hasIdPart()) { 1125 throw new InvalidRequestException(Msg.code(967) + "Can not perform delete, no ID provided"); 1126 } 1127 } 1128 1129 private <MT extends IBaseMetaType> void doMetaAdd( 1130 MT theMetaAdd, 1131 BaseHasResource<?> theEntity, 1132 RequestDetails theRequestDetails, 1133 TransactionDetails theTransactionDetails) { 1134 IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1135 1136 List<TagDefinition> tags = toTagList(theMetaAdd); 1137 for (TagDefinition nextDef : tags) { 1138 1139 boolean entityHasTag = false; 1140 for (BaseTag next : theEntity.getTags()) { 1141 if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType()) 1142 && Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) 1143 && Objects.equals(next.getTag().getCode(), nextDef.getCode()) 1144 && Objects.equals(next.getTag().getVersion(), nextDef.getVersion()) 1145 && Objects.equals(next.getTag().getUserSelected(), nextDef.getUserSelected())) { 1146 entityHasTag = true; 1147 break; 1148 } 1149 } 1150 1151 if (!entityHasTag) { 1152 theEntity.setHasTags(true); 1153 1154 TagDefinition def = cacheTagDefinitionDao.getTagOrNull( 1155 theTransactionDetails, 1156 nextDef.getTagType(), 1157 nextDef.getSystem(), 1158 nextDef.getCode(), 1159 nextDef.getDisplay(), 1160 nextDef.getVersion(), 1161 nextDef.getUserSelected()); 1162 if (def != null) { 1163 BaseTag newEntity = theEntity.addTag(def); 1164 if (newEntity.getTagId() == null) { 1165 myEntityManager.persist(newEntity); 1166 } 1167 } 1168 } 1169 } 1170 1171 validateMetaCount(theEntity.getTags().size()); 1172 1173 myEntityManager.merge(theEntity); 1174 1175 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1176 IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1177 HookParams preStorageParams = 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 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 1184 1185 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1186 HookParams preCommitParams = new HookParams() 1187 .add(IBaseResource.class, oldVersion) 1188 .add(IBaseResource.class, newVersion) 1189 .add(RequestDetails.class, theRequestDetails) 1190 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1191 .add(TransactionDetails.class, theTransactionDetails) 1192 .add( 1193 InterceptorInvocationTimingEnum.class, 1194 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 1195 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 1196 } 1197 1198 private <MT extends IBaseMetaType> void doMetaDelete( 1199 MT theMetaDel, 1200 BaseHasResource theEntity, 1201 RequestDetails theRequestDetails, 1202 TransactionDetails theTransactionDetails) { 1203 1204 // todo mb update hibernate search index if we are storing resources - it assumes inline tags. 1205 IBaseResource oldVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1206 1207 List<TagDefinition> tags = toTagList(theMetaDel); 1208 1209 for (TagDefinition nextDef : tags) { 1210 for (BaseTag next : new ArrayList<BaseTag>(theEntity.getTags())) { 1211 if (Objects.equals(next.getTag().getTagType(), nextDef.getTagType()) 1212 && Objects.equals(next.getTag().getSystem(), nextDef.getSystem()) 1213 && Objects.equals(next.getTag().getCode(), nextDef.getCode())) { 1214 myEntityManager.remove(next); 1215 theEntity.getTags().remove(next); 1216 } 1217 } 1218 } 1219 1220 if (theEntity.getTags().isEmpty()) { 1221 theEntity.setHasTags(false); 1222 } 1223 1224 theEntity = myEntityManager.merge(theEntity); 1225 1226 // Interceptor call: STORAGE_PRECOMMIT_RESOURCE_UPDATED 1227 IBaseResource newVersion = myJpaStorageResourceParser.toResource(theEntity, false); 1228 HookParams preStorageParams = new HookParams() 1229 .add(IBaseResource.class, oldVersion) 1230 .add(IBaseResource.class, newVersion) 1231 .add(RequestDetails.class, theRequestDetails) 1232 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1233 .add(TransactionDetails.class, theTransactionDetails); 1234 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, preStorageParams); 1235 1236 HookParams preCommitParams = new HookParams() 1237 .add(IBaseResource.class, oldVersion) 1238 .add(IBaseResource.class, newVersion) 1239 .add(RequestDetails.class, theRequestDetails) 1240 .addIfMatchesType(ServletRequestDetails.class, theRequestDetails) 1241 .add(TransactionDetails.class, theTransactionDetails) 1242 .add( 1243 InterceptorInvocationTimingEnum.class, 1244 theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)); 1245 1246 myInterceptorBroadcaster.callHooks(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, preCommitParams); 1247 } 1248 1249 @Override 1250 public ExpungeOutcome expunge(IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 1251 HapiTransactionService.noTransactionAllowed(); 1252 validateExpungeEnabled(); 1253 return forceExpungeInExistingTransaction(theId, theExpungeOptions, theRequest); 1254 } 1255 1256 @Override 1257 public ExpungeOutcome expunge(ExpungeOptions theExpungeOptions, RequestDetails theRequestDetails) { 1258 HapiTransactionService.noTransactionAllowed(); 1259 ourLog.info("Beginning TYPE[{}] expunge operation", getResourceName()); 1260 validateExpungeEnabled(); 1261 return myExpungeService.expunge(getResourceName(), null, theExpungeOptions, theRequestDetails); 1262 } 1263 1264 private void validateExpungeEnabled() { 1265 if (!getStorageSettings().isExpungeEnabled()) { 1266 throw new MethodNotAllowedException(Msg.code(968) + "$expunge is not enabled on this server"); 1267 } 1268 } 1269 1270 @Override 1271 public Stream<IResourcePersistentId> fetchAllVersionsOfResources( 1272 RequestDetails theRequestDetails, Collection<IResourcePersistentId> theIds) { 1273 HapiTransactionService.requireTransaction(); 1274 1275 List<JpaPidFk> ids = theIds.stream().map(t -> ((JpaPid) t).toFk()).toList(); 1276 1277 return myResourceHistoryTableDao 1278 .findVersionPidsForResources(Pageable.unpaged(), ids) 1279 .map(t -> (IResourcePersistentId) t); 1280 } 1281 1282 @Override 1283 public ExpungeOutcome forceExpungeInExistingTransaction( 1284 IIdType theId, ExpungeOptions theExpungeOptions, RequestDetails theRequest) { 1285 TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager); 1286 1287 BaseHasResource<?> entity = txTemplate.execute(t -> readEntity(theId, theRequest)); 1288 Validate.notNull(entity, "Resource with ID %s not found in database", theId); 1289 1290 if (theId.hasVersionIdPart()) { 1291 BaseHasResource<?> currentVersion; 1292 currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless(), theRequest)); 1293 Validate.notNull( 1294 currentVersion, 1295 "Current version of resource with ID %s not found in database", 1296 theId.toVersionless()); 1297 1298 if (entity.getVersion() == currentVersion.getVersion()) { 1299 throw new PreconditionFailedException( 1300 Msg.code(969) + "Can not perform version-specific expunge of resource " 1301 + theId.toUnqualified().getValue() + " as this is the current version"); 1302 } 1303 1304 return myExpungeService.expunge(getResourceName(), entity.getResourceId(), theExpungeOptions, theRequest); 1305 } 1306 1307 // Downstream code expects the version to be null in the JpaPid if we're not 1308 // doing a version specific expunge 1309 JpaPid resourceId = entity.getResourceId(); 1310 resourceId = JpaPid.fromId(resourceId.getId(), resourceId.getPartitionId()); 1311 1312 return myExpungeService.expunge(getResourceName(), resourceId, theExpungeOptions, theRequest); 1313 } 1314 1315 @Override 1316 @Nonnull 1317 public String getResourceName() { 1318 return myResourceName; 1319 } 1320 1321 @Override 1322 public Class<T> getResourceType() { 1323 return myResourceType; 1324 } 1325 1326 @SuppressWarnings("unchecked") 1327 public void setResourceType(Class<? extends IBaseResource> theTableType) { 1328 myResourceType = (Class<T>) theTableType; 1329 } 1330 1331 @Override 1332 public IBundleProvider history(Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequestDetails) { 1333 StopWatch w = new StopWatch(); 1334 RequestPartitionId requestPartitionId = 1335 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1336 theRequestDetails, myResourceName, null); 1337 IBundleProvider retVal = myTransactionService 1338 .withRequest(theRequestDetails) 1339 .withRequestPartitionId(requestPartitionId) 1340 .execute(() -> myPersistedJpaBundleProviderFactory.history( 1341 theRequestDetails, myResourceName, null, theSince, theUntil, theOffset, requestPartitionId)); 1342 1343 ourLog.debug("Processed history on {} in {}ms", myResourceName, w.getMillisAndRestart()); 1344 return retVal; 1345 } 1346 1347 /** 1348 * @deprecated Use {@link #history(IIdType, HistorySearchDateRangeParam, RequestDetails)} instead 1349 */ 1350 @Override 1351 public IBundleProvider history( 1352 final IIdType theId, final Date theSince, Date theUntil, Integer theOffset, RequestDetails theRequest) { 1353 StopWatch w = new StopWatch(); 1354 1355 RequestPartitionId requestPartitionId = 1356 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1357 theRequest, myResourceName, theId); 1358 IBundleProvider retVal = myTransactionService 1359 .withRequest(theRequest) 1360 .withRequestPartitionId(requestPartitionId) 1361 .execute(() -> { 1362 IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); 1363 BaseHasResource entity = readEntity(id, requestPartitionId); 1364 1365 return myPersistedJpaBundleProviderFactory.history( 1366 theRequest, 1367 myResourceName, 1368 entity.getResourceId(), 1369 theSince, 1370 theUntil, 1371 theOffset, 1372 requestPartitionId); 1373 }); 1374 1375 ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart()); 1376 return retVal; 1377 } 1378 1379 @Override 1380 public IBundleProvider history( 1381 final IIdType theId, 1382 final HistorySearchDateRangeParam theHistorySearchDateRangeParam, 1383 RequestDetails theRequest) { 1384 StopWatch w = new StopWatch(); 1385 RequestPartitionId requestPartitionId = 1386 myRequestPartitionHelperService.determineReadPartitionForRequestForHistory( 1387 theRequest, myResourceName, theId); 1388 IBundleProvider retVal = myTransactionService 1389 .withRequest(theRequest) 1390 .withRequestPartitionId(requestPartitionId) 1391 .execute(() -> { 1392 IIdType id = theId.withResourceType(myResourceName).toUnqualifiedVersionless(); 1393 BaseHasResource entity = readEntity(id, requestPartitionId); 1394 1395 return myPersistedJpaBundleProviderFactory.history( 1396 theRequest, 1397 myResourceName, 1398 entity.getResourceId(), 1399 theHistorySearchDateRangeParam.getLowerBoundAsInstant(), 1400 theHistorySearchDateRangeParam.getUpperBoundAsInstant(), 1401 theHistorySearchDateRangeParam.getOffset(), 1402 theHistorySearchDateRangeParam.getHistorySearchType(), 1403 requestPartitionId); 1404 }); 1405 1406 ourLog.debug("Processed history on {} in {}ms", theId, w.getMillisAndRestart()); 1407 return retVal; 1408 } 1409 1410 protected boolean isPagingProviderDatabaseBacked(RequestDetails theRequestDetails) { 1411 if (theRequestDetails == null || theRequestDetails.getServer() == null) { 1412 return false; 1413 } 1414 IRestfulServerDefaults server = theRequestDetails.getServer(); 1415 IPagingProvider pagingProvider = server.getPagingProvider(); 1416 return pagingProvider != null; 1417 } 1418 1419 protected void requestReindexForRelatedResources( 1420 Boolean theCurrentlyReindexing, List<String> theBase, RequestDetails theRequestDetails) { 1421 // Avoid endless loops 1422 if (Boolean.TRUE.equals(theCurrentlyReindexing) || shouldSkipReindex(theRequestDetails)) { 1423 return; 1424 } 1425 1426 if (getStorageSettings().isMarkResourcesForReindexingUponSearchParameterChange()) { 1427 1428 ReindexJobParameters params = new ReindexJobParameters(); 1429 1430 List<String> urls = List.of(); 1431 if (!isCommonSearchParam(theBase)) { 1432 urls = theBase.stream().map(t -> t + "?").collect(Collectors.toList()); 1433 } 1434 1435 myJobPartitionProvider.getPartitionedUrls(theRequestDetails, urls).forEach(params::addPartitionedUrl); 1436 1437 JobInstanceStartRequest request = new JobInstanceStartRequest(); 1438 request.setJobDefinitionId(JOB_REINDEX); 1439 request.setParameters(params); 1440 myJobCoordinator.startInstance(theRequestDetails, request); 1441 1442 ourLog.debug("Started reindex job with parameters {}", params); 1443 } 1444 1445 mySearchParamRegistry.requestRefresh(); 1446 } 1447 1448 protected final boolean shouldSkipReindex(RequestDetails theRequestDetails) { 1449 if (theRequestDetails == null) { 1450 return false; 1451 } 1452 Object shouldSkip = theRequestDetails.getUserData().getOrDefault(JpaConstants.SKIP_REINDEX_ON_UPDATE, false); 1453 return Boolean.parseBoolean(shouldSkip.toString()); 1454 } 1455 1456 private boolean isCommonSearchParam(List<String> theBase) { 1457 // If the base contains the special resource "Resource", this is a common SP that applies to all resources 1458 return theBase.stream().map(String::toLowerCase).anyMatch(BASE_RESOURCE_NAME::equals); 1459 } 1460 1461 @Override 1462 @Transactional 1463 public <MT extends IBaseMetaType> MT metaAddOperation( 1464 IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest) { 1465 1466 RequestPartitionId requestPartitionId = 1467 myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( 1468 theRequest, JpaConstants.OPERATION_META_ADD); 1469 1470 myTransactionService 1471 .withRequest(theRequest) 1472 .withRequestPartitionId(requestPartitionId) 1473 .execute(() -> doMetaAddOperation(theResourceId, theMetaAdd, theRequest, requestPartitionId)); 1474 1475 @SuppressWarnings("unchecked") 1476 MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequest); 1477 return retVal; 1478 } 1479 1480 protected <MT extends IBaseMetaType> void doMetaAddOperation( 1481 IIdType theResourceId, MT theMetaAdd, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 1482 TransactionDetails transactionDetails = new TransactionDetails(); 1483 1484 StopWatch w = new StopWatch(); 1485 BaseHasResource entity = readEntity(theResourceId, theRequestPartitionId); 1486 1487 if (isNull(entity)) { 1488 throw new ResourceNotFoundException(Msg.code(1993) + theResourceId); 1489 } 1490 1491 ResourceTable latestVersion = 1492 readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails); 1493 if (latestVersion.getVersion() != entity.getVersion()) { 1494 doMetaAdd(theMetaAdd, entity, theRequest, transactionDetails); 1495 } else { 1496 doMetaAdd(theMetaAdd, latestVersion, theRequest, transactionDetails); 1497 1498 // Also update history entry 1499 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion( 1500 entity.getResourceId().toFk(), entity.getVersion()); 1501 doMetaAdd(theMetaAdd, history, theRequest, transactionDetails); 1502 } 1503 1504 ourLog.debug("Processed metaAddOperation on {} in {}ms", theResourceId, w.getMillisAndRestart()); 1505 } 1506 1507 @Override 1508 @Transactional 1509 public <MT extends IBaseMetaType> MT metaDeleteOperation( 1510 IIdType theResourceId, MT theMetaDel, RequestDetails theRequest) { 1511 1512 RequestPartitionId requestPartitionId = 1513 myRequestPartitionHelperService.determineReadPartitionForRequestForServerOperation( 1514 theRequest, JpaConstants.OPERATION_META_DELETE); 1515 1516 myTransactionService 1517 .withRequest(theRequest) 1518 .withRequestPartitionId(requestPartitionId) 1519 .execute(() -> doMetaDeleteOperation(theResourceId, theMetaDel, theRequest, requestPartitionId)); 1520 1521 @SuppressWarnings("unchecked") 1522 MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequest); 1523 return retVal; 1524 } 1525 1526 @Transactional 1527 public <MT extends IBaseMetaType> void doMetaDeleteOperation( 1528 IIdType theResourceId, MT theMetaDel, RequestDetails theRequest, RequestPartitionId theRequestPartitionId) { 1529 TransactionDetails transactionDetails = new TransactionDetails(); 1530 StopWatch w = new StopWatch(); 1531 1532 BaseHasResource entity = readEntity(theResourceId, theRequestPartitionId); 1533 1534 if (isNull(entity)) { 1535 throw new ResourceNotFoundException(Msg.code(1994) + theResourceId); 1536 } 1537 1538 ResourceTable latestVersion = 1539 readEntityLatestVersion(theRequest, theResourceId, theRequestPartitionId, transactionDetails); 1540 boolean nonVersionedTags = 1541 myStorageSettings.getTagStorageMode() != JpaStorageSettings.TagStorageModeEnum.VERSIONED; 1542 1543 if (latestVersion.getVersion() != entity.getVersion() || nonVersionedTags) { 1544 doMetaDelete(theMetaDel, entity, theRequest, transactionDetails); 1545 } else { 1546 doMetaDelete(theMetaDel, latestVersion, theRequest, transactionDetails); 1547 // Also update history entry 1548 ResourceHistoryTable history = myResourceHistoryTableDao.findForIdAndVersion( 1549 entity.getResourceId().toFk(), entity.getVersion()); 1550 doMetaDelete(theMetaDel, history, theRequest, transactionDetails); 1551 } 1552 1553 ourLog.debug("Processed metaDeleteOperation on {} in {}ms", theResourceId.getValue(), w.getMillisAndRestart()); 1554 } 1555 1556 @Override 1557 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, IIdType theId, RequestDetails theRequest) { 1558 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1559 theRequest, myResourceName, theId); 1560 1561 return myTransactionService 1562 .withRequest(theRequest) 1563 .withRequestPartitionId(requestPartitionId) 1564 .execute(() -> { 1565 Set<TagDefinition> tagDefs = new HashSet<>(); 1566 BaseHasResource<?> entity = readEntity(theId, requestPartitionId); 1567 for (BaseTag next : entity.getTags()) { 1568 tagDefs.add(next.getTag()); 1569 } 1570 MT retVal = toMetaDt(theType, tagDefs); 1571 1572 retVal.setLastUpdated(entity.getUpdatedDate()); 1573 retVal.setVersionId(Long.toString(entity.getVersion())); 1574 1575 return retVal; 1576 }); 1577 } 1578 1579 @Override 1580 @Transactional 1581 public <MT extends IBaseMetaType> MT metaGetOperation(Class<MT> theType, RequestDetails theRequestDetails) { 1582 String sql = 1583 "SELECT d FROM TagDefinition d WHERE d.myId IN (SELECT DISTINCT t.myTagId FROM ResourceTag t WHERE t.myResourceType = :res_type)"; 1584 TypedQuery<TagDefinition> q = myEntityManager.createQuery(sql, TagDefinition.class); 1585 q.setParameter("res_type", myResourceName); 1586 List<TagDefinition> tagDefinitions = q.getResultList(); 1587 1588 return toMetaDt(theType, tagDefinitions); 1589 } 1590 1591 private boolean isDeleted(BaseHasResource entityToUpdate) { 1592 return entityToUpdate.getDeleted() != null; 1593 } 1594 1595 @PostConstruct 1596 @Override 1597 public void start() { 1598 assert getStorageSettings() != null; 1599 1600 RuntimeResourceDefinition def = getContext().getResourceDefinition(myResourceType); 1601 myResourceName = def.getName(); 1602 1603 if (mySearchDao != null && mySearchDao.isDisabled()) { 1604 mySearchDao = null; 1605 } 1606 1607 ourLog.debug("Starting resource DAO for type: {}", getResourceName()); 1608 myInstanceValidator = getApplicationContext().getBean(IInstanceValidatorModule.class); 1609 myTxTemplate = new TransactionTemplate(myPlatformTransactionManager); 1610 1611 myStorageInterceptorHooks = new StorageInterceptorHooksFacade(myInterceptorBroadcaster); 1612 1613 super.start(); 1614 } 1615 1616 /** 1617 * Subclasses may override to provide behaviour. Invoked within a delete 1618 * transaction with the resource that is about to be deleted. 1619 */ 1620 protected void preDelete(T theResourceToDelete, ResourceTable theEntityToDelete, RequestDetails theRequestDetails) { 1621 // nothing by default 1622 } 1623 1624 @Override 1625 @Transactional 1626 public T readByPid(IResourcePersistentId thePid) { 1627 return readByPid(thePid, false); 1628 } 1629 1630 @Override 1631 @Transactional 1632 public T readByPid(IResourcePersistentId thePid, boolean theDeletedOk) { 1633 StopWatch w = new StopWatch(); 1634 JpaPid jpaPid = (JpaPid) thePid; 1635 1636 Optional<ResourceTable> entity = myResourceTableDao.findById(jpaPid); 1637 if (entity.isEmpty()) { 1638 throw new ResourceNotFoundException(Msg.code(975) + "No resource found with PID " + jpaPid); 1639 } 1640 if (isDeleted(entity.get()) && !theDeletedOk) { 1641 throw createResourceGoneException(entity.get()); 1642 } 1643 1644 T retVal = myJpaStorageResourceParser.toResource(null, myResourceType, entity.get(), null, false); 1645 1646 ourLog.debug("Processed read on {} in {}ms", jpaPid, w.getMillis()); 1647 return retVal; 1648 } 1649 1650 /** 1651 * @deprecated Use {@link #read(IIdType, RequestDetails)} instead 1652 */ 1653 @Override 1654 public T read(IIdType theId) { 1655 return read(theId, null); 1656 } 1657 1658 @Override 1659 public T read(IIdType theId, RequestDetails theRequestDetails) { 1660 return read(theId, theRequestDetails, false); 1661 } 1662 1663 @Override 1664 public T read(IIdType theId, RequestDetails theRequest, boolean theDeletedOk) { 1665 validateResourceTypeAndThrowInvalidRequestException(theId); 1666 TransactionDetails transactionDetails = new TransactionDetails(); 1667 1668 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1669 theRequest, myResourceName, theId); 1670 1671 return myTransactionService 1672 .withRequest(theRequest) 1673 .withTransactionDetails(transactionDetails) 1674 .withRequestPartitionId(requestPartitionId) 1675 .read(partition -> doReadInTransaction(theId, theRequest, theDeletedOk, partition)); 1676 } 1677 1678 private T doReadInTransaction( 1679 IIdType theId, RequestDetails theRequest, boolean theDeletedOk, RequestPartitionId theRequestPartitionId) { 1680 assert TransactionSynchronizationManager.isActualTransactionActive(); 1681 1682 StopWatch w = new StopWatch(); 1683 BaseHasResource<?> entity = readEntity(theId, theRequestPartitionId); 1684 validateResourceType(entity); 1685 1686 T retVal = myJpaStorageResourceParser.toResource(theRequest, myResourceType, entity, null, false); 1687 1688 if (!theDeletedOk) { 1689 if (isDeleted(entity)) { 1690 throw createResourceGoneException(entity); 1691 } 1692 } 1693 // If the resolved fhir model is null, we don't need to run pre-access over or pre-show over it. 1694 if (retVal != null) { 1695 invokeStoragePreAccessResources(theId, theRequest, retVal); 1696 retVal = invokeStoragePreShowResources(theRequest, retVal); 1697 } 1698 1699 ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart()); 1700 return retVal; 1701 } 1702 1703 @Nullable 1704 private T invokeStoragePreShowResources(RequestDetails theRequest, T retVal) { 1705 retVal = invokeStoragePreShowResources(myInterceptorBroadcaster, theRequest, retVal); 1706 return retVal; 1707 } 1708 1709 private void invokeStoragePreAccessResources(IIdType theId, RequestDetails theRequest, T theResource) { 1710 invokeStoragePreAccessResources(myInterceptorBroadcaster, theRequest, theId, theResource); 1711 } 1712 1713 private Optional<T> invokeStoragePreAccessResources(RequestDetails theRequest, T theResource) { 1714 IInterceptorBroadcaster compositeBroadcaster = 1715 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest); 1716 if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) { 1717 SimplePreResourceAccessDetails accessDetails = new SimplePreResourceAccessDetails(theResource); 1718 HookParams params = new HookParams() 1719 .add(IPreResourceAccessDetails.class, accessDetails) 1720 .add(RequestDetails.class, theRequest) 1721 .addIfMatchesType(ServletRequestDetails.class, theRequest); 1722 compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params); 1723 if (accessDetails.isDontReturnResourceAtIndex(0)) { 1724 return Optional.empty(); 1725 } 1726 } 1727 return Optional.of(theResource); 1728 } 1729 1730 @Override 1731 public BaseHasResource readEntity(IIdType theId, RequestDetails theRequest) { 1732 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 1733 theRequest, myResourceName, theId); 1734 return myTransactionService 1735 .withRequest(theRequest) 1736 .withRequestPartitionId(requestPartitionId) 1737 .execute(() -> readEntity(theId, requestPartitionId)); 1738 } 1739 1740 @Override 1741 public ReindexOutcome reindex( 1742 IResourcePersistentId thePid, 1743 ReindexParameters theReindexParameters, 1744 RequestDetails theRequest, 1745 TransactionDetails theTransactionDetails) { 1746 ReindexOutcome retVal = new ReindexOutcome(); 1747 1748 JpaPid jpaPid = (JpaPid) thePid; 1749 1750 // Careful! Reindex only reads ResourceTable, but we tell Hibernate to check version 1751 // to ensure Hibernate will catch concurrent updates (PUT/DELETE) elsewhere. 1752 // Otherwise, we may index stale data. See #4584 1753 // We use the main entity as the lock object since all the index rows hang off it. 1754 ResourceTable entity; 1755 if (theReindexParameters.isOptimisticLock()) { 1756 entity = myEntityManager.find(ResourceTable.class, jpaPid, LockModeType.OPTIMISTIC); 1757 } else { 1758 entity = myEntityManager.find(ResourceTable.class, jpaPid); 1759 } 1760 1761 IIdType id = myFhirContext.getVersion().newIdType(entity.getResourceType(), entity.getFhirId()); 1762 retVal.setResourceId(id); 1763 1764 if (entity == null) { 1765 retVal.addWarning("Unable to find entity with PID: " + jpaPid.getId()); 1766 return retVal; 1767 } 1768 1769 boolean reindexSearchParameters = 1770 theReindexParameters.getReindexSearchParameters() != ReindexParameters.ReindexSearchParametersEnum.NONE; 1771 boolean correctCurrentVersion = 1772 theReindexParameters.getCorrectCurrentVersion() != ReindexParameters.CorrectCurrentVersionModeEnum.NONE; 1773 boolean optimizeStorage = 1774 theReindexParameters.getOptimizeStorage() != ReindexParameters.OptimizeStorageModeEnum.NONE; 1775 1776 if (correctCurrentVersion) { 1777 CorrectCurrentVersionResponse outcome = 1778 reindexCorrectCurrentVersion(entity, theReindexParameters.getReindexSearchParameters()); 1779 if (outcome.forceReindexSps()) { 1780 reindexSearchParameters = true; 1781 } 1782 if (outcome.forceReloadResourceEntity() && (reindexSearchParameters || optimizeStorage)) { 1783 entity = myEntityManager.find(ResourceTable.class, jpaPid); 1784 if (outcome.currentVersion() != null) { 1785 entity.setCurrentVersionEntity(outcome.currentVersion()); 1786 } 1787 } 1788 } 1789 1790 if (reindexSearchParameters) { 1791 reindexSearchParameters(entity, retVal, theTransactionDetails); 1792 } 1793 1794 if (optimizeStorage) { 1795 reindexOptimizeStorage(entity, theReindexParameters.getOptimizeStorage()); 1796 } 1797 1798 return retVal; 1799 } 1800 1801 /** 1802 * @return Returns an updated copy of the entity if it was modified (returns null otherwise) 1803 */ 1804 private CorrectCurrentVersionResponse reindexCorrectCurrentVersion( 1805 ResourceTable theEntity, ReindexParameters.ReindexSearchParametersEnum theReindexSearchParameters) { 1806 ResourceHistoryTable version = theEntity.getCurrentVersionEntity(); 1807 1808 if (version == null) { 1809 Optional<ResourceHistoryTable> currentVersionOpt = myResourceHistoryTableDao 1810 .findVersionsForResource( 1811 SINGLE_RESULT, theEntity.getResourceId().toFk()) 1812 .findFirst(); 1813 1814 if (currentVersionOpt.isPresent()) { 1815 Long newVersion = currentVersionOpt.get().getVersion(); 1816 if (newVersion == theEntity.getVersion()) { 1817 // Already have the correct version 1818 return null; 1819 } 1820 1821 ourLog.info( 1822 "Correcting current version for {}/{} (PID {}) from version {} to version {}", 1823 theEntity.getResourceType(), 1824 theEntity.getFhirId(), 1825 theEntity.getResourceId(), 1826 theEntity.getVersion(), 1827 newVersion); 1828 1829 /* 1830 * We are going to manually change (i.e. correct) the version number on the HFJ_RESOURCE table. The 1831 * version number is used as the optimistic locking key, so we need to detach the entity first, or 1832 * we'll get an optimistic lock failure later. 1833 */ 1834 myEntityManager.detach(theEntity); 1835 1836 myResourceTableDao.updateVersionAndLastUpdated(theEntity.getId(), newVersion, new Date()); 1837 1838 return new CorrectCurrentVersionResponse(false, true, currentVersionOpt.get()); 1839 1840 } else { 1841 ourLog.info( 1842 "No versions exist for {}/{} (PID {}), marking resource as deleted", 1843 theEntity.getResourceType(), 1844 theEntity.getFhirId(), 1845 theEntity.getResourceId()); 1846 theEntity.setDeleted(new Date()); 1847 return new CorrectCurrentVersionResponse(true, false, null); 1848 } 1849 } 1850 1851 return new CorrectCurrentVersionResponse(false, false, null); 1852 } 1853 1854 @SuppressWarnings("unchecked") 1855 private void reindexSearchParameters( 1856 ResourceTable entity, ReindexOutcome theReindexOutcome, TransactionDetails theTransactionDetails) { 1857 try { 1858 T resource = (T) myJpaStorageResourceParser.toResource(entity, false); 1859 reindexSearchParameters(resource, entity, theTransactionDetails); 1860 } catch (Exception e) { 1861 ourLog.warn("Failure during reindex: {}", e.toString()); 1862 theReindexOutcome.addWarning("Failed to reindex resource " + entity.getIdDt() + ": " + e); 1863 myResourceTableDao.updateIndexStatus(entity.getId(), EntityIndexStatusEnum.INDEXING_FAILED); 1864 } 1865 } 1866 1867 /** 1868 * @deprecated Use {@link #reindex(IResourcePersistentId, ReindexParameters, RequestDetails, TransactionDetails)} 1869 */ 1870 @Deprecated 1871 @Override 1872 public void reindex(T theResource, IBasePersistedResource theEntity) { 1873 assert TransactionSynchronizationManager.isActualTransactionActive(); 1874 ResourceTable entity = (ResourceTable) theEntity; 1875 TransactionDetails transactionDetails = new TransactionDetails(entity.getUpdatedDate()); 1876 1877 reindexSearchParameters(theResource, theEntity, transactionDetails); 1878 } 1879 1880 private void reindexSearchParameters( 1881 T theResource, IBasePersistedResource theEntity, TransactionDetails transactionDetails) { 1882 ourLog.debug("Indexing resource {} - PID {}", theEntity.getIdDt().getValue(), theEntity.getPersistentId()); 1883 if (theResource != null) { 1884 CURRENTLY_REINDEXING.put(theResource, Boolean.TRUE); 1885 } 1886 1887 SystemRequestDetails request = new SystemRequestDetails(); 1888 request.getUserData().put(JpaConstants.SKIP_REINDEX_ON_UPDATE, Boolean.TRUE); 1889 1890 updateEntity( 1891 request, theResource, theEntity, theEntity.getDeleted(), true, false, transactionDetails, true, false); 1892 if (theResource != null) { 1893 CURRENTLY_REINDEXING.put(theResource, null); 1894 } 1895 } 1896 1897 private void reindexOptimizeStorage( 1898 ResourceTable entity, ReindexParameters.OptimizeStorageModeEnum theOptimizeStorageMode) { 1899 ResourceHistoryTable historyEntity = entity.getCurrentVersionEntity(); 1900 if (historyEntity != null) { 1901 reindexOptimizeStorageHistoryEntity(entity, historyEntity); 1902 if (theOptimizeStorageMode == ReindexParameters.OptimizeStorageModeEnum.ALL_VERSIONS) { 1903 int pageSize = 100; 1904 for (int page = 0; ((long) page * pageSize) < entity.getVersion(); page++) { 1905 1906 // We need to sort the pages, because we are updating the same data we are paging through. 1907 // If not sorted explicitly, a database like Postgres returns the same data multiple times on 1908 // different pages as the underlying data gets updated. 1909 PageRequest pageRequest = PageRequest.of(page, pageSize, Sort.by("myId")); 1910 Slice<ResourceHistoryTable> historyEntities = 1911 myResourceHistoryTableDao.findAllVersionsExceptSpecificForResourcePid( 1912 pageRequest, entity.getId().toFk(), historyEntity.getVersion()); 1913 1914 for (ResourceHistoryTable next : historyEntities) { 1915 reindexOptimizeStorageHistoryEntity(entity, next); 1916 } 1917 } 1918 } 1919 } 1920 } 1921 1922 /** 1923 * Note that the entity will be detached after being saved if it has changed 1924 * in order to avoid growing the number of resources in memory to be too big 1925 */ 1926 private void reindexOptimizeStorageHistoryEntity(ResourceTable entity, ResourceHistoryTable historyEntity) { 1927 if (historyEntity.getEncoding() == ResourceEncodingEnum.JSONC 1928 || historyEntity.getEncoding() == ResourceEncodingEnum.JSON) { 1929 byte[] resourceBytes = historyEntity.getResource(); 1930 if (resourceBytes != null) { 1931 String resourceText = decodeResource(resourceBytes, historyEntity.getEncoding()); 1932 myResourceHistoryCalculator.conditionallyAlterHistoryEntity(entity, historyEntity, resourceText); 1933 } 1934 } 1935 if (myStorageSettings.isAccessMetaSourceInformationFromProvenanceTable()) { 1936 if (isBlank(historyEntity.getSourceUri()) && isBlank(historyEntity.getRequestId())) { 1937 IdAndPartitionId id = historyEntity.getId().asIdAndPartitionId(); 1938 Optional<ResourceHistoryProvenanceEntity> provenanceEntityOpt = 1939 myResourceHistoryProvenanceDao.findById(id); 1940 if (provenanceEntityOpt.isPresent()) { 1941 ResourceHistoryProvenanceEntity provenanceEntity = provenanceEntityOpt.get(); 1942 historyEntity.setSourceUri(provenanceEntity.getSourceUri()); 1943 historyEntity.setRequestId(provenanceEntity.getRequestId()); 1944 myResourceHistoryProvenanceDao.delete(provenanceEntity); 1945 } 1946 } 1947 } 1948 } 1949 1950 private BaseHasResource<?> readEntity(IIdType theId, RequestPartitionId requestPartitionId) { 1951 validateResourceTypeAndThrowInvalidRequestException(theId); 1952 1953 BaseHasResource entity; 1954 JpaPid pid = myIdHelperService 1955 .resolveResourceIdentity( 1956 requestPartitionId, 1957 getResourceName(), 1958 theId.getIdPart(), 1959 ResolveIdentityMode.includeDeleted().cacheOk()) 1960 .getPersistentId(); 1961 Set<Integer> readPartitions = null; 1962 if (requestPartitionId.isAllPartitions()) { 1963 entity = myEntityManager.find(ResourceTable.class, pid); 1964 } else { 1965 readPartitions = myRequestPartitionHelperService.toReadPartitions(requestPartitionId); 1966 if (readPartitions.size() == 1) { 1967 if (readPartitions.contains(null)) { 1968 entity = myResourceTableDao 1969 .readByPartitionIdNull(pid.getId()) 1970 .orElse(null); 1971 } else { 1972 entity = myResourceTableDao 1973 .readByPartitionId(readPartitions.iterator().next(), pid.getId()) 1974 .orElse(null); 1975 } 1976 } else { 1977 if (readPartitions.contains(null)) { 1978 List<Integer> readPartitionsWithoutNull = 1979 readPartitions.stream().filter(Objects::nonNull).collect(Collectors.toList()); 1980 entity = myResourceTableDao 1981 .readByPartitionIdsOrNull(readPartitionsWithoutNull, pid.getId()) 1982 .orElse(null); 1983 } else { 1984 entity = myResourceTableDao 1985 .readByPartitionIds(readPartitions, pid.getId()) 1986 .orElse(null); 1987 } 1988 } 1989 } 1990 1991 // Verify that the resource is for the correct partition 1992 if (entity != null && readPartitions != null && entity.getPartitionId() != null) { 1993 if (!readPartitions.contains(entity.getPartitionId().getPartitionId())) { 1994 ourLog.debug( 1995 "Performing a read for PartitionId={} but entity has partition: {}", 1996 requestPartitionId, 1997 entity.getPartitionId()); 1998 entity = null; 1999 } 2000 } 2001 2002 if (theId.hasVersionIdPart()) { 2003 if (!theId.isVersionIdPartValidLong()) { 2004 throw new ResourceNotFoundException(Msg.code(978) 2005 + getContext() 2006 .getLocalizer() 2007 .getMessageSanitized( 2008 BaseStorageDao.class, 2009 "invalidVersion", 2010 theId.getVersionIdPart(), 2011 theId.toUnqualifiedVersionless())); 2012 } 2013 if (entity.getVersion() != theId.getVersionIdPartAsLong()) { 2014 entity = null; 2015 } 2016 } 2017 2018 if (entity == null) { 2019 if (theId.hasVersionIdPart()) { 2020 TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery( 2021 "SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", 2022 ResourceHistoryTable.class); 2023 q.setParameter("RID", pid.getId()); 2024 q.setParameter("RTYP", myResourceName); 2025 q.setParameter("RVER", theId.getVersionIdPartAsLong()); 2026 try { 2027 entity = q.getSingleResult(); 2028 } catch (NoResultException e) { 2029 throw new ResourceNotFoundException(Msg.code(979) 2030 + getContext() 2031 .getLocalizer() 2032 .getMessageSanitized( 2033 BaseStorageDao.class, 2034 "invalidVersion", 2035 theId.getVersionIdPart(), 2036 theId.toUnqualifiedVersionless())); 2037 } 2038 } 2039 } 2040 2041 if (entity == null) { 2042 throw new ResourceNotFoundException(Msg.code(1996) + "Resource " + theId + " is not known"); 2043 } 2044 2045 validateResourceType(entity); 2046 validateGivenIdIsAppropriateToRetrieveResource(theId, entity); 2047 2048 return entity; 2049 } 2050 2051 @Override 2052 protected IBasePersistedResource readEntityLatestVersion( 2053 IResourcePersistentId thePersistentId, 2054 RequestDetails theRequestDetails, 2055 TransactionDetails theTransactionDetails) { 2056 JpaPid jpaPid = (JpaPid) thePersistentId; 2057 return myEntityManager.find(ResourceTable.class, jpaPid); 2058 } 2059 2060 @Override 2061 @Nonnull 2062 protected ResourceTable readEntityLatestVersion( 2063 IIdType theId, RequestDetails theRequestDetails, TransactionDetails theTransactionDetails) { 2064 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 2065 theRequestDetails, getResourceName(), theId); 2066 2067 return myTransactionService 2068 .withRequest(theRequestDetails) 2069 .withRequestPartitionId(requestPartitionId) 2070 .execute(() -> 2071 readEntityLatestVersion(theRequestDetails, theId, requestPartitionId, theTransactionDetails)); 2072 } 2073 2074 @Nonnull 2075 private ResourceTable readEntityLatestVersion( 2076 RequestDetails theRequestDetails, 2077 IIdType theId, 2078 @Nonnull RequestPartitionId theRequestPartitionId, 2079 TransactionDetails theTransactionDetails) { 2080 HapiTransactionService.requireTransaction(); 2081 2082 IIdType id = theId; 2083 validateResourceTypeAndThrowInvalidRequestException(id); 2084 if (!id.hasResourceType()) { 2085 id = id.withResourceType(getResourceName()); 2086 } 2087 2088 JpaPid persistentId = null; 2089 if (theTransactionDetails != null) { 2090 if (theTransactionDetails.isResolvedResourceIdEmpty(id.toUnqualifiedVersionless())) { 2091 throw new ResourceNotFoundException(Msg.code(1997) + id); 2092 } 2093 if (theTransactionDetails.hasResolvedResourceIds()) { 2094 persistentId = (JpaPid) theTransactionDetails.getResolvedResourceId(id); 2095 } 2096 } 2097 2098 if (persistentId == null) { 2099 persistentId = myIdHelperService.resolveResourceIdentityPid( 2100 theRequestPartitionId, 2101 id.getResourceType(), 2102 id.getIdPart(), 2103 ResolveIdentityMode.includeDeleted().cacheOk()); 2104 } 2105 2106 ResourceTable entity = myEntityManager.find(ResourceTable.class, persistentId); 2107 if (entity == null) { 2108 throw new ResourceNotFoundException(Msg.code(1998) + id); 2109 } 2110 validateGivenIdIsAppropriateToRetrieveResource(id, entity); 2111 return entity; 2112 } 2113 2114 @Transactional 2115 @Override 2116 public void removeTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm) { 2117 removeTag(theId, theTagType, theScheme, theTerm, null); 2118 } 2119 2120 @Transactional 2121 @Override 2122 public void removeTag( 2123 IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, RequestDetails theRequest) { 2124 StopWatch w = new StopWatch(); 2125 BaseHasResource<?> entity = readEntity(theId, theRequest); 2126 if (entity == null) { 2127 throw new ResourceNotFoundException(Msg.code(1999) + theId); 2128 } 2129 2130 for (BaseTag next : new ArrayList<>(entity.getTags())) { 2131 if (Objects.equals(next.getTag().getTagType(), theTagType) 2132 && Objects.equals(next.getTag().getSystem(), theScheme) 2133 && Objects.equals(next.getTag().getCode(), theTerm)) { 2134 myEntityManager.remove(next); 2135 entity.getTags().remove(next); 2136 } 2137 } 2138 2139 if (entity.getTags().isEmpty()) { 2140 entity.setHasTags(false); 2141 } 2142 2143 myEntityManager.merge(entity); 2144 2145 ourLog.debug( 2146 "Processed remove tag {}/{} on {} in {}ms", 2147 theScheme, 2148 theTerm, 2149 theId.getValue(), 2150 w.getMillisAndRestart()); 2151 } 2152 2153 /** 2154 * @deprecated Use {@link #search(SearchParameterMap, RequestDetails)} instead 2155 */ 2156 @Override 2157 public IBundleProvider search(final SearchParameterMap theParams) { 2158 return search(theParams, null); 2159 } 2160 2161 @Override 2162 public IBundleProvider search(final SearchParameterMap theParams, RequestDetails theRequest) { 2163 return search(theParams, theRequest, null); 2164 } 2165 2166 @Override 2167 public IBundleProvider search( 2168 final SearchParameterMap theParams, RequestDetails theRequest, HttpServletResponse theServletResponse) { 2169 2170 if (theParams.getSearchContainedMode() == SearchContainedModeEnum.BOTH) { 2171 throw new MethodNotAllowedException(Msg.code(983) + "Contained mode 'both' is not currently supported"); 2172 } 2173 if (theParams.getSearchContainedMode() != SearchContainedModeEnum.FALSE 2174 && !myStorageSettings.isIndexOnContainedResources()) { 2175 throw new MethodNotAllowedException( 2176 Msg.code(984) + "Searching with _contained mode enabled is not enabled on this server"); 2177 } 2178 2179 translateListSearchParams(theParams); 2180 2181 setOffsetAndCount(theParams, theRequest); 2182 2183 CacheControlDirective cacheControlDirective = new CacheControlDirective(); 2184 if (theRequest != null) { 2185 cacheControlDirective.parse(theRequest.getHeaders(Constants.HEADER_CACHE_CONTROL)); 2186 } 2187 2188 IBundleProvider retVal = mySearchCoordinatorSvc.registerSearch( 2189 this, theParams, getResourceName(), cacheControlDirective, theRequest); 2190 2191 if (retVal instanceof PersistedJpaBundleProvider provider) { 2192 // Note: we calculate the partition -after- calling registerSearch, since that 2193 // method invokes several interceptors that can affect the partition selection. 2194 RequestPartitionId requestPartitionId = 2195 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2196 theRequest, getResourceName(), theParams); 2197 2198 provider.setRequestPartitionId(requestPartitionId); 2199 if (provider.getCacheStatus() == SearchCacheStatusEnum.HIT) { 2200 if (theServletResponse != null && theRequest != null) { 2201 String value = "HIT from " + theRequest.getFhirServerBase(); 2202 theServletResponse.addHeader(Constants.HEADER_X_CACHE, value); 2203 } 2204 } 2205 } 2206 2207 return retVal; 2208 } 2209 2210 private void translateListSearchParams(SearchParameterMap theParams) { 2211 2212 Set<Map.Entry<String, List<List<IQueryParameterType>>>> entryHashSet = new HashSet<>(theParams.entrySet()); 2213 2214 // Translate _list=42 to _has=List:item:_id=42 2215 for (Map.Entry<String, List<List<IQueryParameterType>>> stringListEntry : entryHashSet) { 2216 String key = stringListEntry.getKey(); 2217 if (Constants.PARAM_LIST.equals((key))) { 2218 List<List<IQueryParameterType>> andOrValues = theParams.get(key); 2219 theParams.remove(key); 2220 List<List<IQueryParameterType>> hasParamValues = new ArrayList<>(); 2221 for (List<IQueryParameterType> orValues : andOrValues) { 2222 List<IQueryParameterType> orList = new ArrayList<>(); 2223 for (IQueryParameterType value : orValues) { 2224 orList.add(new HasParam( 2225 "List", ListResource.SP_ITEM, BaseResource.SP_RES_ID, value.getValueAsQueryToken())); 2226 } 2227 hasParamValues.add(orList); 2228 } 2229 theParams.put(Constants.PARAM_HAS, hasParamValues); 2230 } 2231 } 2232 } 2233 2234 protected void setOffsetAndCount(SearchParameterMap theParams, RequestDetails theRequest) { 2235 if (theRequest != null) { 2236 2237 if (theRequest.isSubRequest()) { 2238 Integer max = getStorageSettings().getMaximumSearchResultCountInTransaction(); 2239 if (max != null) { 2240 Validate.inclusiveBetween( 2241 1, 2242 Integer.MAX_VALUE, 2243 max, 2244 "Maximum search result count in transaction must be a positive integer"); 2245 theParams.setLoadSynchronousUpTo(getStorageSettings().getMaximumSearchResultCountInTransaction()); 2246 } 2247 } 2248 2249 final Integer offset = RestfulServerUtils.extractOffsetParameter(theRequest); 2250 if (offset != null || !isPagingProviderDatabaseBacked(theRequest)) { 2251 theParams.setLoadSynchronous(true); 2252 if (offset != null) { 2253 Validate.inclusiveBetween(0, Integer.MAX_VALUE, offset, "Offset must be a positive integer"); 2254 } 2255 theParams.setOffset(offset); 2256 } 2257 2258 Integer count = RestfulServerUtils.extractCountParameter(theRequest); 2259 if (count != null) { 2260 Integer maxPageSize = theRequest.getServer().getMaximumPageSize(); 2261 if (maxPageSize != null && count > maxPageSize) { 2262 ourLog.info( 2263 "Reducing {} from {} to {} which is the maximum allowable page size.", 2264 Constants.PARAM_COUNT, 2265 count, 2266 maxPageSize); 2267 count = maxPageSize; 2268 } 2269 theParams.setCount(count); 2270 } else if (theRequest.getServer().getDefaultPageSize() != null) { 2271 theParams.setCount(theRequest.getServer().getDefaultPageSize()); 2272 } 2273 } 2274 } 2275 2276 @Override 2277 public List<JpaPid> searchForIds( 2278 SearchParameterMap theParams, 2279 RequestDetails theRequest, 2280 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 2281 TransactionDetails transactionDetails = new TransactionDetails(); 2282 RequestPartitionId requestPartitionId = 2283 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2284 theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull); 2285 2286 myStorageInterceptorHooks.callStoragePresearchRegistered( 2287 theRequest, theParams, new NonPersistedSearch(getResourceName()), requestPartitionId); 2288 2289 return myTransactionService 2290 .withRequest(theRequest) 2291 .withTransactionDetails(transactionDetails) 2292 .withRequestPartitionId(requestPartitionId) 2293 .searchList(partition -> { 2294 if (isNull(theParams.getLoadSynchronousUpTo())) { 2295 theParams.setLoadSynchronousUpTo(myStorageSettings.getInternalSynchronousSearchSize()); 2296 } 2297 2298 ISearchBuilder<JpaPid> builder = 2299 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2300 2301 List<JpaPid> ids = new ArrayList<>(); 2302 2303 String uuid = UUID.randomUUID().toString(); 2304 2305 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2306 try (IResultIterator<JpaPid> iter = 2307 builder.createQuery(theParams, searchRuntimeDetails, theRequest, partition)) { 2308 while (iter.hasNext()) { 2309 ids.add(iter.next()); 2310 } 2311 } catch (IOException e) { 2312 ourLog.error("IO failure during database access", e); 2313 } 2314 2315 return ids; 2316 }); 2317 } 2318 2319 @Override 2320 public <PID extends IResourcePersistentId<?>> Stream<PID> searchForIdStream( 2321 SearchParameterMap theParams, 2322 RequestDetails theRequest, 2323 @Nullable IBaseResource theConditionalOperationTargetOrNull) { 2324 2325 // the Stream is useless outside the bound connection time, so require our caller to have a session. 2326 HapiTransactionService.requireTransaction(); 2327 2328 RequestPartitionId requestPartitionId = 2329 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2330 theRequest, myResourceName, theParams, theConditionalOperationTargetOrNull); 2331 2332 ISearchBuilder<JpaPid> builder = mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2333 2334 String uuid = UUID.randomUUID().toString(); 2335 2336 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2337 //noinspection unchecked 2338 return (Stream<PID>) myTransactionService 2339 .withRequest(theRequest) 2340 .withRequestPartitionId(requestPartitionId) 2341 .search(partition -> builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, partition)); 2342 } 2343 2344 @Override 2345 public List<T> searchForResources(SearchParameterMap theParams, RequestDetails theRequest) { 2346 return searchForTransformedIds(theParams, theRequest, this::pidsToResource); 2347 } 2348 2349 @Override 2350 public List<IIdType> searchForResourceIds(SearchParameterMap theParams, RequestDetails theRequest) { 2351 return searchForTransformedIds(theParams, theRequest, this::pidsToIds); 2352 } 2353 2354 private <V> List<V> searchForTransformedIds( 2355 SearchParameterMap theParams, 2356 RequestDetails theRequest, 2357 TriFunction<RequestDetails, Stream<JpaPid>, RequestPartitionId, Stream<V>> transform) { 2358 RequestPartitionId requestPartitionId = 2359 myRequestPartitionHelperService.determineReadPartitionForRequestForSearchType( 2360 theRequest, myResourceName, theParams); 2361 2362 String uuid = UUID.randomUUID().toString(); 2363 2364 SearchRuntimeDetails searchRuntimeDetails = new SearchRuntimeDetails(theRequest, uuid); 2365 return myTransactionService 2366 .withRequest(theRequest) 2367 .withPropagation(Propagation.REQUIRED) 2368 .withRequestPartitionId(requestPartitionId) 2369 .searchList(partition -> { 2370 ISearchBuilder<JpaPid> builder = 2371 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2372 Stream<JpaPid> pidStream = 2373 builder.createQueryStream(theParams, searchRuntimeDetails, theRequest, partition); 2374 2375 Stream<V> transformedStream = transform.apply(theRequest, pidStream, partition); 2376 2377 return transformedStream.collect(Collectors.toList()); 2378 }); 2379 } 2380 2381 /** 2382 * Fetch the resources in chunks and apply PreAccess/PreShow interceptors. 2383 */ 2384 @Nonnull 2385 private Stream<T> pidsToResource( 2386 RequestDetails theRequest, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) { 2387 ISearchBuilder<JpaPid> searchBuilder = 2388 mySearchBuilderFactory.newSearchBuilder(getResourceName(), getResourceType()); 2389 @SuppressWarnings("unchecked") 2390 Stream<T> resourceStream = (Stream<T>) new QueryChunker<>() 2391 .chunk(thePidStream, SearchBuilder.getMaximumPageSize()) 2392 .flatMap(pidChunk -> searchBuilder.loadResourcesByPid(pidChunk, theRequest).stream()); 2393 // apply interceptors 2394 return resourceStream 2395 .flatMap(resource -> resource == null 2396 ? Stream.empty() 2397 : invokeStoragePreAccessResources(theRequest, resource).stream()) 2398 .flatMap(resource -> Optional.ofNullable(invokeStoragePreShowResources(theRequest, resource)).stream()); 2399 } 2400 2401 /** 2402 * get the Ids from the ResourceTable entities in chunks. 2403 */ 2404 @Nonnull 2405 private Stream<IIdType> pidsToIds( 2406 RequestDetails theRequestDetails, Stream<JpaPid> thePidStream, RequestPartitionId theRequestPartitionId) { 2407 2408 Stream<JpaPid> pidStream = thePidStream; 2409 2410 if (!theRequestPartitionId.isAllPartitions() 2411 && theRequestPartitionId.getPartitionIds().size() == 1) { 2412 pidStream = pidStream.map(t -> t.setPartitionIdIfNotAlreadySet( 2413 theRequestPartitionId.getPartitionIds().get(0))); 2414 } 2415 2416 return new QueryChunker<>() 2417 .chunk(pidStream, SearchBuilder.getMaximumPageSize()) 2418 .flatMap(ids -> myResourceTableDao.findAllById(ids).stream()) 2419 .map(ResourceTable::getIdDt); 2420 } 2421 2422 protected <MT extends IBaseMetaType> MT toMetaDt(Class<MT> theType, Collection<TagDefinition> tagDefinitions) { 2423 MT retVal = ReflectionUtil.newInstance(theType); 2424 for (TagDefinition next : tagDefinitions) { 2425 switch (next.getTagType()) { 2426 case PROFILE: 2427 retVal.addProfile(next.getCode()); 2428 break; 2429 case SECURITY_LABEL: 2430 retVal.addSecurity() 2431 .setSystem(next.getSystem()) 2432 .setCode(next.getCode()) 2433 .setDisplay(next.getDisplay()); 2434 break; 2435 case TAG: 2436 retVal.addTag() 2437 .setSystem(next.getSystem()) 2438 .setCode(next.getCode()) 2439 .setDisplay(next.getDisplay()); 2440 break; 2441 } 2442 } 2443 myMetaTagSorter.sort(retVal); 2444 return retVal; 2445 } 2446 2447 private ArrayList<TagDefinition> toTagList(IBaseMetaType theMeta) { 2448 ArrayList<TagDefinition> retVal = new ArrayList<>(); 2449 2450 for (IBaseCoding next : theMeta.getTag()) { 2451 retVal.add(new TagDefinition(TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay())); 2452 } 2453 for (IBaseCoding next : theMeta.getSecurity()) { 2454 retVal.add( 2455 new TagDefinition(TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay())); 2456 } 2457 for (IPrimitiveType<String> next : theMeta.getProfile()) { 2458 retVal.add(new TagDefinition(TagTypeEnum.PROFILE, BaseHapiFhirDao.NS_JPA_PROFILE, next.getValue(), null)); 2459 } 2460 2461 return retVal; 2462 } 2463 2464 /** 2465 * @deprecated Use {@link #update(T, RequestDetails)} instead 2466 */ 2467 @Override 2468 public DaoMethodOutcome update(T theResource) { 2469 return update(theResource, null, null); 2470 } 2471 2472 @Override 2473 public DaoMethodOutcome update(T theResource, RequestDetails theRequestDetails) { 2474 return update(theResource, null, theRequestDetails); 2475 } 2476 2477 /** 2478 * @deprecated Use {@link #update(T, String, RequestDetails)} instead 2479 */ 2480 @Override 2481 public DaoMethodOutcome update(T theResource, String theMatchUrl) { 2482 return update(theResource, theMatchUrl, null); 2483 } 2484 2485 @Override 2486 public DaoMethodOutcome update(T theResource, String theMatchUrl, RequestDetails theRequestDetails) { 2487 return update(theResource, theMatchUrl, true, theRequestDetails); 2488 } 2489 2490 @Override 2491 public DaoMethodOutcome update( 2492 T theResource, String theMatchUrl, boolean thePerformIndexing, RequestDetails theRequestDetails) { 2493 return update(theResource, theMatchUrl, thePerformIndexing, false, theRequestDetails, new TransactionDetails()); 2494 } 2495 2496 @Override 2497 public DaoMethodOutcome update( 2498 T theResource, 2499 String theMatchUrl, 2500 boolean thePerformIndexing, 2501 boolean theForceUpdateVersion, 2502 RequestDetails theRequest, 2503 @Nonnull TransactionDetails theTransactionDetails) { 2504 if (theResource == null) { 2505 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "missingBody"); 2506 throw new InvalidRequestException(Msg.code(986) + msg); 2507 } 2508 if (!theResource.getIdElement().hasIdPart() && isBlank(theMatchUrl)) { 2509 String type = myFhirContext.getResourceType(theResource); 2510 String msg = myFhirContext.getLocalizer().getMessage(BaseStorageDao.class, "updateWithNoId", type); 2511 throw new InvalidRequestException(Msg.code(987) + msg); 2512 } 2513 2514 /* 2515 * Resource updates will modify/update the version of the resource with the new version. This is generally helpful 2516 * but leads to issues if the transaction is rolled back and retried. So if we do a rollback, we reset the resource 2517 * version to what it was. 2518 */ 2519 String id = theResource.getIdElement().getValue(); 2520 Runnable onRollback = () -> { 2521 ourLog.trace("Rolling back from {} to {}", theResource.getIdElement(), id); 2522 theResource.getIdElement().setValue(id); 2523 }; 2524 theTransactionDetails.addRollbackUndoAction(onRollback); 2525 2526 RequestPartitionId requestPartitionId = myRequestPartitionHelperService.determineCreatePartitionForRequest( 2527 theRequest, theResource, getResourceName()); 2528 2529 boolean rewriteHistory = theRequest != null && theRequest.isRewriteHistory(); 2530 if (rewriteHistory && !myStorageSettings.isUpdateWithHistoryRewriteEnabled()) { 2531 String msg = 2532 getContext().getLocalizer().getMessage(BaseStorageDao.class, "updateWithHistoryRewriteDisabled"); 2533 throw new InvalidRequestException(Msg.code(2781) + msg); 2534 } 2535 2536 Callable<DaoMethodOutcome> updateCallback; 2537 if (rewriteHistory) { 2538 updateCallback = () -> doUpdateWithHistoryRewrite( 2539 theResource, theRequest, theTransactionDetails, requestPartitionId, RestOperationTypeEnum.UPDATE); 2540 } else { 2541 updateCallback = () -> doUpdate( 2542 theResource, 2543 theMatchUrl, 2544 thePerformIndexing, 2545 theForceUpdateVersion, 2546 theRequest, 2547 theTransactionDetails, 2548 requestPartitionId); 2549 } 2550 2551 // Execute the update in a retryable transaction 2552 return myTransactionService 2553 .withRequest(theRequest) 2554 .withTransactionDetails(theTransactionDetails) 2555 .withRequestPartitionId(requestPartitionId) 2556 .execute(updateCallback); 2557 } 2558 2559 private DaoMethodOutcome doUpdate( 2560 T theResource, 2561 String theMatchUrl, 2562 boolean thePerformIndexing, 2563 boolean theForceUpdateVersion, 2564 RequestDetails theRequest, 2565 TransactionDetails theTransactionDetails, 2566 RequestPartitionId theRequestPartitionId) { 2567 DaoMethodOutcome outcome = null; 2568 preProcessResourceForStorage(theResource); 2569 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, thePerformIndexing); 2570 2571 ResourceTable entity = null; 2572 2573 IIdType resourceId = null; 2574 RestOperationTypeEnum update = RestOperationTypeEnum.UPDATE; 2575 if (isNotBlank(theMatchUrl)) { 2576 // Validate that the supplied resource matches the conditional. 2577 Set<JpaPid> match = myMatchResourceUrlService.processMatchUrl( 2578 theMatchUrl, myResourceType, theTransactionDetails, theRequest, theResource, theRequestPartitionId); 2579 if (match.size() > 1) { 2580 String msg = getContext() 2581 .getLocalizer() 2582 .getMessageSanitized( 2583 BaseStorageDao.class, 2584 "transactionOperationWithMultipleMatchFailure", 2585 "UPDATE", 2586 myResourceName, 2587 theMatchUrl, 2588 match.size()); 2589 throw new PreconditionFailedException(Msg.code(988) + msg); 2590 } else if (match.size() == 1) { 2591 JpaPid pid = match.iterator().next(); 2592 entity = myEntityManager.find(ResourceTable.class, pid); 2593 resourceId = entity.getIdDt(); 2594 if (myFhirContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4) 2595 && theResource.getIdElement().getIdPart() != null) { 2596 if (!Objects.equals(theResource.getIdElement().getIdPart(), resourceId.getIdPart())) { 2597 String msg = getContext() 2598 .getLocalizer() 2599 .getMessageSanitized( 2600 BaseStorageDao.class, 2601 "transactionOperationWithIdNotMatchFailure", 2602 "UPDATE", 2603 theMatchUrl); 2604 throw new InvalidRequestException(Msg.code(2279) + msg); 2605 } 2606 } 2607 } else { 2608 // assign UUID if no id provided in the request (numeric id mode is handled in doCreateForPostOrPut) 2609 if (!theResource.getIdElement().hasIdPart() 2610 && getStorageSettings().getResourceServerIdStrategy() 2611 == JpaStorageSettings.IdStrategyEnum.UUID) { 2612 theResource.setId(UUID.randomUUID().toString()); 2613 theResource.setUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED, Boolean.TRUE); 2614 } 2615 outcome = doCreateForPostOrPut( 2616 theRequest, 2617 theResource, 2618 theMatchUrl, 2619 false, 2620 thePerformIndexing, 2621 theRequestPartitionId, 2622 update, 2623 theTransactionDetails); 2624 2625 // Pre-cache the match URL 2626 if (outcome.getPersistentId() != null) { 2627 myMatchResourceUrlService.matchUrlResolved( 2628 theTransactionDetails, getResourceName(), theMatchUrl, (JpaPid) outcome.getPersistentId()); 2629 } 2630 } 2631 } else { 2632 /* 2633 * Note: resourceId will not be null or empty here, because we 2634 * check it and reject requests in 2635 * BaseOutcomeReturningMethodBindingWithResourceParam 2636 */ 2637 resourceId = theResource.getIdElement(); 2638 assert resourceId != null; 2639 assert resourceId.hasIdPart(); 2640 2641 if (!resourceId.hasResourceType()) { 2642 resourceId = resourceId.withResourceType(getResourceName()); 2643 } 2644 2645 boolean create = false; 2646 2647 if (theRequest != null) { 2648 String existenceCheck = theRequest.getHeader(JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK); 2649 if (JpaConstants.HEADER_UPSERT_EXISTENCE_CHECK_DISABLED.equals(existenceCheck)) { 2650 create = true; 2651 } 2652 } 2653 2654 if (!create) { 2655 if (theTransactionDetails != null && theTransactionDetails.hasNullResolvedResourceId(resourceId)) { 2656 create = true; 2657 } else { 2658 try { 2659 entity = readEntityLatestVersion( 2660 theRequest, resourceId, theRequestPartitionId, theTransactionDetails); 2661 if (myPartitionSettings.isPartitioningEnabled()) { 2662 validatePartitionIdMatch(theResource.getIdElement(), theRequestPartitionId, entity); 2663 } 2664 } catch (ResourceNotFoundException e) { 2665 create = true; 2666 } 2667 } 2668 } 2669 2670 if (create) { 2671 outcome = doCreateForPostOrPut( 2672 theRequest, 2673 theResource, 2674 null, 2675 false, 2676 thePerformIndexing, 2677 theRequestPartitionId, 2678 update, 2679 theTransactionDetails); 2680 } 2681 } 2682 2683 // Start 2684 if (outcome == null) { 2685 UpdateParameters updateParameters = new UpdateParameters<>() 2686 .setRequestDetails(theRequest) 2687 .setResourceIdToUpdate(resourceId) 2688 .setMatchUrl(theMatchUrl) 2689 .setShouldPerformIndexing(thePerformIndexing) 2690 .setShouldForceUpdateVersion(theForceUpdateVersion) 2691 .setResource(theResource) 2692 .setEntity(entity) 2693 .setOperationType(update) 2694 .setTransactionDetails(theTransactionDetails) 2695 .setShouldForcePopulateOldResourceForProcessing(false); 2696 2697 outcome = doUpdateForUpdateOrPatch(updateParameters); 2698 } 2699 2700 postUpdateTransaction(theTransactionDetails); 2701 2702 return outcome; 2703 } 2704 2705 @SuppressWarnings("rawtypes") 2706 protected void postUpdateTransaction(TransactionDetails theTransactionDetails) { 2707 // Transactions will delete these at the end of the entire transaction 2708 if (!theTransactionDetails.isFhirTransaction()) { 2709 Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds(); 2710 if (resourceIds != null && !resourceIds.isEmpty()) { 2711 @Nonnull 2712 List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList()); 2713 myResourceSearchUrlSvc.deleteByResIds(ids); 2714 } 2715 } 2716 } 2717 2718 @Override 2719 protected DaoMethodOutcome doUpdateForUpdateOrPatch(UpdateParameters<T> theUpdateParameters) { 2720 2721 /* 2722 * We stored a resource searchUrl at creation time to prevent resource duplication. 2723 * We'll clear any currently existing urls from the db, otherwise we could hit 2724 * duplicate index violations if we try to add another (after this create/update) 2725 */ 2726 ResourceTable entity = (ResourceTable) theUpdateParameters.getEntity(); 2727 2728 /* 2729 * If we read back the entity from hibernate, and it's already saying 2730 * it's been updated in this transaction, then someone is probably 2731 * doing multiple modifications of the same resource within the same 2732 * database transaction. The only way for this to work cleanly is for 2733 * us to flush hibernate now. That way we can increment the version 2734 * a second time, create multiple history entries, etc. 2735 */ 2736 if (entity != null && entity.isVersionUpdatedInCurrentTransaction()) { 2737 myEntityManager.flush(); 2738 } 2739 2740 if (entity.isSearchUrlPresent()) { 2741 JpaPid persistentId = entity.getResourceId(); 2742 theUpdateParameters.getTransactionDetails().addUpdatedResourceId(persistentId); 2743 2744 entity.setSearchUrlPresent(false); // it will be removed at the end 2745 } 2746 2747 if (entity.isDeleted()) { 2748 // We're un-deleting this entity so let's inform the memory cache service 2749 myIdHelperService.addResolvedPidToFhirIdAfterCommit( 2750 entity.getPersistentId(), 2751 entity.getPartitionId() == null 2752 ? RequestPartitionId.defaultPartition() 2753 : entity.getPartitionId().toPartitionId(), 2754 entity.getResourceType(), 2755 entity.getFhirId(), 2756 null); 2757 } 2758 2759 boolean shouldForcePopulateOldResourceForProcessing = myInterceptorBroadcaster instanceof InterceptorService 2760 && ((InterceptorService) myInterceptorBroadcaster) 2761 .hasRegisteredInterceptor(PatientCompartmentEnforcingInterceptor.class); 2762 theUpdateParameters.setShouldForcePopulateOldResourceForProcessing(shouldForcePopulateOldResourceForProcessing); 2763 return super.doUpdateForUpdateOrPatch(theUpdateParameters); 2764 } 2765 2766 /** 2767 * Method for updating the historical version of the resource when a history version id is included in the request. 2768 * 2769 * @param theResource to be saved 2770 * @param theRequest details of the request 2771 * @param theTransactionDetails details of the transaction 2772 * @param theRequestPartitionId the partition on which to perform the request 2773 * @param theRestOperationType the rest operation type (update or patch) 2774 * @return the outcome of the operation 2775 */ 2776 DaoMethodOutcome doUpdateWithHistoryRewrite( 2777 T theResource, 2778 RequestDetails theRequest, 2779 TransactionDetails theTransactionDetails, 2780 RequestPartitionId theRequestPartitionId, 2781 RestOperationTypeEnum theRestOperationType) { 2782 StopWatch w = new StopWatch(); 2783 2784 // No need for indexing as this will update a non-current version of the resource which will not be searchable 2785 preProcessResourceForStorage(theResource, theRequest, theTransactionDetails, false); 2786 2787 BaseHasResource entity; 2788 BaseHasResource currentEntity; 2789 2790 IIdType resourceId; 2791 2792 resourceId = theResource.getIdElement(); 2793 assert resourceId != null; 2794 assert resourceId.hasIdPart(); 2795 2796 try { 2797 currentEntity = readEntityLatestVersion( 2798 theRequest, resourceId.toVersionless(), theRequestPartitionId, theTransactionDetails); 2799 2800 if (!resourceId.hasVersionIdPart()) { 2801 throw new InvalidRequestException( 2802 Msg.code(2093) + "Invalid resource ID, ID must contain a history version"); 2803 } 2804 entity = readEntity(resourceId, theRequest); 2805 validateResourceType(entity); 2806 } catch (ResourceNotFoundException e) { 2807 throw new ResourceNotFoundException( 2808 Msg.code(2087) + "Resource not found [" + resourceId + "] - Doesn't exist"); 2809 } 2810 2811 if (resourceId.hasResourceType() && !resourceId.getResourceType().equals(getResourceName())) { 2812 throw new UnprocessableEntityException( 2813 Msg.code(2088) + "Invalid resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] of type[" 2814 + entity.getResourceType() + "] - Does not match expected [" + getResourceName() + "]"); 2815 } 2816 assert resourceId.hasVersionIdPart(); 2817 2818 boolean wasDeleted = isDeleted(entity); 2819 entity.setDeleted(null); 2820 boolean isUpdatingCurrent = resourceId.hasVersionIdPart() 2821 && Long.parseLong(resourceId.getVersionIdPart()) == currentEntity.getVersion(); 2822 IBasePersistedResource<?> savedEntity = updateHistoryEntity( 2823 theRequest, theResource, currentEntity, entity, resourceId, theTransactionDetails, isUpdatingCurrent); 2824 DaoMethodOutcome outcome = toMethodOutcome(theRequest, savedEntity, theResource, null, theRestOperationType) 2825 .setCreated(wasDeleted); 2826 2827 populateOperationOutcomeForUpdate(w, outcome, null, theRestOperationType, theTransactionDetails); 2828 2829 return outcome; 2830 } 2831 2832 @Override 2833 public MethodOutcome validate( 2834 T theResource, 2835 IIdType theId, 2836 String theRawResource, 2837 EncodingEnum theEncoding, 2838 ValidationModeEnum theMode, 2839 String theProfile, 2840 RequestDetails theRequest) { 2841 TransactionDetails transactionDetails = new TransactionDetails(); 2842 2843 if (theMode == ValidationModeEnum.DELETE) { 2844 if (theId == null || !theId.hasIdPart()) { 2845 throw new InvalidRequestException( 2846 Msg.code(991) + "No ID supplied. ID is required when validating with mode=DELETE"); 2847 } 2848 2849 RequestPartitionId requestPartitionId = 2850 myRequestPartitionHelperService.determineReadPartitionForRequestForRead( 2851 theRequest, getResourceName(), theId); 2852 2853 return myTransactionService 2854 .withRequest(theRequest) 2855 .withRequestPartitionId(requestPartitionId) 2856 .execute(() -> { 2857 final ResourceTable entity = 2858 readEntityLatestVersion(theRequest, theId, requestPartitionId, transactionDetails); 2859 2860 // Validate that there are no resources pointing to the candidate that 2861 // would prevent deletion 2862 DeleteConflictList deleteConflicts = new DeleteConflictList(); 2863 if (getStorageSettings().isEnforceReferentialIntegrityOnDelete()) { 2864 myDeleteConflictService.validateOkToDelete( 2865 deleteConflicts, entity, true, theRequest, new TransactionDetails()); 2866 } 2867 DeleteConflictUtil.validateDeleteConflictsEmptyOrThrowException(getContext(), deleteConflicts); 2868 2869 IBaseOperationOutcome oo = createInfoOperationOutcome("Ok to delete"); 2870 return new MethodOutcome(new IdDt(theId.getValue()), oo); 2871 }); 2872 } 2873 2874 FhirValidator validator = getContext().newValidator(); 2875 validator.setInterceptorBroadcaster( 2876 CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest)); 2877 validator.registerValidatorModule(getInstanceValidator()); 2878 validator.registerValidatorModule(new IdChecker(theMode)); 2879 2880 IBaseResource resourceToValidateById = null; 2881 if (theId != null && theId.hasResourceType() && theId.hasIdPart()) { 2882 Class<? extends IBaseResource> type = 2883 getContext().getResourceDefinition(theId.getResourceType()).getImplementingClass(); 2884 IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDaoOrNull(type); 2885 resourceToValidateById = dao.read(theId, theRequest); 2886 } 2887 2888 ValidationResult result; 2889 ValidationOptions options = new ValidationOptions().addProfileIfNotBlank(theProfile); 2890 2891 if (theResource == null) { 2892 if (resourceToValidateById != null) { 2893 result = validator.validateWithResult(resourceToValidateById, options); 2894 } else { 2895 String msg = getContext().getLocalizer().getMessage(BaseStorageDao.class, "cantValidateWithNoResource"); 2896 throw new InvalidRequestException(Msg.code(992) + msg); 2897 } 2898 } else if (isNotBlank(theRawResource)) { 2899 result = validator.validateWithResult(theRawResource, options); 2900 } else { 2901 result = validator.validateWithResult(theResource, options); 2902 } 2903 2904 MethodOutcome retVal = new MethodOutcome(); 2905 retVal.setOperationOutcome(result.toOperationOutcome()); 2906 // Note an earlier version of this code returned PreconditionFailedException when the validation 2907 // failed, but we since realized the spec requires we return 200 regardless of the validation result. 2908 return retVal; 2909 } 2910 2911 /** 2912 * Get the resource definition from the criteria which specifies the resource type 2913 */ 2914 @Override 2915 public RuntimeResourceDefinition validateCriteriaAndReturnResourceDefinition(String criteria) { 2916 String resourceName; 2917 if (criteria == null || criteria.trim().isEmpty()) { 2918 throw new IllegalArgumentException(Msg.code(994) + "Criteria cannot be empty"); 2919 } 2920 if (criteria.contains("?")) { 2921 resourceName = criteria.substring(0, criteria.indexOf("?")); 2922 } else { 2923 resourceName = criteria; 2924 } 2925 2926 return getContext().getResourceDefinition(resourceName); 2927 } 2928 2929 private void validateGivenIdIsAppropriateToRetrieveResource(IIdType theId, BaseHasResource entity) { 2930 if (!entity.getIdDt().getIdPart().equals(theId.getIdPart())) { 2931 // This means that the resource with the given numeric ID exists, but it has a "forced ID", meaning 2932 // that 2933 // as far as the outside world is concerned, the given ID doesn't exist (it's just an internal 2934 // pointer 2935 // to the 2936 // forced ID) 2937 throw new ResourceNotFoundException(Msg.code(2000) + theId); 2938 } 2939 } 2940 2941 private void validateResourceType(IBasePersistedResource<?> entity) { 2942 validateResourceType(entity, myResourceName); 2943 } 2944 2945 private void validateResourceTypeAndThrowInvalidRequestException(IIdType theId) { 2946 if (theId.hasResourceType() && !theId.getResourceType().equals(myResourceName)) { 2947 // Note- Throw a HAPI FHIR exception here so that hibernate doesn't try to translate it into a database 2948 // exception 2949 throw new InvalidRequestException(Msg.code(996) + "Incorrect resource type (" + theId.getResourceType() 2950 + ") for this DAO, wanted: " + myResourceName); 2951 } 2952 } 2953 2954 @VisibleForTesting 2955 public void setIdHelperSvcForUnitTest(IIdHelperService theIdHelperService) { 2956 myIdHelperService = theIdHelperService; 2957 } 2958 2959 @SuppressWarnings("BooleanMethodIsAlwaysInverted") 2960 public static <T extends IBaseResource> boolean isResourceIdServerAssigned(IBaseResource theResource) { 2961 return Boolean.TRUE.equals(theResource.getUserData(JpaConstants.RESOURCE_ID_SERVER_ASSIGNED)); 2962 } 2963 2964 private static class IdChecker implements IValidatorModule { 2965 2966 private final ValidationModeEnum myMode; 2967 2968 IdChecker(ValidationModeEnum theMode) { 2969 myMode = theMode; 2970 } 2971 2972 @Override 2973 public void validateResource(IValidationContext<IBaseResource> theCtx) { 2974 IBaseResource resource = theCtx.getResource(); 2975 if (resource instanceof Parameters) { 2976 List<ParametersParameterComponent> params = ((Parameters) resource).getParameter(); 2977 params = params.stream() 2978 .filter(param -> param.getName().contains("resource")) 2979 .collect(Collectors.toList()); 2980 resource = params.get(0).getResource(); 2981 } 2982 boolean hasId = resource.getIdElement().hasIdPart(); 2983 if (myMode == ValidationModeEnum.CREATE) { 2984 if (hasId) { 2985 throw new UnprocessableEntityException( 2986 Msg.code(997) + "Resource has an ID - ID must not be populated for a FHIR create"); 2987 } 2988 } else if (myMode == ValidationModeEnum.UPDATE) { 2989 if (!hasId) { 2990 throw new UnprocessableEntityException( 2991 Msg.code(998) + "Resource has no ID - ID must be populated for a FHIR update"); 2992 } 2993 } 2994 } 2995 } 2996 2997 /** 2998 * Validates that the partition ID of the request resource matches the partition ID of the entity. 2999 * <p> 3000 * If the request is for all partitions, this method does nothing. Otherwise, it checks that the resource type 3001 * and FHIR ID of the entity match those of the provided resource ID type, and that the partition IDs are equal. 3002 * If there is a mismatch, an {@link InvalidRequestException} is thrown. 3003 * 3004 * @param theResourceIdType The resource ID type being processed 3005 * @param theRequestPartitionId The partition ID from the request 3006 * @param theEntity The entity being updated 3007 * @throws InvalidRequestException if the partition IDs do not match for the same resource type and FHIR ID 3008 */ 3009 @VisibleForTesting 3010 protected void validatePartitionIdMatch( 3011 @Nonnull IIdType theResourceIdType, 3012 @Nonnull RequestPartitionId theRequestPartitionId, 3013 @Nonnull ResourceTable theEntity) { 3014 if (theRequestPartitionId.isAllPartitions()) { 3015 return; 3016 } 3017 3018 boolean sameResType = Objects.equals(theEntity.getResourceType(), theResourceIdType.getResourceType()); 3019 boolean sameFhirId = Objects.equals(theResourceIdType.getIdPart(), theEntity.getFhirId()); 3020 boolean differentPartition = 3021 !theRequestPartitionId.isPartition(theEntity.getPartitionId().getPartitionId()); 3022 3023 if (sameResType && sameFhirId && differentPartition) { 3024 String msg = getContext() 3025 .getLocalizer() 3026 .getMessageSanitized( 3027 BaseHapiFhirDao.class, 3028 "resourceTypeAndFhirIdConflictAcrossPartitions", 3029 theEntity.getResourceType(), 3030 theResourceIdType.getIdPart(), 3031 theRequestPartitionId.getFirstPartitionNameOrNull()); 3032 throw new InvalidRequestException(Msg.code(2733) + msg); 3033 } 3034 } 3035 3036 /** 3037 * Method return type for {@link #reindexCorrectCurrentVersion(ResourceTable, ReindexParameters.ReindexSearchParametersEnum)} 3038 */ 3039 private record CorrectCurrentVersionResponse( 3040 boolean forceReindexSps, boolean forceReloadResourceEntity, ResourceHistoryTable currentVersion) {} 3041}