001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.dao;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.interceptor.model.ReadPartitionIdRequestDetails;
025import ca.uhn.fhir.interceptor.model.RequestPartitionId;
026import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
027import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
028import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
029import ca.uhn.fhir.jpa.api.model.PersistentIdToForcedIdMap;
030import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
031import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
032import ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect;
033import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
034import ca.uhn.fhir.jpa.model.cross.JpaResourceLookup;
035import ca.uhn.fhir.jpa.model.dao.JpaPid;
036import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
037import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
038import ca.uhn.fhir.jpa.model.entity.ResourceTable;
039import ca.uhn.fhir.jpa.model.entity.StorageSettings;
040import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
041import ca.uhn.fhir.jpa.search.ResourceSearchUrlSvc;
042import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
043import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
044import ca.uhn.fhir.jpa.util.MemoryCacheService;
045import ca.uhn.fhir.jpa.util.QueryChunker;
046import ca.uhn.fhir.model.api.IQueryParameterType;
047import ca.uhn.fhir.rest.api.server.RequestDetails;
048import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
049import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
050import ca.uhn.fhir.rest.param.TokenParam;
051import ca.uhn.fhir.util.FhirTerser;
052import ca.uhn.fhir.util.ResourceReferenceInfo;
053import ca.uhn.fhir.util.StopWatch;
054import ca.uhn.fhir.util.TaskChunker;
055import com.google.common.annotations.VisibleForTesting;
056import com.google.common.collect.ArrayListMultimap;
057import com.google.common.collect.ListMultimap;
058import com.google.common.collect.MultimapBuilder;
059import com.google.common.collect.SetMultimap;
060import jakarta.annotation.Nonnull;
061import jakarta.annotation.Nullable;
062import jakarta.persistence.EntityManager;
063import jakarta.persistence.FlushModeType;
064import jakarta.persistence.PersistenceContext;
065import jakarta.persistence.PersistenceContextType;
066import jakarta.persistence.PersistenceException;
067import jakarta.persistence.Tuple;
068import jakarta.persistence.TypedQuery;
069import jakarta.persistence.criteria.CriteriaBuilder;
070import jakarta.persistence.criteria.CriteriaQuery;
071import jakarta.persistence.criteria.Join;
072import jakarta.persistence.criteria.Predicate;
073import jakarta.persistence.criteria.Root;
074import org.apache.commons.collections4.ListUtils;
075import org.apache.commons.lang3.Validate;
076import org.hibernate.internal.SessionImpl;
077import org.hl7.fhir.instance.model.api.IBase;
078import org.hl7.fhir.instance.model.api.IBaseBundle;
079import org.hl7.fhir.instance.model.api.IBaseResource;
080import org.hl7.fhir.instance.model.api.IIdType;
081import org.slf4j.Logger;
082import org.slf4j.LoggerFactory;
083import org.springframework.beans.factory.annotation.Autowired;
084import org.springframework.context.ApplicationContext;
085
086import java.util.ArrayList;
087import java.util.Collection;
088import java.util.HashMap;
089import java.util.HashSet;
090import java.util.IdentityHashMap;
091import java.util.Iterator;
092import java.util.List;
093import java.util.Map;
094import java.util.Optional;
095import java.util.Set;
096import java.util.TreeMap;
097import java.util.regex.Pattern;
098import java.util.stream.Collectors;
099
100import static ca.uhn.fhir.util.UrlUtil.determineResourceTypeInResourceUrl;
101import static java.util.stream.Collectors.groupingBy;
102import static org.apache.commons.lang3.StringUtils.countMatches;
103import static org.apache.commons.lang3.StringUtils.isNotBlank;
104
105public class TransactionProcessor extends BaseTransactionProcessor {
106
107        /**
108         * Matches conditional URLs in the form of [resourceType]?[paramName]=[paramValue]{...more params...}
109         */
110        public static final Pattern MATCH_URL_PATTERN = Pattern.compile("^[^?]++[?][a-z0-9-]+=[^&,]++");
111
112        public static final int CONDITIONAL_URL_FETCH_CHUNK_SIZE = 100;
113        private static final Logger ourLog = LoggerFactory.getLogger(TransactionProcessor.class);
114
115        @Autowired
116        private ApplicationContext myApplicationContext;
117
118        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
119        private EntityManager myEntityManager;
120
121        @Autowired(required = false)
122        private HapiFhirHibernateJpaDialect myHapiFhirHibernateJpaDialect;
123
124        @Autowired
125        private IIdHelperService<JpaPid> myIdHelperService;
126
127        @Autowired
128        private JpaStorageSettings myStorageSettings;
129
130        @Autowired
131        private FhirContext myFhirContext;
132
133        @Autowired
134        private MatchResourceUrlService<JpaPid> myMatchResourceUrlService;
135
136        @Autowired
137        private MatchUrlService myMatchUrlService;
138
139        @Autowired
140        private ResourceSearchUrlSvc myResourceSearchUrlSvc;
141
142        @Autowired
143        private MemoryCacheService myMemoryCacheService;
144
145        @Autowired
146        private IRequestPartitionHelperSvc myRequestPartitionHelperSvc;
147
148        public void setEntityManagerForUnitTest(EntityManager theEntityManager) {
149                myEntityManager = theEntityManager;
150        }
151
152        @Override
153        protected void validateDependencies() {
154                super.validateDependencies();
155
156                Validate.notNull(myEntityManager, "EntityManager must not be null");
157        }
158
159        @VisibleForTesting
160        public void setFhirContextForUnitTest(FhirContext theFhirContext) {
161                myFhirContext = theFhirContext;
162        }
163
164        @Override
165        public void setStorageSettings(StorageSettings theStorageSettings) {
166                myStorageSettings = (JpaStorageSettings) theStorageSettings;
167                super.setStorageSettings(theStorageSettings);
168        }
169
170        @Override
171        protected EntriesToProcessMap doTransactionWriteOperations(
172                        final RequestDetails theRequest,
173                        RequestPartitionId theRequestPartitionId,
174                        String theActionName,
175                        TransactionDetails theTransactionDetails,
176                        Set<IIdType> theAllIds,
177                        IdSubstitutionMap theIdSubstitutions,
178                        Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome,
179                        IBaseBundle theResponse,
180                        IdentityHashMap<IBase, Integer> theOriginalRequestOrder,
181                        List<IBase> theEntries,
182                        StopWatch theTransactionStopWatch) {
183
184                /*
185                 * We temporarily set the flush mode for the duration of the DB transaction
186                 * from the default of AUTO to the temporary value of COMMIT here. We do this
187                 * because in AUTO mode, if any SQL SELECTs are required during the
188                 * processing of an individual transaction entry, the server will flush the
189                 * pending INSERTs/UPDATEs to the database before executing the SELECT.
190                 * This hurts performance since we don't get the benefit of batching those
191                 * write operations as much as possible. The tradeoff here is that we
192                 * could theoretically have transaction operations which try to read
193                 * data previously written in the same transaction, and they won't see it.
194                 * This shouldn't actually be an issue anyhow - we pre-fetch conditional
195                 * URLs and reference targets at the start of the transaction. But this
196                 * tradeoff still feels worth it, since the most common use of transactions
197                 * is for fast writing of data.
198                 *
199                 * Note that it's probably not necessary to reset it back, it should
200                 * automatically go back to the default value after the transaction, but
201                 * we reset it just to be safe.
202                 */
203                FlushModeType initialFlushMode = myEntityManager.getFlushMode();
204                try {
205                        myEntityManager.setFlushMode(FlushModeType.COMMIT);
206
207                        ITransactionProcessorVersionAdapter<?, ?> versionAdapter = getVersionAdapter();
208
209                        if (theRequestPartitionId != null) {
210                                preFetch(theRequest, theTransactionDetails, theEntries, versionAdapter, theRequestPartitionId);
211                        }
212
213                        return super.doTransactionWriteOperations(
214                                        theRequest,
215                                        theRequestPartitionId,
216                                        theActionName,
217                                        theTransactionDetails,
218                                        theAllIds,
219                                        theIdSubstitutions,
220                                        theIdToPersistedOutcome,
221                                        theResponse,
222                                        theOriginalRequestOrder,
223                                        theEntries,
224                                        theTransactionStopWatch);
225                } finally {
226                        myEntityManager.setFlushMode(initialFlushMode);
227                }
228        }
229
230        @SuppressWarnings("rawtypes")
231        private void preFetch(
232                        RequestDetails theRequestDetails,
233                        TransactionDetails theTransactionDetails,
234                        List<IBase> theEntries,
235                        ITransactionProcessorVersionAdapter theVersionAdapter,
236                        RequestPartitionId theRequestPartitionId) {
237                Set<JpaPid> idsToPreFetchBodiesFor = new HashSet<>();
238                Set<JpaPid> idsToPreFetchVersionsFor = new HashSet<>();
239                Set<JpaPid> idsToPreFetchFhirIdsFor = new HashSet<>();
240
241                /*
242                 * Pre-Fetch any resources that are referred to normally by ID, e.g.
243                 * regular FHIR updates within the transaction.
244                 */
245                preFetchResourcesById(
246                                theRequestDetails,
247                                theTransactionDetails,
248                                theEntries,
249                                theVersionAdapter,
250                                theRequestPartitionId,
251                                idsToPreFetchBodiesFor);
252
253                /*
254                 * Pre-resolve any conditional URLs we can
255                 */
256                preFetchConditionalUrls(
257                                theRequestDetails,
258                                theTransactionDetails,
259                                theEntries,
260                                theVersionAdapter,
261                                theRequestPartitionId,
262                                idsToPreFetchBodiesFor,
263                                idsToPreFetchVersionsFor,
264                                idsToPreFetchFhirIdsFor);
265
266                /*
267                 * Pre-Fetch Resource Bodies (this will happen for any resources we are potentially
268                 * going to update)
269                 */
270                IFhirSystemDao<?, ?> systemDao = myApplicationContext.getBean(IFhirSystemDao.class);
271                systemDao.preFetchResources(List.copyOf(idsToPreFetchBodiesFor), true);
272
273                /*
274                 * Pre-Fetch Resource Versions (this will happen for any resources we are doing a
275                 * conditional create on, meaning we don't actually care about the contents, just
276                 * the ID and version)
277                 */
278                preFetchResourceVersions(idsToPreFetchVersionsFor);
279
280                preFetchFhirIds(idsToPreFetchFhirIdsFor, theTransactionDetails);
281        }
282
283        private void preFetchFhirIds(Set<JpaPid> theIdsToPreFetchFhirIdsFor, TransactionDetails theTransactionDetails) {
284                PersistentIdToForcedIdMap<JpaPid> forcedIds =
285                                myIdHelperService.translatePidsToForcedIds(theIdsToPreFetchFhirIdsFor);
286                for (JpaPid nextId : theIdsToPreFetchFhirIdsFor) {
287                        Optional<String> fhirIdOpt = forcedIds.get(nextId);
288                        if (fhirIdOpt.isPresent()) {
289                                String fhirIdString = fhirIdOpt.get();
290                                IIdType fhirId = myFhirContext.getVersion().newIdType(fhirIdString);
291                                theTransactionDetails.addResolvedResourceId(fhirId, nextId);
292                        }
293                }
294        }
295
296        /**
297         * Given a collection of {@link JpaPid}, loads the current version associated with
298         * each PID and puts it into the {@link JpaPid#setVersion(Long)} field.
299         */
300        private void preFetchResourceVersions(Set<JpaPid> theIds) {
301                ourLog.trace("Versions to fetch: {}", theIds);
302
303                for (Iterator<JpaPid> it = theIds.iterator(); it.hasNext(); ) {
304                        JpaPid pid = it.next();
305                        Long version = myMemoryCacheService.getIfPresent(
306                                        MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid);
307                        if (version != null) {
308                                it.remove();
309                                pid.setVersion(version);
310                        }
311                }
312
313                if (!theIds.isEmpty()) {
314                        Map<JpaPid, JpaPid> idMap = theIds.stream().collect(Collectors.toMap(t -> t, t -> t));
315
316                        QueryChunker.chunk(theIds, ids -> {
317                                CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
318                                CriteriaQuery<Tuple> cq = cb.createTupleQuery();
319                                Root<ResourceTable> from = cq.from(ResourceTable.class);
320                                cq.multiselect(from.get("myPid"), from.get("myVersion"));
321                                cq.where(from.get("myPid").in(ids));
322                                TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
323                                List<Tuple> results = query.getResultList();
324
325                                for (Tuple tuple : results) {
326                                        JpaPid pid = tuple.get(0, JpaPid.class);
327                                        Long version = tuple.get(1, Long.class);
328                                        idMap.get(pid).setVersion(version);
329
330                                        myMemoryCacheService.putAfterCommit(
331                                                        MemoryCacheService.CacheEnum.RESOURCE_CONDITIONAL_CREATE_VERSION, pid, version);
332                                }
333                        });
334                }
335        }
336
337        @Override
338        @SuppressWarnings("rawtypes")
339        protected void postTransactionProcess(TransactionDetails theTransactionDetails) {
340                Set<IResourcePersistentId> resourceIds = theTransactionDetails.getUpdatedResourceIds();
341                if (resourceIds != null && !resourceIds.isEmpty()) {
342                        List<JpaPid> ids = resourceIds.stream().map(r -> (JpaPid) r).collect(Collectors.toList());
343                        myResourceSearchUrlSvc.deleteByResIds(ids);
344                }
345        }
346
347        @SuppressWarnings({"unchecked", "rawtypes"})
348        private void preFetchResourcesById(
349                        RequestDetails theRequestDetails,
350                        TransactionDetails theTransactionDetails,
351                        List<IBase> theEntries,
352                        ITransactionProcessorVersionAdapter theVersionAdapter,
353                        RequestPartitionId theRequestPartitionId,
354                        Set<JpaPid> theIdsToPreFetchBodiesFor) {
355
356                FhirTerser terser = myFhirContext.newTerser();
357
358                Map<IIdType, PrefetchReasonEnum> idsToPreResolve = new HashMap<>(theEntries.size() * 3);
359
360                for (IBase nextEntry : theEntries) {
361                        IBaseResource resource = theVersionAdapter.getResource(nextEntry);
362                        if (resource != null) {
363                                String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry);
364
365                                /*
366                                 * Pre-fetch any resources that are being updated or patched within
367                                 * the transaction
368                                 */
369                                if ("PUT".equals(verb) || "PATCH".equals(verb)) {
370                                        String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry);
371                                        if (countMatches(requestUrl, '?') == 0) {
372                                                IIdType id = myFhirContext.getVersion().newIdType();
373                                                id.setValue(requestUrl);
374                                                IIdType unqualifiedVersionless = id.toUnqualifiedVersionless();
375                                                idsToPreResolve.put(unqualifiedVersionless, PrefetchReasonEnum.DIRECT_TARGET);
376                                        }
377                                }
378
379                                /*
380                                 * If there are any resource references anywhere in any resources being
381                                 * created or updated that point to another target resource directly by
382                                 * ID, we also want to prefetch the identity of that target ID
383                                 */
384                                if ("PUT".equals(verb) || "POST".equals(verb)) {
385                                        for (ResourceReferenceInfo referenceInfo : terser.getAllResourceReferences(resource)) {
386                                                IIdType reference = referenceInfo.getResourceReference().getReferenceElement();
387                                                if (reference != null
388                                                                && !reference.isLocal()
389                                                                && !reference.isUuid()
390                                                                && reference.hasResourceType()
391                                                                && reference.hasIdPart()
392                                                                && !reference.getValue().contains("?")) {
393
394                                                        // We use putIfAbsent here because if we're already fetching
395                                                        // as a direct target we don't want to downgrade to just a
396                                                        // reference target
397                                                        idsToPreResolve.putIfAbsent(
398                                                                        reference.toUnqualifiedVersionless(), PrefetchReasonEnum.REFERENCE_TARGET);
399                                                }
400                                        }
401                                }
402                        }
403                }
404
405                /*
406                 * If any of the entries in the pre-fetch ID map have a value of REFERENCE_TARGET,
407                 * this means we can't rely on cached identities because we need to know the
408                 * current deleted status of at least one of them. This is because another thread
409                 * (or potentially even another process elsewhere) could have moved the resource
410                 * to "deleted", and we can't allow someone to add a reference to a deleted
411                 * resource. If deletes are disabled on this server though, we can trust that
412                 * nothing has been moved to "deleted" status since it was put in the cache, and
413                 * it's safe to use the cache.
414                 *
415                 * On the other hand, if all resource IDs we want to prefetch have a value of
416                 * DIRECT_UPDATE, that means these IDs are all resources we're about to
417                 * modify. In that case it doesn't even matter if the resource is currently
418                 * deleted because we're going to resurrect it in that case.
419                 */
420                boolean preFetchIncludesReferences =
421                                idsToPreResolve.values().stream().anyMatch(t -> t == PrefetchReasonEnum.REFERENCE_TARGET);
422                ResolveIdentityMode resolveMode = preFetchIncludesReferences
423                                ? ResolveIdentityMode.includeDeleted().noCacheUnlessDeletesDisabled()
424                                : ResolveIdentityMode.includeDeleted().cacheOk();
425
426                SetMultimap<RequestPartitionId, IIdType> partitionToIds = null;
427                Set<IIdType> referenceTargetIds = new HashSet<>(idsToPreResolve.keySet());
428                RequestPartitionId requestPartitionId = theRequestPartitionId;
429
430                /*
431                 * If specific resources are on different non-compatible partitions, we will pre-fetch them separately
432                 * in a separate transaction that is scoped to the appropriate partition.
433                 */
434                if (myPartitionSettings.isPartitioningEnabled()) {
435                        for (Iterator<IIdType> iterator = referenceTargetIds.iterator(); iterator.hasNext(); ) {
436                                IIdType nextId = iterator.next();
437                                RequestPartitionId partition = theTransactionDetails.getResolvedPartition(nextId.getValue());
438                                if (partition == null) {
439                                        ReadPartitionIdRequestDetails readDetails = ReadPartitionIdRequestDetails.forRead(nextId);
440                                        partition = myRequestPartitionHelperSvc.determineReadPartitionForRequest(
441                                                        theRequestDetails, readDetails);
442                                }
443                                if (!partition.isAllPartitions()
444                                                && !myHapiTransactionService.isCompatiblePartition(theRequestPartitionId, partition)) {
445                                        iterator.remove();
446                                        if (partitionToIds == null) {
447                                                partitionToIds =
448                                                                MultimapBuilder.hashKeys().hashSetValues().build();
449                                        }
450                                        partitionToIds.put(partition, nextId);
451                                } else {
452                                        requestPartitionId = requestPartitionId.mergeIds(partition);
453                                }
454                        }
455                }
456
457                doPreFetchResourcesById(
458                                theTransactionDetails,
459                                requestPartitionId,
460                                referenceTargetIds,
461                                idsToPreResolve,
462                                resolveMode,
463                                theIdsToPreFetchBodiesFor);
464
465                if (partitionToIds != null) {
466                        for (RequestPartitionId nextPartition : partitionToIds.keySet()) {
467                                Set<IIdType> ids = partitionToIds.get(nextPartition);
468                                doPreFetchResourcesById(
469                                                theTransactionDetails,
470                                                nextPartition,
471                                                ids,
472                                                idsToPreResolve,
473                                                resolveMode,
474                                                theIdsToPreFetchBodiesFor);
475                        }
476                }
477        }
478
479        private void doPreFetchResourcesById(
480                        TransactionDetails theTransactionDetails,
481                        RequestPartitionId theRequestPartitionId,
482                        Set<IIdType> theInputIdsToPreFetch,
483                        Map<IIdType, PrefetchReasonEnum> theInputIdsToPreResolve,
484                        ResolveIdentityMode theResolveMode,
485                        Set<JpaPid> theOutputIdsToPreFetchBodiesFor) {
486
487                Set<String> foundIds = new HashSet<>();
488
489                // If any of the IDs are already resolved in the TransactionDetails, just
490                // use the resolution from there
491                Map<IIdType, IResourceLookup<JpaPid>> outcomesFromTransactionDetails = null;
492                if (theRequestPartitionId.hasPartitionIds()
493                                && theRequestPartitionId.getPartitionIds().size() == 1) {
494                        for (IIdType inputIdToResult : theInputIdsToPreFetch) {
495                                JpaPid pidResolvedInTransaction = (JpaPid) theTransactionDetails.getResolvedResourceId(inputIdToResult);
496                                if (pidResolvedInTransaction != null) {
497                                        if (outcomesFromTransactionDetails == null) {
498                                                outcomesFromTransactionDetails = new HashMap<>();
499                                        }
500                                        JpaResourceLookup resourceLookup = new JpaResourceLookup(
501                                                        inputIdToResult.getResourceType(),
502                                                        inputIdToResult.getIdPart(),
503                                                        pidResolvedInTransaction,
504                                                        null,
505                                                        PartitionablePartitionId.with(theRequestPartitionId.getFirstPartitionIdOrNull(), null));
506                                        outcomesFromTransactionDetails.put(inputIdToResult, resourceLookup);
507                                }
508                        }
509                }
510
511                Set<IIdType> inputIdsToPreFetch = theInputIdsToPreFetch;
512                if (outcomesFromTransactionDetails != null) {
513                        inputIdsToPreFetch = new HashSet<>(theInputIdsToPreFetch);
514                        inputIdsToPreFetch.removeAll(outcomesFromTransactionDetails.keySet());
515                }
516
517                Map<IIdType, IResourceLookup<JpaPid>> outcomes =
518                                myIdHelperService.resolveResourceIdentities(theRequestPartitionId, inputIdsToPreFetch, theResolveMode);
519
520                if (outcomesFromTransactionDetails != null) {
521                        outcomes.putAll(outcomesFromTransactionDetails);
522                }
523
524                for (Iterator<Map.Entry<IIdType, IResourceLookup<JpaPid>>> iterator =
525                                                outcomes.entrySet().iterator();
526                                iterator.hasNext(); ) {
527                        Map.Entry<IIdType, IResourceLookup<JpaPid>> entry = iterator.next();
528                        JpaPid next = entry.getValue().getPersistentId();
529                        IIdType unqualifiedVersionlessId = entry.getKey();
530                        switch (theInputIdsToPreResolve.get(unqualifiedVersionlessId)) {
531                                case DIRECT_TARGET -> {
532                                        if (myStorageSettings.getResourceClientIdStrategy() != JpaStorageSettings.ClientIdStrategyEnum.ANY
533                                                        || (next.getAssociatedResourceId() != null
534                                                                        && !next.getAssociatedResourceId().isIdPartValidLong())) {
535                                                theOutputIdsToPreFetchBodiesFor.add(next);
536                                        }
537                                }
538                                case REFERENCE_TARGET -> {
539                                        if (entry.getValue().getDeleted() != null) {
540                                                iterator.remove();
541                                                continue;
542                                        }
543                                }
544                        }
545
546                        foundIds.add(unqualifiedVersionlessId.getValue());
547                        theTransactionDetails.addResolvedResourceId(unqualifiedVersionlessId, next);
548                }
549
550                // Any IDs that could not be resolved are presumably not there, so
551                // cache that fact so we don't look again later
552                for (IIdType next : theInputIdsToPreFetch) {
553                        if (!foundIds.contains(next.getValue())) {
554                                theTransactionDetails.addResolvedResourceId(next.toUnqualifiedVersionless(), null);
555                        }
556                }
557        }
558
559        @Override
560        protected void handleVerbChangeInTransactionWriteOperations() {
561                super.handleVerbChangeInTransactionWriteOperations();
562
563                myEntityManager.flush();
564        }
565
566        @SuppressWarnings({"rawtypes", "unchecked"})
567        private void preFetchConditionalUrls(
568                        RequestDetails theRequestDetails,
569                        TransactionDetails theTransactionDetails,
570                        List<IBase> theEntries,
571                        ITransactionProcessorVersionAdapter theVersionAdapter,
572                        RequestPartitionId theRequestPartitionId,
573                        Set<JpaPid> theIdsToPreFetchBodiesFor,
574                        Set<JpaPid> theIdsToPreFetchVersionsFor,
575                        Set<JpaPid> theIdsToPreFetchFhirIdsFor) {
576
577                List<MatchUrlToResolve> searchParameterMapsToResolve = new ArrayList<>();
578                for (IBase nextEntry : theEntries) {
579                        IBaseResource resource = theVersionAdapter.getResource(nextEntry);
580                        if (resource != null) {
581                                String verb = theVersionAdapter.getEntryRequestVerb(myFhirContext, nextEntry);
582                                String requestUrl = theVersionAdapter.getEntryRequestUrl(nextEntry);
583                                String requestIfNoneExist = theVersionAdapter.getEntryIfNoneExist(nextEntry);
584                                String resourceType = determineResourceTypeInResourceUrl(myFhirContext, requestUrl);
585                                if (resourceType == null) {
586                                        resourceType = myFhirContext.getResourceType(resource);
587                                }
588                                if (("PUT".equals(verb) || "PATCH".equals(verb)) && requestUrl != null && requestUrl.contains("?")) {
589                                        IBaseResource associatedResource = null;
590                                        if ("PUT".equals(verb)) {
591                                                associatedResource = resource;
592                                        }
593                                        processConditionalUrlForPreFetching(
594                                                        theRequestPartitionId,
595                                                        resourceType,
596                                                        associatedResource,
597                                                        requestUrl,
598                                                        true,
599                                                        false,
600                                                        theIdsToPreFetchBodiesFor,
601                                                        theIdsToPreFetchFhirIdsFor,
602                                                        searchParameterMapsToResolve);
603                                } else if ("POST".equals(verb) && requestIfNoneExist != null && requestIfNoneExist.contains("?")) {
604                                        processConditionalUrlForPreFetching(
605                                                        theRequestPartitionId,
606                                                        resourceType,
607                                                        resource,
608                                                        requestIfNoneExist,
609                                                        false,
610                                                        true,
611                                                        theIdsToPreFetchBodiesFor,
612                                                        theIdsToPreFetchFhirIdsFor,
613                                                        searchParameterMapsToResolve);
614                                }
615
616                                if (myStorageSettings.isAllowInlineMatchUrlReferences()) {
617                                        List<ResourceReferenceInfo> references =
618                                                        myFhirContext.newTerser().getAllResourceReferences(resource);
619                                        for (ResourceReferenceInfo next : references) {
620                                                String referenceUrl = next.getResourceReference()
621                                                                .getReferenceElement()
622                                                                .getValue();
623                                                String refResourceType = determineResourceTypeInResourceUrl(myFhirContext, referenceUrl);
624                                                if (refResourceType != null) {
625                                                        processConditionalUrlForPreFetching(
626                                                                        theRequestPartitionId,
627                                                                        refResourceType,
628                                                                        null,
629                                                                        referenceUrl,
630                                                                        false,
631                                                                        false,
632                                                                        theIdsToPreFetchBodiesFor,
633                                                                        theIdsToPreFetchFhirIdsFor,
634                                                                        searchParameterMapsToResolve);
635                                                }
636                                        }
637                                }
638                        }
639                }
640
641                // group things by match url so we can run them together.
642                record MatchTarget(String url, String resourceType) {
643                        @Nonnull
644                        static MatchTarget getMatchTarget(MatchUrlToResolve r) {
645                                return new MatchTarget(r.myRequestUrl, r.myResourceDefinition.getName());
646                        }
647                }
648
649                ListMultimap<RequestPartitionId, MatchUrlToResolve> partitionToMatchUrls = groupMatchTargetListByPartitionId(
650                                theRequestDetails, searchParameterMapsToResolve, theRequestPartitionId);
651                for (RequestPartitionId partitionId : partitionToMatchUrls.keySet()) {
652                        myHapiTransactionService
653                                        .withRequest(theRequestDetails)
654                                        .withRequestPartitionId(partitionId)
655                                        .execute(() -> {
656                                                List<MatchUrlToResolve> matchUrls = partitionToMatchUrls.get(partitionId);
657
658                                                /*
659                                                 * Chunk references into query-friendly sizes to resolve in batches.
660                                                 * Note: we can have 1000s of references all using the same url.
661                                                 * E.g. Organization references in big patient bundles. But if
662                                                 * these are scattered among other different URLs within the Bundle,
663                                                 * we don't want to end up resolving the same URL over and over.
664                                                 * So we build batches by url, not by reference.
665                                                 */
666                                                Map<MatchTarget, List<MatchUrlToResolve>> byMatchUrl =
667                                                                matchUrls.stream().collect(groupingBy(MatchTarget::getMatchTarget));
668
669                                                TaskChunker.chunk(byMatchUrl.entrySet(), CONDITIONAL_URL_FETCH_CHUNK_SIZE, nextUrlChunk -> {
670
671                                                        /*
672                                                         * Combine all resolve entries under this chunk of urls. If we have several entries with
673                                                         * the exact same URL, that means we'll have several entries in the following list, but
674                                                         * preFetchSearchParameterMaps(..) will only add one parameter to the SQL it generates for
675                                                         * each URL's SP hash value.
676                                                         */
677                                                        List<MatchUrlToResolve> combinedChunk = nextUrlChunk.stream()
678                                                                        .flatMap(cc -> cc.getValue().stream())
679                                                                        .toList();
680
681                                                        preFetchSearchParameterMaps(
682                                                                        theRequestDetails,
683                                                                        theTransactionDetails,
684                                                                        partitionId,
685                                                                        combinedChunk,
686                                                                        theIdsToPreFetchBodiesFor,
687                                                                        theIdsToPreFetchVersionsFor);
688                                                });
689                                        });
690                }
691        }
692
693        /**
694         * Given a collection of {@link MatchUrlToResolve} objects, calculates the read
695         * {@link RequestPartitionId} associated with each one and returns a
696         * map of the partition ID to the list of associated match URLs.
697         * <p>
698         * If two different {@link RequestPartitionId} are considered compatible per the
699         * {@link ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService#isCompatiblePartition(RequestPartitionId, RequestPartitionId)}
700         * method, the two partitions are combined into a single {@link RequestPartitionId} object and all
701         * match URLs associated with both will be returned in a single list associated with the
702         * combined {@link RequestPartitionId}.
703         * </p>
704         */
705        @Nonnull
706        private ListMultimap<RequestPartitionId, MatchUrlToResolve> groupMatchTargetListByPartitionId(
707                        RequestDetails theRequestDetails,
708                        List<MatchUrlToResolve> theMatchUrls,
709                        RequestPartitionId theOuterRequestPartitionId) {
710                ListMultimap<RequestPartitionId, MatchUrlToResolve> retVal =
711                                MultimapBuilder.hashKeys().arrayListValues().build();
712
713                /*
714                 * For each Match URL, calculate the request partition and populate a Multimap
715                 */
716                for (MatchUrlToResolve next : theMatchUrls) {
717                        RequestPartitionId partition = RequestPartitionId.allPartitions();
718                        if (myPartitionSettings.isPartitioningEnabled()) {
719                                partition = myRequestPartitionHelperSvc.determineReadPartitionForRequestForSearchType(
720                                                theRequestDetails,
721                                                next.myResourceDefinition.getName(),
722                                                next.myMatchUrlSearchMap,
723                                                next.getAssociatedResource());
724                                if (partition.isAllPartitions()) {
725                                        partition = theOuterRequestPartitionId;
726                                }
727                        }
728
729                        retVal.put(partition, next);
730                }
731
732                /*
733                 * Try to combine any request partitions which are considered compatible by the
734                 * transaction service. We're just using a brute force way to determine this, which
735                 * could probably be optimized some, but it's not expected that we'll typically have
736                 * many different partitions in the same transaction so it probably doesn't matter
737                 * too much.
738                 */
739                while (true) {
740                        boolean changes = false;
741
742                        List<RequestPartitionId> partitionUds = new ArrayList<>(retVal.keySet());
743                        for (int indexA = 0; indexA < partitionUds.size(); indexA++) {
744                                for (int indexB = 0; indexB < partitionUds.size(); indexB++) {
745                                        if (indexA == indexB) {
746                                                continue;
747                                        }
748
749                                        RequestPartitionId partitionA = partitionUds.get(indexA);
750                                        RequestPartitionId partitionB = partitionUds.get(indexB);
751                                        if (partitionA == null
752                                                        || partitionA.isAllPartitions()
753                                                        || partitionB == null
754                                                        || partitionB.isAllPartitions()) {
755                                                continue;
756                                        }
757
758                                        if (myHapiTransactionService.isCompatiblePartition(partitionA, partitionB)) {
759                                                changes = true;
760                                                List<MatchUrlToResolve> matchUrlsA = retVal.removeAll(partitionA);
761                                                List<MatchUrlToResolve> matchUrlsB = retVal.removeAll(partitionB);
762
763                                                RequestPartitionId partitionBoth = partitionA.mergeIds(partitionB);
764                                                List<MatchUrlToResolve> matchUrlsBoth = ListUtils.union(matchUrlsA, matchUrlsB);
765
766                                                retVal.putAll(partitionBoth, matchUrlsBoth);
767                                                partitionUds.set(indexA, null);
768                                                partitionUds.set(indexB, null);
769                                        }
770                                }
771                        }
772
773                        if (!changes) {
774                                break;
775                        }
776                }
777                return retVal;
778        }
779
780        /**
781         * This method attempts to resolve a collection of conditional URLs that were found
782         * in a FHIR transaction bundle being processed.
783         *
784         * @param theRequestDetails              The active request
785         * @param theTransactionDetails          The active transaction details
786         * @param theRequestPartitionId          The active partition
787         * @param theInputParameters             These are the conditional URLs that will actually be resolved
788         * @param theOutputPidsToLoadBodiesFor   This list will be added to with any resource PIDs that need to be fully
789         *                                       preloaded (i.e. fetch the actual resource body since we're presumably
790         *                                       going to update it and will need to see its current state eventually)
791         * @param theOutputPidsToLoadVersionsFor This list will be added to with any resource PIDs that need to have
792         *                                       their current version resolved. This is used for conditional creates,
793         *                                       where we don't actually care about the body of the resource, only
794         *                                       the version it has (since the version is returned in the response,
795         *                                       and potentially used if we're auto-versioning references).
796         */
797        @VisibleForTesting
798        public void preFetchSearchParameterMaps(
799                        RequestDetails theRequestDetails,
800                        TransactionDetails theTransactionDetails,
801                        RequestPartitionId theRequestPartitionId,
802                        List<MatchUrlToResolve> theInputParameters,
803                        Set<JpaPid> theOutputPidsToLoadBodiesFor,
804                        Set<JpaPid> theOutputPidsToLoadVersionsFor) {
805
806                Set<Long> systemAndValueHashes = new HashSet<>();
807                Set<Long> valueHashes = new HashSet<>();
808
809                for (MatchUrlToResolve next : theInputParameters) {
810                        Collection<List<List<IQueryParameterType>>> values = next.myMatchUrlSearchMap.values();
811
812                        /*
813                         * Any conditional URLs that consist of a single token parameter are batched
814                         * up into a single query against the HFJ_SPIDX_TOKEN table so that we only
815                         * perform one SQL query for all of them.
816                         *
817                         * We could potentially add other patterns in the future, but it's much more
818                         * tricky to implement this when there are multiple parameters, and non-token
819                         * parameter types aren't often used on their own in conditional URLs. So for
820                         * now we handle single-token only, and that's probably good enough.
821                         */
822                        boolean canBeHandledInAggregateQuery = false;
823
824                        if (values.size() == 1) {
825                                List<List<IQueryParameterType>> andList = values.iterator().next();
826                                IQueryParameterType param = andList.get(0).get(0);
827
828                                if (param instanceof TokenParam tokenParam) {
829                                        canBeHandledInAggregateQuery = buildHashPredicateFromTokenParam(
830                                                        tokenParam, theRequestPartitionId, next, systemAndValueHashes, valueHashes);
831                                }
832                        }
833
834                        if (!canBeHandledInAggregateQuery) {
835                                Set<JpaPid> matchUrlResults = myMatchResourceUrlService.processMatchUrl(
836                                                next.myRequestUrl,
837                                                next.myResourceDefinition.getImplementingClass(),
838                                                theTransactionDetails,
839                                                theRequestDetails,
840                                                theRequestPartitionId);
841                                for (JpaPid matchUrlResult : matchUrlResults) {
842                                        handleFoundPreFetchResourceId(
843                                                        theTransactionDetails,
844                                                        theOutputPidsToLoadBodiesFor,
845                                                        theOutputPidsToLoadVersionsFor,
846                                                        next,
847                                                        matchUrlResult);
848                                }
849                        }
850                }
851
852                preFetchSearchParameterMapsToken(
853                                "myHashSystemAndValue",
854                                systemAndValueHashes,
855                                theTransactionDetails,
856                                theRequestPartitionId,
857                                theInputParameters,
858                                theOutputPidsToLoadBodiesFor,
859                                theOutputPidsToLoadVersionsFor);
860                preFetchSearchParameterMapsToken(
861                                "myHashValue",
862                                valueHashes,
863                                theTransactionDetails,
864                                theRequestPartitionId,
865                                theInputParameters,
866                                theOutputPidsToLoadBodiesFor,
867                                theOutputPidsToLoadVersionsFor);
868
869                // For each SP Map which did not return a result, tag it as not found.
870                theInputParameters.stream()
871                                // No matches
872                                .filter(match -> !match.myResolved)
873                                .forEach(match -> {
874                                        ourLog.debug("Was unable to match url {} from database", match.myRequestUrl);
875                                        theTransactionDetails.addResolvedMatchUrl(
876                                                        myFhirContext, match.myRequestUrl, TransactionDetails.NOT_FOUND);
877                                });
878        }
879
880        /**
881         * Here we do a select against the {@link ResourceIndexedSearchParamToken} table for any rows that have the
882         * specific sys+val or val hashes we know we need to pre-fetch.
883         * <p>
884         * Note that we do a tuple query for only 2 columns in order to ensure that we can get by with only
885         * the data in the index (ie no need to load the actual table rows).
886         */
887        public void preFetchSearchParameterMapsToken(
888                        String theIndexColumnName,
889                        Set<Long> theHashesForIndexColumn,
890                        TransactionDetails theTransactionDetails,
891                        RequestPartitionId theRequestPartitionId,
892                        List<MatchUrlToResolve> theInputParameters,
893                        Set<JpaPid> theOutputPidsToLoadFully,
894                        Set<JpaPid> theOutputPidsToLoadVersionsFor) {
895                if (!theHashesForIndexColumn.isEmpty()) {
896                        ListMultimap<Long, MatchUrlToResolve> hashToSearchMap =
897                                        buildHashToSearchMap(theInputParameters, theIndexColumnName);
898                        CriteriaBuilder cb = myEntityManager.getCriteriaBuilder();
899                        CriteriaQuery<Tuple> cq = cb.createTupleQuery();
900                        Root<ResourceIndexedSearchParamToken> token = cq.from(ResourceIndexedSearchParamToken.class);
901                        Join<ResourceIndexedSearchParamToken, ResourceTable> resourceTable = token.join("myResource");
902
903                        cq.multiselect(
904                                        token.get("myPartitionIdValue"),
905                                        token.get("myResourcePid"),
906                                        token.get(theIndexColumnName),
907                                        resourceTable.get("myFhirId"),
908                                        resourceTable.get("myResourceType"));
909
910                        Predicate masterPredicate;
911                        if (theHashesForIndexColumn.size() == 1) {
912                                masterPredicate = cb.equal(
913                                                token.get(theIndexColumnName),
914                                                theHashesForIndexColumn.iterator().next());
915                        } else {
916                                masterPredicate = token.get(theIndexColumnName).in(theHashesForIndexColumn);
917                        }
918
919                        if (myPartitionSettings.isPartitioningEnabled()
920                                        && !myPartitionSettings.isIncludePartitionInSearchHashes()) {
921                                if (myRequestPartitionHelperSvc.isDefaultPartition(theRequestPartitionId)
922                                                && myPartitionSettings.getDefaultPartitionId() == null) {
923                                        Predicate partitionIdCriteria = cb.isNull(token.get("myPartitionIdValue"));
924                                        masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
925                                } else if (!theRequestPartitionId.isAllPartitions()) {
926                                        Predicate partitionIdCriteria =
927                                                        token.get("myPartitionIdValue").in(theRequestPartitionId.getPartitionIds());
928                                        masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
929                                }
930                        }
931
932                        cq.where(masterPredicate);
933
934                        TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
935
936                        /*
937                         * If we have 10 unique conditional URLs we're resolving, each one should
938                         * resolve to 0..1 resources if they are valid as conditional URLs. So we would
939                         * expect this query to return 0..10 rows, since conditional URLs for all
940                         * conditional operations except DELETE (which isn't being applied here) are
941                         * only allowed to resolve to 0..1 resources.
942                         *
943                         * If a conditional URL matches 2+ resources that is an error, and we'll
944                         * be throwing an exception below. This limit is here for safety just to
945                         * ensure that if someone uses a conditional URL that matches a million resources,
946                         * we don't do a super-expensive fetch.
947                         */
948                        query.setMaxResults(theHashesForIndexColumn.size() + 1);
949
950                        List<Tuple> results = query.getResultList();
951
952                        for (Tuple nextResult : results) {
953                                Integer nextPartitionId = nextResult.get(0, Integer.class);
954                                Long nextResourcePid = nextResult.get(1, Long.class);
955                                Long nextHash = nextResult.get(2, Long.class);
956                                String idPart = nextResult.get(3, String.class);
957                                String resourceType = nextResult.get(4, String.class);
958
959                                JpaPid pid = JpaPid.fromId(nextResourcePid, nextPartitionId);
960                                IIdType fhirId = myFhirContext.getVersion().newIdType(resourceType, idPart);
961                                theTransactionDetails.addResolvedResourceId(fhirId, pid);
962
963                                List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash);
964                                matchedSearch.forEach(matchUrl -> {
965                                        ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl);
966
967                                        handleFoundPreFetchResourceId(
968                                                        theTransactionDetails,
969                                                        theOutputPidsToLoadFully,
970                                                        theOutputPidsToLoadVersionsFor,
971                                                        matchUrl,
972                                                        pid);
973                                });
974                        }
975                }
976        }
977
978        private void handleFoundPreFetchResourceId(
979                        TransactionDetails theTransactionDetails,
980                        Set<JpaPid> theOutputPidsToLoadFully,
981                        Set<JpaPid> theOutputPidsToLoadVersionsFor,
982                        MatchUrlToResolve theMatchUrl,
983                        JpaPid theFoundPid) {
984                if (theMatchUrl.myShouldPreFetchResourceBody) {
985                        theOutputPidsToLoadFully.add(theFoundPid);
986                }
987                if (theMatchUrl.myShouldPreFetchResourceVersion) {
988                        theOutputPidsToLoadVersionsFor.add(theFoundPid);
989                }
990                myMatchResourceUrlService.matchUrlResolved(
991                                theTransactionDetails,
992                                theMatchUrl.myResourceDefinition.getName(),
993                                theMatchUrl.myRequestUrl,
994                                theFoundPid);
995                theTransactionDetails.addResolvedMatchUrl(myFhirContext, theMatchUrl.myRequestUrl, theFoundPid);
996                theMatchUrl.setResolved(true);
997        }
998
999        /**
1000         * Examines a conditional URL, and potentially adds it to either {@literal theOutputIdsToPreFetchBodiesFor}
1001         * or {@literal theOutputSearchParameterMapsToResolve}.
1002         * <p>
1003         * Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match
1004         * URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving
1005         * a match URL because it's there for a conditional update, we'll eagerly fetch the
1006         * actual resource because we need to know its current state in order to update it. However, if
1007         * the match URL is from an inline match URL in a resource body, we really only care about
1008         * the PID and don't need the body so we don't load it. This does have a security implication, since
1009         * it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut
1010         * isn't fired even though the user has resolved the URL (meaning they may be able to test for
1011         * the existence of a resource using a match URL). There is a test for this called
1012         * {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff
1013         * is acceptable since we're only prefetching things with very simple match URLs (nothing with
1014         * a reference in it for example) so it's not really possible to doing anything useful with this.
1015         * </p>
1016         *
1017         * @param thePartitionId                        The partition ID of the associated resource (can be null)
1018         * @param theResourceType                       The resource type associated with the match URL (ie what resource type should it resolve to)
1019         * @param theRequestUrl                         The actual match URL, which could be as simple as just parameters or could include the resource type too
1020         * @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.
1021         * @param theOutputIdsToPreFetchBodiesFor       This will be populated with any resource PIDs that need to be pre-fetched
1022         * @param theOutputIdsToPreFetchFhirIdsFor
1023         * @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve
1024         */
1025        private void processConditionalUrlForPreFetching(
1026                        RequestPartitionId thePartitionId,
1027                        String theResourceType,
1028                        @Nullable IBaseResource theAssociatedResource,
1029                        String theRequestUrl,
1030                        boolean theShouldPreFetchResourceBody,
1031                        boolean theShouldPreFetchResourceVersion,
1032                        Set<JpaPid> theOutputIdsToPreFetchBodiesFor,
1033                        Set<JpaPid> theOutputIdsToPreFetchFhirIdsFor,
1034                        List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) {
1035                JpaPid cachedId =
1036                                myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl, thePartitionId);
1037                if (cachedId != null) {
1038                        if (theShouldPreFetchResourceBody) {
1039                                theOutputIdsToPreFetchBodiesFor.add(cachedId);
1040                        } else {
1041                                theOutputIdsToPreFetchFhirIdsFor.add(cachedId);
1042                        }
1043                } else if (MATCH_URL_PATTERN.matcher(theRequestUrl).find()) {
1044                        RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
1045                        SearchParameterMap matchUrlSearchMap =
1046                                        myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition);
1047                        assert matchUrlSearchMap != null;
1048                        theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve(
1049                                        theRequestUrl,
1050                                        theAssociatedResource,
1051                                        matchUrlSearchMap,
1052                                        resourceDefinition,
1053                                        theShouldPreFetchResourceBody,
1054                                        theShouldPreFetchResourceVersion));
1055                }
1056        }
1057
1058        /**
1059         * Given a token parameter, build the query predicate based on its hash. Uses system and value if both are available, otherwise just value.
1060         * If neither are available, it returns null.
1061         *
1062         * @return Returns {@literal true} if the param was added to one of the output lists
1063         */
1064        private boolean buildHashPredicateFromTokenParam(
1065                        TokenParam theTokenParam,
1066                        RequestPartitionId theRequestPartitionId,
1067                        MatchUrlToResolve theMatchUrl,
1068                        Set<Long> theOutputSysAndValuePredicates,
1069                        Set<Long> theOutputValuePredicates) {
1070                if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) {
1071                        theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(
1072                                        myPartitionSettings,
1073                                        theRequestPartitionId,
1074                                        theMatchUrl.myResourceDefinition.getName(),
1075                                        theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
1076                                        theTokenParam.getSystem(),
1077                                        theTokenParam.getValue());
1078                        theOutputSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue);
1079                        return true;
1080                } else if (isNotBlank(theTokenParam.getValue())) {
1081                        theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue(
1082                                        myPartitionSettings,
1083                                        theRequestPartitionId,
1084                                        theMatchUrl.myResourceDefinition.getName(),
1085                                        theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
1086                                        theTokenParam.getValue());
1087                        theOutputValuePredicates.add(theMatchUrl.myHashValue);
1088                        return true;
1089                }
1090
1091                return false;
1092        }
1093
1094        private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap(
1095                        List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) {
1096                ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create();
1097                // Build a lookup map so we don't have to iterate over the searches repeatedly.
1098                for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) {
1099                        if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) {
1100                                hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap);
1101                        }
1102                        if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) {
1103                                hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap);
1104                        }
1105                }
1106                return hashToSearch;
1107        }
1108
1109        @Override
1110        protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) {
1111                try {
1112                        int insertionCount;
1113                        int updateCount;
1114                        SessionImpl session = myEntityManager.unwrap(SessionImpl.class);
1115                        if (session != null) {
1116                                insertionCount = session.getActionQueue().numberOfInsertions();
1117                                updateCount = session.getActionQueue().numberOfUpdates();
1118                        } else {
1119                                insertionCount = -1;
1120                                updateCount = -1;
1121                        }
1122
1123                        StopWatch sw = new StopWatch();
1124                        myEntityManager.flush();
1125                        ourLog.debug(
1126                                        "Session flush took {}ms for {} inserts and {} updates",
1127                                        sw.getMillis(),
1128                                        insertionCount,
1129                                        updateCount);
1130                } catch (PersistenceException e) {
1131                        if (myHapiFhirHibernateJpaDialect != null) {
1132                                String transactionTypes = createDescriptionOfResourceTypesInBundle(theIdToPersistedOutcome);
1133                                String message = "Error flushing transaction with resource types: " + transactionTypes;
1134                                throw myHapiFhirHibernateJpaDialect.translate(e, message);
1135                        }
1136                        throw e;
1137                }
1138        }
1139
1140        @VisibleForTesting
1141        public void setIdHelperServiceForUnitTest(IIdHelperService<JpaPid> theIdHelperService) {
1142                myIdHelperService = theIdHelperService;
1143        }
1144
1145        @VisibleForTesting
1146        public void setApplicationContextForUnitTest(ApplicationContext theAppCtx) {
1147                myApplicationContext = theAppCtx;
1148        }
1149
1150        /**
1151         * Creates a description of resource types in the provided bundle, indicating the types of resources
1152         * and their counts within the input map. This is intended only to be helpful for troubleshooting, since
1153         * it can be helpful to see details about the transaction which failed in the logs.
1154         * <p>
1155         * Example output: <code>[Patient (x3), Observation (x14)]</code>
1156         * </p>
1157         *
1158         * @param theIdToPersistedOutcome A map where the key is an {@code IIdType} object representing a resource ID
1159         *                                and the value is a {@code DaoMethodOutcome} object representing the outcome
1160         *                                of the persistence operation for that resource.
1161         * @return A string describing the resource types and their respective counts in a formatted list.
1162         */
1163        @Nonnull
1164        private static String createDescriptionOfResourceTypesInBundle(
1165                        Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) {
1166                TreeMap<String, Integer> types = new TreeMap<>();
1167                for (IIdType t : theIdToPersistedOutcome.keySet()) {
1168                        if (t != null) {
1169                                String resourceType = t.getResourceType();
1170                                int count = types.getOrDefault(resourceType, 0);
1171                                types.put(resourceType, count + 1);
1172                        }
1173                }
1174
1175                StringBuilder typesBuilder = new StringBuilder();
1176                typesBuilder.append("[");
1177                for (Iterator<Map.Entry<String, Integer>> iter = types.entrySet().iterator(); iter.hasNext(); ) {
1178                        Map.Entry<String, Integer> entry = iter.next();
1179                        typesBuilder.append(entry.getKey());
1180                        if (entry.getValue() > 1) {
1181                                typesBuilder.append(" (x").append(entry.getValue()).append(")");
1182                        }
1183                        if (iter.hasNext()) {
1184                                typesBuilder.append(", ");
1185                        }
1186                }
1187                typesBuilder.append("]");
1188                return typesBuilder.toString();
1189        }
1190
1191        public static class MatchUrlToResolve {
1192
1193                private final String myRequestUrl;
1194                private final SearchParameterMap myMatchUrlSearchMap;
1195                private final RuntimeResourceDefinition myResourceDefinition;
1196                private final boolean myShouldPreFetchResourceBody;
1197                private final boolean myShouldPreFetchResourceVersion;
1198                private final IBaseResource myAssociatedResource;
1199
1200                public boolean myResolved;
1201                private Long myHashValue;
1202                private Long myHashSystemAndValue;
1203
1204                public MatchUrlToResolve(
1205                                @Nonnull String theRequestUrl,
1206                                @Nullable IBaseResource theAssociatedResource,
1207                                @Nonnull SearchParameterMap theMatchUrlSearchMap,
1208                                @Nonnull RuntimeResourceDefinition theResourceDefinition,
1209                                boolean theShouldPreFetchResourceBody,
1210                                boolean theShouldPreFetchResourceVersion) {
1211                        Validate.notNull(theRequestUrl, "theRequestUrl must not be null");
1212                        Validate.notNull(theMatchUrlSearchMap, "theMatchUrlSearchMap must not be null");
1213                        Validate.notNull(theResourceDefinition, "theResourceDefinition must not be null");
1214                        myAssociatedResource = theAssociatedResource;
1215                        myRequestUrl = theRequestUrl;
1216                        myMatchUrlSearchMap = theMatchUrlSearchMap;
1217                        myResourceDefinition = theResourceDefinition;
1218                        myShouldPreFetchResourceBody = theShouldPreFetchResourceBody;
1219                        myShouldPreFetchResourceVersion = theShouldPreFetchResourceVersion;
1220                }
1221
1222                public IBaseResource getAssociatedResource() {
1223                        return myAssociatedResource;
1224                }
1225
1226                public void setResolved(boolean theResolved) {
1227                        myResolved = theResolved;
1228                }
1229        }
1230
1231        enum PrefetchReasonEnum {
1232                /**
1233                 * The ID is being prefetched because it is the ID in a resource reference
1234                 * within a resource being updated. In this case, we care whether the resource
1235                 * is deleted (since you can't reference a deleted resource), but we don't
1236                 * need to fetch the body since we don't actually care about its contents.
1237                 */
1238                REFERENCE_TARGET,
1239                /**
1240                 * The ID is being prefetched because it is the ID of a resource being
1241                 * updated directly by the transaction. In this case we don't care if it's
1242                 * deleted (since it's fine to update a deleted resource), and we do need
1243                 * to prefetch the current body so we can tell how it has changed.
1244                 */
1245                DIRECT_TARGET
1246        }
1247}