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