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