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