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