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