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