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.search.builder;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
024import ca.uhn.fhir.context.ComboSearchParamType;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.FhirVersionEnum;
027import ca.uhn.fhir.context.RuntimeResourceDefinition;
028import ca.uhn.fhir.context.RuntimeSearchParam;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.interceptor.api.HookParams;
031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
032import ca.uhn.fhir.interceptor.api.Pointcut;
033import ca.uhn.fhir.interceptor.model.RequestPartitionId;
034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
035import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
036import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
037import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
038import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
039import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
040import ca.uhn.fhir.jpa.dao.BaseStorageDao;
041import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
042import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser;
043import ca.uhn.fhir.jpa.dao.IResultIterator;
044import ca.uhn.fhir.jpa.dao.ISearchBuilder;
045import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
046import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTagDao;
047import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
048import ca.uhn.fhir.jpa.dao.search.ResourceNotFoundInIndexException;
049import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
050import ca.uhn.fhir.jpa.model.config.PartitionSettings;
051import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
052import ca.uhn.fhir.jpa.model.dao.JpaPid;
053import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
054import ca.uhn.fhir.jpa.model.entity.BaseTag;
055import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
056import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTag;
057import ca.uhn.fhir.jpa.model.entity.ResourceTag;
058import ca.uhn.fhir.jpa.model.search.SearchBuilderLoadIncludesParameters;
059import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
060import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
061import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc;
062import ca.uhn.fhir.jpa.search.SearchConstants;
063import ca.uhn.fhir.jpa.search.builder.models.ResolvedSearchQueryExecutor;
064import ca.uhn.fhir.jpa.search.builder.sql.GeneratedSql;
065import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
066import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryExecutor;
067import ca.uhn.fhir.jpa.search.builder.sql.SqlObjectFactory;
068import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
069import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
070import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper;
071import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
072import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
073import ca.uhn.fhir.jpa.util.BaseIterator;
074import ca.uhn.fhir.jpa.util.CartesianProductUtil;
075import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
076import ca.uhn.fhir.jpa.util.QueryChunker;
077import ca.uhn.fhir.jpa.util.SqlQueryList;
078import ca.uhn.fhir.model.api.IQueryParameterType;
079import ca.uhn.fhir.model.api.Include;
080import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
081import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
082import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
083import ca.uhn.fhir.rest.api.Constants;
084import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
085import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
086import ca.uhn.fhir.rest.api.SortOrderEnum;
087import ca.uhn.fhir.rest.api.SortSpec;
088import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
089import ca.uhn.fhir.rest.api.server.RequestDetails;
090import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId;
091import ca.uhn.fhir.rest.param.BaseParamWithPrefix;
092import ca.uhn.fhir.rest.param.DateParam;
093import ca.uhn.fhir.rest.param.DateRangeParam;
094import ca.uhn.fhir.rest.param.ParameterUtil;
095import ca.uhn.fhir.rest.param.ReferenceParam;
096import ca.uhn.fhir.rest.param.StringParam;
097import ca.uhn.fhir.rest.param.TokenParam;
098import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
099import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
100import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
101import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
102import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
103import ca.uhn.fhir.system.HapiSystemProperties;
104import ca.uhn.fhir.util.StopWatch;
105import ca.uhn.fhir.util.StringUtil;
106import ca.uhn.fhir.util.UrlUtil;
107import com.google.common.annotations.VisibleForTesting;
108import com.google.common.collect.ListMultimap;
109import com.google.common.collect.Lists;
110import com.google.common.collect.MultimapBuilder;
111import com.healthmarketscience.sqlbuilder.Condition;
112import jakarta.annotation.Nonnull;
113import jakarta.annotation.Nullable;
114import jakarta.persistence.EntityManager;
115import jakarta.persistence.PersistenceContext;
116import jakarta.persistence.PersistenceContextType;
117import jakarta.persistence.Query;
118import jakarta.persistence.Tuple;
119import jakarta.persistence.TypedQuery;
120import jakarta.persistence.criteria.CriteriaBuilder;
121import org.apache.commons.collections4.ListUtils;
122import org.apache.commons.lang3.StringUtils;
123import org.apache.commons.lang3.Validate;
124import org.apache.commons.lang3.math.NumberUtils;
125import org.apache.commons.lang3.tuple.Pair;
126import org.hl7.fhir.instance.model.api.IAnyResource;
127import org.hl7.fhir.instance.model.api.IBaseResource;
128import org.hl7.fhir.instance.model.api.IIdType;
129import org.slf4j.Logger;
130import org.slf4j.LoggerFactory;
131import org.springframework.beans.factory.annotation.Autowired;
132import org.springframework.jdbc.core.JdbcTemplate;
133import org.springframework.transaction.support.TransactionSynchronizationManager;
134
135import java.util.ArrayList;
136import java.util.Collection;
137import java.util.Collections;
138import java.util.Comparator;
139import java.util.HashMap;
140import java.util.HashSet;
141import java.util.Iterator;
142import java.util.LinkedList;
143import java.util.List;
144import java.util.Map;
145import java.util.Objects;
146import java.util.Set;
147import java.util.stream.Collectors;
148
149import static ca.uhn.fhir.jpa.model.util.JpaConstants.UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE;
150import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION;
151import static ca.uhn.fhir.jpa.search.builder.QueryStack.SearchForIdsParams.with;
152import static ca.uhn.fhir.jpa.util.InClauseNormalizer.normalizeIdListForInClause;
153import static java.util.Objects.requireNonNull;
154import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
155import static org.apache.commons.lang3.StringUtils.defaultString;
156import static org.apache.commons.lang3.StringUtils.isBlank;
157import static org.apache.commons.lang3.StringUtils.isNotBlank;
158
159/**
160 * The SearchBuilder is responsible for actually forming the SQL query that handles
161 * searches for resources
162 */
163public class SearchBuilder implements ISearchBuilder<JpaPid> {
164
165        /**
166         * See loadResourcesByPid
167         * for an explanation of why we use the constant 800
168         */
169        // NB: keep public
170        @Deprecated
171        public static final int MAXIMUM_PAGE_SIZE = SearchConstants.MAX_PAGE_SIZE;
172
173        public static final String RESOURCE_ID_ALIAS = "resource_id";
174        public static final String PARTITION_ID_ALIAS = "partition_id";
175        public static final String RESOURCE_VERSION_ALIAS = "resource_version";
176        private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
177        private static final JpaPid NO_MORE = JpaPid.fromId(-1L);
178        private static final String MY_SOURCE_RESOURCE_PID = "mySourceResourcePid";
179        private static final String MY_SOURCE_RESOURCE_PARTITION_ID = "myPartitionIdValue";
180        private static final String MY_SOURCE_RESOURCE_TYPE = "mySourceResourceType";
181        private static final String MY_TARGET_RESOURCE_PID = "myTargetResourcePid";
182        private static final String MY_TARGET_RESOURCE_PARTITION_ID = "myTargetResourcePartitionId";
183        private static final String MY_TARGET_RESOURCE_TYPE = "myTargetResourceType";
184        private static final String MY_TARGET_RESOURCE_VERSION = "myTargetResourceVersion";
185        public static final JpaPid[] EMPTY_JPA_PID_ARRAY = new JpaPid[0];
186        public static boolean myUseMaxPageSize50ForTest = false;
187        public static Integer myMaxPageSizeForTests = null;
188        protected final IInterceptorBroadcaster myInterceptorBroadcaster;
189        protected final IResourceTagDao myResourceTagDao;
190        private String myResourceName;
191        private final Class<? extends IBaseResource> myResourceType;
192        private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
193        private final SqlObjectFactory mySqlBuilderFactory;
194        private final HibernatePropertiesProvider myDialectProvider;
195        private final ISearchParamRegistry mySearchParamRegistry;
196        private final PartitionSettings myPartitionSettings;
197        private final DaoRegistry myDaoRegistry;
198        private final FhirContext myContext;
199        private final IIdHelperService<JpaPid> myIdHelperService;
200        private final JpaStorageSettings myStorageSettings;
201
202        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
203        protected EntityManager myEntityManager;
204
205        private CriteriaBuilder myCriteriaBuilder;
206        private SearchParameterMap myParams;
207        private String mySearchUuid;
208        private int myFetchSize;
209        private Integer myMaxResultsToFetch;
210        private Set<JpaPid> myPidSet;
211        private boolean myHasNextIteratorQuery = false;
212        private RequestPartitionId myRequestPartitionId;
213
214        @Autowired(required = false)
215        private IFulltextSearchSvc myFulltextSearchSvc;
216
217        @Autowired(required = false)
218        private IElasticsearchSvc myIElasticsearchSvc;
219
220        @Autowired
221        private IJpaStorageResourceParser myJpaStorageResourceParser;
222
223        @Autowired
224        private IResourceHistoryTableDao myResourceHistoryTableDao;
225
226        @Autowired
227        private IResourceHistoryTagDao myResourceHistoryTagDao;
228
229        @Autowired
230        private IRequestPartitionHelperSvc myPartitionHelperSvc;
231
232        /**
233         * Constructor
234         */
235        @SuppressWarnings({"rawtypes", "unchecked"})
236        public SearchBuilder(
237                        String theResourceName,
238                        JpaStorageSettings theStorageSettings,
239                        HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory,
240                        SqlObjectFactory theSqlBuilderFactory,
241                        HibernatePropertiesProvider theDialectProvider,
242                        ISearchParamRegistry theSearchParamRegistry,
243                        PartitionSettings thePartitionSettings,
244                        IInterceptorBroadcaster theInterceptorBroadcaster,
245                        IResourceTagDao theResourceTagDao,
246                        DaoRegistry theDaoRegistry,
247                        FhirContext theContext,
248                        IIdHelperService theIdHelperService,
249                        Class<? extends IBaseResource> theResourceType) {
250                myResourceName = theResourceName;
251                myResourceType = theResourceType;
252                myStorageSettings = theStorageSettings;
253
254                myEntityManagerFactory = theEntityManagerFactory;
255                mySqlBuilderFactory = theSqlBuilderFactory;
256                myDialectProvider = theDialectProvider;
257                mySearchParamRegistry = theSearchParamRegistry;
258                myPartitionSettings = thePartitionSettings;
259                myInterceptorBroadcaster = theInterceptorBroadcaster;
260                myResourceTagDao = theResourceTagDao;
261                myDaoRegistry = theDaoRegistry;
262                myContext = theContext;
263                myIdHelperService = theIdHelperService;
264        }
265
266        @VisibleForTesting
267        void setResourceName(String theName) {
268                myResourceName = theName;
269        }
270
271        @Override
272        public void setMaxResultsToFetch(Integer theMaxResultsToFetch) {
273                myMaxResultsToFetch = theMaxResultsToFetch;
274        }
275
276        private void searchForIdsWithAndOr(
277                        SearchQueryBuilder theSearchSqlBuilder,
278                        QueryStack theQueryStack,
279                        @Nonnull SearchParameterMap theParams,
280                        RequestDetails theRequest) {
281                myParams = theParams;
282
283                // Remove any empty parameters
284                theParams.clean();
285
286                // For DSTU3, pull out near-distance first so when it comes time to evaluate near, we already know the distance
287                if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) {
288                        Dstu3DistanceHelper.setNearDistance(myResourceType, theParams);
289                }
290
291                // Attempt to lookup via composite unique key.
292                if (isCompositeUniqueSpCandidate()) {
293                        attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest);
294                }
295
296                // Handle _id and _tag last, since they can typically be tacked onto a different parameter
297                List<String> paramNames = myParams.keySet().stream()
298                                .filter(t -> !t.equals(IAnyResource.SP_RES_ID))
299                                .filter(t -> !t.equals(Constants.PARAM_TAG))
300                                .collect(Collectors.toList());
301                if (myParams.containsKey(IAnyResource.SP_RES_ID)) {
302                        paramNames.add(IAnyResource.SP_RES_ID);
303                }
304                if (myParams.containsKey(Constants.PARAM_TAG)) {
305                        paramNames.add(Constants.PARAM_TAG);
306                }
307
308                // Handle each parameter
309                for (String nextParamName : paramNames) {
310                        if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) {
311                                // Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by
312                                // Elasticsearch
313                                continue;
314                        }
315                        List<List<IQueryParameterType>> andOrParams = myParams.get(nextParamName);
316                        Condition predicate = theQueryStack.searchForIdsWithAndOr(with().setResourceName(myResourceName)
317                                        .setParamName(nextParamName)
318                                        .setAndOrParams(andOrParams)
319                                        .setRequest(theRequest)
320                                        .setRequestPartitionId(myRequestPartitionId));
321                        if (predicate != null) {
322                                theSearchSqlBuilder.addPredicate(predicate);
323                        }
324                }
325        }
326
327        /**
328         * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the
329         * parameters all have no modifiers.
330         */
331        private boolean isCompositeUniqueSpCandidate() {
332                return myStorageSettings.isUniqueIndexesEnabled() && myParams.getEverythingMode() == null;
333        }
334
335        @SuppressWarnings("ConstantConditions")
336        @Override
337        public Long createCountQuery(
338                        SearchParameterMap theParams,
339                        String theSearchUuid,
340                        RequestDetails theRequest,
341                        @Nonnull RequestPartitionId theRequestPartitionId) {
342
343                assert theRequestPartitionId != null;
344                assert TransactionSynchronizationManager.isActualTransactionActive();
345
346                init(theParams, theSearchUuid, theRequestPartitionId);
347
348                if (checkUseHibernateSearch()) {
349                        return myFulltextSearchSvc.count(myResourceName, theParams.clone());
350                }
351
352                List<ISearchQueryExecutor> queries = createQuery(theParams.clone(), null, null, null, true, theRequest, null);
353                if (queries.isEmpty()) {
354                        return 0L;
355                } else {
356                        JpaPid jpaPid = queries.get(0).next();
357                        return jpaPid.getId();
358                }
359        }
360
361        /**
362         * @param thePidSet May be null
363         */
364        @Override
365        public void setPreviouslyAddedResourcePids(@Nonnull List<JpaPid> thePidSet) {
366                myPidSet = new HashSet<>(thePidSet);
367        }
368
369        @SuppressWarnings("ConstantConditions")
370        @Override
371        public IResultIterator<JpaPid> createQuery(
372                        SearchParameterMap theParams,
373                        SearchRuntimeDetails theSearchRuntimeDetails,
374                        RequestDetails theRequest,
375                        @Nonnull RequestPartitionId theRequestPartitionId) {
376                assert theRequestPartitionId != null;
377                assert TransactionSynchronizationManager.isActualTransactionActive();
378
379                init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId);
380
381                if (myPidSet == null) {
382                        myPidSet = new HashSet<>();
383                }
384
385                return new QueryIterator(theSearchRuntimeDetails, theRequest);
386        }
387
388        private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) {
389                myCriteriaBuilder = myEntityManager.getCriteriaBuilder();
390                // we mutate the params.  Make a private copy.
391                myParams = theParams.clone();
392                mySearchUuid = theSearchUuid;
393                myRequestPartitionId = theRequestPartitionId;
394        }
395
396        private List<ISearchQueryExecutor> createQuery(
397                        SearchParameterMap theParams,
398                        SortSpec sort,
399                        Integer theOffset,
400                        Integer theMaximumResults,
401                        boolean theCountOnlyFlag,
402                        RequestDetails theRequest,
403                        SearchRuntimeDetails theSearchRuntimeDetails) {
404
405                ArrayList<ISearchQueryExecutor> queries = new ArrayList<>();
406
407                if (checkUseHibernateSearch()) {
408                        // we're going to run at least part of the search against the Fulltext service.
409
410                        // Ugh - we have two different return types for now
411                        ISearchQueryExecutor fulltextExecutor = null;
412                        List<JpaPid> fulltextMatchIds = null;
413                        int resultCount = 0;
414                        if (myParams.isLastN()) {
415                                fulltextMatchIds = executeLastNAgainstIndex(theMaximumResults);
416                                resultCount = fulltextMatchIds.size();
417                        } else if (myParams.getEverythingMode() != null) {
418                                fulltextMatchIds = queryHibernateSearchForEverythingPids(theRequest);
419                                resultCount = fulltextMatchIds.size();
420                        } else {
421                                // todo performance MB - some queries must intersect with JPA (e.g. they have a chain, or we haven't
422                                // enabled SP indexing).
423                                // and some queries don't need JPA.  We only need the scroll when we need to intersect with JPA.
424                                // It would be faster to have a non-scrolled search in this case, since creating the scroll requires
425                                // extra work in Elastic.
426                                // if (eligibleToSkipJPAQuery) fulltextExecutor = myFulltextSearchSvc.searchNotScrolled( ...
427
428                                // we might need to intersect with JPA.  So we might need to traverse ALL results from lucene, not just
429                                // a page.
430                                fulltextExecutor = myFulltextSearchSvc.searchScrolled(myResourceName, myParams, theRequest);
431                        }
432
433                        if (fulltextExecutor == null) {
434                                fulltextExecutor =
435                                                SearchQueryExecutors.from(fulltextMatchIds != null ? fulltextMatchIds : new ArrayList<>());
436                        }
437
438                        if (theSearchRuntimeDetails != null) {
439                                theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount);
440                                IInterceptorBroadcaster compositeBroadcaster =
441                                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
442                                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE)) {
443                                        HookParams params = new HookParams()
444                                                        .add(RequestDetails.class, theRequest)
445                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
446                                                        .add(SearchRuntimeDetails.class, theSearchRuntimeDetails);
447                                        compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, params);
448                                }
449                        }
450
451                        // can we skip the database entirely and return the pid list from here?
452                        boolean canSkipDatabase =
453                                        // if we processed an AND clause, and it returned nothing, then nothing can match.
454                                        !fulltextExecutor.hasNext()
455                                                        ||
456                                                        // Our hibernate search query doesn't respect partitions yet
457                                                        (!myPartitionSettings.isPartitioningEnabled()
458                                                                        &&
459                                                                        // were there AND terms left?  Then we still need the db.
460                                                                        theParams.isEmpty()
461                                                                        &&
462                                                                        // not every param is a param. :-(
463                                                                        theParams.getNearDistanceParam() == null
464                                                                        &&
465                                                                        // todo MB don't we support _lastUpdated and _offset now?
466                                                                        theParams.getLastUpdated() == null
467                                                                        && theParams.getEverythingMode() == null
468                                                                        && theParams.getOffset() == null);
469
470                        if (canSkipDatabase) {
471                                ourLog.trace("Query finished after HSearch.  Skip db query phase");
472                                if (theMaximumResults != null) {
473                                        fulltextExecutor = SearchQueryExecutors.limited(fulltextExecutor, theMaximumResults);
474                                }
475                                queries.add(fulltextExecutor);
476                        } else {
477                                ourLog.trace("Query needs db after HSearch.  Chunking.");
478                                // Finish the query in the database for the rest of the search parameters, sorting, partitioning, etc.
479                                // We break the pids into chunks that fit in the 1k limit for jdbc bind params.
480                                new QueryChunker<JpaPid>()
481                                                .chunk(
482                                                                fulltextExecutor,
483                                                                SearchBuilder.getMaximumPageSize(),
484                                                                // for each list of (SearchBuilder.getMaximumPageSize())
485                                                                // we create a chunked query and add it to 'queries'
486                                                                t -> doCreateChunkedQueries(
487                                                                                theParams, t, theOffset, sort, theCountOnlyFlag, theRequest, queries));
488                        }
489                } else {
490                        // do everything in the database.
491                        createChunkedQuery(
492                                        theParams, sort, theOffset, theMaximumResults, theCountOnlyFlag, theRequest, null, queries);
493                }
494
495                return queries;
496        }
497
498        /**
499         * Check to see if query should use Hibernate Search, and error if the query can't continue.
500         *
501         * @return true if the query should first be processed by Hibernate Search
502         * @throws InvalidRequestException if fulltext search is not enabled but the query requires it - _content or _text
503         */
504        private boolean checkUseHibernateSearch() {
505                boolean fulltextEnabled = (myFulltextSearchSvc != null) && !myFulltextSearchSvc.isDisabled();
506
507                if (!fulltextEnabled) {
508                        failIfUsed(Constants.PARAM_TEXT);
509                        failIfUsed(Constants.PARAM_CONTENT);
510                } else {
511                        for (SortSpec sortSpec : myParams.getAllChainsInOrder()) {
512                                final String paramName = sortSpec.getParamName();
513                                if (paramName.contains(".")) {
514                                        failIfUsedWithChainedSort(Constants.PARAM_TEXT);
515                                        failIfUsedWithChainedSort(Constants.PARAM_CONTENT);
516                                }
517                        }
518                }
519
520                // someday we'll want a query planner to figure out if we _should_ or _must_ use the ft index, not just if we
521                // can.
522                return fulltextEnabled
523                                && myParams != null
524                                && myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
525                                && myFulltextSearchSvc.canUseHibernateSearch(myResourceName, myParams)
526                                && myFulltextSearchSvc.supportsAllSortTerms(myResourceName, myParams);
527        }
528
529        private void failIfUsed(String theParamName) {
530                if (myParams.containsKey(theParamName)) {
531                        throw new InvalidRequestException(Msg.code(1192)
532                                        + "Fulltext search is not enabled on this service, can not process parameter: " + theParamName);
533                }
534        }
535
536        private void failIfUsedWithChainedSort(String theParamName) {
537                if (myParams.containsKey(theParamName)) {
538                        throw new InvalidRequestException(Msg.code(2524)
539                                        + "Fulltext search combined with chained sorts are not supported, can not process parameter: "
540                                        + theParamName);
541                }
542        }
543
544        private List<JpaPid> executeLastNAgainstIndex(Integer theMaximumResults) {
545                // Can we use our hibernate search generated index on resource to support lastN?:
546                if (myStorageSettings.isAdvancedHSearchIndexing()) {
547                        if (myFulltextSearchSvc == null) {
548                                throw new InvalidRequestException(Msg.code(2027)
549                                                + "LastN operation is not enabled on this service, can not process this request");
550                        }
551                        List<IResourcePersistentId> persistentIds = myFulltextSearchSvc.lastN(myParams, theMaximumResults);
552                        return persistentIds.stream().map(t -> (JpaPid) t).collect(Collectors.toList());
553                } else {
554                        throw new InvalidRequestException(
555                                        Msg.code(2033) + "LastN operation is not enabled on this service, can not process this request");
556                }
557        }
558
559        private List<JpaPid> queryHibernateSearchForEverythingPids(RequestDetails theRequestDetails) {
560                JpaPid pid = null;
561                if (myParams.get(IAnyResource.SP_RES_ID) != null) {
562                        String idParamValue;
563                        IQueryParameterType idParam =
564                                        myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
565                        if (idParam instanceof TokenParam) {
566                                TokenParam idParm = (TokenParam) idParam;
567                                idParamValue = idParm.getValue();
568                        } else {
569                                StringParam idParm = (StringParam) idParam;
570                                idParamValue = idParm.getValue();
571                        }
572
573                        pid = myIdHelperService
574                                        .resolveResourceIdentity(
575                                                        myRequestPartitionId,
576                                                        myResourceName,
577                                                        idParamValue,
578                                                        ResolveIdentityMode.includeDeleted().cacheOk())
579                                        .getPersistentId();
580                }
581                return myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails);
582        }
583
584        private void doCreateChunkedQueries(
585                        SearchParameterMap theParams,
586                        List<JpaPid> thePids,
587                        Integer theOffset,
588                        SortSpec sort,
589                        boolean theCount,
590                        RequestDetails theRequest,
591                        ArrayList<ISearchQueryExecutor> theQueries) {
592
593                if (thePids.size() < getMaximumPageSize()) {
594                        thePids = normalizeIdListForInClause(thePids);
595                }
596                createChunkedQuery(theParams, sort, theOffset, thePids.size(), theCount, theRequest, thePids, theQueries);
597        }
598
599        /**
600         * Combs through the params for any _id parameters and extracts the PIDs for them
601         */
602        private void extractTargetPidsFromIdParams(Set<JpaPid> theTargetPids) {
603                // get all the IQueryParameterType objects
604                // for _id -> these should all be StringParam values
605                HashSet<IIdType> ids = new HashSet<>();
606                List<List<IQueryParameterType>> params = myParams.get(IAnyResource.SP_RES_ID);
607                for (List<IQueryParameterType> paramList : params) {
608                        for (IQueryParameterType param : paramList) {
609                                String id;
610                                if (param instanceof StringParam) {
611                                        // we expect all _id values to be StringParams
612                                        id = ((StringParam) param).getValue();
613                                } else if (param instanceof TokenParam) {
614                                        id = ((TokenParam) param).getValue();
615                                } else {
616                                        // we do not expect the _id parameter to be a non-string value
617                                        throw new IllegalArgumentException(
618                                                        Msg.code(1193) + "_id parameter must be a StringParam or TokenParam");
619                                }
620
621                                IIdType idType = myContext.getVersion().newIdType();
622                                if (id.contains("/")) {
623                                        idType.setValue(id);
624                                } else {
625                                        idType.setValue(myResourceName + "/" + id);
626                                }
627                                ids.add(idType);
628                        }
629                }
630
631                // fetch our target Pids
632                // this will throw if an id is not found
633                Map<IIdType, IResourceLookup<JpaPid>> idToIdentity = myIdHelperService.resolveResourceIdentities(
634                                myRequestPartitionId,
635                                new ArrayList<>(ids),
636                                ResolveIdentityMode.failOnDeleted().noCacheUnlessDeletesDisabled());
637
638                // add the pids to targetPids
639                for (IResourceLookup<JpaPid> pid : idToIdentity.values()) {
640                        theTargetPids.add(pid.getPersistentId());
641                }
642        }
643
644        private void createChunkedQuery(
645                        SearchParameterMap theParams,
646                        SortSpec sort,
647                        Integer theOffset,
648                        Integer theMaximumResults,
649                        boolean theCountOnlyFlag,
650                        RequestDetails theRequest,
651                        List<JpaPid> thePidList,
652                        List<ISearchQueryExecutor> theSearchQueryExecutors) {
653                if (myParams.getEverythingMode() != null) {
654                        createChunkedQueryForEverythingSearch(
655                                        theRequest,
656                                        theParams,
657                                        theOffset,
658                                        theMaximumResults,
659                                        theCountOnlyFlag,
660                                        thePidList,
661                                        theSearchQueryExecutors);
662                } else {
663                        createChunkedQueryNormalSearch(
664                                        theParams, sort, theOffset, theCountOnlyFlag, theRequest, thePidList, theSearchQueryExecutors);
665                }
666        }
667
668        private void createChunkedQueryNormalSearch(
669                        SearchParameterMap theParams,
670                        SortSpec sort,
671                        Integer theOffset,
672                        boolean theCountOnlyFlag,
673                        RequestDetails theRequest,
674                        List<JpaPid> thePidList,
675                        List<ISearchQueryExecutor> theSearchQueryExecutors) {
676                SearchQueryBuilder sqlBuilder = new SearchQueryBuilder(
677                                myContext,
678                                myStorageSettings,
679                                myPartitionSettings,
680                                myRequestPartitionId,
681                                myResourceName,
682                                mySqlBuilderFactory,
683                                myDialectProvider,
684                                theCountOnlyFlag);
685                QueryStack queryStack3 = new QueryStack(
686                                theRequest,
687                                theParams,
688                                myStorageSettings,
689                                myContext,
690                                sqlBuilder,
691                                mySearchParamRegistry,
692                                myPartitionSettings);
693
694                if (theParams.keySet().size() > 1
695                                || theParams.getSort() != null
696                                || theParams.keySet().contains(Constants.PARAM_HAS)
697                                || isPotentiallyContainedReferenceParameterExistsAtRoot(theParams)) {
698                        List<RuntimeSearchParam> activeComboParams = mySearchParamRegistry.getActiveComboSearchParams(
699                                        myResourceName, theParams.keySet(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
700                        if (activeComboParams.isEmpty()) {
701                                sqlBuilder.setNeedResourceTableRoot(true);
702                        }
703                }
704
705                /*
706                 * If we're doing a filter, always use the resource table as the root - This avoids the possibility of
707                 * specific filters with ORs as their root from working around the natural resource type / deletion
708                 * status / partition IDs built into queries.
709                 */
710                if (theParams.containsKey(Constants.PARAM_FILTER)) {
711                        Condition partitionIdPredicate = sqlBuilder
712                                        .getOrCreateResourceTablePredicateBuilder()
713                                        .createPartitionIdPredicate(myRequestPartitionId);
714                        if (partitionIdPredicate != null) {
715                                sqlBuilder.addPredicate(partitionIdPredicate);
716                        }
717                }
718
719                // Normal search
720                searchForIdsWithAndOr(sqlBuilder, queryStack3, myParams, theRequest);
721
722                // If we haven't added any predicates yet, we're doing a search for all resources. Make sure we add the
723                // partition ID predicate in that case.
724                if (!sqlBuilder.haveAtLeastOnePredicate()) {
725                        Condition partitionIdPredicate = sqlBuilder
726                                        .getOrCreateResourceTablePredicateBuilder()
727                                        .createPartitionIdPredicate(myRequestPartitionId);
728                        if (partitionIdPredicate != null) {
729                                sqlBuilder.addPredicate(partitionIdPredicate);
730                        }
731                }
732
733                // Add PID list predicate for full text search and/or lastn operation
734                addPidListPredicate(thePidList, sqlBuilder);
735
736                // Last updated
737                addLastUpdatePredicate(sqlBuilder);
738
739                /*
740                 * Exclude the pids already in the previous iterator. This is an optimization, as opposed
741                 * to something needed to guarantee correct results.
742                 *
743                 * Why do we need it? Suppose for example, a query like:
744                 *    Observation?category=foo,bar,baz
745                 * And suppose you have many resources that have all 3 of these category codes. In this case
746                 * the SQL query will probably return the same PIDs multiple times, and if this happens enough
747                 * we may exhaust the query results without getting enough distinct results back. When that
748                 * happens we re-run the query with a larger limit. Excluding results we already know about
749                 * tries to ensure that we get new unique results.
750                 *
751                 * The challenge with that though is that lots of DBs have an issue with too many
752                 * parameters in one query. So we only do this optimization if there aren't too
753                 * many results.
754                 */
755                if (myHasNextIteratorQuery) {
756                        if (myPidSet.size() + sqlBuilder.countBindVariables() < 900) {
757                                sqlBuilder.excludeResourceIdsPredicate(myPidSet);
758                        }
759                }
760
761                /*
762                 * If offset is present, we want deduplicate the results by using GROUP BY
763                 */
764                if (theOffset != null) {
765                        queryStack3.addGrouping();
766                        queryStack3.setUseAggregate(true);
767                }
768
769                /*
770                 * Sort
771                 *
772                 * If we have a sort, we wrap the criteria search (the search that actually
773                 * finds the appropriate resources) in an outer search which is then sorted
774                 */
775                if (sort != null) {
776                        assert !theCountOnlyFlag;
777
778                        createSort(queryStack3, sort, theParams);
779                }
780
781                /*
782                 * Now perform the search
783                 */
784                executeSearch(theOffset, theSearchQueryExecutors, sqlBuilder);
785        }
786
787        private void executeSearch(
788                        Integer theOffset, List<ISearchQueryExecutor> theSearchQueryExecutors, SearchQueryBuilder sqlBuilder) {
789                GeneratedSql generatedSql = sqlBuilder.generate(theOffset, myMaxResultsToFetch);
790                if (!generatedSql.isMatchNothing()) {
791                        SearchQueryExecutor executor =
792                                        mySqlBuilderFactory.newSearchQueryExecutor(generatedSql, myMaxResultsToFetch);
793                        theSearchQueryExecutors.add(executor);
794                }
795        }
796
797        private void createChunkedQueryForEverythingSearch(
798                        RequestDetails theRequest,
799                        SearchParameterMap theParams,
800                        Integer theOffset,
801                        Integer theMaximumResults,
802                        boolean theCountOnlyFlag,
803                        List<JpaPid> thePidList,
804                        List<ISearchQueryExecutor> theSearchQueryExecutors) {
805
806                SearchQueryBuilder sqlBuilder = new SearchQueryBuilder(
807                                myContext,
808                                myStorageSettings,
809                                myPartitionSettings,
810                                myRequestPartitionId,
811                                null,
812                                mySqlBuilderFactory,
813                                myDialectProvider,
814                                theCountOnlyFlag);
815
816                QueryStack queryStack3 = new QueryStack(
817                                theRequest,
818                                theParams,
819                                myStorageSettings,
820                                myContext,
821                                sqlBuilder,
822                                mySearchParamRegistry,
823                                myPartitionSettings);
824
825                JdbcTemplate jdbcTemplate = initializeJdbcTemplate(theMaximumResults);
826
827                Set<JpaPid> targetPids = new HashSet<>();
828                if (myParams.get(IAnyResource.SP_RES_ID) != null) {
829
830                        extractTargetPidsFromIdParams(targetPids);
831
832                        // add the target pids to our executors as the first
833                        // results iterator to go through
834                        theSearchQueryExecutors.add(new ResolvedSearchQueryExecutor(new ArrayList<>(targetPids)));
835                } else {
836                        // For Everything queries, we make the query root by the ResourceLink table, since this query
837                        // is basically a reverse-include search. For type/Everything (as opposed to instance/Everything)
838                        // the one problem with this approach is that it doesn't catch Patients that have absolutely
839                        // nothing linked to them. So we do one additional query to make sure we catch those too.
840                        SearchQueryBuilder fetchPidsSqlBuilder = new SearchQueryBuilder(
841                                        myContext,
842                                        myStorageSettings,
843                                        myPartitionSettings,
844                                        myRequestPartitionId,
845                                        myResourceName,
846                                        mySqlBuilderFactory,
847                                        myDialectProvider,
848                                        theCountOnlyFlag);
849                        GeneratedSql allTargetsSql = fetchPidsSqlBuilder.generate(theOffset, myMaxResultsToFetch);
850                        String sql = allTargetsSql.getSql();
851                        Object[] args = allTargetsSql.getBindVariables().toArray(new Object[0]);
852
853                        List<JpaPid> output =
854                                        jdbcTemplate.query(sql, args, new JpaPidRowMapper(myPartitionSettings.isPartitioningEnabled()));
855
856                        // we add a search executor to fetch unlinked patients first
857                        theSearchQueryExecutors.add(new ResolvedSearchQueryExecutor(output));
858                }
859
860                List<String> typeSourceResources = new ArrayList<>();
861                if (myParams.get(Constants.PARAM_TYPE) != null) {
862                        typeSourceResources.addAll(extractTypeSourceResourcesFromParams());
863                }
864
865                queryStack3.addPredicateEverythingOperation(
866                                myResourceName, typeSourceResources, targetPids.toArray(EMPTY_JPA_PID_ARRAY));
867
868                // Add PID list predicate for full text search and/or lastn operation
869                addPidListPredicate(thePidList, sqlBuilder);
870
871                /*
872                 * If offset is present, we want deduplicate the results by using GROUP BY
873                 * ORDER BY is required to make sure we return unique results for each page
874                 */
875                if (theOffset != null) {
876                        queryStack3.addGrouping();
877                        queryStack3.addOrdering();
878                        queryStack3.setUseAggregate(true);
879                }
880
881                /*
882                 * Now perform the search
883                 */
884                executeSearch(theOffset, theSearchQueryExecutors, sqlBuilder);
885        }
886
887        private void addPidListPredicate(List<JpaPid> thePidList, SearchQueryBuilder theSqlBuilder) {
888                if (thePidList != null && !thePidList.isEmpty()) {
889                        theSqlBuilder.addResourceIdsPredicate(thePidList);
890                }
891        }
892
893        private void addLastUpdatePredicate(SearchQueryBuilder theSqlBuilder) {
894                DateRangeParam lu = myParams.getLastUpdated();
895                if (lu != null && !lu.isEmpty()) {
896                        Condition lastUpdatedPredicates = theSqlBuilder.addPredicateLastUpdated(lu);
897                        theSqlBuilder.addPredicate(lastUpdatedPredicates);
898                }
899        }
900
901        private JdbcTemplate initializeJdbcTemplate(Integer theMaximumResults) {
902                JdbcTemplate jdbcTemplate = new JdbcTemplate(myEntityManagerFactory.getDataSource());
903                jdbcTemplate.setFetchSize(myFetchSize);
904                if (theMaximumResults != null) {
905                        jdbcTemplate.setMaxRows(theMaximumResults);
906                }
907                return jdbcTemplate;
908        }
909
910        private Collection<String> extractTypeSourceResourcesFromParams() {
911
912                List<List<IQueryParameterType>> listOfList = myParams.get(Constants.PARAM_TYPE);
913
914                // first off, let's flatten the list of list
915                List<IQueryParameterType> iQueryParameterTypesList =
916                                listOfList.stream().flatMap(List::stream).collect(Collectors.toList());
917
918                // then, extract all elements of each CSV into one big list
919                List<String> resourceTypes = iQueryParameterTypesList.stream()
920                                .map(param -> ((StringParam) param).getValue())
921                                .map(csvString -> List.of(csvString.split(",")))
922                                .flatMap(List::stream)
923                                .collect(Collectors.toList());
924
925                Set<String> knownResourceTypes = myContext.getResourceTypes();
926
927                // remove leading/trailing whitespaces if any and remove duplicates
928                Set<String> retVal = new HashSet<>();
929
930                for (String type : resourceTypes) {
931                        String trimmed = type.trim();
932                        if (!knownResourceTypes.contains(trimmed)) {
933                                throw new ResourceNotFoundException(
934                                                Msg.code(2197) + "Unknown resource type '" + trimmed + "' in _type parameter.");
935                        }
936                        retVal.add(trimmed);
937                }
938
939                return retVal;
940        }
941
942        private boolean isPotentiallyContainedReferenceParameterExistsAtRoot(SearchParameterMap theParams) {
943                return myStorageSettings.isIndexOnContainedResources()
944                                && theParams.values().stream()
945                                                .flatMap(Collection::stream)
946                                                .flatMap(Collection::stream)
947                                                .anyMatch(ReferenceParam.class::isInstance);
948        }
949
950        private void createSort(QueryStack theQueryStack, SortSpec theSort, SearchParameterMap theParams) {
951                if (theSort == null || isBlank(theSort.getParamName())) {
952                        return;
953                }
954
955                boolean ascending = (theSort.getOrder() == null) || (theSort.getOrder() == SortOrderEnum.ASC);
956
957                if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) {
958
959                        theQueryStack.addSortOnResourceId(ascending);
960
961                } else if (Constants.PARAM_PID.equals(theSort.getParamName())) {
962
963                        theQueryStack.addSortOnResourcePID(ascending);
964
965                } else if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) {
966
967                        theQueryStack.addSortOnLastUpdated(ascending);
968
969                } else {
970                        RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(
971                                        myResourceName, theSort.getParamName(), ISearchParamRegistry.SearchParamLookupContextEnum.SORT);
972
973                        /*
974                         * If we have a sort like _sort=subject.name and we  have an
975                         * uplifted refchain for that combination we can do it more efficiently
976                         * by using the index associated with the uplifted refchain. In this case,
977                         * we need to find the actual target search parameter (corresponding
978                         * to "name" in this example) so that we know what datatype it is.
979                         */
980                        String paramName = theSort.getParamName();
981                        if (param == null && myStorageSettings.isIndexOnUpliftedRefchains()) {
982                                String[] chains = StringUtils.split(paramName, '.');
983                                if (chains.length == 2) {
984
985                                        // Given: Encounter?_sort=Patient:subject.name
986                                        String referenceParam = chains[0]; // subject
987                                        String referenceParamTargetType = null; // Patient
988                                        String targetParam = chains[1]; // name
989
990                                        int colonIdx = referenceParam.indexOf(':');
991                                        if (colonIdx > -1) {
992                                                referenceParamTargetType = referenceParam.substring(0, colonIdx);
993                                                referenceParam = referenceParam.substring(colonIdx + 1);
994                                        }
995                                        RuntimeSearchParam outerParam = mySearchParamRegistry.getActiveSearchParam(
996                                                        myResourceName, referenceParam, ISearchParamRegistry.SearchParamLookupContextEnum.SORT);
997                                        if (outerParam == null) {
998                                                throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam);
999                                        } else if (outerParam.hasUpliftRefchain(targetParam)) {
1000                                                for (String nextTargetType : outerParam.getTargets()) {
1001                                                        if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) {
1002                                                                continue;
1003                                                        }
1004                                                        RuntimeSearchParam innerParam = mySearchParamRegistry.getActiveSearchParam(
1005                                                                        nextTargetType,
1006                                                                        targetParam,
1007                                                                        ISearchParamRegistry.SearchParamLookupContextEnum.SORT);
1008                                                        if (innerParam != null) {
1009                                                                param = innerParam;
1010                                                                break;
1011                                                        }
1012                                                }
1013                                        }
1014                                }
1015                        }
1016
1017                        int colonIdx = paramName.indexOf(':');
1018                        String referenceTargetType = null;
1019                        if (colonIdx > -1) {
1020                                referenceTargetType = paramName.substring(0, colonIdx);
1021                                paramName = paramName.substring(colonIdx + 1);
1022                        }
1023
1024                        int dotIdx = paramName.indexOf('.');
1025                        String chainName = null;
1026                        if (param == null && dotIdx > -1) {
1027                                chainName = paramName.substring(dotIdx + 1);
1028                                paramName = paramName.substring(0, dotIdx);
1029                                if (chainName.contains(".")) {
1030                                        String msg = myContext
1031                                                        .getLocalizer()
1032                                                        .getMessageSanitized(
1033                                                                        BaseStorageDao.class,
1034                                                                        "invalidSortParameterTooManyChains",
1035                                                                        paramName + "." + chainName);
1036                                        throw new InvalidRequestException(Msg.code(2286) + msg);
1037                                }
1038                        }
1039
1040                        if (param == null) {
1041                                param = mySearchParamRegistry.getActiveSearchParam(
1042                                                myResourceName, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SORT);
1043                        }
1044
1045                        if (param == null) {
1046                                throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName);
1047                        }
1048
1049                        // param will never be null here (the above line throws if it does)
1050                        // this is just to prevent the warning
1051                        assert param != null;
1052                        if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
1053                                throw new InvalidRequestException(
1054                                                Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter");
1055                        }
1056
1057                        switch (param.getParamType()) {
1058                                case STRING:
1059                                        theQueryStack.addSortOnString(myResourceName, paramName, ascending);
1060                                        break;
1061                                case DATE:
1062                                        theQueryStack.addSortOnDate(myResourceName, paramName, ascending);
1063                                        break;
1064                                case REFERENCE:
1065                                        theQueryStack.addSortOnResourceLink(
1066                                                        myResourceName, referenceTargetType, paramName, chainName, ascending, theParams);
1067                                        break;
1068                                case TOKEN:
1069                                        theQueryStack.addSortOnToken(myResourceName, paramName, ascending);
1070                                        break;
1071                                case NUMBER:
1072                                        theQueryStack.addSortOnNumber(myResourceName, paramName, ascending);
1073                                        break;
1074                                case URI:
1075                                        theQueryStack.addSortOnUri(myResourceName, paramName, ascending);
1076                                        break;
1077                                case QUANTITY:
1078                                        theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending);
1079                                        break;
1080                                case COMPOSITE:
1081                                        List<RuntimeSearchParam> compositeList =
1082                                                        JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param);
1083                                        if (compositeList == null) {
1084                                                throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName
1085                                                                + " is not defined by the resource " + myResourceName);
1086                                        }
1087                                        if (compositeList.size() != 2) {
1088                                                throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName
1089                                                                + " must have 2 composite types declared in parameter annotation, found "
1090                                                                + compositeList.size());
1091                                        }
1092                                        RuntimeSearchParam left = compositeList.get(0);
1093                                        RuntimeSearchParam right = compositeList.get(1);
1094
1095                                        createCompositeSort(theQueryStack, left.getParamType(), left.getName(), ascending);
1096                                        createCompositeSort(theQueryStack, right.getParamType(), right.getName(), ascending);
1097
1098                                        break;
1099                                case SPECIAL:
1100                                        if (LOCATION_POSITION.equals(param.getPath())) {
1101                                                theQueryStack.addSortOnCoordsNear(paramName, ascending, theParams);
1102                                                break;
1103                                        }
1104                                        throw new InvalidRequestException(
1105                                                        Msg.code(2306) + "This server does not support _sort specifications of type "
1106                                                                        + param.getParamType() + " - Can't serve _sort=" + paramName);
1107
1108                                case HAS:
1109                                default:
1110                                        throw new InvalidRequestException(
1111                                                        Msg.code(1197) + "This server does not support _sort specifications of type "
1112                                                                        + param.getParamType() + " - Can't serve _sort=" + paramName);
1113                        }
1114                }
1115
1116                // Recurse
1117                createSort(theQueryStack, theSort.getChain(), theParams);
1118        }
1119
1120        private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) {
1121                Collection<String> validSearchParameterNames = mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(
1122                                theResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SORT);
1123                String msg = myContext
1124                                .getLocalizer()
1125                                .getMessageSanitized(
1126                                                BaseStorageDao.class,
1127                                                "invalidSortParameter",
1128                                                theParamName,
1129                                                theResourceName,
1130                                                validSearchParameterNames);
1131                throw new InvalidRequestException(Msg.code(1194) + msg);
1132        }
1133
1134        private void createCompositeSort(
1135                        QueryStack theQueryStack,
1136                        RestSearchParameterTypeEnum theParamType,
1137                        String theParamName,
1138                        boolean theAscending) {
1139
1140                switch (theParamType) {
1141                        case STRING:
1142                                theQueryStack.addSortOnString(myResourceName, theParamName, theAscending);
1143                                break;
1144                        case DATE:
1145                                theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending);
1146                                break;
1147                        case TOKEN:
1148                                theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending);
1149                                break;
1150                        case QUANTITY:
1151                                theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending);
1152                                break;
1153                        case NUMBER:
1154                        case REFERENCE:
1155                        case COMPOSITE:
1156                        case URI:
1157                        case HAS:
1158                        case SPECIAL:
1159                        default:
1160                                throw new InvalidRequestException(
1161                                                Msg.code(1198) + "Don't know how to handle composite parameter with type of " + theParamType
1162                                                                + " on _sort=" + theParamName);
1163                }
1164        }
1165
1166        private void doLoadPids(
1167                        Collection<JpaPid> thePids,
1168                        Collection<JpaPid> theIncludedPids,
1169                        List<IBaseResource> theResourceListToPopulate,
1170                        boolean theForHistoryOperation,
1171                        Map<Long, Integer> thePosition) {
1172
1173                Map<JpaPid, Long> resourcePidToVersion = null;
1174                for (JpaPid next : thePids) {
1175                        if (next.getVersion() != null && myStorageSettings.isRespectVersionsForSearchIncludes()) {
1176                                if (resourcePidToVersion == null) {
1177                                        resourcePidToVersion = new HashMap<>();
1178                                }
1179                                resourcePidToVersion.put(next, next.getVersion());
1180                        }
1181                }
1182
1183                List<JpaPid> versionlessPids = new ArrayList<>(thePids);
1184                if (versionlessPids.size() < getMaximumPageSize()) {
1185                        versionlessPids = normalizeIdListForInClause(versionlessPids);
1186                }
1187
1188                // Load the resource bodies
1189                List<ResourceHistoryTable> resourceSearchViewList =
1190                                myResourceHistoryTableDao.findCurrentVersionsByResourcePidsAndFetchResourceTable(
1191                                                JpaPid.toLongList(versionlessPids));
1192
1193                /*
1194                 * If we have specific versions to load, replace the history entries with the
1195                 * correct ones
1196                 *
1197                 * TODO: this could definitely be made more efficient, probably by not loading the wrong
1198                 * version entity first, and by batching the fetches. But this is a fairly infrequently
1199                 * used feature, and loading history entities by PK is a very efficient query so it's
1200                 * not the end of the world
1201                 */
1202                if (resourcePidToVersion != null) {
1203                        for (int i = 0; i < resourceSearchViewList.size(); i++) {
1204                                ResourceHistoryTable next = resourceSearchViewList.get(i);
1205                                JpaPid resourceId = next.getPersistentId();
1206                                Long version = resourcePidToVersion.get(resourceId);
1207                                resourceId.setVersion(version);
1208                                if (version != null && !version.equals(next.getVersion())) {
1209                                        ResourceHistoryTable replacement =
1210                                                        myResourceHistoryTableDao.findForIdAndVersion(next.getResourceId(), version);
1211                                        resourceSearchViewList.set(i, replacement);
1212                                }
1213                        }
1214                }
1215
1216                // -- preload all tags with tag definition if any
1217                Map<JpaPid, Collection<BaseTag>> tagMap = getResourceTagMap(resourceSearchViewList);
1218
1219                for (ResourceHistoryTable next : resourceSearchViewList) {
1220                        if (next.getDeleted() != null) {
1221                                continue;
1222                        }
1223
1224                        Class<? extends IBaseResource> resourceType =
1225                                        myContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
1226
1227                        JpaPid resourceId = next.getPersistentId();
1228
1229                        if (resourcePidToVersion != null) {
1230                                Long version = resourcePidToVersion.get(resourceId);
1231                                resourceId.setVersion(version);
1232                        }
1233
1234                        IBaseResource resource = null;
1235                        if (next != null) {
1236                                resource = myJpaStorageResourceParser.toResource(
1237                                                resourceType, next, tagMap.get(JpaPid.fromId(next.getResourceId())), theForHistoryOperation);
1238                        }
1239                        if (resource == null) {
1240                                if (next != null) {
1241                                        ourLog.warn(
1242                                                        "Unable to find resource {}/{}/_history/{} in database",
1243                                                        next.getResourceType(),
1244                                                        next.getIdDt().getIdPart(),
1245                                                        next.getVersion());
1246                                } else {
1247                                        ourLog.warn("Unable to find resource in database.");
1248                                }
1249                                continue;
1250                        }
1251
1252                        Integer index = thePosition.get(resourceId.getId());
1253                        if (index == null) {
1254                                ourLog.warn("Got back unexpected resource PID {}", resourceId);
1255                                continue;
1256                        }
1257
1258                        if (theIncludedPids.contains(resourceId)) {
1259                                ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.INCLUDE);
1260                        } else {
1261                                ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.MATCH);
1262                        }
1263
1264                        theResourceListToPopulate.set(index, resource);
1265                }
1266        }
1267
1268        private Map<JpaPid, Collection<BaseTag>> getResourceTagMap(Collection<ResourceHistoryTable> theHistoryTables) {
1269
1270                switch (myStorageSettings.getTagStorageMode()) {
1271                        case VERSIONED:
1272                                return getPidToTagMapVersioned(theHistoryTables);
1273                        case NON_VERSIONED:
1274                                return getPidToTagMapUnversioned(theHistoryTables);
1275                        case INLINE:
1276                        default:
1277                                return Map.of();
1278                }
1279        }
1280
1281        @Nonnull
1282        private Map<JpaPid, Collection<BaseTag>> getPidToTagMapVersioned(
1283                        Collection<ResourceHistoryTable> theHistoryTables) {
1284                List<Long> idList = new ArrayList<>(theHistoryTables.size());
1285
1286                // -- find all resource has tags
1287                for (ResourceHistoryTable resource : theHistoryTables) {
1288                        if (resource.isHasTags()) {
1289                                idList.add(resource.getId());
1290                        }
1291                }
1292
1293                Map<JpaPid, Collection<BaseTag>> tagMap = new HashMap<>();
1294
1295                // -- no tags
1296                if (idList.isEmpty()) {
1297                        return tagMap;
1298                }
1299
1300                // -- get all tags for the idList
1301                Collection<ResourceHistoryTag> tagList = myResourceHistoryTagDao.findByVersionIds(idList);
1302
1303                // -- build the map, key = resourceId, value = list of ResourceTag
1304                JpaPid resourceId;
1305                Collection<BaseTag> tagCol;
1306                for (ResourceHistoryTag tag : tagList) {
1307
1308                        resourceId = JpaPid.fromId(tag.getResourceId());
1309                        tagCol = tagMap.get(resourceId);
1310                        if (tagCol == null) {
1311                                tagCol = new ArrayList<>();
1312                                tagCol.add(tag);
1313                                tagMap.put(resourceId, tagCol);
1314                        } else {
1315                                tagCol.add(tag);
1316                        }
1317                }
1318
1319                return tagMap;
1320        }
1321
1322        @Nonnull
1323        private Map<JpaPid, Collection<BaseTag>> getPidToTagMapUnversioned(
1324                        Collection<ResourceHistoryTable> theHistoryTables) {
1325                List<JpaPid> idList = new ArrayList<>(theHistoryTables.size());
1326
1327                // -- find all resource has tags
1328                for (ResourceHistoryTable resource : theHistoryTables) {
1329                        if (resource.isHasTags()) {
1330                                idList.add(JpaPid.fromId(resource.getResourceId()));
1331                        }
1332                }
1333
1334                Map<JpaPid, Collection<BaseTag>> tagMap = new HashMap<>();
1335
1336                // -- no tags
1337                if (idList.isEmpty()) {
1338                        return tagMap;
1339                }
1340
1341                // -- get all tags for the idList
1342                Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(JpaPid.toLongList(idList));
1343
1344                // -- build the map, key = resourceId, value = list of ResourceTag
1345                JpaPid resourceId;
1346                Collection<BaseTag> tagCol;
1347                for (ResourceTag tag : tagList) {
1348
1349                        resourceId = JpaPid.fromId(tag.getResourceId());
1350                        tagCol = tagMap.get(resourceId);
1351                        if (tagCol == null) {
1352                                tagCol = new ArrayList<>();
1353                                tagCol.add(tag);
1354                                tagMap.put(resourceId, tagCol);
1355                        } else {
1356                                tagCol.add(tag);
1357                        }
1358                }
1359
1360                return tagMap;
1361        }
1362
1363        @Override
1364        public void loadResourcesByPid(
1365                        Collection<JpaPid> thePids,
1366                        Collection<JpaPid> theIncludedPids,
1367                        List<IBaseResource> theResourceListToPopulate,
1368                        boolean theForHistoryOperation,
1369                        RequestDetails theDetails) {
1370                if (thePids.isEmpty()) {
1371                        ourLog.debug("The include pids are empty");
1372                }
1373
1374                // Dupes will cause a crash later anyhow, but this is expensive so only do it
1375                // when running asserts
1376                assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids;
1377
1378                Map<Long, Integer> position = new HashMap<>();
1379                for (JpaPid next : thePids) {
1380                        position.put(next.getId(), theResourceListToPopulate.size());
1381                        theResourceListToPopulate.add(null);
1382                }
1383
1384                // Can we fast track this loading by checking elastic search?
1385                if (isLoadingFromElasticSearchSupported(thePids)) {
1386                        try {
1387                                theResourceListToPopulate.addAll(loadResourcesFromElasticSearch(thePids));
1388                                return;
1389
1390                        } catch (ResourceNotFoundInIndexException theE) {
1391                                // some resources were not found in index, so we will inform this and resort to JPA search
1392                                ourLog.warn(
1393                                                "Some resources were not found in index. Make sure all resources were indexed. Resorting to database search.");
1394                        }
1395                }
1396
1397                // We only chunk because some jdbc drivers can't handle long param lists.
1398                new QueryChunker<JpaPid>()
1399                                .chunk(
1400                                                thePids,
1401                                                t -> doLoadPids(
1402                                                                t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position));
1403        }
1404
1405        /**
1406         * Check if we can load the resources from Hibernate Search instead of the database.
1407         * We assume this is faster.
1408         * <p>
1409         * Hibernate Search only stores the current version, and only if enabled.
1410         *
1411         * @param thePids the pids to check for versioned references
1412         * @return can we fetch from Hibernate Search?
1413         */
1414        private boolean isLoadingFromElasticSearchSupported(Collection<JpaPid> thePids) {
1415                // is storage enabled?
1416                return myStorageSettings.isStoreResourceInHSearchIndex()
1417                                && myStorageSettings.isAdvancedHSearchIndexing()
1418                                &&
1419                                // we don't support history
1420                                thePids.stream().noneMatch(p -> p.getVersion() != null)
1421                                &&
1422                                // skip the complexity for metadata in dstu2
1423                                myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3);
1424        }
1425
1426        private List<IBaseResource> loadResourcesFromElasticSearch(Collection<JpaPid> thePids) {
1427                // Do we use the fulltextsvc via hibernate-search to load resources or be backwards compatible with older ES
1428                // only impl
1429                // to handle lastN?
1430                if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) {
1431                        List<Long> pidList = thePids.stream().map(JpaPid::getId).collect(Collectors.toList());
1432
1433                        return myFulltextSearchSvc.getResources(pidList);
1434                } else if (!Objects.isNull(myParams) && myParams.isLastN()) {
1435                        // legacy LastN implementation
1436                        return myIElasticsearchSvc.getObservationResources(thePids);
1437                } else {
1438                        return Collections.emptyList();
1439                }
1440        }
1441
1442        /**
1443         * THIS SHOULD RETURN HASHSET and not just Set because we add to it later
1444         * so it can't be Collections.emptySet() or some such thing.
1445         * The JpaPid returned will have resource type populated.
1446         */
1447        @Override
1448        public Set<JpaPid> loadIncludes(
1449                        FhirContext theContext,
1450                        EntityManager theEntityManager,
1451                        Collection<JpaPid> theMatches,
1452                        Collection<Include> theIncludes,
1453                        boolean theReverseMode,
1454                        DateRangeParam theLastUpdated,
1455                        String theSearchIdOrDescription,
1456                        RequestDetails theRequest,
1457                        Integer theMaxCount) {
1458                SearchBuilderLoadIncludesParameters<JpaPid> parameters = new SearchBuilderLoadIncludesParameters<>();
1459                parameters.setFhirContext(theContext);
1460                parameters.setEntityManager(theEntityManager);
1461                parameters.setMatches(theMatches);
1462                parameters.setIncludeFilters(theIncludes);
1463                parameters.setReverseMode(theReverseMode);
1464                parameters.setLastUpdated(theLastUpdated);
1465                parameters.setSearchIdOrDescription(theSearchIdOrDescription);
1466                parameters.setRequestDetails(theRequest);
1467                parameters.setMaxCount(theMaxCount);
1468                return loadIncludes(parameters);
1469        }
1470
1471        @Override
1472        public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theParameters) {
1473                Collection<JpaPid> matches = theParameters.getMatches();
1474                Collection<Include> currentIncludes = theParameters.getIncludeFilters();
1475                boolean reverseMode = theParameters.isReverseMode();
1476                EntityManager entityManager = theParameters.getEntityManager();
1477                Integer maxCount = theParameters.getMaxCount();
1478                FhirContext fhirContext = theParameters.getFhirContext();
1479                RequestDetails request = theParameters.getRequestDetails();
1480                String searchIdOrDescription = theParameters.getSearchIdOrDescription();
1481                List<String> desiredResourceTypes = theParameters.getDesiredResourceTypes();
1482                boolean hasDesiredResourceTypes = desiredResourceTypes != null && !desiredResourceTypes.isEmpty();
1483                IInterceptorBroadcaster compositeBroadcaster =
1484                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, request);
1485
1486                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) {
1487                        CurrentThreadCaptureQueriesListener.startCapturing();
1488                }
1489                if (matches.isEmpty()) {
1490                        return new HashSet<>();
1491                }
1492                if (currentIncludes == null || currentIncludes.isEmpty()) {
1493                        return new HashSet<>();
1494                }
1495                String searchPidFieldName = reverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID;
1496                String searchPartitionIdFieldName =
1497                                reverseMode ? MY_TARGET_RESOURCE_PARTITION_ID : MY_SOURCE_RESOURCE_PARTITION_ID;
1498                String findPidFieldName = reverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID;
1499                String findPartitionIdFieldName =
1500                                reverseMode ? MY_SOURCE_RESOURCE_PARTITION_ID : MY_TARGET_RESOURCE_PARTITION_ID;
1501                String findResourceTypeFieldName = reverseMode ? MY_SOURCE_RESOURCE_TYPE : MY_TARGET_RESOURCE_TYPE;
1502                String findVersionFieldName = null;
1503                if (!reverseMode && myStorageSettings.isRespectVersionsForSearchIncludes()) {
1504                        findVersionFieldName = MY_TARGET_RESOURCE_VERSION;
1505                }
1506
1507                List<JpaPid> nextRoundMatches = new ArrayList<>(matches);
1508                HashSet<JpaPid> allAdded = new HashSet<>();
1509                HashSet<JpaPid> original = new HashSet<>(matches);
1510                ArrayList<Include> includes = new ArrayList<>(currentIncludes);
1511
1512                int roundCounts = 0;
1513                StopWatch w = new StopWatch();
1514
1515                boolean addedSomeThisRound;
1516                do {
1517                        roundCounts++;
1518
1519                        HashSet<JpaPid> pidsToInclude = new HashSet<>();
1520
1521                        for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
1522                                Include nextInclude = iter.next();
1523                                if (!nextInclude.isRecurse()) {
1524                                        iter.remove();
1525                                }
1526
1527                                // Account for _include=*
1528                                boolean matchAll = "*".equals(nextInclude.getValue());
1529
1530                                // Account for _include=[resourceType]:*
1531                                String wantResourceType = null;
1532                                if (!matchAll) {
1533                                        if ("*".equals(nextInclude.getParamName())) {
1534                                                wantResourceType = nextInclude.getParamType();
1535                                                matchAll = true;
1536                                        }
1537                                }
1538
1539                                if (matchAll) {
1540                                        loadIncludesMatchAll(
1541                                                        findPidFieldName,
1542                                                        findPartitionIdFieldName,
1543                                                        findResourceTypeFieldName,
1544                                                        findVersionFieldName,
1545                                                        searchPidFieldName,
1546                                                        searchPartitionIdFieldName,
1547                                                        wantResourceType,
1548                                                        reverseMode,
1549                                                        hasDesiredResourceTypes,
1550                                                        nextRoundMatches,
1551                                                        entityManager,
1552                                                        maxCount,
1553                                                        desiredResourceTypes,
1554                                                        pidsToInclude,
1555                                                        request);
1556                                } else {
1557                                        loadIncludesMatchSpecific(
1558                                                        nextInclude,
1559                                                        fhirContext,
1560                                                        findPidFieldName,
1561                                                        findPartitionIdFieldName,
1562                                                        findVersionFieldName,
1563                                                        searchPidFieldName,
1564                                                        searchPartitionIdFieldName,
1565                                                        reverseMode,
1566                                                        nextRoundMatches,
1567                                                        entityManager,
1568                                                        maxCount,
1569                                                        pidsToInclude,
1570                                                        request);
1571                                }
1572                        }
1573
1574                        nextRoundMatches.clear();
1575                        for (JpaPid next : pidsToInclude) {
1576                                if (!original.contains(next) && !allAdded.contains(next)) {
1577                                        nextRoundMatches.add(next);
1578                                }
1579                        }
1580
1581                        addedSomeThisRound = allAdded.addAll(pidsToInclude);
1582
1583                        if (maxCount != null && allAdded.size() >= maxCount) {
1584                                break;
1585                        }
1586
1587                } while (!includes.isEmpty() && !nextRoundMatches.isEmpty() && addedSomeThisRound);
1588
1589                allAdded.removeAll(original);
1590
1591                ourLog.info(
1592                                "Loaded {} {} in {} rounds and {} ms for search {}",
1593                                allAdded.size(),
1594                                reverseMode ? "_revincludes" : "_includes",
1595                                roundCounts,
1596                                w.getMillisAndRestart(),
1597                                searchIdOrDescription);
1598
1599                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL)) {
1600                        callRawSqlHookWithCurrentThreadQueries(request, compositeBroadcaster);
1601                }
1602
1603                // Interceptor call: STORAGE_PREACCESS_RESOURCES
1604                // This can be used to remove results from the search result details before
1605                // the user has a chance to know that they were in the results
1606                if (!allAdded.isEmpty()) {
1607
1608                        if (compositeBroadcaster.hasHooks(Pointcut.STORAGE_PREACCESS_RESOURCES)) {
1609                                List<JpaPid> includedPidList = new ArrayList<>(allAdded);
1610                                JpaPreResourceAccessDetails accessDetails =
1611                                                new JpaPreResourceAccessDetails(includedPidList, () -> this);
1612                                HookParams params = new HookParams()
1613                                                .add(IPreResourceAccessDetails.class, accessDetails)
1614                                                .add(RequestDetails.class, request)
1615                                                .addIfMatchesType(ServletRequestDetails.class, request);
1616                                compositeBroadcaster.callHooks(Pointcut.STORAGE_PREACCESS_RESOURCES, params);
1617
1618                                for (int i = includedPidList.size() - 1; i >= 0; i--) {
1619                                        if (accessDetails.isDontReturnResourceAtIndex(i)) {
1620                                                JpaPid value = includedPidList.remove(i);
1621                                                if (value != null) {
1622                                                        allAdded.remove(value);
1623                                                }
1624                                        }
1625                                }
1626                        }
1627                }
1628
1629                return allAdded;
1630        }
1631
1632        private void loadIncludesMatchSpecific(
1633                        Include nextInclude,
1634                        FhirContext fhirContext,
1635                        String findPidFieldName,
1636                        String findPartitionFieldName,
1637                        String findVersionFieldName,
1638                        String searchPidFieldName,
1639                        String searchPartitionFieldName,
1640                        boolean reverseMode,
1641                        List<JpaPid> nextRoundMatches,
1642                        EntityManager entityManager,
1643                        Integer maxCount,
1644                        HashSet<JpaPid> pidsToInclude,
1645                        RequestDetails theRequest) {
1646                List<String> paths;
1647
1648                // Start replace
1649                RuntimeSearchParam param;
1650                String resType = nextInclude.getParamType();
1651                if (isBlank(resType)) {
1652                        return;
1653                }
1654                RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
1655                if (def == null) {
1656                        ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
1657                        return;
1658                }
1659
1660                String paramName = nextInclude.getParamName();
1661                if (isNotBlank(paramName)) {
1662                        param = mySearchParamRegistry.getActiveSearchParam(
1663                                        resType, paramName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
1664                } else {
1665                        param = null;
1666                }
1667                if (param == null) {
1668                        ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
1669                        return;
1670                }
1671
1672                paths = param.getPathsSplitForResourceType(resType);
1673                // end replace
1674
1675                Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
1676
1677                for (String nextPath : paths) {
1678                        String findPidFieldSqlColumn =
1679                                        findPidFieldName.equals(MY_SOURCE_RESOURCE_PID) ? "src_resource_id" : "target_resource_id";
1680                        String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
1681                        if (findVersionFieldName != null) {
1682                                fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
1683                        }
1684                        if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1685                                fieldsToLoad += ", r.";
1686                                fieldsToLoad += findPartitionFieldName.equals(MY_SOURCE_RESOURCE_PARTITION_ID)
1687                                                ? "partition_id"
1688                                                : "target_res_partition_id";
1689                                fieldsToLoad += " as " + PARTITION_ID_ALIAS;
1690                        }
1691
1692                        // Query for includes lookup has 2 cases
1693                        // Case 1: Where target_resource_id is available in hfj_res_link table for local references
1694                        // Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
1695                        // url in target_resource_url
1696
1697                        // Case 1:
1698                        Map<String, Object> localReferenceQueryParams = new HashMap<>();
1699
1700                        String searchPidFieldSqlColumn =
1701                                        searchPidFieldName.equals(MY_TARGET_RESOURCE_PID) ? "target_resource_id" : "src_resource_id";
1702                        StringBuilder localReferenceQuery = new StringBuilder();
1703                        localReferenceQuery.append("SELECT ").append(fieldsToLoad);
1704                        localReferenceQuery.append(" FROM hfj_res_link r ");
1705                        localReferenceQuery.append("WHERE r.src_path = :src_path");
1706                        if (!"target_resource_id".equals(searchPidFieldSqlColumn)) {
1707                                localReferenceQuery.append(" AND r.target_resource_id IS NOT NULL");
1708                        }
1709                        localReferenceQuery
1710                                        .append(" AND r.")
1711                                        .append(searchPidFieldSqlColumn)
1712                                        .append(" IN (:target_pids) ");
1713                        if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1714                                String partitionFieldToSearch = findPartitionFieldName.equals(MY_SOURCE_RESOURCE_PARTITION_ID)
1715                                                ? "target_res_partition_id"
1716                                                : "partition_id";
1717                                localReferenceQuery
1718                                                .append("AND r.")
1719                                                .append(partitionFieldToSearch)
1720                                                .append(" = :search_partition_id ");
1721                        }
1722                        localReferenceQueryParams.put("src_path", nextPath);
1723                        // we loop over target_pids later.
1724                        if (targetResourceTypes != null) {
1725                                if (targetResourceTypes.size() == 1) {
1726                                        localReferenceQuery.append("AND r.target_resource_type = :target_resource_type ");
1727                                        localReferenceQueryParams.put(
1728                                                        "target_resource_type",
1729                                                        targetResourceTypes.iterator().next());
1730                                } else {
1731                                        localReferenceQuery.append("AND r.target_resource_type in (:target_resource_types) ");
1732                                        localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
1733                                }
1734                        }
1735
1736                        // Case 2:
1737                        Pair<String, Map<String, Object>> canonicalQuery =
1738                                        buildCanonicalUrlQuery(findVersionFieldName, targetResourceTypes, reverseMode, theRequest);
1739
1740                        String sql = localReferenceQuery + "UNION " + canonicalQuery.getLeft();
1741
1742                        Map<String, Object> limitParams = new HashMap<>();
1743                        if (maxCount != null) {
1744                                LinkedList<Object> bindVariables = new LinkedList<>();
1745                                sql = SearchQueryBuilder.applyLimitToSql(
1746                                                myDialectProvider.getDialect(), null, maxCount, sql, null, bindVariables);
1747
1748                                // The dialect SQL limiter uses positional params, but we're using
1749                                // named params here, so we need to replace the positional params
1750                                // with equivalent named ones
1751                                StringBuilder sb = new StringBuilder();
1752                                for (int i = 0; i < sql.length(); i++) {
1753                                        char nextChar = sql.charAt(i);
1754                                        if (nextChar == '?') {
1755                                                String nextName = "limit" + i;
1756                                                sb.append(':').append(nextName);
1757                                                limitParams.put(nextName, bindVariables.removeFirst());
1758                                        } else {
1759                                                sb.append(nextChar);
1760                                        }
1761                                }
1762                                sql = sb.toString();
1763                        }
1764
1765                        List<Collection<JpaPid>> partitions = partitionBySizeAndPartitionId(nextRoundMatches, getMaximumPageSize());
1766                        for (Collection<JpaPid> nextPartition : partitions) {
1767                                Query q = entityManager.createNativeQuery(sql, Tuple.class);
1768                                q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
1769                                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1770                                        q.setParameter(
1771                                                        "search_partition_id",
1772                                                        nextPartition.iterator().next().getPartitionId());
1773                                }
1774                                localReferenceQueryParams.forEach(q::setParameter);
1775                                canonicalQuery.getRight().forEach(q::setParameter);
1776                                limitParams.forEach(q::setParameter);
1777
1778                                @SuppressWarnings("unchecked")
1779                                List<Tuple> results = q.getResultList();
1780                                for (Tuple result : results) {
1781                                        if (result != null) {
1782                                                Long resourceId = NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
1783                                                Long resourceVersion = null;
1784                                                if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
1785                                                        resourceVersion =
1786                                                                        NumberUtils.createLong(String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
1787                                                }
1788                                                Integer partitionId = null;
1789                                                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1790                                                        partitionId = result.get(PARTITION_ID_ALIAS, Integer.class);
1791                                                }
1792
1793                                                JpaPid pid = JpaPid.fromIdAndVersion(resourceId, resourceVersion);
1794                                                pid.setPartitionId(partitionId);
1795                                                pidsToInclude.add(pid);
1796                                        }
1797                                }
1798                        }
1799                }
1800        }
1801
1802        private void loadIncludesMatchAll(
1803                        String findPidFieldName,
1804                        String findPartitionFieldName,
1805                        String findResourceTypeFieldName,
1806                        String findVersionFieldName,
1807                        String searchPidFieldName,
1808                        String searchPartitionFieldName,
1809                        String wantResourceType,
1810                        boolean reverseMode,
1811                        boolean hasDesiredResourceTypes,
1812                        List<JpaPid> nextRoundMatches,
1813                        EntityManager entityManager,
1814                        Integer maxCount,
1815                        List<String> desiredResourceTypes,
1816                        HashSet<JpaPid> pidsToInclude,
1817                        RequestDetails request) {
1818                StringBuilder sqlBuilder = new StringBuilder();
1819                sqlBuilder.append("SELECT r.").append(findPidFieldName);
1820                sqlBuilder.append(", r.").append(findResourceTypeFieldName);
1821                sqlBuilder.append(", r.myTargetResourceUrl");
1822                if (findVersionFieldName != null) {
1823                        sqlBuilder.append(", r.").append(findVersionFieldName);
1824                }
1825                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1826                        sqlBuilder.append(", r.").append(findPartitionFieldName);
1827                }
1828                sqlBuilder.append(" FROM ResourceLink r WHERE ");
1829
1830                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1831                        sqlBuilder.append("r.").append(searchPartitionFieldName);
1832                        sqlBuilder.append(" = :target_partition_id AND ");
1833                }
1834
1835                sqlBuilder.append("r.").append(searchPidFieldName);
1836                sqlBuilder.append(" IN (:target_pids)");
1837
1838                /*
1839                 * We need to set the resource type in 2 cases only:
1840                 * 1) we are in $everything mode
1841                 *              (where we only want to fetch specific resource types, regardless of what is
1842                 *              available to fetch)
1843                 * 2) we are doing revincludes
1844                 *
1845                 *      Technically if the request is a qualified star (e.g. _include=Observation:*) we
1846                 * should always be checking the source resource type on the resource link. We don't
1847                 * actually index that column though by default, so in order to try and be efficient
1848                 * we don't actually include it for includes (but we do for revincludes). This is
1849                 * because for an include, it doesn't really make sense to include a different
1850                 * resource type than the one you are searching on.
1851                 */
1852                if (wantResourceType != null && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
1853                        // because mySourceResourceType is not part of the HFJ_RES_LINK
1854                        // index, this might not be the most optimal performance.
1855                        // but it is for an $everything operation (and maybe we should update the index)
1856                        sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
1857                } else {
1858                        wantResourceType = null;
1859                }
1860
1861                // When calling $everything on a Patient instance, we don't want to recurse into new Patient
1862                // resources
1863                // (e.g. via Provenance, List, or Group) when in an $everything operation
1864                if (myParams != null
1865                                && myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
1866                        sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
1867                        sqlBuilder.append(UNDESIRED_RESOURCE_LINKAGES_FOR_EVERYTHING_ON_PATIENT_INSTANCE.stream()
1868                                        .collect(Collectors.joining("', '", " AND r.mySourceResourceType NOT IN ('", "')")));
1869                }
1870                if (hasDesiredResourceTypes) {
1871                        sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
1872                }
1873
1874                String sql = sqlBuilder.toString();
1875                List<Collection<JpaPid>> partitions = partitionBySizeAndPartitionId(nextRoundMatches, getMaximumPageSize());
1876                for (Collection<JpaPid> nextPartition : partitions) {
1877                        TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
1878                        q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
1879                        if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1880                                q.setParameter(
1881                                                "target_partition_id", nextPartition.iterator().next().getPartitionId());
1882                        }
1883                        if (wantResourceType != null) {
1884                                q.setParameter("want_resource_type", wantResourceType);
1885                        }
1886                        if (maxCount != null) {
1887                                q.setMaxResults(maxCount);
1888                        }
1889                        if (hasDesiredResourceTypes) {
1890                                q.setParameter("desired_target_resource_types", desiredResourceTypes);
1891                        }
1892                        List<?> results = q.getResultList();
1893                        Set<String> canonicalUrls = null;
1894                        for (Object nextRow : results) {
1895                                if (nextRow == null) {
1896                                        // This can happen if there are outgoing references which are canonical or point to
1897                                        // other servers
1898                                        continue;
1899                                }
1900
1901                                Long version = null;
1902                                Long resourceId = (Long) ((Object[]) nextRow)[0];
1903                                String resourceType = (String) ((Object[]) nextRow)[1];
1904                                String resourceCanonicalUrl = (String) ((Object[]) nextRow)[2];
1905                                Integer partitionId = null;
1906                                int offset = 0;
1907                                if (findVersionFieldName != null) {
1908                                        version = (Long) ((Object[]) nextRow)[3];
1909                                        offset++;
1910                                }
1911                                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
1912                                        partitionId = ((Integer) ((Object[]) nextRow)[3 + offset]);
1913                                }
1914
1915                                if (resourceId != null) {
1916                                        JpaPid pid = JpaPid.fromIdAndVersionAndResourceType(resourceId, version, resourceType);
1917                                        pid.setPartitionId(partitionId);
1918                                        pidsToInclude.add(pid);
1919                                } else if (resourceCanonicalUrl != null) {
1920                                        if (canonicalUrls == null) {
1921                                                canonicalUrls = new HashSet<>();
1922                                        }
1923                                        canonicalUrls.add(resourceCanonicalUrl);
1924                                }
1925                        }
1926
1927                        if (canonicalUrls != null) {
1928                                String message =
1929                                                "Search with _include=* can be inefficient when references using canonical URLs are detected. Use more specific _include values instead.";
1930                                firePerformanceWarning(request, message);
1931                                loadCanonicalUrls(request, canonicalUrls, entityManager, pidsToInclude, reverseMode);
1932                        }
1933                }
1934        }
1935
1936        private void loadCanonicalUrls(
1937                        RequestDetails theRequestDetails,
1938                        Set<String> theCanonicalUrls,
1939                        EntityManager theEntityManager,
1940                        HashSet<JpaPid> thePidsToInclude,
1941                        boolean theReverse) {
1942                StringBuilder sqlBuilder;
1943                CanonicalUrlTargets canonicalUrlTargets =
1944                                calculateIndexUriIdentityHashesForResourceTypes(theRequestDetails, null, theReverse);
1945                List<List<String>> canonicalUrlPartitions = ListUtils.partition(
1946                                List.copyOf(theCanonicalUrls), getMaximumPageSize() - canonicalUrlTargets.myHashIdentityValues.size());
1947
1948                sqlBuilder = new StringBuilder();
1949                sqlBuilder.append("SELECT ");
1950                if (myPartitionSettings.isPartitioningEnabled()) {
1951                        sqlBuilder.append("i.myPartitionIdValue, ");
1952                }
1953                sqlBuilder.append("i.myResourcePid ");
1954
1955                sqlBuilder.append("FROM ResourceIndexedSearchParamUri i ");
1956                sqlBuilder.append("WHERE i.myHashIdentity IN (:hash_identity) ");
1957                sqlBuilder.append("AND i.myUri IN (:uris)");
1958
1959                String canonicalResSql = sqlBuilder.toString();
1960
1961                for (Collection<String> nextCanonicalUrlList : canonicalUrlPartitions) {
1962                        TypedQuery<Object[]> canonicalResIdQuery = theEntityManager.createQuery(canonicalResSql, Object[].class);
1963                        canonicalResIdQuery.setParameter("hash_identity", canonicalUrlTargets.myHashIdentityValues);
1964                        canonicalResIdQuery.setParameter("uris", nextCanonicalUrlList);
1965                        List<Object[]> results = canonicalResIdQuery.getResultList();
1966                        for (var next : results) {
1967                                if (next != null) {
1968                                        Integer partitionId = null;
1969                                        Long pid;
1970                                        if (next.length == 1) {
1971                                                pid = (Long) next[0];
1972                                        } else {
1973                                                partitionId = (Integer) ((Object[]) next)[0];
1974                                                pid = (Long) ((Object[]) next)[1];
1975                                        }
1976                                        if (pid != null) {
1977                                                thePidsToInclude.add(JpaPid.fromId(pid, partitionId));
1978                                        }
1979                                }
1980                        }
1981                }
1982        }
1983
1984        /**
1985         * Calls Performance Trace Hook
1986         *
1987         * @param request                 the request deatils
1988         *                                Sends a raw SQL query to the Pointcut for raw SQL queries.
1989         */
1990        private void callRawSqlHookWithCurrentThreadQueries(
1991                        RequestDetails request, IInterceptorBroadcaster theCompositeBroadcaster) {
1992                SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
1993                HookParams params = new HookParams()
1994                                .add(RequestDetails.class, request)
1995                                .addIfMatchesType(ServletRequestDetails.class, request)
1996                                .add(SqlQueryList.class, capturedQueries);
1997                theCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, params);
1998        }
1999
2000        @Nullable
2001        private static Set<String> computeTargetResourceTypes(Include nextInclude, RuntimeSearchParam param) {
2002                String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
2003                boolean haveTargetTypesDefinedByParam = param.hasTargets();
2004                Set<String> targetResourceTypes;
2005                if (targetResourceType != null) {
2006                        targetResourceTypes = Set.of(targetResourceType);
2007                } else if (haveTargetTypesDefinedByParam) {
2008                        targetResourceTypes = param.getTargets();
2009                } else {
2010                        // all types!
2011                        targetResourceTypes = null;
2012                }
2013                return targetResourceTypes;
2014        }
2015
2016        @Nonnull
2017        private Pair<String, Map<String, Object>> buildCanonicalUrlQuery(
2018                        String theVersionFieldName,
2019                        Set<String> theTargetResourceTypes,
2020                        boolean theReverse,
2021                        RequestDetails theRequest) {
2022                String fieldsToLoadFromSpidxUriTable = theReverse ? "r.src_resource_id" : "rUri.res_id";
2023                if (theVersionFieldName != null) {
2024                        // canonical-uri references aren't versioned, but we need to match the column count for the UNION
2025                        fieldsToLoadFromSpidxUriTable += ", NULL";
2026                }
2027
2028                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
2029                        if (theReverse) {
2030                                fieldsToLoadFromSpidxUriTable += ", r.partition_id as " + PARTITION_ID_ALIAS;
2031                        } else {
2032                                fieldsToLoadFromSpidxUriTable += ", rUri.partition_id as " + PARTITION_ID_ALIAS;
2033                        }
2034                }
2035
2036                // The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url.
2037                // But sp_name isn't indexed, so we use hash_identity instead.
2038                CanonicalUrlTargets canonicalUrlTargets =
2039                                calculateIndexUriIdentityHashesForResourceTypes(theRequest, theTargetResourceTypes, theReverse);
2040
2041                Map<String, Object> canonicalUriQueryParams = new HashMap<>();
2042                StringBuilder canonicalUrlQuery = new StringBuilder();
2043                canonicalUrlQuery
2044                                .append("SELECT ")
2045                                .append(fieldsToLoadFromSpidxUriTable)
2046                                .append(' ');
2047                canonicalUrlQuery.append("FROM hfj_res_link r ");
2048
2049                // join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2
2050                canonicalUrlQuery.append("JOIN hfj_spidx_uri rUri ON (");
2051                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
2052                        canonicalUrlQuery.append("rUri.partition_id IN (:uri_partition_id) AND ");
2053                        canonicalUriQueryParams.put("uri_partition_id", canonicalUrlTargets.myPartitionIds);
2054                }
2055                if (canonicalUrlTargets.myHashIdentityValues.size() == 1) {
2056                        canonicalUrlQuery.append("rUri.hash_identity = :uri_identity_hash");
2057                        canonicalUriQueryParams.put(
2058                                        "uri_identity_hash",
2059                                        canonicalUrlTargets.myHashIdentityValues.iterator().next());
2060                } else {
2061                        canonicalUrlQuery.append("rUri.hash_identity in (:uri_identity_hashes)");
2062                        canonicalUriQueryParams.put("uri_identity_hashes", canonicalUrlTargets.myHashIdentityValues);
2063                }
2064                canonicalUrlQuery.append(" AND r.target_resource_url = rUri.sp_uri");
2065                canonicalUrlQuery.append(")");
2066
2067                canonicalUrlQuery.append(" WHERE r.src_path = :src_path AND");
2068                canonicalUrlQuery.append(" r.target_resource_id IS NULL");
2069                canonicalUrlQuery.append(" AND");
2070                if (myPartitionSettings.isPartitionIdsInPrimaryKeys()) {
2071                        if (theReverse) {
2072                                canonicalUrlQuery.append(" rUri.partition_id");
2073                        } else {
2074                                canonicalUrlQuery.append(" r.partition_id");
2075                        }
2076                        canonicalUrlQuery.append(" = :search_partition_id");
2077                        canonicalUrlQuery.append(" AND");
2078                }
2079                if (theReverse) {
2080                        canonicalUrlQuery.append(" rUri.res_id");
2081                } else {
2082                        canonicalUrlQuery.append(" r.src_resource_id");
2083                }
2084                canonicalUrlQuery.append(" IN (:target_pids)");
2085
2086                return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams);
2087        }
2088
2089        @Nonnull
2090        CanonicalUrlTargets calculateIndexUriIdentityHashesForResourceTypes(
2091                        RequestDetails theRequestDetails, Set<String> theTargetResourceTypes, boolean theReverse) {
2092                Set<String> targetResourceTypes = theTargetResourceTypes;
2093                if (targetResourceTypes == null) {
2094                        /*
2095                         * If we don't have a list of valid target types, we need to figure out a list of all
2096                         * possible target types in order to perform the search of the URI index table. This is
2097                         * because the hash_identity column encodes the resource type, so we'll need a hash
2098                         * value for each possible target type.
2099                         */
2100                        targetResourceTypes = new HashSet<>();
2101                        Set<String> possibleTypes = myDaoRegistry.getRegisteredDaoTypes();
2102                        if (theReverse) {
2103                                // For reverse includes, it is really hard to figure out what types
2104                                // are actually potentially pointing to the type we're searching for
2105                                // in this context, so let's just assume it could be anything.
2106                                targetResourceTypes = possibleTypes;
2107                        } else {
2108                                for (var next : mySearchParamRegistry
2109                                                .getActiveSearchParams(myResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH)
2110                                                .values()
2111                                                .stream()
2112                                                .filter(t -> t.getParamType().equals(RestSearchParameterTypeEnum.REFERENCE))
2113                                                .collect(Collectors.toList())) {
2114
2115                                        // If the reference points to a Reference (ie not a canonical or CanonicalReference)
2116                                        // then it doesn't matter here anyhow. The logic here only works for elements at the
2117                                        // root level of the document (e.g. QuestionnaireResponse.subject or
2118                                        // QuestionnaireResponse.subject.where(...)) but this is just an optimization
2119                                        // anyhow.
2120                                        if (next.getPath().startsWith(myResourceName + ".")) {
2121                                                String elementName =
2122                                                                next.getPath().substring(next.getPath().indexOf('.') + 1);
2123                                                int secondDotIndex = elementName.indexOf('.');
2124                                                if (secondDotIndex != -1) {
2125                                                        elementName = elementName.substring(0, secondDotIndex);
2126                                                }
2127                                                BaseRuntimeChildDefinition child =
2128                                                                myContext.getResourceDefinition(myResourceName).getChildByName(elementName);
2129                                                if (child != null) {
2130                                                        BaseRuntimeElementDefinition<?> childDef = child.getChildByName(elementName);
2131                                                        if (childDef != null) {
2132                                                                if (childDef.getName().equals("Reference")) {
2133                                                                        continue;
2134                                                                }
2135                                                        }
2136                                                }
2137                                        }
2138
2139                                        if (!next.getTargets().isEmpty()) {
2140                                                // For each reference parameter on the resource type we're searching for,
2141                                                // add all the potential target types to the list of possible target
2142                                                // resource types we can look up.
2143                                                for (var nextTarget : next.getTargets()) {
2144                                                        if (possibleTypes.contains(nextTarget)) {
2145                                                                targetResourceTypes.add(nextTarget);
2146                                                        }
2147                                                }
2148                                        } else {
2149                                                // If we have any references that don't define any target types, then
2150                                                // we need to assume that all enabled resource types are possible target
2151                                                // types
2152                                                targetResourceTypes.addAll(possibleTypes);
2153                                                break;
2154                                        }
2155                                }
2156                        }
2157                }
2158                assert !targetResourceTypes.isEmpty();
2159
2160                Set<Long> hashIdentityValues = new HashSet<>();
2161                Set<Integer> partitionIds = new HashSet<>();
2162                for (String type : targetResourceTypes) {
2163
2164                        RequestPartitionId readPartition;
2165                        if (myPartitionSettings.isPartitioningEnabled()) {
2166                                readPartition =
2167                                                myPartitionHelperSvc.determineReadPartitionForRequestForSearchType(theRequestDetails, type);
2168                        } else {
2169                                readPartition = RequestPartitionId.defaultPartition();
2170                        }
2171                        if (readPartition.hasPartitionIds()) {
2172                                partitionIds.addAll(readPartition.getPartitionIds());
2173                        }
2174
2175                        Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(
2176                                        myPartitionSettings, readPartition, type, "url");
2177                        hashIdentityValues.add(hashIdentity);
2178                }
2179
2180                return new CanonicalUrlTargets(hashIdentityValues, partitionIds);
2181        }
2182
2183        static class CanonicalUrlTargets {
2184
2185                @Nonnull
2186                final Set<Long> myHashIdentityValues;
2187
2188                @Nonnull
2189                final Set<Integer> myPartitionIds;
2190
2191                public CanonicalUrlTargets(@Nonnull Set<Long> theHashIdentityValues, @Nonnull Set<Integer> thePartitionIds) {
2192                        myHashIdentityValues = theHashIdentityValues;
2193                        myPartitionIds = thePartitionIds;
2194                }
2195        }
2196
2197        /**
2198         * This method takes in a list of {@link JpaPid}'s and returns a series of sublists containing
2199         * those pids where:
2200         * <ul>
2201         *     <li>No single list is most than {@literal theMaxLoad} entries</li>
2202         *     <li>Each list only contains JpaPids with the same partition ID</li>
2203         * </ul>
2204         */
2205        static List<Collection<JpaPid>> partitionBySizeAndPartitionId(List<JpaPid> theNextRoundMatches, int theMaxLoad) {
2206
2207                if (theNextRoundMatches.size() <= theMaxLoad) {
2208                        boolean allSamePartition = true;
2209                        for (int i = 1; i < theNextRoundMatches.size(); i++) {
2210                                if (!Objects.equals(
2211                                                theNextRoundMatches.get(i - 1).getPartitionId(),
2212                                                theNextRoundMatches.get(i).getPartitionId())) {
2213                                        allSamePartition = false;
2214                                        break;
2215                                }
2216                        }
2217                        if (allSamePartition) {
2218                                return Collections.singletonList(theNextRoundMatches);
2219                        }
2220                }
2221
2222                // Break into partitioned sublists
2223                ListMultimap<String, JpaPid> lists =
2224                                MultimapBuilder.hashKeys().arrayListValues().build();
2225                for (JpaPid nextRoundMatch : theNextRoundMatches) {
2226                        String partitionId = nextRoundMatch.getPartitionId() != null
2227                                        ? nextRoundMatch.getPartitionId().toString()
2228                                        : "";
2229                        lists.put(partitionId, nextRoundMatch);
2230                }
2231
2232                List<Collection<JpaPid>> retVal = new ArrayList<>();
2233                for (String key : lists.keySet()) {
2234                        List<List<JpaPid>> nextPartition = Lists.partition(lists.get(key), theMaxLoad);
2235                        retVal.addAll(nextPartition);
2236                }
2237
2238                // In unit test mode, we sort the results just for unit test predictability
2239                if (HapiSystemProperties.isUnitTestModeEnabled()) {
2240                        retVal = retVal.stream()
2241                                        .map(t -> t.stream().sorted().collect(Collectors.toList()))
2242                                        .collect(Collectors.toList());
2243                }
2244
2245                return retVal;
2246        }
2247
2248        private void attemptComboUniqueSpProcessing(
2249                        QueryStack theQueryStack, @Nonnull SearchParameterMap theParams, RequestDetails theRequest) {
2250                RuntimeSearchParam comboParam = null;
2251                List<String> comboParamNames = null;
2252                List<RuntimeSearchParam> exactMatchParams = mySearchParamRegistry.getActiveComboSearchParams(
2253                                myResourceName, theParams.keySet(), ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2254                if (!exactMatchParams.isEmpty()) {
2255                        comboParam = exactMatchParams.get(0);
2256                        comboParamNames = new ArrayList<>(theParams.keySet());
2257                }
2258
2259                if (comboParam == null) {
2260                        List<RuntimeSearchParam> candidateComboParams = mySearchParamRegistry.getActiveComboSearchParams(
2261                                        myResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2262                        for (RuntimeSearchParam nextCandidate : candidateComboParams) {
2263                                List<String> nextCandidateParamNames =
2264                                                JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream()
2265                                                                .map(RuntimeSearchParam::getName)
2266                                                                .collect(Collectors.toList());
2267                                if (theParams.keySet().containsAll(nextCandidateParamNames)) {
2268                                        comboParam = nextCandidate;
2269                                        comboParamNames = nextCandidateParamNames;
2270                                        break;
2271                                }
2272                        }
2273                }
2274
2275                if (comboParam != null) {
2276                        Collections.sort(comboParamNames);
2277
2278                        // Since we're going to remove elements below
2279                        theParams.values().forEach(this::ensureSubListsAreWritable);
2280
2281                        /*
2282                         * Apply search against the combo param index in a loop:
2283                         *
2284                         * 1. First we check whether the actual parameter values in the
2285                         * parameter map are actually usable for searching against the combo
2286                         * param index. E.g. no search modifiers, date comparators, etc.,
2287                         * since these mean you can't use the combo index.
2288                         *
2289                         * 2. Apply and create the join SQl. We remove parameter values from
2290                         * the map as we apply them, so any parameter values remaining in the
2291                         * map after each loop haven't yet been factored into the SQL.
2292                         *
2293                         * The loop allows us to create multiple combo index joins if there
2294                         * are multiple AND expressions for the related parameters.
2295                         */
2296                        while (validateParamValuesAreValidForComboParam(theRequest, theParams, comboParamNames, comboParam)) {
2297                                applyComboSearchParam(theQueryStack, theParams, theRequest, comboParamNames, comboParam);
2298                        }
2299                }
2300        }
2301
2302        private void applyComboSearchParam(
2303                        QueryStack theQueryStack,
2304                        @Nonnull SearchParameterMap theParams,
2305                        RequestDetails theRequest,
2306                        List<String> theComboParamNames,
2307                        RuntimeSearchParam theComboParam) {
2308
2309                List<List<IQueryParameterType>> inputs = new ArrayList<>();
2310                for (String nextParamName : theComboParamNames) {
2311                        List<IQueryParameterType> nextValues = theParams.get(nextParamName).remove(0);
2312                        inputs.add(nextValues);
2313                }
2314
2315                List<List<IQueryParameterType>> inputPermutations = Lists.cartesianProduct(inputs);
2316                List<String> indexStrings = new ArrayList<>(CartesianProductUtil.calculateCartesianProductSize(inputs));
2317                for (List<IQueryParameterType> nextPermutation : inputPermutations) {
2318
2319                        StringBuilder searchStringBuilder = new StringBuilder();
2320                        searchStringBuilder.append(myResourceName);
2321                        searchStringBuilder.append("?");
2322
2323                        boolean first = true;
2324                        for (int paramIndex = 0; paramIndex < theComboParamNames.size(); paramIndex++) {
2325
2326                                String nextParamName = theComboParamNames.get(paramIndex);
2327                                IQueryParameterType nextOr = nextPermutation.get(paramIndex);
2328                                String nextOrValue = nextOr.getValueAsQueryToken(myContext);
2329
2330                                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(
2331                                                myResourceName, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2332                                if (theComboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) {
2333                                        if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) {
2334                                                nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue);
2335                                        }
2336                                }
2337
2338                                if (first) {
2339                                        first = false;
2340                                } else {
2341                                        searchStringBuilder.append('&');
2342                                }
2343
2344                                nextParamName = UrlUtil.escapeUrlParam(nextParamName);
2345                                nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
2346
2347                                searchStringBuilder.append(nextParamName).append('=').append(nextOrValue);
2348                        }
2349
2350                        String indexString = searchStringBuilder.toString();
2351                        ourLog.debug(
2352                                        "Checking for {} combo index for query: {}", theComboParam.getComboSearchParamType(), indexString);
2353
2354                        indexStrings.add(indexString);
2355                }
2356
2357                // Just to make sure we're stable for tests
2358                indexStrings.sort(Comparator.naturalOrder());
2359
2360                // Interceptor broadcast: JPA_PERFTRACE_INFO
2361                IInterceptorBroadcaster compositeBroadcaster =
2362                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
2363                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) {
2364                        String indexStringForLog = indexStrings.size() > 1 ? indexStrings.toString() : indexStrings.get(0);
2365                        StorageProcessingMessage msg = new StorageProcessingMessage()
2366                                        .setMessage("Using " + theComboParam.getComboSearchParamType() + " index(es) for query for search: "
2367                                                        + indexStringForLog);
2368                        HookParams params = new HookParams()
2369                                        .add(RequestDetails.class, theRequest)
2370                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
2371                                        .add(StorageProcessingMessage.class, msg);
2372                        compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params);
2373                }
2374
2375                switch (requireNonNull(theComboParam.getComboSearchParamType())) {
2376                        case UNIQUE:
2377                                theQueryStack.addPredicateCompositeUnique(indexStrings, myRequestPartitionId);
2378                                break;
2379                        case NON_UNIQUE:
2380                                theQueryStack.addPredicateCompositeNonUnique(indexStrings, myRequestPartitionId);
2381                                break;
2382                }
2383
2384                // Remove any empty parameters remaining after this
2385                theParams.clean();
2386        }
2387
2388        /**
2389         * Returns {@literal true} if the actual parameter instances in a given query are actually usable for
2390         * searching against a combo param with the given parameter names. This might be {@literal false} if
2391         * parameters have modifiers (e.g. <code>?name:exact=SIMPSON</code>), prefixes
2392         * (e.g. <code>?date=gt2024-02-01</code>), etc.
2393         */
2394        private boolean validateParamValuesAreValidForComboParam(
2395                        RequestDetails theRequest,
2396                        @Nonnull SearchParameterMap theParams,
2397                        List<String> theComboParamNames,
2398                        RuntimeSearchParam theComboParam) {
2399                boolean paramValuesAreValidForCombo = true;
2400                List<List<IQueryParameterType>> paramOrValues = new ArrayList<>(theComboParamNames.size());
2401
2402                for (String nextParamName : theComboParamNames) {
2403                        List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
2404
2405                        if (nextValues == null || nextValues.isEmpty()) {
2406                                paramValuesAreValidForCombo = false;
2407                                break;
2408                        }
2409
2410                        List<IQueryParameterType> nextAndValue = nextValues.get(0);
2411                        paramOrValues.add(nextAndValue);
2412
2413                        for (IQueryParameterType nextOrValue : nextAndValue) {
2414                                if (nextOrValue instanceof DateParam) {
2415                                        DateParam dateParam = (DateParam) nextOrValue;
2416                                        if (dateParam.getPrecision() != TemporalPrecisionEnum.DAY) {
2417                                                String message = "Search with params " + theComboParamNames
2418                                                                + " is not a candidate for combo searching - Date search with non-DAY precision for parameter '"
2419                                                                + nextParamName + "'";
2420                                                firePerformanceInfo(theRequest, message);
2421                                                paramValuesAreValidForCombo = false;
2422                                                break;
2423                                        }
2424                                }
2425                                if (nextOrValue instanceof BaseParamWithPrefix) {
2426                                        BaseParamWithPrefix<?> paramWithPrefix = (BaseParamWithPrefix<?>) nextOrValue;
2427                                        if (paramWithPrefix.getPrefix() != null) {
2428                                                String message = "Search with params " + theComboParamNames
2429                                                                + " is not a candidate for combo searching - Parameter '" + nextParamName
2430                                                                + "' has prefix: '"
2431                                                                + paramWithPrefix.getPrefix().getValue() + "'";
2432                                                firePerformanceInfo(theRequest, message);
2433                                                paramValuesAreValidForCombo = false;
2434                                                break;
2435                                        }
2436                                }
2437                                if (isNotBlank(nextOrValue.getQueryParameterQualifier())) {
2438                                        String message = "Search with params " + theComboParamNames
2439                                                        + " is not a candidate for combo searching - Parameter '" + nextParamName
2440                                                        + "' has modifier: '" + nextOrValue.getQueryParameterQualifier() + "'";
2441                                        firePerformanceInfo(theRequest, message);
2442                                        paramValuesAreValidForCombo = false;
2443                                        break;
2444                                }
2445                        }
2446
2447                        // Reference params are only eligible for using a composite index if they
2448                        // are qualified
2449                        RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(
2450                                        myResourceName, nextParamName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH);
2451                        if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
2452                                ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0);
2453                                if (isBlank(param.getResourceType())) {
2454                                        ourLog.debug(
2455                                                        "Search is not a candidate for unique combo searching - Reference with no type specified");
2456                                        paramValuesAreValidForCombo = false;
2457                                        break;
2458                                }
2459                        }
2460
2461                        // Date params are not eligible for using composite unique index
2462                        // as index could contain date with different precision (e.g. DAY, SECOND)
2463                        if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.DATE
2464                                        && theComboParam.getComboSearchParamType() == ComboSearchParamType.UNIQUE) {
2465                                ourLog.debug(
2466                                                "Search with params {} is not a candidate for combo searching - "
2467                                                                + "Unique combo search parameter '{}' has DATE type",
2468                                                theComboParamNames,
2469                                                nextParamName);
2470                                paramValuesAreValidForCombo = false;
2471                                break;
2472                        }
2473                }
2474
2475                if (CartesianProductUtil.calculateCartesianProductSize(paramOrValues) > 500) {
2476                        ourLog.debug(
2477                                        "Search is not a candidate for unique combo searching - Too many OR values would result in too many permutations");
2478                        paramValuesAreValidForCombo = false;
2479                }
2480
2481                return paramValuesAreValidForCombo;
2482        }
2483
2484        private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) {
2485                for (int i = 0; i < theListOfLists.size(); i++) {
2486                        List<T> oldSubList = theListOfLists.get(i);
2487                        if (!(oldSubList instanceof ArrayList)) {
2488                                List<T> newSubList = new ArrayList<>(oldSubList);
2489                                theListOfLists.set(i, newSubList);
2490                        }
2491                }
2492        }
2493
2494        @Override
2495        public void setFetchSize(int theFetchSize) {
2496                myFetchSize = theFetchSize;
2497        }
2498
2499        public SearchParameterMap getParams() {
2500                return myParams;
2501        }
2502
2503        public CriteriaBuilder getBuilder() {
2504                return myCriteriaBuilder;
2505        }
2506
2507        public Class<? extends IBaseResource> getResourceType() {
2508                return myResourceType;
2509        }
2510
2511        public String getResourceName() {
2512                return myResourceName;
2513        }
2514
2515        /**
2516         * IncludesIterator, used to recursively fetch resources from the provided list of PIDs
2517         */
2518        public class IncludesIterator extends BaseIterator<JpaPid> implements Iterator<JpaPid> {
2519
2520                private final RequestDetails myRequest;
2521                private final Set<JpaPid> myCurrentPids;
2522                private Iterator<JpaPid> myCurrentIterator;
2523                private JpaPid myNext;
2524
2525                IncludesIterator(Set<JpaPid> thePidSet, RequestDetails theRequest) {
2526                        myCurrentPids = new HashSet<>(thePidSet);
2527                        myCurrentIterator = null;
2528                        myRequest = theRequest;
2529                }
2530
2531                private void fetchNext() {
2532                        while (myNext == null) {
2533
2534                                if (myCurrentIterator == null) {
2535                                        Set<Include> includes = new HashSet<>();
2536                                        if (myParams.containsKey(Constants.PARAM_TYPE)) {
2537                                                for (List<IQueryParameterType> typeList : myParams.get(Constants.PARAM_TYPE)) {
2538                                                        for (IQueryParameterType type : typeList) {
2539                                                                String queryString = ParameterUtil.unescape(type.getValueAsQueryToken(myContext));
2540                                                                for (String resourceType : queryString.split(",")) {
2541                                                                        String rt = resourceType.trim();
2542                                                                        if (isNotBlank(rt)) {
2543                                                                                includes.add(new Include(rt + ":*", true));
2544                                                                        }
2545                                                                }
2546                                                        }
2547                                                }
2548                                        }
2549                                        if (includes.isEmpty()) {
2550                                                includes.add(new Include("*", true));
2551                                        }
2552                                        Set<JpaPid> newPids = loadIncludes(
2553                                                        myContext,
2554                                                        myEntityManager,
2555                                                        myCurrentPids,
2556                                                        includes,
2557                                                        false,
2558                                                        getParams().getLastUpdated(),
2559                                                        mySearchUuid,
2560                                                        myRequest,
2561                                                        null);
2562                                        myCurrentIterator = newPids.iterator();
2563                                }
2564
2565                                if (myCurrentIterator.hasNext()) {
2566                                        myNext = myCurrentIterator.next();
2567                                } else {
2568                                        myNext = NO_MORE;
2569                                }
2570                        }
2571                }
2572
2573                @Override
2574                public boolean hasNext() {
2575                        fetchNext();
2576                        return !NO_MORE.equals(myNext);
2577                }
2578
2579                @Override
2580                public JpaPid next() {
2581                        fetchNext();
2582                        JpaPid retVal = myNext;
2583                        myNext = null;
2584                        return retVal;
2585                }
2586        }
2587
2588        /**
2589         * Basic Query iterator, used to fetch the results of a query.
2590         */
2591        private final class QueryIterator extends BaseIterator<JpaPid> implements IResultIterator<JpaPid> {
2592
2593                private final SearchRuntimeDetails mySearchRuntimeDetails;
2594                private final RequestDetails myRequest;
2595                private final boolean myHaveRawSqlHooks;
2596                private final boolean myHavePerfTraceFoundIdHook;
2597                private final SortSpec mySort;
2598                private final Integer myOffset;
2599                private final IInterceptorBroadcaster myCompositeBroadcaster;
2600                private boolean myFirst = true;
2601                private IncludesIterator myIncludesIterator;
2602                /**
2603                 * The next JpaPid value of the next result in this query.
2604                 * Will not be null if fetched using getNext()
2605                 */
2606                private JpaPid myNext;
2607                /**
2608                 * The current query result iterator running sql and supplying PIDs
2609                 * @see #myQueryList
2610                 */
2611                private ISearchQueryExecutor myResultsIterator;
2612
2613                private boolean myFetchIncludesForEverythingOperation;
2614                /**
2615                 * The count of resources skipped because they were seen in earlier results
2616                 */
2617                private int mySkipCount = 0;
2618                /**
2619                 * The count of resources that are new in this search
2620                 * (ie, not cached in previous searches)
2621                 */
2622                private int myNonSkipCount = 0;
2623
2624                /**
2625                 * The list of queries to use to find all results.
2626                 * Normal JPA queries will normally have a single entry.
2627                 * Queries that involve Hibernate Search/Elastisearch may have
2628                 * multiple queries because of chunking.
2629                 * The $everything operation also jams some extra results in.
2630                 */
2631                private List<ISearchQueryExecutor> myQueryList = new ArrayList<>();
2632
2633                private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
2634                        mySearchRuntimeDetails = theSearchRuntimeDetails;
2635                        mySort = myParams.getSort();
2636                        myOffset = myParams.getOffset();
2637                        myRequest = theRequest;
2638                        myCompositeBroadcaster =
2639                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
2640
2641                        // everything requires fetching recursively all related resources
2642                        if (myParams.getEverythingMode() != null) {
2643                                myFetchIncludesForEverythingOperation = true;
2644                        }
2645
2646                        myHavePerfTraceFoundIdHook = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID);
2647                        myHaveRawSqlHooks = myCompositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL);
2648                }
2649
2650                private void fetchNext() {
2651                        try {
2652                                if (myHaveRawSqlHooks) {
2653                                        CurrentThreadCaptureQueriesListener.startCapturing();
2654                                }
2655
2656                                // If we don't have a query yet, create one
2657                                if (myResultsIterator == null) {
2658                                        if (myMaxResultsToFetch == null) {
2659                                                myMaxResultsToFetch = calculateMaxResultsToFetch();
2660                                        }
2661
2662                                        /*
2663                                         * assigns the results iterator
2664                                         * and populates the myQueryList.
2665                                         */
2666                                        initializeIteratorQuery(myOffset, myMaxResultsToFetch);
2667                                }
2668
2669                                if (myNext == null) {
2670                                        // no next means we need a new query (if one is available)
2671                                        while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) {
2672                                                /*
2673                                                 * Because we combine our DB searches with Lucene
2674                                                 * sometimes we can have multiple results iterators
2675                                                 * (with only some having data in them to extract).
2676                                                 *
2677                                                 * We'll iterate our results iterators until we
2678                                                 * either run out of results iterators, or we
2679                                                 * have one that actually has data in it.
2680                                                 */
2681                                                while (!myResultsIterator.hasNext() && !myQueryList.isEmpty()) {
2682                                                        retrieveNextIteratorQuery();
2683                                                }
2684
2685                                                if (!myResultsIterator.hasNext()) {
2686                                                        // we couldn't find a results iterator;
2687                                                        // we're done here
2688                                                        break;
2689                                                }
2690
2691                                                JpaPid nextPid = myResultsIterator.next();
2692                                                if (myHavePerfTraceFoundIdHook) {
2693                                                        callPerformanceTracingHook(nextPid);
2694                                                }
2695
2696                                                if (nextPid != null) {
2697                                                        if (myPidSet.add(nextPid) && doNotSkipNextPidForEverything()) {
2698                                                                myNext = nextPid;
2699                                                                myNonSkipCount++;
2700                                                                break;
2701                                                        } else {
2702                                                                mySkipCount++;
2703                                                        }
2704                                                }
2705
2706                                                if (!myResultsIterator.hasNext()) {
2707                                                        if (myMaxResultsToFetch != null && (mySkipCount + myNonSkipCount == myMaxResultsToFetch)) {
2708                                                                if (mySkipCount > 0 && myNonSkipCount == 0) {
2709
2710                                                                        sendProcessingMsgAndFirePerformanceHook();
2711
2712                                                                        myMaxResultsToFetch += 1000;
2713                                                                        initializeIteratorQuery(myOffset, myMaxResultsToFetch);
2714                                                                }
2715                                                        }
2716                                                }
2717                                        }
2718                                }
2719
2720                                if (myNext == null) {
2721                                        // if we got here, it means the current JpaPid has already been processed,
2722                                        // and we will decide (here) if we need to fetch related resources recursively
2723                                        if (myFetchIncludesForEverythingOperation) {
2724                                                myIncludesIterator = new IncludesIterator(myPidSet, myRequest);
2725                                                myFetchIncludesForEverythingOperation = false;
2726                                        }
2727                                        if (myIncludesIterator != null) {
2728                                                while (myIncludesIterator.hasNext()) {
2729                                                        JpaPid next = myIncludesIterator.next();
2730                                                        if (next != null && myPidSet.add(next) && doNotSkipNextPidForEverything()) {
2731                                                                myNext = next;
2732                                                                break;
2733                                                        }
2734                                                }
2735                                                if (myNext == null) {
2736                                                        myNext = NO_MORE;
2737                                                }
2738                                        } else {
2739                                                myNext = NO_MORE;
2740                                        }
2741                                }
2742
2743                                mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
2744
2745                        } finally {
2746                                // search finished - fire hooks
2747                                if (myHaveRawSqlHooks) {
2748                                        callRawSqlHookWithCurrentThreadQueries(myRequest, myCompositeBroadcaster);
2749                                }
2750                        }
2751
2752                        if (myFirst) {
2753                                HookParams params = new HookParams()
2754                                                .add(RequestDetails.class, myRequest)
2755                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
2756                                                .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
2757                                myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params);
2758                                myFirst = false;
2759                        }
2760
2761                        if (NO_MORE.equals(myNext)) {
2762                                HookParams params = new HookParams()
2763                                                .add(RequestDetails.class, myRequest)
2764                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
2765                                                .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
2766                                myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params);
2767                        }
2768                }
2769
2770                private Integer calculateMaxResultsToFetch() {
2771                        if (myParams.getLoadSynchronousUpTo() != null) {
2772                                return myParams.getLoadSynchronousUpTo();
2773                        } else if (myParams.getOffset() != null && myParams.getCount() != null) {
2774                                return myParams.getEverythingMode() != null
2775                                                ? myParams.getOffset() + myParams.getCount()
2776                                                : myParams.getCount();
2777                        } else {
2778                                return myStorageSettings.getFetchSizeDefaultMaximum();
2779                        }
2780                }
2781
2782                private boolean doNotSkipNextPidForEverything() {
2783                        return !(myParams.getEverythingMode() != null && (myOffset != null && myOffset >= myPidSet.size()));
2784                }
2785
2786                private void callPerformanceTracingHook(JpaPid theNextPid) {
2787                        HookParams params = new HookParams()
2788                                        .add(Integer.class, System.identityHashCode(this))
2789                                        .add(Object.class, theNextPid);
2790                        myCompositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params);
2791                }
2792
2793                private void sendProcessingMsgAndFirePerformanceHook() {
2794                        String msg = "Pass completed with no matching results seeking rows "
2795                                        + myPidSet.size() + "-" + mySkipCount
2796                                        + ". This indicates an inefficient query! Retrying with new max count of "
2797                                        + myMaxResultsToFetch;
2798                        firePerformanceWarning(myRequest, msg);
2799                }
2800
2801                private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) {
2802                        Integer offset = theOffset;
2803                        if (myQueryList.isEmpty()) {
2804                                // Capture times for Lucene/Elasticsearch queries as well
2805                                mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
2806
2807                                // setting offset to 0 to fetch all resource ids to guarantee
2808                                // correct output result for everything operation during paging
2809                                if (myParams.getEverythingMode() != null) {
2810                                        offset = 0;
2811                                }
2812                                myQueryList = createQuery(
2813                                                myParams, mySort, offset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails);
2814                        }
2815
2816                        mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
2817
2818                        retrieveNextIteratorQuery();
2819
2820                        mySkipCount = 0;
2821                        myNonSkipCount = 0;
2822                }
2823
2824                private void retrieveNextIteratorQuery() {
2825                        close();
2826                        if (isNotEmpty(myQueryList)) {
2827                                myResultsIterator = myQueryList.remove(0);
2828                                myHasNextIteratorQuery = true;
2829                        } else {
2830                                myResultsIterator = SearchQueryExecutor.emptyExecutor();
2831                                myHasNextIteratorQuery = false;
2832                        }
2833                }
2834
2835                @Override
2836                public boolean hasNext() {
2837                        if (myNext == null) {
2838                                fetchNext();
2839                        }
2840                        return !NO_MORE.equals(myNext);
2841                }
2842
2843                @Override
2844                public JpaPid next() {
2845                        fetchNext();
2846                        JpaPid retVal = myNext;
2847                        myNext = null;
2848                        Validate.isTrue(!NO_MORE.equals(retVal), "No more elements");
2849                        return retVal;
2850                }
2851
2852                @Override
2853                public int getSkippedCount() {
2854                        return mySkipCount;
2855                }
2856
2857                @Override
2858                public int getNonSkippedCount() {
2859                        return myNonSkipCount;
2860                }
2861
2862                @Override
2863                public Collection<JpaPid> getNextResultBatch(long theBatchSize) {
2864                        Collection<JpaPid> batch = new ArrayList<>();
2865                        while (this.hasNext() && batch.size() < theBatchSize) {
2866                                batch.add(this.next());
2867                        }
2868                        return batch;
2869                }
2870
2871                @Override
2872                public void close() {
2873                        if (myResultsIterator != null) {
2874                                myResultsIterator.close();
2875                        }
2876                        myResultsIterator = null;
2877                }
2878        }
2879
2880        private void firePerformanceInfo(RequestDetails theRequest, String theMessage) {
2881                // Only log at debug level since these messages aren't considered important enough
2882                // that we should be cluttering the system log, but they are important to the
2883                // specific query being executed to we'll INFO level them there
2884                ourLog.debug(theMessage);
2885                firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_INFO);
2886        }
2887
2888        private void firePerformanceWarning(RequestDetails theRequest, String theMessage) {
2889                ourLog.warn(theMessage);
2890                firePerformanceMessage(theRequest, theMessage, Pointcut.JPA_PERFTRACE_WARNING);
2891        }
2892
2893        private void firePerformanceMessage(RequestDetails theRequest, String theMessage, Pointcut thePointcut) {
2894                IInterceptorBroadcaster compositeBroadcaster =
2895                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
2896                if (compositeBroadcaster.hasHooks(thePointcut)) {
2897                        StorageProcessingMessage message = new StorageProcessingMessage();
2898                        message.setMessage(theMessage);
2899                        HookParams params = new HookParams()
2900                                        .add(RequestDetails.class, theRequest)
2901                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
2902                                        .add(StorageProcessingMessage.class, message);
2903                        compositeBroadcaster.callHooks(thePointcut, params);
2904                }
2905        }
2906
2907        public static int getMaximumPageSize() {
2908                if (myMaxPageSizeForTests != null) {
2909                        return myMaxPageSizeForTests;
2910                }
2911                return MAXIMUM_PAGE_SIZE;
2912        }
2913
2914        public static void setMaxPageSizeForTest(Integer theTestSize) {
2915                myMaxPageSizeForTests = theTestSize;
2916        }
2917}