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