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