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