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