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