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