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