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