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