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