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