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