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