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