001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.StorageSettings; 035import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; 036import ca.uhn.fhir.jpa.searchparam.MatchUrlService; 037import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 038import ca.uhn.fhir.model.api.IQueryParameterType; 039import ca.uhn.fhir.rest.api.server.RequestDetails; 040import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; 041import ca.uhn.fhir.rest.param.TokenParam; 042import ca.uhn.fhir.util.FhirTerser; 043import ca.uhn.fhir.util.ResourceReferenceInfo; 044import ca.uhn.fhir.util.StopWatch; 045import ca.uhn.fhir.util.TaskChunker; 046import com.google.common.annotations.VisibleForTesting; 047import com.google.common.collect.ArrayListMultimap; 048import com.google.common.collect.ListMultimap; 049import jakarta.annotation.Nullable; 050import jakarta.persistence.EntityManager; 051import jakarta.persistence.FlushModeType; 052import jakarta.persistence.PersistenceContext; 053import jakarta.persistence.PersistenceContextType; 054import jakarta.persistence.PersistenceException; 055import jakarta.persistence.Tuple; 056import jakarta.persistence.TypedQuery; 057import jakarta.persistence.criteria.CriteriaBuilder; 058import jakarta.persistence.criteria.CriteriaQuery; 059import jakarta.persistence.criteria.Predicate; 060import jakarta.persistence.criteria.Root; 061import org.apache.commons.lang3.Validate; 062import org.hibernate.internal.SessionImpl; 063import org.hl7.fhir.instance.model.api.IBase; 064import org.hl7.fhir.instance.model.api.IBaseBundle; 065import org.hl7.fhir.instance.model.api.IBaseResource; 066import org.hl7.fhir.instance.model.api.IIdType; 067import org.slf4j.Logger; 068import org.slf4j.LoggerFactory; 069import org.springframework.beans.factory.annotation.Autowired; 070import org.springframework.context.ApplicationContext; 071 072import java.util.ArrayList; 073import java.util.Collection; 074import java.util.HashMap; 075import java.util.HashSet; 076import java.util.IdentityHashMap; 077import java.util.List; 078import java.util.Map; 079import java.util.Set; 080import java.util.regex.Pattern; 081import java.util.stream.Collectors; 082 083import static ca.uhn.fhir.util.UrlUtil.determineResourceTypeInResourceUrl; 084import static org.apache.commons.lang3.StringUtils.countMatches; 085import static org.apache.commons.lang3.StringUtils.isNotBlank; 086 087public class TransactionProcessor extends BaseTransactionProcessor { 088 089 public static final Pattern SINGLE_PARAMETER_MATCH_URL_PATTERN = Pattern.compile("^[^?]+[?][a-z0-9-]+=[^&,]+$"); 090 private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class); 091 public static final int CONDITIONAL_URL_FETCH_CHUNK_SIZE = 100; 092 093 @Autowired 094 private ApplicationContext myApplicationContext; 095 096 @PersistenceContext(type = PersistenceContextType.TRANSACTION) 097 private EntityManager myEntityManager; 098 099 @Autowired(required = false) 100 private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect; 101 102 @Autowired 103 private IIdHelperService<JpaPid> myIdHelperService; 104 105 @Autowired 106 private JpaStorageSettings myStorageSettings; 107 108 @Autowired 109 private FhirContext myFhirContext; 110 111 @Autowired 112 private MatchResourceUrlService<JpaPid> myMatchResourceUrlService; 113 114 @Autowired 115 private MatchUrlService myMatchUrlService; 116 117 @Autowired 118 private IRequestPartitionHelperSvc myRequestPartitionSvc; 119 120 public void setEntityManagerForUnitTest(EntityManager theEntityManager) { 121 myEntityManager = theEntityManager; 122 } 123 124 @Override 125 protected void validateDependencies() { 126 super.validateDependencies(); 127 128 Validate.notNull(myEntityManager); 129 } 130 131 @VisibleForTesting 132 public void setFhirContextForUnitTest(FhirContext theFhirContext) { 133 myFhirContext = theFhirContext; 134 } 135 136 @Override 137 public void setStorageSettings(StorageSettings theStorageSettings) { 138 myStorageSettings = (JpaStorageSettings) theStorageSettings; 139 super.setStorageSettings(theStorageSettings); 140 } 141 142 @Override 143 protected EntriesToProcessMap doTransactionWriteOperations( 144 final RequestDetails theRequest, 145 String theActionName, 146 TransactionDetails theTransactionDetails, 147 Set<IIdType> theAllIds, 148 IdSubstitutionMap theIdSubstitutions, 149 Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome, 150 IBaseBundle theResponse, 151 IdentityHashMap<IBase, Integer> theOriginalRequestOrder, 152 List<IBase> theEntries, 153 StopWatch theTransactionStopWatch) { 154 155 /* 156 * We temporarily set the flush mode for the duration of the DB transaction 157 * from the default of AUTO to the temporary value of COMMIT here. We do this 158 * because in AUTO mode, if any SQL SELECTs are required during the 159 * processing of an individual transaction entry, the server will flush the 160 * pending INSERTs/UPDATEs to the database before executing the SELECT. 161 * This hurts performance since we don't get the benefit of batching those 162 * write operations as much as possible. The tradeoff here is that we 163 * could theoretically have transaction operations which try to read 164 * data previously written in the same transaction, and they won't see it. 165 * This shouldn't actually be an issue anyhow - we pre-fetch conditional 166 * URLs and reference targets at the start of the transaction. But this 167 * tradeoff still feels worth it, since the most common use of transactions 168 * is for fast writing of data. 169 * 170 * Note that it's probably not necessary to reset it back, it should 171 * automatically go back to the default value after the transaction but 172 * we reset it just to be safe. 173 */ 174 FlushModeType initialFlushMode = myEntityManager.getFlushMode(); 175 try { 176 myEntityManager.setFlushMode(FlushModeType.COMMIT); 177 178 ITransactionProcessorVersionAdapter<?, ?> versionAdapter = getVersionAdapter(); 179 RequestPartitionId requestPartitionId = 180 super.determineRequestPartitionIdForWriteEntries(theRequest, theEntries); 181 182 if (requestPartitionId != null) { 183 preFetch(theTransactionDetails, theEntries, versionAdapter, requestPartitionId); 184 } 185 186 return super.doTransactionWriteOperations( 187 theRequest, 188 theActionName, 189 theTransactionDetails, 190 theAllIds, 191 theIdSubstitutions, 192 theIdToPersistedOutcome, 193 theResponse, 194 theOriginalRequestOrder, 195 theEntries, 196 theTransactionStopWatch); 197 } finally { 198 myEntityManager.setFlushMode(initialFlushMode); 199 } 200 } 201 202 private void preFetch( 203 TransactionDetails theTransactionDetails, 204 List<IBase> theEntries, 205 ITransactionProcessorVersionAdapter theVersionAdapter, 206 RequestPartitionId theRequestPartitionId) { 207 Set<String> foundIds = new HashSet<>(); 208 List<Long> idsToPreFetch = new ArrayList<>(); 209 210 /* 211 * Pre-Fetch any resources that are referred to normally by ID, e.g. 212 * regular FHIR updates within the transaction. 213 */ 214 preFetchResourcesById( 215 theTransactionDetails, theEntries, theVersionAdapter, theRequestPartitionId, foundIds, idsToPreFetch); 216 217 /* 218 * Pre-resolve any conditional URLs we can 219 */ 220 preFetchConditionalUrls( 221 theTransactionDetails, theEntries, theVersionAdapter, theRequestPartitionId, idsToPreFetch); 222 223 IFhirSystemDao<?, ?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class); 224 systemDao.preFetchResources(JpaPid.fromLongList(idsToPreFetch), true); 225 } 226 227 private void preFetchResourcesById( 228 TransactionDetails theTransactionDetails, 229 List<IBase> theEntries, 230 ITransactionProcessorVersionAdapter theVersionAdapter, 231 RequestPartitionId theRequestPartitionId, 232 Set<String> foundIds, 233 List<Long> idsToPreFetch) { 234 235 FhirTerser terser = myFhirContext.newTerser(); 236 237 // Key: The ID of the resource 238 // Value: TRUE if we should prefetch the existing resource details and all stored indexes, 239 // FALSE if we should prefetch only the identity (resource ID and deleted status) 240 Map<IIdType, Boolean> idsToPreResolve = new HashMap<>(theEntries.size() * 3); 241 242 for (IBase nextEntry : theEntries) { 243 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 244 if (resource != null) { 245 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 246 247 /* 248 * Pre-fetch any resources that are potentially being directly updated by ID 249 */ 250 if ("PUT".equals(verb) || "PATCH".equals(verb)) { 251 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 252 if (countMatches(requestUrl, '?') == 0) { 253 IIdType id = myFhirContext.getVersion().newIdType(); 254 id.setValue(requestUrl); 255 IIdType unqualifiedVersionless = id.toUnqualifiedVersionless(); 256 idsToPreResolve.put(unqualifiedVersionless, Boolean.TRUE); 257 } 258 } 259 260 /* 261 * Pre-fetch any resources that are referred to directly by ID (don't replace 262 * the TRUE flag with FALSE in case we're updating a resource but also 263 * pointing to that resource elsewhere in the bundle) 264 */ 265 if ("PUT".equals(verb) || "POST".equals(verb)) { 266 for (ResourceReferenceInfo referenceInfo : terser.getAllResourceReferences(resource)) { 267 IIdType reference = referenceInfo.getResourceReference().getReferenceElement(); 268 if (reference != null 269 && !reference.isLocal() 270 && !reference.isUuid() 271 && reference.hasResourceType() 272 && reference.hasIdPart() 273 && !reference.getValue().contains("?")) { 274 idsToPreResolve.putIfAbsent(reference.toUnqualifiedVersionless(), Boolean.FALSE); 275 } 276 } 277 } 278 } 279 } 280 281 /* 282 * If all the entries in the pre-fetch ID map have a value of TRUE, this 283 * means we only have IDs associated with resources we're going to directly 284 * update/patch within the transaction. In that case, it's fine to include 285 * deleted resources, since updating them will bring them back to life. 286 * 287 * If we have any FALSE entries, we're also pre-fetching reference targets 288 * which means we don't want deleted resources, because those are not OK 289 * to reference. 290 */ 291 boolean preFetchIncludesReferences = idsToPreResolve.values().stream().anyMatch(t -> !t); 292 ResolveIdentityMode resolveMode = preFetchIncludesReferences 293 ? ResolveIdentityMode.excludeDeleted().noCacheUnlessDeletesDisabled() 294 : ResolveIdentityMode.includeDeleted().cacheOk(); 295 296 Map<IIdType, IResourceLookup<JpaPid>> outcomes = myIdHelperService.resolveResourceIdentities( 297 theRequestPartitionId, idsToPreResolve.keySet(), resolveMode); 298 for (Map.Entry<IIdType, IResourceLookup<JpaPid>> entry : outcomes.entrySet()) { 299 JpaPid next = (JpaPid) entry.getValue().getPersistentId(); 300 IIdType unqualifiedVersionlessId = entry.getKey(); 301 foundIds.add(unqualifiedVersionlessId.getValue()); 302 theTransactionDetails.addResolvedResourceId(unqualifiedVersionlessId, next); 303 if (idsToPreResolve.get(unqualifiedVersionlessId) == Boolean.TRUE) { 304 if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY 305 || (next.getAssociatedResourceId() != null 306 && !next.getAssociatedResourceId().isIdPartValidLong())) { 307 idsToPreFetch.add(next.getId()); 308 } 309 } 310 } 311 312 // Any IDs that could not be resolved are presumably not there, so 313 // cache that fact so we don't look again later 314 for (IIdType next : idsToPreResolve.keySet()) { 315 if (!foundIds.contains(next.getValue())) { 316 theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null); 317 } 318 } 319 } 320 321 @Override 322 protected void handleVerbChangeInTransactionWriteOperations() { 323 super.handleVerbChangeInTransactionWriteOperations(); 324 325 myEntityManager.flush(); 326 } 327 328 private void preFetchConditionalUrls( 329 TransactionDetails theTransactionDetails, 330 List<IBase> theEntries, 331 ITransactionProcessorVersionAdapter theVersionAdapter, 332 RequestPartitionId theRequestPartitionId, 333 List<Long> idsToPreFetch) { 334 List<MatchUrlToResolve> searchParameterMapsToResolve = new ArrayList<>(); 335 for (IBase nextEntry : theEntries) { 336 IBaseResource resource = theVersionAdapter.getResource(nextEntry); 337 if (resource != null) { 338 String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry); 339 String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry); 340 String requestIfNoneExist = theVersionAdapter.getEntryIfNoneExist(nextEntry); 341 String resourceType = determineResourceTypeInResourceUrl(myFhirContext, requestUrl); 342 if (resourceType == null && resource != null) { 343 resourceType = myFhirContext.getResourceType(resource); 344 } 345 if (("PUT".equals(verb) || "PATCH".equals(verb)) && requestUrl != null && requestUrl.contains("?")) { 346 preFetchConditionalUrl(resourceType, requestUrl, true, idsToPreFetch, searchParameterMapsToResolve); 347 } else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) { 348 preFetchConditionalUrl( 349 resourceType, requestIfNoneExist, false, idsToPreFetch, searchParameterMapsToResolve); 350 } 351 352 if (myStorageSettings.isAllowInlineMatchUrlReferences()) { 353 List<ResourceReferenceInfo> references = 354 myFhirContext.newTerser().getAllResourceReferences(resource); 355 for (ResourceReferenceInfo next : references) { 356 String referenceUrl = next.getResourceReference() 357 .getReferenceElement() 358 .getValue(); 359 String refResourceType = determineResourceTypeInResourceUrl(myFhirContext, referenceUrl); 360 if (refResourceType != null) { 361 preFetchConditionalUrl( 362 refResourceType, referenceUrl, false, idsToPreFetch, searchParameterMapsToResolve); 363 } 364 } 365 } 366 } 367 } 368 369 TaskChunker.chunk( 370 searchParameterMapsToResolve, 371 CONDITIONAL_URL_FETCH_CHUNK_SIZE, 372 map -> preFetchSearchParameterMaps(theTransactionDetails, theRequestPartitionId, map, idsToPreFetch)); 373 } 374 375 /** 376 * @param theTransactionDetails The active transaction details 377 * @param theRequestPartitionId The active partition 378 * @param theInputParameters These are the search parameter maps that will actually be resolved 379 * @param theOutputPidsToLoadFully This list will be added to with any resource PIDs that need to be fully 380 * pre-loaded (ie. fetch the actual resource body since we're presumably 381 * going to update it and will need to see its current state eventually) 382 */ 383 private void preFetchSearchParameterMaps( 384 TransactionDetails theTransactionDetails, 385 RequestPartitionId theRequestPartitionId, 386 List<MatchUrlToResolve> theInputParameters, 387 List<Long> theOutputPidsToLoadFully) { 388 Set<Long> systemAndValueHashes = new HashSet<>(); 389 Set<Long> valueHashes = new HashSet<>(); 390 for (MatchUrlToResolve next : theInputParameters) { 391 Collection<List<List<IQueryParameterType>>> values = next.myMatchUrlSearchMap.values(); 392 if (values.size() == 1) { 393 List<List<IQueryParameterType>> andList = values.iterator().next(); 394 IQueryParameterType param = andList.get(0).get(0); 395 396 if (param instanceof TokenParam) { 397 buildHashPredicateFromTokenParam( 398 (TokenParam) param, theRequestPartitionId, next, systemAndValueHashes, valueHashes); 399 } 400 } 401 } 402 403 preFetchSearchParameterMapsToken( 404 "myHashSystemAndValue", 405 systemAndValueHashes, 406 theTransactionDetails, 407 theRequestPartitionId, 408 theInputParameters, 409 theOutputPidsToLoadFully); 410 preFetchSearchParameterMapsToken( 411 "myHashValue", 412 valueHashes, 413 theTransactionDetails, 414 theRequestPartitionId, 415 theInputParameters, 416 theOutputPidsToLoadFully); 417 418 // For each SP Map which did not return a result, tag it as not found. 419 if (!valueHashes.isEmpty() || !systemAndValueHashes.isEmpty()) { 420 theInputParameters.stream() 421 // No matches 422 .filter(match -> !match.myResolved) 423 .forEach(match -> { 424 ourLog.debug("Was unable to match url {} from database", match.myRequestUrl); 425 theTransactionDetails.addResolvedMatchUrl( 426 myFhirContext, match.myRequestUrl, TransactionDetails.NOT_FOUND); 427 }); 428 } 429 } 430 431 /** 432 * Here we do a select against the {@link ResourceIndexedSearchParamToken} table for any rows that have the 433 * specific sys+val or val hashes we know we need to pre-fetch. 434 * <p> 435 * Note that we do a tuple query for only 2 columns in order to ensure that we can get by with only 436 * the data in the index (ie no need to load the actual table rows). 437 */ 438 private void preFetchSearchParameterMapsToken( 439 String theIndexColumnName, 440 Set<Long> theHashesForIndexColumn, 441 TransactionDetails theTransactionDetails, 442 RequestPartitionId theRequestPartitionId, 443 List<MatchUrlToResolve> theInputParameters, 444 List<Long> theOutputPidsToLoadFully) { 445 if (!theHashesForIndexColumn.isEmpty()) { 446 ListMultimap<Long, MatchUrlToResolve> hashToSearchMap = 447 buildHashToSearchMap(theInputParameters, theIndexColumnName); 448 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 449 CriteriaQuery<Tuple> cq = cb.createTupleQuery(); 450 Root<ResourceIndexedSearchParamToken> from = cq.from(ResourceIndexedSearchParamToken.class); 451 cq.multiselect(from.get("myResourcePid"), from.get(theIndexColumnName)); 452 453 Predicate masterPredicate; 454 if (theHashesForIndexColumn.size() == 1) { 455 masterPredicate = cb.equal( 456 from.get(theIndexColumnName), 457 theHashesForIndexColumn.iterator().next()); 458 } else { 459 masterPredicate = from.get(theIndexColumnName).in(theHashesForIndexColumn); 460 } 461 462 if (myPartitionSettings.isPartitioningEnabled() 463 && !myPartitionSettings.isIncludePartitionInSearchHashes()) { 464 if (theRequestPartitionId.isDefaultPartition()) { 465 Predicate partitionIdCriteria = cb.isNull(from.get("myPartitionIdValue")); 466 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 467 } else if (!theRequestPartitionId.isAllPartitions()) { 468 Predicate partitionIdCriteria = 469 from.get("myPartitionIdValue").in(theRequestPartitionId.getPartitionIds()); 470 masterPredicate = cb.and(partitionIdCriteria, masterPredicate); 471 } 472 } 473 474 cq.where(masterPredicate); 475 476 TypedQuery<Tuple> query = myEntityManager.createQuery(cq); 477 478 /* 479 * If we have 10 unique conditional URLs we're resolving, each one should 480 * resolve to 0..1 resources if they are valid as conditional URLs. So we would 481 * expect this query to return 0..10 rows, since conditional URLs for all 482 * conditional operations except DELETE (which isn't being applied here) are 483 * only allowed to resolve to 0..1 resources. 484 * 485 * If a conditional URL matches 2+ resources that is an error, and we'll 486 * be throwing an exception below. This limit is here for safety just to 487 * ensure that if someone uses a conditional URL that matches a million resources, 488 * we don't do a super-expensive fetch. 489 */ 490 query.setMaxResults(theHashesForIndexColumn.size() + 1); 491 492 List<Tuple> results = query.getResultList(); 493 494 for (Tuple nextResult : results) { 495 Long nextResourcePid = nextResult.get(0, Long.class); 496 Long nextHash = nextResult.get(1, Long.class); 497 List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash); 498 matchedSearch.forEach(matchUrl -> { 499 ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl); 500 if (matchUrl.myShouldPreFetchResourceBody) { 501 theOutputPidsToLoadFully.add(nextResourcePid); 502 } 503 myMatchResourceUrlService.matchUrlResolved( 504 theTransactionDetails, 505 matchUrl.myResourceDefinition.getName(), 506 matchUrl.myRequestUrl, 507 JpaPid.fromId(nextResourcePid)); 508 theTransactionDetails.addResolvedMatchUrl( 509 myFhirContext, matchUrl.myRequestUrl, JpaPid.fromId(nextResourcePid)); 510 matchUrl.setResolved(true); 511 }); 512 } 513 } 514 } 515 516 /** 517 * Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match 518 * URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving 519 * a match URL because it's there for a conditional update, we'll eagerly fetch the 520 * actual resource because we need to know its current state in order to update it. However, if 521 * the match URL is from an inline match URL in a resource body, we really only care about 522 * the PID and don't need the body so we don't load it. This does have a security implication, since 523 * it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut 524 * isn't fired even though the user has resolved the URL (meaning they may be able to test for 525 * the existence of a resource using a match URL). There is a test for this called 526 * {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff 527 * is acceptable since we're only prefetching things with very simple match URLs (nothing with 528 * a reference in it for example) so it's not really possible to doing anything useful with this. 529 * 530 * @param theResourceType The resource type associated with the match URL (ie what resource type should it resolve to) 531 * @param theRequestUrl The actual match URL, which could be as simple as just parameters or could include the resource type too 532 * @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. 533 * @param theOutputIdsToPreFetch This will be populated with any resource PIDs that need to be pre-fetched 534 * @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve 535 */ 536 private void preFetchConditionalUrl( 537 String theResourceType, 538 String theRequestUrl, 539 boolean theShouldPreFetchResourceBody, 540 List<Long> theOutputIdsToPreFetch, 541 List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) { 542 JpaPid cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl); 543 if (cachedId != null) { 544 if (theShouldPreFetchResourceBody) { 545 theOutputIdsToPreFetch.add(cachedId.getId()); 546 } 547 } else if (SINGLE_PARAMETER_MATCH_URL_PATTERN.matcher(theRequestUrl).matches()) { 548 RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType); 549 SearchParameterMap matchUrlSearchMap = 550 myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition); 551 theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve( 552 theRequestUrl, matchUrlSearchMap, resourceDefinition, theShouldPreFetchResourceBody)); 553 } 554 } 555 556 /** 557 * Given a token parameter, build the query predicate based on its hash. Uses system and value if both are available, otherwise just value. 558 * If neither are available, it returns null. 559 */ 560 @Nullable 561 private void buildHashPredicateFromTokenParam( 562 TokenParam theTokenParam, 563 RequestPartitionId theRequestPartitionId, 564 MatchUrlToResolve theMatchUrl, 565 Set<Long> theSysAndValuePredicates, 566 Set<Long> theValuePredicates) { 567 if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) { 568 theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue( 569 myPartitionSettings, 570 theRequestPartitionId, 571 theMatchUrl.myResourceDefinition.getName(), 572 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 573 theTokenParam.getSystem(), 574 theTokenParam.getValue()); 575 theSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue); 576 } else if (isNotBlank(theTokenParam.getValue())) { 577 theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue( 578 myPartitionSettings, 579 theRequestPartitionId, 580 theMatchUrl.myResourceDefinition.getName(), 581 theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(), 582 theTokenParam.getValue()); 583 theValuePredicates.add(theMatchUrl.myHashValue); 584 } 585 } 586 587 private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap( 588 List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) { 589 ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create(); 590 // Build a lookup map so we don't have to iterate over the searches repeatedly. 591 for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) { 592 if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) { 593 hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap); 594 } 595 if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) { 596 hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap); 597 } 598 } 599 return hashToSearch; 600 } 601 602 @Override 603 protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) { 604 try { 605 int insertionCount; 606 int updateCount; 607 SessionImpl session = myEntityManager.unwrap(SessionImpl.class); 608 if (session != null) { 609 insertionCount = session.getActionQueue().numberOfInsertions(); 610 updateCount = session.getActionQueue().numberOfUpdates(); 611 } else { 612 insertionCount = -1; 613 updateCount = -1; 614 } 615 616 StopWatch sw = new StopWatch(); 617 myEntityManager.flush(); 618 ourLog.debug( 619 "Session flush took {}ms for {} inserts and {} updates", 620 sw.getMillis(), 621 insertionCount, 622 updateCount); 623 } catch (PersistenceException e) { 624 if (myHapiFhirHibernateJpaDialect != null) { 625 List<String> types = theIdToPersistedOutcome.keySet().stream() 626 .filter(t -> t != null) 627 .map(t -> t.getResourceType()) 628 .collect(Collectors.toList()); 629 String message = "Error flushing transaction with resource types: " + types; 630 throw myHapiFhirHibernateJpaDialect.translate(e, message); 631 } 632 throw e; 633 } 634 } 635 636 @VisibleForTesting 637 public void setIdHelperServiceForUnitTest(IIdHelperService theIdHelperService) { 638 myIdHelperService = theIdHelperService; 639 } 640 641 @VisibleForTesting 642 public void setApplicationContextForUnitTest(ApplicationContext theAppCtx) { 643 myApplicationContext = theAppCtx; 644 } 645 646 private static class MatchUrlToResolve { 647 648 private final String myRequestUrl; 649 private final SearchParameterMap myMatchUrlSearchMap; 650 private final RuntimeResourceDefinition myResourceDefinition; 651 private final boolean myShouldPreFetchResourceBody; 652 public boolean myResolved; 653 private Long myHashValue; 654 private Long myHashSystemAndValue; 655 656 public MatchUrlToResolve( 657 String theRequestUrl, 658 SearchParameterMap theMatchUrlSearchMap, 659 RuntimeResourceDefinition theResourceDefinition, 660 boolean theShouldPreFetchResourceBody) { 661 myRequestUrl = theRequestUrl; 662 myMatchUrlSearchMap = theMatchUrlSearchMap; 663 myResourceDefinition = theResourceDefinition; 664 myShouldPreFetchResourceBody = theShouldPreFetchResourceBody; 665 } 666 667 public void setResolved(boolean theResolved) { 668 myResolved = theResolved; 669 } 670 } 671}