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