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