
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.dao; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails; 025import ca.uhn.fhir.interceptor.model.RequestPartitionId; 026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 027import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 028import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 029import ca.uhn.fhir.jpa.api.model.PersistentIdToForcedIdMap; 030import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 031import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 032import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect; 033import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 034import ca.uhn.fhir.jpa.model.cross.JpaResourceLookup; 035import ca.uhn.fhir.jpa.model.dao.JpaPid; 036import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 037import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 038import ca.uhn.fhir.jpa.model.entity.ResourceTable; 039import ca.uhn.fhir.jpa.model.entity.StorageSettings; 040import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 041import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc; 042import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 043import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 044import ca.uhn.fhir.jpa.util.MemoryCacheService; 045import ca.uhn.fhir.jpa.util.QueryChunker; 046import ca.uhn.fhir.model.api.IQueryParameterType; 047import ca.uhn.fhir.rest.api.server.RequestDetails; 048import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 049import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 050import ca.uhn.fhir.rest.param.TokenParam; 051import ca.uhn.fhir.util.FhirTerser; 052import ca.uhn.fhir.util.ResourceReferenceInfo; 053import ca.uhn.fhir.util.StopWatch; 054import ca.uhn.fhir.util.TaskChunker; 055import com.google.common.annotations.VisibleForTesting; 056import com.google.common.collect.ArrayListMultimap; 057import com.google.common.collect.ListMultimap; 058import com.google.common.collect.MultimapBuilder; 059import com.google.common.collect.SetMultimap; 060import jakarta.annotation.Nonnull; 061import jakarta.annotation.Nullable; 062import jakarta.persistence.EntityManager; 063import jakarta.persistence.FlushModeType; 064import jakarta.persistence.PersistenceContext; 065import jakarta.persistence.PersistenceContextType; 066import jakarta.persistence.PersistenceException; 067import jakarta.persistence.Tuple; 068import jakarta.persistence.TypedQuery; 069import jakarta.persistence.criteria.CriteriaBuilder; 070import jakarta.persistence.criteria.CriteriaQuery; 071import jakarta.persistence.criteria.Join; 072import jakarta.persistence.criteria.Predicate; 073import jakarta.persistence.criteria.Root; 074import org.apache.commons.collections4.ListUtils; 075import org.apache.commons.lang3.Validate; 076import org.hibernate.internal.SessionImpl; 077import org.hl7.fhir.instance.model.api.IBase; 078import org.hl7.fhir.instance.model.api.IBaseBundle; 079import org.hl7.fhir.instance.model.api.IBaseResource; 080import org.hl7.fhir.instance.model.api.IIdType; 081import org.slf4j.Logger; 082import org.slf4j.LoggerFactory; 083import org.springframework.beans.factory.annotation.Autowired; 084import org.springframework.context.ApplicationContext; 085 086import java.util.ArrayList; 087import java.util.Collection; 088import java.util.HashMap; 089import java.util.HashSet; 090import java.util.IdentityHashMap; 091import java.util.Iterator; 092import java.util.List; 093import java.util.Map; 094import java.util.Optional; 095import java.util.Set; 096import java.util.TreeMap; 097import java.util.regex.Pattern; 098import java.util.stream.Collectors; 099 100import static ca.uhn.fhir.util.UrlUtil.determineResourceTypeInResourceUrl; 101import static java.util.stream.Collectors.groupingBy; 102import static org.apache.commons.lang3.StringUtils.countMatches; 103import static org.apache.commons.lang3.StringUtils.isNotBlank; 104 105public class TransactionProcessor extends BaseTransactionProcessor { 106 107 /** 108 * Matches conditional URLs in the form of [resourceType]?[paramName]=[paramValue]{...more params...} 109 */ 110 public static final Pattern MATCH_URL_PATTERN = Pattern.compile("^[^?]++[?][a-z0-9-]+=[^&,]++"); 111 112 public static final int CONDITIONAL_URL_FETCH_CHUNK_SIZE = 100; 113 private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class); 114 115 @Autowired 116 private ApplicationContext myApplicationContext; 117 118 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 119 private EntityManager myEntityManager; 120 121 @Autowired(required = false) 122 private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect; 123 124 @Autowired 125 private IIdHelperService<JpaPid> myIdHelperService; 126 127 @Autowired 128 private JpaStorageSettings myStorageSettings; 129 130 @Autowired 131 private FhirContext myFhirContext; 132 133 @Autowired 134 private MatchResourceUrlService<JpaPid> myMatchResourceUrlService; 135 136 @Autowired 137 private MatchUrlService myMatchUrlService; 138 139 @Autowired 140 private ResourceSearchUrlSvc myResourceSearchUrlSvc; 141 142 @Autowired 143 private MemoryCacheService myMemoryCacheService; 144 145 @Autowired 146 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 147 148 public void setEntityManagerForUnitTest(EntityManager theEntityManager) { 149 myEntityManager = theEntityManager; 150 } 151 152 @Override 153 protected void validateDependencies() { 154 super.validateDependencies(); 155 156 Validate.notNull(myEntityManager, "EntityManager must not be null"); 157 } 158 159 @VisibleForTesting 160 public void setFhirContextForUnitTest(FhirContext theFhirContext) { 161 myFhirContext = theFhirContext; 162 } 163 164 @Override 165 public void setStorageSettings(StorageSettings theStorageSettings) { 166 myStorageSettings = (JpaStorageSettings) theStorageSettings; 167 super.setStorageSettings(theStorageSettings); 168 } 169 170 @Override 171 protected EntriesToProcessMap doTransactionWriteOperations( 172 final RequestDetails theRequest, 173 RequestPartitionId theRequestPartitionId, 174 String theActionName, 175 TransactionDetails theTransactionDetails, 176 Set<IIdType> theAllIds, 177 IdSubstitutionMap theIdSubstitutions, 178 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 179 IBaseBundle theResponse, 180 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 181 List<IBase> theEntries, 182 StopWatch theTransactionStopWatch) { 183 184 /* 185 * We temporarily set the flush mode for the duration of the DB transaction 186 * from the default of AUTO to the temporary value of COMMIT here. We do this 187 * because in AUTO mode, if any SQL SELECTs are required during the 188 * processing of an individual transaction entry, the server will flush the 189 * pending INSERTs/UPDATEs to the database before executing the SELECT. 190 * This hurts performance since we don't get the benefit of batching those 191 * write operations as much as possible. The tradeoff here is that we 192 * could theoretically have transaction operations which try to read 193 * data previously written in the same transaction, and they won't see it. 194 * This shouldn't actually be an issue anyhow - we pre-fetch conditional 195 * URLs and reference targets at the start of the transaction. But this 196 * tradeoff still feels worth it, since the most common use of transactions 197 * is for fast writing of data. 198 * 199 * Note that it's probably not necessary to reset it back, it should 200 * automatically go back to the default value after the transaction, but 201 * we reset it just to be safe. 202 */ 203 FlushModeType initialFlushMode = myEntityManager.getFlushMode(); 204 try { 205 myEntityManager.setFlushMode(FlushModeType.COMMIT); 206 207 ITransactionProcessorVersionAdapter<?, ?> versionAdapter = getVersionAdapter(); 208 209 if (theRequestPartitionId != null) { 210 preFetch(theRequest, theTransactionDetails, theEntries, versionAdapter, theRequestPartitionId); 211 } 212 213 return super.doTransactionWriteOperations( 214 theRequest, 215 theRequestPartitionId, 216 theActionName, 217 theTransactionDetails, 218 theAllIds, 219 theIdSubstitutions, 220 theIdToPersistedOutcome, 221 theResponse, 222 theOriginalRequestOrder, 223 theEntries, 224 theTransactionStopWatch); 225 } finally { 226 myEntityManager.setFlushMode(initialFlushMode); 227 } 228 } 229 230 @SuppressWarnings("rawtypes") 231 private void preFetch( 232 RequestDetails theRequestDetails, 233 TransactionDetails theTransactionDetails, 234 List<IBase> theEntries, 235 ITransactionProcessorVersionAdapter theVersionAdapter, 236 RequestPartitionId theRequestPartitionId) { 237 Set<JpaPid> idsToPreFetchBodiesFor = new HashSet<>(); 238 Set<JpaPid> idsToPreFetchVersionsFor = new HashSet<>(); 239 Set<JpaPid> idsToPreFetchFhirIdsFor = new HashSet<>(); 240 241 /* 242 * Pre-Fetch any resources that are referred to normally by ID, e.g. 243 * regular FHIR updates within the transaction. 244 */ 245 preFetchResourcesById( 246 theRequestDetails, 247 theTransactionDetails, 248 theEntries, 249 theVersionAdapter, 250 theRequestPartitionId, 251 idsToPreFetchBodiesFor); 252 253 /* 254 * Pre-resolve any conditional URLs we can 255 */ 256 preFetchConditionalUrls( 257 theRequestDetails, 258 theTransactionDetails, 259 theEntries, 260 theVersionAdapter, 261 theRequestPartitionId, 262 idsToPreFetchBodiesFor, 263 idsToPreFetchVersionsFor, 264 idsToPreFetchFhirIdsFor); 265 266 /* 267 * Pre-Fetch Resource Bodies (this will happen for any resources we are potentially 268 * going to update) 269 */ 270 IFhirSystemDao<?, ?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class); 271 systemDao.preFetchResources(List.copyOf(idsToPreFetchBodiesFor), true); 272 273 /* 274 * Pre-Fetch Resource Versions (this will happen for any resources we are doing a 275 * conditional create on, meaning we don't actually care about the contents, just 276 * the ID and version) 277 */ 278 preFetchResourceVersions(idsToPreFetchVersionsFor); 279 280 preFetchFhirIds(idsToPreFetchFhirIdsFor, theTransactionDetails); 281 } 282 283 private void preFetchFhirIds(Set<JpaPid> theIdsToPreFetchFhirIdsFor, TransactionDetails theTransactionDetails) { 284 PersistentIdToForcedIdMap<JpaPid> forcedIds = 285 myIdHelperService.translatePidsToForcedIds(theIdsToPreFetchFhirIdsFor); 286 for (JpaPid nextId : theIdsToPreFetchFhirIdsFor) { 287 Optional<String> fhirIdOpt = forcedIds.get(nextId); 288 if (fhirIdOpt.isPresent()) { 289 String fhirIdString = fhirIdOpt.get(); 290 IIdType fhirId = myFhirContext.getVersion().newIdType(fhirIdString); 291 theTransactionDetails.addResolvedResourceId(fhirId, nextId); 292 } 293 } 294 } 295 296 /** 297 * Given a collection of {@link JpaPid}, loads the current version associated with 298 * each PID and puts it into the {@link JpaPid#setVersion(Long)} field. 299 */ 300 private void preFetchResourceVersions(Set<JpaPid> theIds) { 301 ourLog.trace("Versions to fetch: {}", theIds); 302 303 for (Iterator<JpaPid> it = theIds.iterator(); it.hasNext(); ) { 304 JpaPid pid = it.next(); 305 Long version = myMemoryCacheService.getIfPresent( 306 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid); 307 if (version != null) { 308 it.remove(); 309 pid.setVersion(version); 310 } 311 } 312 313 if (!theIds.isEmpty()) { 314 Map<JpaPid, JpaPid> idMap = theIds.stream().collect(Collectors.toMap(t -> t, t -> t)); 315 316 QueryChunker.chunk(theIds, ids -> { 317 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 318 CriteriaQuery<Tuple> cq = cb.createTupleQuery(); 319 Root<ResourceTable> from = cq.from(ResourceTable.class); 320 cq.multiselect(from.get("myPid"), from.get("myVersion")); 321 cq.where(from.get("myPid").in(ids)); 322 TypedQuery<Tuple> query = myEntityManager.createQuery(cq); 323 List<Tuple> results = query.getResultList(); 324 325 for (Tuple tuple : results) { 326 JpaPid pid = tuple.get(0, JpaPid.class); 327 Long version = tuple.get(1, Long.class); 328 idMap.get(pid).setVersion(version); 329 330 myMemoryCacheService.putAfterCommit( 331 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid, version); 332 } 333 }); 334 } 335 } 336 337 @Override 338 @SuppressWarnings("rawtypes") 339 protected void postTransactionProcess(TransactionDetails theTransactionDetails) { 340 Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds(); 341 if (resourceIds != null && !resourceIds.isEmpty()) { 342 List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList()); 343 myResourceSearchUrlSvc.deleteByResIds(ids); 344 } 345 } 346 347 @SuppressWarnings({"unchecked", "rawtypes"}) 348 private void preFetchResourcesById( 349 RequestDetails theRequestDetails, 350 TransactionDetails theTransactionDetails, 351 List<IBase> theEntries, 352 ITransactionProcessorVersionAdapter theVersionAdapter, 353 RequestPartitionId theRequestPartitionId, 354 Set<JpaPid> theIdsToPreFetchBodiesFor) { 355 356 FhirTerser terser = myFhirContext.newTerser(); 357 358 Map<IIdType, PrefetchReasonEnum> idsToPreResolve = new HashMap<>(theEntries.size() * 3); 359 360 for (IBase nextEntry : theEntries) { 361 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 362 if (resource != null) { 363 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 364 365 /* 366 * Pre-fetch any resources that are being updated or patched within 367 * the transaction 368 */ 369 if ("PUT".equals(verb) || "PATCH".equals(verb)) { 370 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 371 if (countMatches(requestUrl, '?') == 0) { 372 IIdType id = myFhirContext.getVersion().newIdType(); 373 id.setValue(requestUrl); 374 IIdType unqualifiedVersionless = id.toUnqualifiedVersionless(); 375 idsToPreResolve.put(unqualifiedVersionless, PrefetchReasonEnum.DIRECT_TARGET); 376 } 377 } 378 379 /* 380 * If there are any resource references anywhere in any resources being 381 * created or updated that point to another target resource directly by 382 * ID, we also want to prefetch the identity of that target ID 383 */ 384 if ("PUT".equals(verb) || "POST".equals(verb)) { 385 for (ResourceReferenceInfo referenceInfo : terser.getAllResourceReferences(resource)) { 386 IIdType reference = referenceInfo.getResourceReference().getReferenceElement(); 387 if (reference != null 388 && !reference.isLocal() 389 && !reference.isUuid() 390 && reference.hasResourceType() 391 && reference.hasIdPart() 392 && !reference.getValue().contains("?")) { 393 394 // We use putIfAbsent here because if we're already fetching 395 // as a direct target we don't want to downgrade to just a 396 // reference target 397 idsToPreResolve.putIfAbsent( 398 reference.toUnqualifiedVersionless(), PrefetchReasonEnum.REFERENCE_TARGET); 399 } 400 } 401 } 402 } 403 } 404 405 /* 406 * If any of the entries in the pre-fetch ID map have a value of REFERENCE_TARGET, 407 * this means we can't rely on cached identities because we need to know the 408 * current deleted status of at least one of them. This is because another thread 409 * (or potentially even another process elsewhere) could have moved the resource 410 * to "deleted", and we can't allow someone to add a reference to a deleted 411 * resource. If deletes are disabled on this server though, we can trust that 412 * nothing has been moved to "deleted" status since it was put in the cache, and 413 * it's safe to use the cache. 414 * 415 * On the other hand, if all resource IDs we want to prefetch have a value of 416 * DIRECT_UPDATE, that means these IDs are all resources we're about to 417 * modify. In that case it doesn't even matter if the resource is currently 418 * deleted because we're going to resurrect it in that case. 419 */ 420 boolean preFetchIncludesReferences = 421 idsToPreResolve.values().stream().anyMatch(t -> t == PrefetchReasonEnum.REFERENCE_TARGET); 422 ResolveIdentityMode resolveMode = preFetchIncludesReferences 423 ? ResolveIdentityMode.includeDeleted().noCacheUnlessDeletesDisabled() 424 : ResolveIdentityMode.includeDeleted().cacheOk(); 425 426 SetMultimap<RequestPartitionId, IIdType> partitionToIds = null; 427 Set<IIdType> referenceTargetIds = new HashSet<>(idsToPreResolve.keySet()); 428 RequestPartitionId requestPartitionId = theRequestPartitionId; 429 430 /* 431 * If specific resources are on different non-compatible partitions, we will pre-fetch them separately 432 * in a separate transaction that is scoped to the appropriate partition. 433 */ 434 if (myPartitionSettings.isPartitioningEnabled()) { 435 for (Iterator<IIdType> iterator = referenceTargetIds.iterator(); iterator.hasNext(); ) { 436 IIdType nextId = iterator.next(); 437 RequestPartitionId partition = theTransactionDetails.getResolvedPartition(nextId.getValue()); 438 if (partition == null) { 439 ReadPartitionIdRequestDetails readDetails = ReadPartitionIdRequestDetails.forRead(nextId); 440 partition = myRequestPartitionHelperSvc.determineReadPartitionForRequest( 441 theRequestDetails, readDetails); 442 } 443 if (!partition.isAllPartitions() 444 && !myHapiTransactionService.isCompatiblePartition(theRequestPartitionId, partition)) { 445 iterator.remove(); 446 if (partitionToIds == null) { 447 partitionToIds = 448 MultimapBuilder.hashKeys().hashSetValues().build(); 449 } 450 partitionToIds.put(partition, nextId); 451 } else { 452 requestPartitionId = requestPartitionId.mergeIds(partition); 453 } 454 } 455 } 456 457 doPreFetchResourcesById( 458 theTransactionDetails, 459 requestPartitionId, 460 referenceTargetIds, 461 idsToPreResolve, 462 resolveMode, 463 theIdsToPreFetchBodiesFor); 464 465 if (partitionToIds != null) { 466 for (RequestPartitionId nextPartition : partitionToIds.keySet()) { 467 Set<IIdType> ids = partitionToIds.get(nextPartition); 468 doPreFetchResourcesById( 469 theTransactionDetails, 470 nextPartition, 471 ids, 472 idsToPreResolve, 473 resolveMode, 474 theIdsToPreFetchBodiesFor); 475 } 476 } 477 } 478 479 private void doPreFetchResourcesById( 480 TransactionDetails theTransactionDetails, 481 RequestPartitionId theRequestPartitionId, 482 Set<IIdType> theInputIdsToPreFetch, 483 Map<IIdType, PrefetchReasonEnum> theInputIdsToPreResolve, 484 ResolveIdentityMode theResolveMode, 485 Set<JpaPid> theOutputIdsToPreFetchBodiesFor) { 486 487 Set<String> foundIds = new HashSet<>(); 488 489 // If any of the IDs are already resolved in the TransactionDetails, just 490 // use the resolution from there 491 Map<IIdType, IResourceLookup<JpaPid>> outcomesFromTransactionDetails = null; 492 if (theRequestPartitionId.hasPartitionIds() 493 && theRequestPartitionId.getPartitionIds().size() == 1) { 494 for (IIdType inputIdToResult : theInputIdsToPreFetch) { 495 JpaPid pidResolvedInTransaction = (JpaPid) theTransactionDetails.getResolvedResourceId(inputIdToResult); 496 if (pidResolvedInTransaction != null) { 497 if (outcomesFromTransactionDetails == null) { 498 outcomesFromTransactionDetails = new HashMap<>(); 499 } 500 JpaResourceLookup resourceLookup = new JpaResourceLookup( 501 inputIdToResult.getResourceType(), 502 inputIdToResult.getIdPart(), 503 pidResolvedInTransaction, 504 null, 505 PartitionablePartitionId.with(theRequestPartitionId.getFirstPartitionIdOrNull(), null)); 506 outcomesFromTransactionDetails.put(inputIdToResult, resourceLookup); 507 } 508 } 509 } 510 511 Set<IIdType> inputIdsToPreFetch = theInputIdsToPreFetch; 512 if (outcomesFromTransactionDetails != null) { 513 inputIdsToPreFetch = new HashSet<>(theInputIdsToPreFetch); 514 inputIdsToPreFetch.removeAll(outcomesFromTransactionDetails.keySet()); 515 } 516 517 Map<IIdType, IResourceLookup<JpaPid>> outcomes = 518 myIdHelperService.resolveResourceIdentities(theRequestPartitionId, inputIdsToPreFetch, theResolveMode); 519 520 if (outcomesFromTransactionDetails != null) { 521 outcomes.putAll(outcomesFromTransactionDetails); 522 } 523 524 for (Iterator<Map.Entry<IIdType, IResourceLookup<JpaPid>>> iterator = 525 outcomes.entrySet().iterator(); 526 iterator.hasNext(); ) { 527 Map.Entry<IIdType, IResourceLookup<JpaPid>> entry = iterator.next(); 528 JpaPid next = entry.getValue().getPersistentId(); 529 IIdType unqualifiedVersionlessId = entry.getKey(); 530 switch (theInputIdsToPreResolve.get(unqualifiedVersionlessId)) { 531 case DIRECT_TARGET -> { 532 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY 533 || (next.getAssociatedResourceId() != null 534 && !next.getAssociatedResourceId().isIdPartValidLong())) { 535 theOutputIdsToPreFetchBodiesFor.add(next); 536 } 537 } 538 case REFERENCE_TARGET -> { 539 if (entry.getValue().getDeleted() != null) { 540 iterator.remove(); 541 continue; 542 } 543 } 544 } 545 546 foundIds.add(unqualifiedVersionlessId.getValue()); 547 theTransactionDetails.addResolvedResourceId(unqualifiedVersionlessId, next); 548 } 549 550 // Any IDs that could not be resolved are presumably not there, so 551 // cache that fact so we don't look again later 552 for (IIdType next : theInputIdsToPreFetch) { 553 if (!foundIds.contains(next.getValue())) { 554 theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null); 555 } 556 } 557 } 558 559 @Override 560 protected void handleVerbChangeInTransactionWriteOperations() { 561 super.handleVerbChangeInTransactionWriteOperations(); 562 563 myEntityManager.flush(); 564 } 565 566 @SuppressWarnings({"rawtypes", "unchecked"}) 567 private void preFetchConditionalUrls( 568 RequestDetails theRequestDetails, 569 TransactionDetails theTransactionDetails, 570 List<IBase> theEntries, 571 ITransactionProcessorVersionAdapter theVersionAdapter, 572 RequestPartitionId theRequestPartitionId, 573 Set<JpaPid> theIdsToPreFetchBodiesFor, 574 Set<JpaPid> theIdsToPreFetchVersionsFor, 575 Set<JpaPid> theIdsToPreFetchFhirIdsFor) { 576 577 List<MatchUrlToResolve> searchParameterMapsToResolve = new ArrayList<>(); 578 for (IBase nextEntry : theEntries) { 579 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 580 if (resource != null) { 581 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 582 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 583 String requestIfNoneExist = theVersionAdapter.getEntryIfNoneExist(nextEntry); 584 String resourceType = determineResourceTypeInResourceUrl(myFhirContext, requestUrl); 585 if (resourceType == null) { 586 resourceType = myFhirContext.getResourceType(resource); 587 } 588 if (("PUT".equals(verb) || "PATCH".equals(verb)) && requestUrl != null && requestUrl.contains("?")) { 589 IBaseResource associatedResource = null; 590 if ("PUT".equals(verb)) { 591 associatedResource = resource; 592 } 593 processConditionalUrlForPreFetching( 594 theRequestPartitionId, 595 resourceType, 596 associatedResource, 597 requestUrl, 598 true, 599 false, 600 theIdsToPreFetchBodiesFor, 601 theIdsToPreFetchFhirIdsFor, 602 searchParameterMapsToResolve); 603 } else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) { 604 processConditionalUrlForPreFetching( 605 theRequestPartitionId, 606 resourceType, 607 resource, 608 requestIfNoneExist, 609 false, 610 true, 611 theIdsToPreFetchBodiesFor, 612 theIdsToPreFetchFhirIdsFor, 613 searchParameterMapsToResolve); 614 } 615 616 if (myStorageSettings.isAllowInlineMatchUrlReferences()) { 617 List<ResourceReferenceInfo> references = 618 myFhirContext.newTerser().getAllResourceReferences(resource); 619 for (ResourceReferenceInfo next : references) { 620 String referenceUrl = next.getResourceReference() 621 .getReferenceElement() 622 .getValue(); 623 String refResourceType = determineResourceTypeInResourceUrl(myFhirContext, referenceUrl); 624 if (refResourceType != null) { 625 processConditionalUrlForPreFetching( 626 theRequestPartitionId, 627 refResourceType, 628 null, 629 referenceUrl, 630 false, 631 false, 632 theIdsToPreFetchBodiesFor, 633 theIdsToPreFetchFhirIdsFor, 634 searchParameterMapsToResolve); 635 } 636 } 637 } 638 } 639 } 640 641 // group things by match url so we can run them together. 642 record MatchTarget(String url, String resourceType) { 643 @Nonnull 644 static MatchTarget getMatchTarget(MatchUrlToResolve r) { 645 return new MatchTarget(r.myRequestUrl, r.myResourceDefinition.getName()); 646 } 647 } 648 649 ListMultimap<RequestPartitionId, MatchUrlToResolve> partitionToMatchUrls = groupMatchTargetListByPartitionId( 650 theRequestDetails, searchParameterMapsToResolve, theRequestPartitionId); 651 for (RequestPartitionId partitionId : partitionToMatchUrls.keySet()) { 652 myHapiTransactionService 653 .withRequest(theRequestDetails) 654 .withRequestPartitionId(partitionId) 655 .execute(() -> { 656 List<MatchUrlToResolve> matchUrls = partitionToMatchUrls.get(partitionId); 657 658 /* 659 * Chunk references into query-friendly sizes to resolve in batches. 660 * Note: we can have 1000s of references all using the same url. 661 * E.g. Organization references in big patient bundles. But if 662 * these are scattered among other different URLs within the Bundle, 663 * we don't want to end up resolving the same URL over and over. 664 * So we build batches by url, not by reference. 665 */ 666 Map<MatchTarget, List<MatchUrlToResolve>> byMatchUrl = 667 matchUrls.stream().collect(groupingBy(MatchTarget::getMatchTarget)); 668 669 TaskChunker.chunk(byMatchUrl.entrySet(), CONDITIONAL_URL_FETCH_CHUNK_SIZE, nextUrlChunk -> { 670 671 /* 672 * Combine all resolve entries under this chunk of urls. If we have several entries with 673 * the exact same URL, that means we'll have several entries in the following list, but 674 * preFetchSearchParameterMaps(..) will only add one parameter to the SQL it generates for 675 * each URL's SP hash value. 676 */ 677 List<MatchUrlToResolve> combinedChunk = nextUrlChunk.stream() 678 .flatMap(cc -> cc.getValue().stream()) 679 .toList(); 680 681 preFetchSearchParameterMaps( 682 theRequestDetails, 683 theTransactionDetails, 684 partitionId, 685 combinedChunk, 686 theIdsToPreFetchBodiesFor, 687 theIdsToPreFetchVersionsFor); 688 }); 689 }); 690 } 691 } 692 693 /** 694 * Given a collection of {@link MatchUrlToResolve} objects, calculates the read 695 * {@link RequestPartitionId} associated with each one and returns a 696 * map of the partition ID to the list of associated match URLs. 697 * <p> 698 * If two different {@link RequestPartitionId} are considered compatible per the 699 * {@link ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService#isCompatiblePartition(RequestPartitionId, RequestPartitionId)} 700 * method, the two partitions are combined into a single {@link RequestPartitionId} object and all 701 * match URLs associated with both will be returned in a single list associated with the 702 * combined {@link RequestPartitionId}. 703 * </p> 704 */ 705 @Nonnull 706 private ListMultimap<RequestPartitionId, MatchUrlToResolve> groupMatchTargetListByPartitionId( 707 RequestDetails theRequestDetails, 708 List<MatchUrlToResolve> theMatchUrls, 709 RequestPartitionId theOuterRequestPartitionId) { 710 ListMultimap<RequestPartitionId, MatchUrlToResolve> retVal = 711 MultimapBuilder.hashKeys().arrayListValues().build(); 712 713 /* 714 * For each Match URL, calculate the request partition and populate a Multimap 715 */ 716 for (MatchUrlToResolve next : theMatchUrls) { 717 RequestPartitionId partition = RequestPartitionId.allPartitions(); 718 if (myPartitionSettings.isPartitioningEnabled()) { 719 partition = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType( 720 theRequestDetails, 721 next.myResourceDefinition.getName(), 722 next.myMatchUrlSearchMap, 723 next.getAssociatedResource()); 724 if (partition.isAllPartitions()) { 725 partition = theOuterRequestPartitionId; 726 } 727 } 728 729 retVal.put(partition, next); 730 } 731 732 /* 733 * Try to combine any request partitions which are considered compatible by the 734 * transaction service. We're just using a brute force way to determine this, which 735 * could probably be optimized some, but it's not expected that we'll typically have 736 * many different partitions in the same transaction so it probably doesn't matter 737 * too much. 738 */ 739 while (true) { 740 boolean changes = false; 741 742 List<RequestPartitionId> partitionUds = new ArrayList<>(retVal.keySet()); 743 for (int indexA = 0; indexA < partitionUds.size(); indexA++) { 744 for (int indexB = 0; indexB < partitionUds.size(); indexB++) { 745 if (indexA == indexB) { 746 continue; 747 } 748 749 RequestPartitionId partitionA = partitionUds.get(indexA); 750 RequestPartitionId partitionB = partitionUds.get(indexB); 751 if (partitionA == null 752 || partitionA.isAllPartitions() 753 || partitionB == null 754 || partitionB.isAllPartitions()) { 755 continue; 756 } 757 758 if (myHapiTransactionService.isCompatiblePartition(partitionA, partitionB)) { 759 changes = true; 760 List<MatchUrlToResolve> matchUrlsA = retVal.removeAll(partitionA); 761 List<MatchUrlToResolve> matchUrlsB = retVal.removeAll(partitionB); 762 763 RequestPartitionId partitionBoth = partitionA.mergeIds(partitionB); 764 List<MatchUrlToResolve> matchUrlsBoth = ListUtils.union(matchUrlsA, matchUrlsB); 765 766 retVal.putAll(partitionBoth, matchUrlsBoth); 767 partitionUds.set(indexA, null); 768 partitionUds.set(indexB, null); 769 } 770 } 771 } 772 773 if (!changes) { 774 break; 775 } 776 } 777 return retVal; 778 } 779 780 /** 781 * This method attempts to resolve a collection of conditional URLs that were found 782 * in a FHIR transaction bundle being processed. 783 * 784 * @param theRequestDetails The active request 785 * @param theTransactionDetails The active transaction details 786 * @param theRequestPartitionId The active partition 787 * @param theInputParameters These are the conditional URLs that will actually be resolved 788 * @param theOutputPidsToLoadBodiesFor This list will be added to with any resource PIDs that need to be fully 789 * preloaded (i.e. fetch the actual resource body since we're presumably 790 * going to update it and will need to see its current state eventually) 791 * @param theOutputPidsToLoadVersionsFor This list will be added to with any resource PIDs that need to have 792 * their current version resolved. This is used for conditional creates, 793 * where we don't actually care about the body of the resource, only 794 * the version it has (since the version is returned in the response, 795 * and potentially used if we're auto-versioning references). 796 */ 797 @VisibleForTesting 798 public void preFetchSearchParameterMaps( 799 RequestDetails theRequestDetails, 800 TransactionDetails theTransactionDetails, 801 RequestPartitionId theRequestPartitionId, 802 List<MatchUrlToResolve> theInputParameters, 803 Set<JpaPid> theOutputPidsToLoadBodiesFor, 804 Set<JpaPid> theOutputPidsToLoadVersionsFor) { 805 806 Set<Long> systemAndValueHashes = new HashSet<>(); 807 Set<Long> valueHashes = new HashSet<>(); 808 809 for (MatchUrlToResolve next : theInputParameters) { 810 Collection<List<List<IQueryParameterType>>> values = next.myMatchUrlSearchMap.values(); 811 812 /* 813 * Any conditional URLs that consist of a single token parameter are batched 814 * up into a single query against the HFJ_SPIDX_TOKEN table so that we only 815 * perform one SQL query for all of them. 816 * 817 * We could potentially add other patterns in the future, but it's much more 818 * tricky to implement this when there are multiple parameters, and non-token 819 * parameter types aren't often used on their own in conditional URLs. So for 820 * now we handle single-token only, and that's probably good enough. 821 */ 822 boolean canBeHandledInAggregateQuery = false; 823 824 if (values.size() == 1) { 825 List<List<IQueryParameterType>> andList = values.iterator().next(); 826 IQueryParameterType param = andList.get(0).get(0); 827 828 if (param instanceof TokenParam tokenParam) { 829 canBeHandledInAggregateQuery = buildHashPredicateFromTokenParam( 830 tokenParam, theRequestPartitionId, next, systemAndValueHashes, valueHashes); 831 } 832 } 833 834 if (!canBeHandledInAggregateQuery) { 835 Set<JpaPid> matchUrlResults = myMatchResourceUrlService.processMatchUrl( 836 next.myRequestUrl, 837 next.myResourceDefinition.getImplementingClass(), 838 theTransactionDetails, 839 theRequestDetails, 840 theRequestPartitionId); 841 for (JpaPid matchUrlResult : matchUrlResults) { 842 handleFoundPreFetchResourceId( 843 theTransactionDetails, 844 theOutputPidsToLoadBodiesFor, 845 theOutputPidsToLoadVersionsFor, 846 next, 847 matchUrlResult); 848 } 849 } 850 } 851 852 preFetchSearchParameterMapsToken( 853 "myHashSystemAndValue", 854 systemAndValueHashes, 855 theTransactionDetails, 856 theRequestPartitionId, 857 theInputParameters, 858 theOutputPidsToLoadBodiesFor, 859 theOutputPidsToLoadVersionsFor); 860 preFetchSearchParameterMapsToken( 861 "myHashValue", 862 valueHashes, 863 theTransactionDetails, 864 theRequestPartitionId, 865 theInputParameters, 866 theOutputPidsToLoadBodiesFor, 867 theOutputPidsToLoadVersionsFor); 868 869 // For each SP Map which did not return a result, tag it as not found. 870 theInputParameters.stream() 871 // No matches 872 .filter(match -> !match.myResolved) 873 .forEach(match -> { 874 ourLog.debug("Was unable to match url {} from database", match.myRequestUrl); 875 theTransactionDetails.addResolvedMatchUrl( 876 myFhirContext, match.myRequestUrl, TransactionDetails.NOT_FOUND); 877 }); 878 } 879 880 /** 881 * Here we do a select against the {@link ResourceIndexedSearchParamToken} table for any rows that have the 882 * specific sys+val or val hashes we know we need to pre-fetch. 883 * <p> 884 * Note that we do a tuple query for only 2 columns in order to ensure that we can get by with only 885 * the data in the index (ie no need to load the actual table rows). 886 */ 887 public void preFetchSearchParameterMapsToken( 888 String theIndexColumnName, 889 Set<Long> theHashesForIndexColumn, 890 TransactionDetails theTransactionDetails, 891 RequestPartitionId theRequestPartitionId, 892 List<MatchUrlToResolve> theInputParameters, 893 Set<JpaPid> theOutputPidsToLoadFully, 894 Set<JpaPid> theOutputPidsToLoadVersionsFor) { 895 if (!theHashesForIndexColumn.isEmpty()) { 896 ListMultimap<Long, MatchUrlToResolve> hashToSearchMap = 897 buildHashToSearchMap(theInputParameters, theIndexColumnName); 898 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 899 CriteriaQuery<Tuple> cq = cb.createTupleQuery(); 900 Root<ResourceIndexedSearchParamToken> token = cq.from(ResourceIndexedSearchParamToken.class); 901 Join<ResourceIndexedSearchParamToken, ResourceTable> resourceTable = token.join("myResource"); 902 903 cq.multiselect( 904 token.get("myPartitionIdValue"), 905 token.get("myResourcePid"), 906 token.get(theIndexColumnName), 907 resourceTable.get("myFhirId"), 908 resourceTable.get("myResourceType")); 909 910 Predicate masterPredicate; 911 if (theHashesForIndexColumn.size() == 1) { 912 masterPredicate = cb.equal( 913 token.get(theIndexColumnName), 914 theHashesForIndexColumn.iterator().next()); 915 } else { 916 masterPredicate = token.get(theIndexColumnName).in(theHashesForIndexColumn); 917 } 918 919 if (myPartitionSettings.isPartitioningEnabled() 920 && !myPartitionSettings.isIncludePartitionInSearchHashes()) { 921 if (myRequestPartitionHelperSvc.isDefaultPartition(theRequestPartitionId) 922 && myPartitionSettings.getDefaultPartitionId() == null) { 923 Predicate partitionIdCriteria = cb.isNull(token.get("myPartitionIdValue")); 924 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 925 } else if (!theRequestPartitionId.isAllPartitions()) { 926 Predicate partitionIdCriteria = 927 token.get("myPartitionIdValue").in(theRequestPartitionId.getPartitionIds()); 928 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 929 } 930 } 931 932 cq.where(masterPredicate); 933 934 TypedQuery<Tuple> query = myEntityManager.createQuery(cq); 935 936 /* 937 * If we have 10 unique conditional URLs we're resolving, each one should 938 * resolve to 0..1 resources if they are valid as conditional URLs. So we would 939 * expect this query to return 0..10 rows, since conditional URLs for all 940 * conditional operations except DELETE (which isn't being applied here) are 941 * only allowed to resolve to 0..1 resources. 942 * 943 * If a conditional URL matches 2+ resources that is an error, and we'll 944 * be throwing an exception below. This limit is here for safety just to 945 * ensure that if someone uses a conditional URL that matches a million resources, 946 * we don't do a super-expensive fetch. 947 */ 948 query.setMaxResults(theHashesForIndexColumn.size() + 1); 949 950 List<Tuple> results = query.getResultList(); 951 952 for (Tuple nextResult : results) { 953 Integer nextPartitionId = nextResult.get(0, Integer.class); 954 Long nextResourcePid = nextResult.get(1, Long.class); 955 Long nextHash = nextResult.get(2, Long.class); 956 String idPart = nextResult.get(3, String.class); 957 String resourceType = nextResult.get(4, String.class); 958 959 JpaPid pid = JpaPid.fromId(nextResourcePid, nextPartitionId); 960 IIdType fhirId = myFhirContext.getVersion().newIdType(resourceType, idPart); 961 theTransactionDetails.addResolvedResourceId(fhirId, pid); 962 963 List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash); 964 matchedSearch.forEach(matchUrl -> { 965 ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl); 966 967 handleFoundPreFetchResourceId( 968 theTransactionDetails, 969 theOutputPidsToLoadFully, 970 theOutputPidsToLoadVersionsFor, 971 matchUrl, 972 pid); 973 }); 974 } 975 } 976 } 977 978 private void handleFoundPreFetchResourceId( 979 TransactionDetails theTransactionDetails, 980 Set<JpaPid> theOutputPidsToLoadFully, 981 Set<JpaPid> theOutputPidsToLoadVersionsFor, 982 MatchUrlToResolve theMatchUrl, 983 JpaPid theFoundPid) { 984 if (theMatchUrl.myShouldPreFetchResourceBody) { 985 theOutputPidsToLoadFully.add(theFoundPid); 986 } 987 if (theMatchUrl.myShouldPreFetchResourceVersion) { 988 theOutputPidsToLoadVersionsFor.add(theFoundPid); 989 } 990 myMatchResourceUrlService.matchUrlResolved( 991 theTransactionDetails, 992 theMatchUrl.myResourceDefinition.getName(), 993 theMatchUrl.myRequestUrl, 994 theFoundPid); 995 theTransactionDetails.addResolvedMatchUrl(myFhirContext, theMatchUrl.myRequestUrl, theFoundPid); 996 theMatchUrl.setResolved(true); 997 } 998 999 /** 1000 * Examines a conditional URL, and potentially adds it to either {@literal theOutputIdsToPreFetchBodiesFor} 1001 * or {@literal theOutputSearchParameterMapsToResolve}. 1002 * <p> 1003 * Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match 1004 * URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving 1005 * a match URL because it's there for a conditional update, we'll eagerly fetch the 1006 * actual resource because we need to know its current state in order to update it. However, if 1007 * the match URL is from an inline match URL in a resource body, we really only care about 1008 * the PID and don't need the body so we don't load it. This does have a security implication, since 1009 * it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut 1010 * isn't fired even though the user has resolved the URL (meaning they may be able to test for 1011 * the existence of a resource using a match URL). There is a test for this called 1012 * {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff 1013 * is acceptable since we're only prefetching things with very simple match URLs (nothing with 1014 * a reference in it for example) so it's not really possible to doing anything useful with this. 1015 * </p> 1016 * 1017 * @param thePartitionId The partition ID of the associated resource (can be null) 1018 * @param theResourceType The resource type associated with the match URL (ie what resource type should it resolve to) 1019 * @param theRequestUrl The actual match URL, which could be as simple as just parameters or could include the resource type too 1020 * @param theShouldPreFetchResourceBody Should we also fetch the actual resource body, or just figure out the PID associated with it? See the method javadoc above for some context. 1021 * @param theOutputIdsToPreFetchBodiesFor This will be populated with any resource PIDs that need to be pre-fetched 1022 * @param theOutputIdsToPreFetchFhirIdsFor 1023 * @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve 1024 */ 1025 private void processConditionalUrlForPreFetching( 1026 RequestPartitionId thePartitionId, 1027 String theResourceType, 1028 @Nullable IBaseResource theAssociatedResource, 1029 String theRequestUrl, 1030 boolean theShouldPreFetchResourceBody, 1031 boolean theShouldPreFetchResourceVersion, 1032 Set<JpaPid> theOutputIdsToPreFetchBodiesFor, 1033 Set<JpaPid> theOutputIdsToPreFetchFhirIdsFor, 1034 List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) { 1035 JpaPid cachedId = 1036 myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl, thePartitionId); 1037 if (cachedId != null) { 1038 if (theShouldPreFetchResourceBody) { 1039 theOutputIdsToPreFetchBodiesFor.add(cachedId); 1040 } else { 1041 theOutputIdsToPreFetchFhirIdsFor.add(cachedId); 1042 } 1043 } else if (MATCH_URL_PATTERN.matcher(theRequestUrl).find()) { 1044 RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType); 1045 SearchParameterMap matchUrlSearchMap = 1046 myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition); 1047 assert matchUrlSearchMap != null; 1048 theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve( 1049 theRequestUrl, 1050 theAssociatedResource, 1051 matchUrlSearchMap, 1052 resourceDefinition, 1053 theShouldPreFetchResourceBody, 1054 theShouldPreFetchResourceVersion)); 1055 } 1056 } 1057 1058 /** 1059 * Given a token parameter, build the query predicate based on its hash. Uses system and value if both are available, otherwise just value. 1060 * If neither are available, it returns null. 1061 * 1062 * @return Returns {@literal true} if the param was added to one of the output lists 1063 */ 1064 private boolean buildHashPredicateFromTokenParam( 1065 TokenParam theTokenParam, 1066 RequestPartitionId theRequestPartitionId, 1067 MatchUrlToResolve theMatchUrl, 1068 Set<Long> theOutputSysAndValuePredicates, 1069 Set<Long> theOutputValuePredicates) { 1070 if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) { 1071 theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue( 1072 myPartitionSettings, 1073 theRequestPartitionId, 1074 theMatchUrl.myResourceDefinition.getName(), 1075 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 1076 theTokenParam.getSystem(), 1077 theTokenParam.getValue()); 1078 theOutputSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue); 1079 return true; 1080 } else if (isNotBlank(theTokenParam.getValue())) { 1081 theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue( 1082 myPartitionSettings, 1083 theRequestPartitionId, 1084 theMatchUrl.myResourceDefinition.getName(), 1085 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 1086 theTokenParam.getValue()); 1087 theOutputValuePredicates.add(theMatchUrl.myHashValue); 1088 return true; 1089 } 1090 1091 return false; 1092 } 1093 1094 private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap( 1095 List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) { 1096 ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create(); 1097 // Build a lookup map so we don't have to iterate over the searches repeatedly. 1098 for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { 1099 if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) { 1100 hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap); 1101 } 1102 if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) { 1103 hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap); 1104 } 1105 } 1106 return hashToSearch; 1107 } 1108 1109 @Override 1110 protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) { 1111 try { 1112 int insertionCount; 1113 int updateCount; 1114 SessionImpl session = myEntityManager.unwrap(SessionImpl.class); 1115 if (session != null) { 1116 insertionCount = session.getActionQueue().numberOfInsertions(); 1117 updateCount = session.getActionQueue().numberOfUpdates(); 1118 } else { 1119 insertionCount = -1; 1120 updateCount = -1; 1121 } 1122 1123 StopWatch sw = new StopWatch(); 1124 myEntityManager.flush(); 1125 ourLog.debug( 1126 "Session flush took {}ms for {} inserts and {} updates", 1127 sw.getMillis(), 1128 insertionCount, 1129 updateCount); 1130 } catch (PersistenceException e) { 1131 if (myHapiFhirHibernateJpaDialect != null) { 1132 String transactionTypes = createDescriptionOfResourceTypesInBundle(theIdToPersistedOutcome); 1133 String message = "Error flushing transaction with resource types: " + transactionTypes; 1134 throw myHapiFhirHibernateJpaDialect.translate(e, message); 1135 } 1136 throw e; 1137 } 1138 } 1139 1140 @VisibleForTesting 1141 public void setIdHelperServiceForUnitTest(IIdHelperService<JpaPid> theIdHelperService) { 1142 myIdHelperService = theIdHelperService; 1143 } 1144 1145 @VisibleForTesting 1146 public void setApplicationContextForUnitTest(ApplicationContext theAppCtx) { 1147 myApplicationContext = theAppCtx; 1148 } 1149 1150 /** 1151 * Creates a description of resource types in the provided bundle, indicating the types of resources 1152 * and their counts within the input map. This is intended only to be helpful for troubleshooting, since 1153 * it can be helpful to see details about the transaction which failed in the logs. 1154 * <p> 1155 * Example output: <code>[Patient (x3), Observation (x14)]</code> 1156 * </p> 1157 * 1158 * @param theIdToPersistedOutcome A map where the key is an {@code IIdType} object representing a resource ID 1159 * and the value is a {@code DaoMethodOutcome} object representing the outcome 1160 * of the persistence operation for that resource. 1161 * @return A string describing the resource types and their respective counts in a formatted list. 1162 */ 1163 @Nonnull 1164 private static String createDescriptionOfResourceTypesInBundle( 1165 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) { 1166 TreeMap<String, Integer> types = new TreeMap<>(); 1167 for (IIdType t : theIdToPersistedOutcome.keySet()) { 1168 if (t != null) { 1169 String resourceType = t.getResourceType(); 1170 int count = types.getOrDefault(resourceType, 0); 1171 types.put(resourceType, count + 1); 1172 } 1173 } 1174 1175 StringBuilder typesBuilder = new StringBuilder(); 1176 typesBuilder.append("["); 1177 for (Iterator<Map.Entry<String, Integer>> iter = types.entrySet().iterator(); iter.hasNext(); ) { 1178 Map.Entry<String, Integer> entry = iter.next(); 1179 typesBuilder.append(entry.getKey()); 1180 if (entry.getValue() > 1) { 1181 typesBuilder.append(" (x").append(entry.getValue()).append(")"); 1182 } 1183 if (iter.hasNext()) { 1184 typesBuilder.append(", "); 1185 } 1186 } 1187 typesBuilder.append("]"); 1188 return typesBuilder.toString(); 1189 } 1190 1191 public static class MatchUrlToResolve { 1192 1193 private final String myRequestUrl; 1194 private final SearchParameterMap myMatchUrlSearchMap; 1195 private final RuntimeResourceDefinition myResourceDefinition; 1196 private final boolean myShouldPreFetchResourceBody; 1197 private final boolean myShouldPreFetchResourceVersion; 1198 private final IBaseResource myAssociatedResource; 1199 1200 public boolean myResolved; 1201 private Long myHashValue; 1202 private Long myHashSystemAndValue; 1203 1204 public MatchUrlToResolve( 1205 @Nonnull String theRequestUrl, 1206 @Nullable IBaseResource theAssociatedResource, 1207 @Nonnull SearchParameterMap theMatchUrlSearchMap, 1208 @Nonnull RuntimeResourceDefinition theResourceDefinition, 1209 boolean theShouldPreFetchResourceBody, 1210 boolean theShouldPreFetchResourceVersion) { 1211 Validate.notNull(theRequestUrl, "theRequestUrl must not be null"); 1212 Validate.notNull(theMatchUrlSearchMap, "theMatchUrlSearchMap must not be null"); 1213 Validate.notNull(theResourceDefinition, "theResourceDefinition must not be null"); 1214 myAssociatedResource = theAssociatedResource; 1215 myRequestUrl = theRequestUrl; 1216 myMatchUrlSearchMap = theMatchUrlSearchMap; 1217 myResourceDefinition = theResourceDefinition; 1218 myShouldPreFetchResourceBody = theShouldPreFetchResourceBody; 1219 myShouldPreFetchResourceVersion = theShouldPreFetchResourceVersion; 1220 } 1221 1222 public IBaseResource getAssociatedResource() { 1223 return myAssociatedResource; 1224 } 1225 1226 public void setResolved(boolean theResolved) { 1227 myResolved = theResolved; 1228 } 1229 } 1230 1231 enum PrefetchReasonEnum { 1232 /** 1233 * The ID is being prefetched because it is the ID in a resource reference 1234 * within a resource being updated. In this case, we care whether the resource 1235 * is deleted (since you can't reference a deleted resource), but we don't 1236 * need to fetch the body since we don't actually care about its contents. 1237 */ 1238 REFERENCE_TARGET, 1239 /** 1240 * The ID is being prefetched because it is the ID of a resource being 1241 * updated directly by the transaction. In this case we don't care if it's 1242 * deleted (since it's fine to update a deleted resource), and we do need 1243 * to prefetch the current body so we can tell how it has changed. 1244 */ 1245 DIRECT_TARGET 1246 } 1247}