001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 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 org.apache.commons.lang3.Validate;
048import org.hibernate.internal.SessionImpl;
049import org.hl7.fhir.instance.model.api.IBase;
050import org.hl7.fhir.instance.model.api.IBaseBundle;
051import org.hl7.fhir.instance.model.api.IBaseResource;
052import org.hl7.fhir.instance.model.api.IIdType;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055import org.springframework.beans.factory.annotation.Autowired;
056import org.springframework.context.ApplicationContext;
057
058import java.util.ArrayList;
059import java.util.Collection;
060import java.util.HashSet;
061import java.util.IdentityHashMap;
062import java.util.List;
063import java.util.Map;
064import java.util.Set;
065import java.util.regex.Pattern;
066import java.util.stream.Collectors;
067import javax.annotation.Nullable;
068import javax.persistence.EntityManager;
069import javax.persistence.PersistenceContext;
070import javax.persistence.PersistenceContextType;
071import javax.persistence.PersistenceException;
072import javax.persistence.Tuple;
073import javax.persistence.TypedQuery;
074import javax.persistence.criteria.CriteriaBuilder;
075import javax.persistence.criteria.CriteriaQuery;
076import javax.persistence.criteria.Predicate;
077import javax.persistence.criteria.Root;
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(
371                                        from.get("myResourcePid").as(Long.class),
372                                        from.get(theIndexColumnName).as(Long.class));
373
374                        Predicate masterPredicate;
375                        if (theHashesForIndexColumn.size() == 1) {
376                                masterPredicate = cb.equal(
377                                                from.get(theIndexColumnName).as(Long.class),
378                                                theHashesForIndexColumn.iterator().next());
379                        } else {
380                                masterPredicate = from.get(theIndexColumnName).as(Long.class).in(theHashesForIndexColumn);
381                        }
382
383                        if (myPartitionSettings.isPartitioningEnabled()
384                                        && !myPartitionSettings.isIncludePartitionInSearchHashes()) {
385                                if (theRequestPartitionId.isDefaultPartition()) {
386                                        Predicate partitionIdCriteria =
387                                                        cb.isNull(from.get("myPartitionIdValue").as(Integer.class));
388                                        masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
389                                } else if (!theRequestPartitionId.isAllPartitions()) {
390                                        Predicate partitionIdCriteria = from.get("myPartitionIdValue")
391                                                        .as(Integer.class)
392                                                        .in(theRequestPartitionId.getPartitionIds());
393                                        masterPredicate = cb.and(partitionIdCriteria, masterPredicate);
394                                }
395                        }
396
397                        cq.where(masterPredicate);
398
399                        TypedQuery<Tuple> query = myEntityManager.createQuery(cq);
400
401                        /*
402                         * If we have 10 unique conditional URLs we're resolving, each one should
403                         * resolve to 0..1 resources if they are valid as conditional URLs. So we would
404                         * expect this query to return 0..10 rows, since conditional URLs for all
405                         * conditional operations except DELETE (which isn't being applied here) are
406                         * only allowed to resolve to 0..1 resources.
407                         *
408                         * If a conditional URL matches 2+ resources that is an error, and we'll
409                         * be throwing an exception below. This limit is here for safety just to
410                         * ensure that if someone uses a conditional URL that matches a million resources,
411                         * we don't do a super-expensive fetch.
412                         */
413                        query.setMaxResults(theHashesForIndexColumn.size() + 1);
414
415                        List<Tuple> results = query.getResultList();
416
417                        for (Tuple nextResult : results) {
418                                Long nextResourcePid = nextResult.get(0, Long.class);
419                                Long nextHash = nextResult.get(1, Long.class);
420                                List<MatchUrlToResolve> matchedSearch = hashToSearchMap.get(nextHash);
421                                matchedSearch.forEach(matchUrl -> {
422                                        ourLog.debug("Matched url {} from database", matchUrl.myRequestUrl);
423                                        if (matchUrl.myShouldPreFetchResourceBody) {
424                                                theOutputPidsToLoadFully.add(nextResourcePid);
425                                        }
426                                        myMatchResourceUrlService.matchUrlResolved(
427                                                        theTransactionDetails,
428                                                        matchUrl.myResourceDefinition.getName(),
429                                                        matchUrl.myRequestUrl,
430                                                        JpaPid.fromId(nextResourcePid));
431                                        theTransactionDetails.addResolvedMatchUrl(
432                                                        myFhirContext, matchUrl.myRequestUrl, JpaPid.fromId(nextResourcePid));
433                                        matchUrl.setResolved(true);
434                                });
435                        }
436                }
437        }
438
439        /**
440         * Note that if {@literal theShouldPreFetchResourceBody} is false, then we'll check if a given match
441         * URL resolves to a resource PID, but we won't actually try to load that resource. If we're resolving
442         * a match URL because it's there for a conditional update, we'll eagerly fetch the
443         * actual resource because we need to know its current state in order to update it. However, if
444         * the match URL is from an inline match URL in a resource body, we really only care about
445         * the PID and don't need the body so we don't load it. This does have a security implication, since
446         * it means that the {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_PRESHOW_RESOURCES} pointcut
447         * isn't fired even though the user has resolved the URL (meaning they may be able to test for
448         * the existence of a resource using a match URL). There is a test for this called
449         * {@literal testTransactionCreateInlineMatchUrlWithAuthorizationDenied()}. This security tradeoff
450         * is acceptable since we're only prefetching things with very simple match URLs (nothing with
451         * a reference in it for example) so it's not really possible to doing anything useful with this.
452         *
453         * @param theResourceType                       The resource type associated with the match URL (ie what resource type should it resolve to)
454         * @param theRequestUrl                         The actual match URL, which could be as simple as just parameters or could include the resource type too
455         * @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.
456         * @param theOutputIdsToPreFetch                This will be populated with any resource PIDs that need to be pre-fetched
457         * @param theOutputSearchParameterMapsToResolve This will be populated with any {@link SearchParameterMap} instances corresponding to match URLs we need to resolve
458         */
459        private void preFetchConditionalUrl(
460                        String theResourceType,
461                        String theRequestUrl,
462                        boolean theShouldPreFetchResourceBody,
463                        List<Long> theOutputIdsToPreFetch,
464                        List<MatchUrlToResolve> theOutputSearchParameterMapsToResolve) {
465                JpaPid cachedId = myMatchResourceUrlService.processMatchUrlUsingCacheOnly(theResourceType, theRequestUrl);
466                if (cachedId != null) {
467                        if (theShouldPreFetchResourceBody) {
468                                theOutputIdsToPreFetch.add(cachedId.getId());
469                        }
470                } else if (SINGLE_PARAMETER_MATCH_URL_PATTERN.matcher(theRequestUrl).matches()) {
471                        RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResourceType);
472                        SearchParameterMap matchUrlSearchMap =
473                                        myMatchUrlService.translateMatchUrl(theRequestUrl, resourceDefinition);
474                        theOutputSearchParameterMapsToResolve.add(new MatchUrlToResolve(
475                                        theRequestUrl, matchUrlSearchMap, resourceDefinition, theShouldPreFetchResourceBody));
476                }
477        }
478
479        private RequestPartitionId getSinglePartitionForAllEntriesOrNull(
480                        RequestDetails theRequest, List<IBase> theEntries, ITransactionProcessorVersionAdapter versionAdapter) {
481                RequestPartitionId retVal = null;
482                Set<RequestPartitionId> requestPartitionIdsForAllEntries = new HashSet<>();
483                for (IBase nextEntry : theEntries) {
484                        IBaseResource resource = versionAdapter.getResource(nextEntry);
485                        if (resource != null) {
486                                RequestPartitionId requestPartition = myRequestPartitionSvc.determineCreatePartitionForRequest(
487                                                theRequest, resource, myFhirContext.getResourceType(resource));
488                                requestPartitionIdsForAllEntries.add(requestPartition);
489                        }
490                }
491                if (requestPartitionIdsForAllEntries.size() == 1) {
492                        retVal = requestPartitionIdsForAllEntries.iterator().next();
493                }
494                return retVal;
495        }
496
497        /**
498         * Given a token parameter, build the query predicate based on its hash. Uses system and value if both are available, otherwise just value.
499         * If neither are available, it returns null.
500         */
501        @Nullable
502        private void buildHashPredicateFromTokenParam(
503                        TokenParam theTokenParam,
504                        RequestPartitionId theRequestPartitionId,
505                        MatchUrlToResolve theMatchUrl,
506                        Set<Long> theSysAndValuePredicates,
507                        Set<Long> theValuePredicates) {
508                if (isNotBlank(theTokenParam.getValue()) && isNotBlank(theTokenParam.getSystem())) {
509                        theMatchUrl.myHashSystemAndValue = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(
510                                        myPartitionSettings,
511                                        theRequestPartitionId,
512                                        theMatchUrl.myResourceDefinition.getName(),
513                                        theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
514                                        theTokenParam.getSystem(),
515                                        theTokenParam.getValue());
516                        theSysAndValuePredicates.add(theMatchUrl.myHashSystemAndValue);
517                } else if (isNotBlank(theTokenParam.getValue())) {
518                        theMatchUrl.myHashValue = ResourceIndexedSearchParamToken.calculateHashValue(
519                                        myPartitionSettings,
520                                        theRequestPartitionId,
521                                        theMatchUrl.myResourceDefinition.getName(),
522                                        theMatchUrl.myMatchUrlSearchMap.keySet().iterator().next(),
523                                        theTokenParam.getValue());
524                        theValuePredicates.add(theMatchUrl.myHashValue);
525                }
526        }
527
528        private ListMultimap<Long, MatchUrlToResolve> buildHashToSearchMap(
529                        List<MatchUrlToResolve> searchParameterMapsToResolve, String theIndex) {
530                ListMultimap<Long, MatchUrlToResolve> hashToSearch = ArrayListMultimap.create();
531                // Build a lookup map so we don't have to iterate over the searches repeatedly.
532                for (MatchUrlToResolve nextSearchParameterMap : searchParameterMapsToResolve) {
533                        if (nextSearchParameterMap.myHashSystemAndValue != null && theIndex.equals("myHashSystemAndValue")) {
534                                hashToSearch.put(nextSearchParameterMap.myHashSystemAndValue, nextSearchParameterMap);
535                        }
536                        if (nextSearchParameterMap.myHashValue != null && theIndex.equals("myHashValue")) {
537                                hashToSearch.put(nextSearchParameterMap.myHashValue, nextSearchParameterMap);
538                        }
539                }
540                return hashToSearch;
541        }
542
543        @Override
544        protected void flushSession(Map<IIdType, DaoMethodOutcome> theIdToPersistedOutcome) {
545                try {
546                        int insertionCount;
547                        int updateCount;
548                        SessionImpl session = myEntityManager.unwrap(SessionImpl.class);
549                        if (session != null) {
550                                insertionCount = session.getActionQueue().numberOfInsertions();
551                                updateCount = session.getActionQueue().numberOfUpdates();
552                        } else {
553                                insertionCount = -1;
554                                updateCount = -1;
555                        }
556
557                        StopWatch sw = new StopWatch();
558                        myEntityManager.flush();
559                        ourLog.debug(
560                                        "Session flush took {}ms for {} inserts and {} updates",
561                                        sw.getMillis(),
562                                        insertionCount,
563                                        updateCount);
564                } catch (PersistenceException e) {
565                        if (myHapiFhirHibernateJpaDialect != null) {
566                                List<String> types = theIdToPersistedOutcome.keySet().stream()
567                                                .filter(t -> t != null)
568                                                .map(t -> t.getResourceType())
569                                                .collect(Collectors.toList());
570                                String message = "Error flushing transaction with resource types: " + types;
571                                throw myHapiFhirHibernateJpaDialect.translate(e, message);
572                        }
573                        throw e;
574                }
575        }
576
577        @VisibleForTesting
578        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
579                myPartitionSettings = thePartitionSettings;
580        }
581
582        @VisibleForTesting
583        public void setIdHelperServiceForUnitTest(IIdHelperService theIdHelperService) {
584                myIdHelperService = theIdHelperService;
585        }
586
587        @VisibleForTesting
588        public void setApplicationContextForUnitTest(ApplicationContext theAppCtx) {
589                myApplicationContext = theAppCtx;
590        }
591
592        private static class MatchUrlToResolve {
593
594                private final String myRequestUrl;
595                private final SearchParameterMap myMatchUrlSearchMap;
596                private final RuntimeResourceDefinition myResourceDefinition;
597                private final boolean myShouldPreFetchResourceBody;
598                public boolean myResolved;
599                private Long myHashValue;
600                private Long myHashSystemAndValue;
601
602                public MatchUrlToResolve(
603                                String theRequestUrl,
604                                SearchParameterMap theMatchUrlSearchMap,
605                                RuntimeResourceDefinition theResourceDefinition,
606                                boolean theShouldPreFetchResourceBody) {
607                        myRequestUrl = theRequestUrl;
608                        myMatchUrlSearchMap = theMatchUrlSearchMap;
609                        myResourceDefinition = theResourceDefinition;
610                        myShouldPreFetchResourceBody = theShouldPreFetchResourceBody;
611                }
612
613                public void setResolved(boolean theResolved) {
614                        myResolved = theResolved;
615                }
616        }
617}