
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.RequestPartitionId; 025import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 026import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; 027import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 028import ca.uhn.fhir.jpa.api.svc.IIdHelperService; 029import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode; 030import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect; 031import ca.uhn.fhir.jpa.model.cross.IResourceLookup; 032import ca.uhn.fhir.jpa.model.dao.JpaPid; 033import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; 034import ca.uhn.fhir.jpa.model.entity.ResourceTable; 035import ca.uhn.fhir.jpa.model.entity.StorageSettings; 036import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 037import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc; 038import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 039import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 040import ca.uhn.fhir.jpa.util.MemoryCacheService; 041import ca.uhn.fhir.jpa.util.QueryChunker; 042import ca.uhn.fhir.model.api.IQueryParameterType; 043import ca.uhn.fhir.rest.api.server.RequestDetails; 044import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; 045import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 046import ca.uhn.fhir.rest.param.TokenParam; 047import ca.uhn.fhir.util.FhirTerser; 048import ca.uhn.fhir.util.ResourceReferenceInfo; 049import ca.uhn.fhir.util.StopWatch; 050import ca.uhn.fhir.util.TaskChunker; 051import com.google.common.annotations.VisibleForTesting; 052import com.google.common.collect.ArrayListMultimap; 053import com.google.common.collect.ListMultimap; 054import jakarta.persistence.EntityManager; 055import jakarta.persistence.FlushModeType; 056import jakarta.persistence.PersistenceContext; 057import jakarta.persistence.PersistenceContextType; 058import jakarta.persistence.PersistenceException; 059import jakarta.persistence.Tuple; 060import jakarta.persistence.TypedQuery; 061import jakarta.persistence.criteria.CriteriaBuilder; 062import jakarta.persistence.criteria.CriteriaQuery; 063import jakarta.persistence.criteria.Predicate; 064import jakarta.persistence.criteria.Root; 065import org.apache.commons.lang3.Validate; 066import org.hibernate.internal.SessionImpl; 067import org.hl7.fhir.instance.model.api.IBase; 068import org.hl7.fhir.instance.model.api.IBaseBundle; 069import org.hl7.fhir.instance.model.api.IBaseResource; 070import org.hl7.fhir.instance.model.api.IIdType; 071import org.slf4j.Logger; 072import org.slf4j.LoggerFactory; 073import org.springframework.beans.factory.annotation.Autowired; 074import org.springframework.context.ApplicationContext; 075 076import java.util.ArrayList; 077import java.util.Collection; 078import java.util.HashMap; 079import java.util.HashSet; 080import java.util.IdentityHashMap; 081import java.util.Iterator; 082import java.util.List; 083import java.util.Map; 084import java.util.Objects; 085import java.util.Set; 086import java.util.regex.Pattern; 087import java.util.stream.Collectors; 088 089import static ca.uhn.fhir.util.UrlUtil.determineResourceTypeInResourceUrl; 090import static org.apache.commons.lang3.StringUtils.countMatches; 091import static org.apache.commons.lang3.StringUtils.isNotBlank; 092 093public class TransactionProcessor extends BaseTransactionProcessor { 094 095 /** 096 * Matches conditional URLs in the form of [resourceType]?[paramName]=[paramValue]{...more params...} 097 * 098 * 099 */ 100 public static final Pattern MATCH_URL_PATTERN = Pattern.compile("^[^?]++[?][a-z0-9-]+=[^&,]++"); 101 102 public static final int CONDITIONAL_URL_FETCH_CHUNK_SIZE = 100; 103 private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class); 104 105 @Autowired 106 private ApplicationContext myApplicationContext; 107 108 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 109 private EntityManager myEntityManager; 110 111 @Autowired(required = false) 112 private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect; 113 114 @Autowired 115 private IIdHelperService<JpaPid> myIdHelperService; 116 117 @Autowired 118 private JpaStorageSettings myStorageSettings; 119 120 @Autowired 121 private FhirContext myFhirContext; 122 123 @Autowired 124 private MatchResourceUrlService<JpaPid> myMatchResourceUrlService; 125 126 @Autowired 127 private MatchUrlService myMatchUrlService; 128 129 @Autowired 130 private ResourceSearchUrlSvc myResourceSearchUrlSvc; 131 132 @Autowired 133 private MemoryCacheService myMemoryCacheService; 134 135 @Autowired 136 private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; 137 138 public void setEntityManagerForUnitTest(EntityManager theEntityManager) { 139 myEntityManager = theEntityManager; 140 } 141 142 @Override 143 protected void validateDependencies() { 144 super.validateDependencies(); 145 146 Validate.notNull(myEntityManager, "EntityManager must not be null"); 147 } 148 149 @VisibleForTesting 150 public void setFhirContextForUnitTest(FhirContext theFhirContext) { 151 myFhirContext = theFhirContext; 152 } 153 154 @Override 155 public void setStorageSettings(StorageSettings theStorageSettings) { 156 myStorageSettings = (JpaStorageSettings) theStorageSettings; 157 super.setStorageSettings(theStorageSettings); 158 } 159 160 @Override 161 protected EntriesToProcessMap doTransactionWriteOperations( 162 final RequestDetails theRequest, 163 String theActionName, 164 TransactionDetails theTransactionDetails, 165 Set<IIdType> theAllIds, 166 IdSubstitutionMap theIdSubstitutions, 167 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 168 IBaseBundle theResponse, 169 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 170 List<IBase> theEntries, 171 StopWatch theTransactionStopWatch) { 172 173 /* 174 * We temporarily set the flush mode for the duration of the DB transaction 175 * from the default of AUTO to the temporary value of COMMIT here. We do this 176 * because in AUTO mode, if any SQL SELECTs are required during the 177 * processing of an individual transaction entry, the server will flush the 178 * pending INSERTs/UPDATEs to the database before executing the SELECT. 179 * This hurts performance since we don't get the benefit of batching those 180 * write operations as much as possible. The tradeoff here is that we 181 * could theoretically have transaction operations which try to read 182 * data previously written in the same transaction, and they won't see it. 183 * This shouldn't actually be an issue anyhow - we pre-fetch conditional 184 * URLs and reference targets at the start of the transaction. But this 185 * tradeoff still feels worth it, since the most common use of transactions 186 * is for fast writing of data. 187 * 188 * Note that it's probably not necessary to reset it back, it should 189 * automatically go back to the default value after the transaction, but 190 * we reset it just to be safe. 191 */ 192 FlushModeType initialFlushMode = myEntityManager.getFlushMode(); 193 try { 194 myEntityManager.setFlushMode(FlushModeType.COMMIT); 195 196 ITransactionProcessorVersionAdapter<?, ?> versionAdapter = getVersionAdapter(); 197 RequestPartitionId requestPartitionId = 198 super.determineRequestPartitionIdForWriteEntries(theRequest, theEntries); 199 200 if (requestPartitionId != null) { 201 preFetch(theRequest, theTransactionDetails, theEntries, versionAdapter, requestPartitionId); 202 } 203 204 return super.doTransactionWriteOperations( 205 theRequest, 206 theActionName, 207 theTransactionDetails, 208 theAllIds, 209 theIdSubstitutions, 210 theIdToPersistedOutcome, 211 theResponse, 212 theOriginalRequestOrder, 213 theEntries, 214 theTransactionStopWatch); 215 } finally { 216 myEntityManager.setFlushMode(initialFlushMode); 217 } 218 } 219 220 @SuppressWarnings("rawtypes") 221 private void preFetch( 222 RequestDetails theRequestDetails, 223 TransactionDetails theTransactionDetails, 224 List<IBase> theEntries, 225 ITransactionProcessorVersionAdapter theVersionAdapter, 226 RequestPartitionId theRequestPartitionId) { 227 Set<String> foundIds = new HashSet<>(); 228 Set<JpaPid> idsToPreFetchBodiesFor = new HashSet<>(); 229 Set<JpaPid> idsToPreFetchVersionsFor = new HashSet<>(); 230 231 /* 232 * Pre-Fetch any resources that are referred to normally by ID, e.g. 233 * regular FHIR updates within the transaction. 234 */ 235 preFetchResourcesById( 236 theTransactionDetails, 237 theEntries, 238 theVersionAdapter, 239 theRequestPartitionId, 240 foundIds, 241 idsToPreFetchBodiesFor); 242 243 /* 244 * Pre-resolve any conditional URLs we can 245 */ 246 preFetchConditionalUrls( 247 theRequestDetails, 248 theTransactionDetails, 249 theEntries, 250 theVersionAdapter, 251 theRequestPartitionId, 252 idsToPreFetchBodiesFor, 253 idsToPreFetchVersionsFor); 254 255 /* 256 * Pre-Fetch Resource Bodies (this will happen for any resources we are potentially 257 * going to update) 258 */ 259 IFhirSystemDao<?, ?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class); 260 systemDao.preFetchResources(List.copyOf(idsToPreFetchBodiesFor), true); 261 262 /* 263 * Pre-Fetch Resource Versions (this will happen for any resources we are doing a 264 * conditional create on, meaning we don't actually care about the contents, just 265 * the ID and version) 266 */ 267 preFetchResourceVersions(idsToPreFetchVersionsFor); 268 } 269 270 /** 271 * Given a collection of {@link JpaPid}, loads the current version associated with 272 * each PID and puts it into the {@link JpaPid#setVersion(Long)} field. 273 */ 274 private void preFetchResourceVersions(Set<JpaPid> theIds) { 275 ourLog.trace("Versions to fetch: {}", theIds); 276 277 for (Iterator<JpaPid> it = theIds.iterator(); it.hasNext(); ) { 278 JpaPid pid = it.next(); 279 Long version = myMemoryCacheService.getIfPresent( 280 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid); 281 if (version != null) { 282 it.remove(); 283 pid.setVersion(version); 284 } 285 } 286 287 if (!theIds.isEmpty()) { 288 Map<JpaPid, JpaPid> idMap = theIds.stream().collect(Collectors.toMap(t -> t, t -> t)); 289 290 QueryChunker.chunk(theIds, ids -> { 291 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 292 CriteriaQuery<Tuple> cq = cb.createTupleQuery(); 293 Root<ResourceTable> from = cq.from(ResourceTable.class); 294 cq.multiselect(from.get("myPid"), from.get("myVersion")); 295 cq.where(from.get("myPid").in(ids)); 296 TypedQuery<Tuple> query = myEntityManager.createQuery(cq); 297 List<Tuple> results = query.getResultList(); 298 299 for (Tuple tuple : results) { 300 JpaPid pid = tuple.get(0, JpaPid.class); 301 Long version = tuple.get(1, Long.class); 302 idMap.get(pid).setVersion(version); 303 304 myMemoryCacheService.putAfterCommit( 305 MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid, version); 306 } 307 }); 308 } 309 } 310 311 @Override 312 @SuppressWarnings("rawtypes") 313 protected void postTransactionProcess(TransactionDetails theTransactionDetails) { 314 Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds(); 315 if (resourceIds != null && !resourceIds.isEmpty()) { 316 List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList()); 317 myResourceSearchUrlSvc.deleteByResIds(ids); 318 } 319 } 320 321 @SuppressWarnings({"unchecked", "rawtypes"}) 322 private void preFetchResourcesById( 323 TransactionDetails theTransactionDetails, 324 List<IBase> theEntries, 325 ITransactionProcessorVersionAdapter theVersionAdapter, 326 RequestPartitionId theRequestPartitionId, 327 Set<String> foundIds, 328 Set<JpaPid> theIdsToPreFetchBodiesFor) { 329 330 FhirTerser terser = myFhirContext.newTerser(); 331 332 // Key: The ID of the resource 333 // Value: TRUE if we should prefetch the existing resource details and all stored indexes, 334 // FALSE if we should prefetch only the identity (resource ID and deleted status) 335 Map<IIdType, Boolean> idsToPreResolve = new HashMap<>(theEntries.size() * 3); 336 337 for (IBase nextEntry : theEntries) { 338 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 339 if (resource != null) { 340 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 341 342 /* 343 * Pre-fetch any resources that are potentially being directly updated by ID 344 */ 345 if ("PUT".equals(verb) || "PATCH".equals(verb)) { 346 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 347 if (countMatches(requestUrl, '?') == 0) { 348 IIdType id = myFhirContext.getVersion().newIdType(); 349 id.setValue(requestUrl); 350 IIdType unqualifiedVersionless = id.toUnqualifiedVersionless(); 351 idsToPreResolve.put(unqualifiedVersionless, Boolean.TRUE); 352 } 353 } 354 355 /* 356 * Pre-fetch any resources that are referred to directly by ID (don't replace 357 * the TRUE flag with FALSE in case we're updating a resource but also 358 * pointing to that resource elsewhere in the bundle) 359 */ 360 if ("PUT".equals(verb) || "POST".equals(verb)) { 361 for (ResourceReferenceInfo referenceInfo : terser.getAllResourceReferences(resource)) { 362 IIdType reference = referenceInfo.getResourceReference().getReferenceElement(); 363 if (reference != null 364 && !reference.isLocal() 365 && !reference.isUuid() 366 && reference.hasResourceType() 367 && reference.hasIdPart() 368 && !reference.getValue().contains("?")) { 369 idsToPreResolve.putIfAbsent(reference.toUnqualifiedVersionless(), Boolean.FALSE); 370 } 371 } 372 } 373 } 374 } 375 376 /* 377 * If all the entries in the pre-fetch ID map have a value of TRUE, this 378 * means we only have IDs associated with resources we're going to directly 379 * update/patch within the transaction. In that case, it's fine to include 380 * deleted resources, since updating them will bring them back to life. 381 * 382 * If we have any FALSE entries, we're also pre-fetching reference targets 383 * which means we don't want deleted resources, because those are not OK 384 * to reference. 385 */ 386 boolean preFetchIncludesReferences = idsToPreResolve.values().stream().anyMatch(t -> !t); 387 ResolveIdentityMode resolveMode = preFetchIncludesReferences 388 ? ResolveIdentityMode.excludeDeleted().noCacheUnlessDeletesDisabled() 389 : ResolveIdentityMode.includeDeleted().cacheOk(); 390 391 Map<IIdType, IResourceLookup<JpaPid>> outcomes = myIdHelperService.resolveResourceIdentities( 392 theRequestPartitionId, idsToPreResolve.keySet(), resolveMode); 393 for (Map.Entry<IIdType, IResourceLookup<JpaPid>> entry : outcomes.entrySet()) { 394 JpaPid next = entry.getValue().getPersistentId(); 395 IIdType unqualifiedVersionlessId = entry.getKey(); 396 foundIds.add(unqualifiedVersionlessId.getValue()); 397 theTransactionDetails.addResolvedResourceId(unqualifiedVersionlessId, next); 398 if (idsToPreResolve.get(unqualifiedVersionlessId) == Boolean.TRUE) { 399 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY 400 || (next.getAssociatedResourceId() != null 401 && !next.getAssociatedResourceId().isIdPartValidLong())) { 402 theIdsToPreFetchBodiesFor.add(next); 403 } 404 } 405 } 406 407 // Any IDs that could not be resolved are presumably not there, so 408 // cache that fact so we don't look again later 409 for (IIdType next : idsToPreResolve.keySet()) { 410 if (!foundIds.contains(next.getValue())) { 411 theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null); 412 } 413 } 414 } 415 416 @Override 417 protected void handleVerbChangeInTransactionWriteOperations() { 418 super.handleVerbChangeInTransactionWriteOperations(); 419 420 myEntityManager.flush(); 421 } 422 423 @SuppressWarnings({"rawtypes", "unchecked"}) 424 private void preFetchConditionalUrls( 425 RequestDetails theRequestDetails, 426 TransactionDetails theTransactionDetails, 427 List<IBase> theEntries, 428 ITransactionProcessorVersionAdapter theVersionAdapter, 429 RequestPartitionId theRequestPartitionId, 430 Set<JpaPid> theIdsToPreFetchBodiesFor, 431 Set<JpaPid> theIdsToPreFetchVersionsFor) { 432 433 List<MatchUrlToResolve> searchParameterMapsToResolve = new ArrayList<>(); 434 for (IBase nextEntry : theEntries) { 435 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 436 if (resource != null) { 437 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 438 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 439 String requestIfNoneExist = theVersionAdapter.getEntryIfNoneExist(nextEntry); 440 String resourceType = determineResourceTypeInResourceUrl(myFhirContext, requestUrl); 441 if (resourceType == null) { 442 resourceType = myFhirContext.getResourceType(resource); 443 } 444 if (("PUT".equals(verb) || "PATCH".equals(verb)) && requestUrl != null && requestUrl.contains("?")) { 445 processConditionalUrlForPreFetching( 446 theRequestPartitionId, 447 resourceType, 448 requestUrl, 449 true, 450 false, 451 theIdsToPreFetchBodiesFor, 452 searchParameterMapsToResolve); 453 } else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) { 454 processConditionalUrlForPreFetching( 455 theRequestPartitionId, 456 resourceType, 457 requestIfNoneExist, 458 false, 459 true, 460 theIdsToPreFetchBodiesFor, 461 searchParameterMapsToResolve); 462 } 463 464 if (myStorageSettings.isAllowInlineMatchUrlReferences()) { 465 List<ResourceReferenceInfo> references = 466 myFhirContext.newTerser().getAllResourceReferences(resource); 467 for (ResourceReferenceInfo next : references) { 468 String referenceUrl = next.getResourceReference() 469 .getReferenceElement() 470 .getValue(); 471 String refResourceType = determineResourceTypeInResourceUrl(myFhirContext, referenceUrl); 472 if (refResourceType != null) { 473 processConditionalUrlForPreFetching( 474 theRequestPartitionId, 475 refResourceType, 476 referenceUrl, 477 false, 478 false, 479 theIdsToPreFetchBodiesFor, 480 searchParameterMapsToResolve); 481 } 482 } 483 } 484 } 485 } 486 487 TaskChunker.chunk( 488 searchParameterMapsToResolve, 489 CONDITIONAL_URL_FETCH_CHUNK_SIZE, 490 map -> preFetchSearchParameterMaps( 491 theRequestDetails, 492 theTransactionDetails, 493 theRequestPartitionId, 494 map, 495 theIdsToPreFetchBodiesFor, 496 theIdsToPreFetchVersionsFor)); 497 } 498 499 /** 500 * This method attempts to resolve a collection of conditional URLs that were found 501 * in a FHIR transaction bundle being processed. 502 * 503 * @param theRequestDetails The active request 504 * @param theTransactionDetails The active transaction details 505 * @param theRequestPartitionId The active partition 506 * @param theInputParameters These are the conditional URLs that will actually be resolved 507 * @param theOutputPidsToLoadBodiesFor This list will be added to with any resource PIDs that need to be fully 508 * preloaded (i.e. fetch the actual resource body since we're presumably 509 * going to update it and will need to see its current state eventually) 510 * @param theOutputPidsToLoadVersionsFor This list will be added to with any resource PIDs that need to have 511 * their current version resolved. This is used for conditional creates, 512 * where we don't actually care about the body of the resource, only 513 * the version it has (since the version is returned in the response, 514 * and potentially used if we're auto-versioning references). 515 */ 516 @VisibleForTesting 517 public void preFetchSearchParameterMaps( 518 RequestDetails theRequestDetails, 519 TransactionDetails theTransactionDetails, 520 RequestPartitionId theRequestPartitionId, 521 List<MatchUrlToResolve> theInputParameters, 522 Set<JpaPid> theOutputPidsToLoadBodiesFor, 523 Set<JpaPid> theOutputPidsToLoadVersionsFor) { 524 525 Set<Long> systemAndValueHashes = new HashSet<>(); 526 Set<Long> valueHashes = new HashSet<>(); 527 528 for (MatchUrlToResolve next : theInputParameters) { 529 Collection<List<List<IQueryParameterType>>> values = next.myMatchUrlSearchMap.values(); 530 531 /* 532 * Any conditional URLs that consist of a single token parameter are batched 533 * up into a single query against the HFJ_SPIDX_TOKEN table so that we only 534 * perform one SQL query for all of them. 535 * 536 * We could potentially add other patterns in the future, but it's much more 537 * tricky to implement this when there are multiple parameters, and non-token 538 * parameter types aren't often used on their own in conditional URLs. So for 539 * now we handle single-token only, and that's probably good enough. 540 */ 541 boolean canBeHandledInAggregateQuery = false; 542 543 if (values.size() == 1) { 544 List<List<IQueryParameterType>> andList = values.iterator().next(); 545 IQueryParameterType param = andList.get(0).get(0); 546 547 if (param instanceof TokenParam) { 548 TokenParam tokenParam = (TokenParam) param; 549 canBeHandledInAggregateQuery = buildHashPredicateFromTokenParam( 550 tokenParam, theRequestPartitionId, next, systemAndValueHashes, valueHashes); 551 } 552 } 553 554 if (!canBeHandledInAggregateQuery) { 555 Set<JpaPid> matchUrlResults = myMatchResourceUrlService.processMatchUrl( 556 next.myRequestUrl, 557 next.myResourceDefinition.getImplementingClass(), 558 theTransactionDetails, 559 theRequestDetails, 560 theRequestPartitionId); 561 for (JpaPid matchUrlResult : matchUrlResults) { 562 handleFoundPreFetchResourceId( 563 theTransactionDetails, 564 theOutputPidsToLoadBodiesFor, 565 theOutputPidsToLoadVersionsFor, 566 next, 567 matchUrlResult); 568 } 569 } 570 } 571 572 preFetchSearchParameterMapsToken( 573 "myHashSystemAndValue", 574 systemAndValueHashes, 575 theTransactionDetails, 576 theRequestPartitionId, 577 theInputParameters, 578 theOutputPidsToLoadBodiesFor, 579 theOutputPidsToLoadVersionsFor); 580 preFetchSearchParameterMapsToken( 581 "myHashValue", 582 valueHashes, 583 theTransactionDetails, 584 theRequestPartitionId, 585 theInputParameters, 586 theOutputPidsToLoadBodiesFor, 587 theOutputPidsToLoadVersionsFor); 588 589 // For each SP Map which did not return a result, tag it as not found. 590 theInputParameters.stream() 591 // No matches 592 .filter(match -> !match.myResolved) 593 .forEach(match -> { 594 ourLog.debug("Was unable to match url {} from database", match.myRequestUrl); 595 theTransactionDetails.addResolvedMatchUrl( 596 myFhirContext, match.myRequestUrl, TransactionDetails.NOT_FOUND); 597 }); 598 } 599 600 /** 601 * Here we do a select against the {@link ResourceIndexedSearchParamToken} table for any rows that have the 602 * specific sys+val or val hashes we know we need to pre-fetch. 603 * <p> 604 * Note that we do a tuple query for only 2 columns in order to ensure that we can get by with only 605 * the data in the index (ie no need to load the actual table rows). 606 */ 607 public void preFetchSearchParameterMapsToken( 608 String theIndexColumnName, 609 Set<Long> theHashesForIndexColumn, 610 TransactionDetails theTransactionDetails, 611 RequestPartitionId theRequestPartitionId, 612 List<MatchUrlToResolve> theInputParameters, 613 Set<JpaPid> theOutputPidsToLoadFully, 614 Set<JpaPid> theOutputPidsToLoadVersionsFor) { 615 if (!theHashesForIndexColumn.isEmpty()) { 616 ListMultimap<Long, MatchUrlToResolve> hashToSearchMap = 617 buildHashToSearchMap(theInputParameters, theIndexColumnName); 618 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 619 CriteriaQuery<Tuple> cq = cb.createTupleQuery(); 620 Root<ResourceIndexedSearchParamToken> from = cq.from(ResourceIndexedSearchParamToken.class); 621 cq.multiselect(from.get("myPartitionIdValue"), from.get("myResourcePid"), from.get(theIndexColumnName)); 622 623 Predicate masterPredicate; 624 if (theHashesForIndexColumn.size() == 1) { 625 masterPredicate = cb.equal( 626 from.get(theIndexColumnName), 627 theHashesForIndexColumn.iterator().next()); 628 } else { 629 masterPredicate = from.get(theIndexColumnName).in(theHashesForIndexColumn); 630 } 631 632 if (myPartitionSettings.isPartitioningEnabled() 633 && !myPartitionSettings.isIncludePartitionInSearchHashes()) { 634 if (myRequestPartitionHelperSvc.isDefaultPartition(theRequestPartitionId) 635 && myPartitionSettings.getDefaultPartitionId() == null) { 636 Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue")); 637 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 638 } else if (!theRequestPartitionId.isAllPartitions()) { 639 Predicate partitionIdCriteria = 640 from.get("myPartitionIdValue").in(theRequestPartitionId.getPartitionIds()); 641 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 642 } 643 } 644 645 cq.where(masterPredicate); 646 647 TypedQuery<Tuple> query = myEntityManager.createQuery(cq); 648 649 /* 650 * If we have 10 unique conditional URLs we're resolving, each one should 651 * resolve to 0..1 resources if they are valid as conditional URLs. So we would 652 * expect this query to return 0..10 rows, since conditional URLs for all 653 * conditional operations except DELETE (which isn't being applied here) are 654 * only allowed to resolve to 0..1 resources. 655 * 656 * If a conditional URL matches 2+ resources that is an error, and we'll 657 * be throwing an exception below. This limit is here for safety just to 658 * ensure that if someone uses a conditional URL that matches a million resources, 659 * we don't do a super-expensive fetch. 660 */ 661 query.setMaxResults(theHashesForIndexColumn.size() + 1); 662 663 List<Tuple> results = query.getResultList(); 664 665 for (Tuple nextResult : results) { 666 Integer nextPartitionId = nextResult.get(0, Integer.class); 667 Long nextResourcePid = nextResult.get(1, Long.class); 668 Long nextHash = nextResult.get(2, Long.class); 669 670 List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash); 671 matchedSearch.forEach(matchUrl -> { 672 ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl); 673 JpaPid pid = JpaPid.fromId(nextResourcePid, nextPartitionId); 674 handleFoundPreFetchResourceId( 675 theTransactionDetails, 676 theOutputPidsToLoadFully, 677 theOutputPidsToLoadVersionsFor, 678 matchUrl, 679 pid); 680 }); 681 } 682 } 683 } 684 685 private void handleFoundPreFetchResourceId( 686 TransactionDetails theTransactionDetails, 687 Set<JpaPid> theOutputPidsToLoadFully, 688 Set<JpaPid> theOutputPidsToLoadVersionsFor, 689 MatchUrlToResolve theMatchUrl, 690 JpaPid theFoundPid) { 691 if (theMatchUrl.myShouldPreFetchResourceBody) { 692 theOutputPidsToLoadFully.add(theFoundPid); 693 } 694 if (theMatchUrl.myShouldPreFetchResourceVersion) { 695 theOutputPidsToLoadVersionsFor.add(theFoundPid); 696 } 697 myMatchResourceUrlService.matchUrlResolved( 698 theTransactionDetails, 699 theMatchUrl.myResourceDefinition.getName(), 700 theMatchUrl.myRequestUrl, 701 theFoundPid); 702 theTransactionDetails.addResolvedMatchUrl(myFhirContext, theMatchUrl.myRequestUrl, theFoundPid); 703 theMatchUrl.setResolved(true); 704 } 705 706 /** 707 * Examines a conditional URL, and potentially adds it to either {@literal theOutputIdsToPreFetchBodiesFor} 708 * or {@literal theOutputSearchParameterMapsToResolve}. 709 * <p> 710 * Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match 711 * URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving 712 * a match URL because it's there for a conditional update, we'll eagerly fetch the 713 * actual resource because we need to know its current state in order to update it. However, if 714 * the match URL is from an inline match URL in a resource body, we really only care about 715 * the PID and don't need the body so we don't load it. This does have a security implication, since 716 * it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut 717 * isn't fired even though the user has resolved the URL (meaning they may be able to test for 718 * the existence of a resource using a match URL). There is a test for this called 719 * {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff 720 * is acceptable since we're only prefetching things with very simple match URLs (nothing with 721 * a reference in it for example) so it's not really possible to doing anything useful with this. 722 * </p> 723 * 724 * @param thePartitionId The partition ID of the associated resource (can be null) 725 * @param theResourceType The resource type associated with the match URL (ie what resource type should it resolve to) 726 * @param theRequestUrl The actual match URL, which could be as simple as just parameters or could include the resource type too 727 * @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. 728 * @param theOutputIdsToPreFetchBodiesFor This will be populated with any resource PIDs that need to be pre-fetched 729 * @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve 730 */ 731 private void processConditionalUrlForPreFetching( 732 RequestPartitionId thePartitionId, 733 String theResourceType, 734 String theRequestUrl, 735 boolean theShouldPreFetchResourceBody, 736 boolean theShouldPreFetchResourceVersion, 737 Set<JpaPid> theOutputIdsToPreFetchBodiesFor, 738 List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) { 739 JpaPid cachedId = 740 myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl, thePartitionId); 741 if (cachedId != null) { 742 if (theShouldPreFetchResourceBody) { 743 theOutputIdsToPreFetchBodiesFor.add(cachedId); 744 } 745 } else if (MATCH_URL_PATTERN.matcher(theRequestUrl).find()) { 746 RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType); 747 SearchParameterMap matchUrlSearchMap = 748 myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition); 749 theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve( 750 theRequestUrl, 751 matchUrlSearchMap, 752 resourceDefinition, 753 theShouldPreFetchResourceBody, 754 theShouldPreFetchResourceVersion)); 755 } 756 } 757 758 /** 759 * Given a token parameter, build the query predicate based on its hash. Uses system and value if both are available, otherwise just value. 760 * If neither are available, it returns null. 761 * 762 * @return Returns {@literal true} if the param was added to one of the output lists 763 */ 764 private boolean buildHashPredicateFromTokenParam( 765 TokenParam theTokenParam, 766 RequestPartitionId theRequestPartitionId, 767 MatchUrlToResolve theMatchUrl, 768 Set<Long> theOutputSysAndValuePredicates, 769 Set<Long> theOutputValuePredicates) { 770 if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) { 771 theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue( 772 myPartitionSettings, 773 theRequestPartitionId, 774 theMatchUrl.myResourceDefinition.getName(), 775 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 776 theTokenParam.getSystem(), 777 theTokenParam.getValue()); 778 theOutputSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue); 779 return true; 780 } else if (isNotBlank(theTokenParam.getValue())) { 781 theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue( 782 myPartitionSettings, 783 theRequestPartitionId, 784 theMatchUrl.myResourceDefinition.getName(), 785 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 786 theTokenParam.getValue()); 787 theOutputValuePredicates.add(theMatchUrl.myHashValue); 788 return true; 789 } 790 791 return false; 792 } 793 794 private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap( 795 List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) { 796 ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create(); 797 // Build a lookup map so we don't have to iterate over the searches repeatedly. 798 for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { 799 if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) { 800 hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap); 801 } 802 if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) { 803 hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap); 804 } 805 } 806 return hashToSearch; 807 } 808 809 @Override 810 protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) { 811 try { 812 int insertionCount; 813 int updateCount; 814 SessionImpl session = myEntityManager.unwrap(SessionImpl.class); 815 if (session != null) { 816 insertionCount = session.getActionQueue().numberOfInsertions(); 817 updateCount = session.getActionQueue().numberOfUpdates(); 818 } else { 819 insertionCount = -1; 820 updateCount = -1; 821 } 822 823 StopWatch sw = new StopWatch(); 824 myEntityManager.flush(); 825 ourLog.debug( 826 "Session flush took {}ms for {} inserts and {} updates", 827 sw.getMillis(), 828 insertionCount, 829 updateCount); 830 } catch (PersistenceException e) { 831 if (myHapiFhirHibernateJpaDialect != null) { 832 List<String> types = theIdToPersistedOutcome.keySet().stream() 833 .filter(Objects::nonNull) 834 .map(IIdType::getResourceType) 835 .collect(Collectors.toList()); 836 String message = "Error flushing transaction with resource types: " + types; 837 throw myHapiFhirHibernateJpaDialect.translate(e, message); 838 } 839 throw e; 840 } 841 } 842 843 @VisibleForTesting 844 public void setIdHelperServiceForUnitTest(IIdHelperService<JpaPid> theIdHelperService) { 845 myIdHelperService = theIdHelperService; 846 } 847 848 @VisibleForTesting 849 public void setApplicationContextForUnitTest(ApplicationContext theAppCtx) { 850 myApplicationContext = theAppCtx; 851 } 852 853 public static class MatchUrlToResolve { 854 855 private final String myRequestUrl; 856 private final SearchParameterMap myMatchUrlSearchMap; 857 private final RuntimeResourceDefinition myResourceDefinition; 858 private final boolean myShouldPreFetchResourceBody; 859 private final boolean myShouldPreFetchResourceVersion; 860 public boolean myResolved; 861 private Long myHashValue; 862 private Long myHashSystemAndValue; 863 864 public MatchUrlToResolve( 865 String theRequestUrl, 866 SearchParameterMap theMatchUrlSearchMap, 867 RuntimeResourceDefinition theResourceDefinition, 868 boolean theShouldPreFetchResourceBody, 869 boolean theShouldPreFetchResourceVersion) { 870 myRequestUrl = theRequestUrl; 871 myMatchUrlSearchMap = theMatchUrlSearchMap; 872 myResourceDefinition = theResourceDefinition; 873 myShouldPreFetchResourceBody = theShouldPreFetchResourceBody; 874 myShouldPreFetchResourceVersion = theShouldPreFetchResourceVersion; 875 } 876 877 public void setResolved(boolean theResolved) { 878 myResolved = theResolved; 879 } 880 } 881}