001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 * http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.search.builder;
021
022import ca.uhn.fhir.context.ComboSearchParamType;
023import ca.uhn.fhir.context.FhirContext;
024import ca.uhn.fhir.context.FhirVersionEnum;
025import ca.uhn.fhir.context.RuntimeResourceDefinition;
026import ca.uhn.fhir.context.RuntimeSearchParam;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.interceptor.api.HookParams;
029import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
030import ca.uhn.fhir.interceptor.api.Pointcut;
031import ca.uhn.fhir.interceptor.model.RequestPartitionId;
032import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
033import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
034import ca.uhn.fhir.jpa.api.dao.IDao;
035import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
036import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
037import ca.uhn.fhir.jpa.config.HapiFhirLocalContainerEntityManagerFactoryBean;
038import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
039import ca.uhn.fhir.jpa.dao.BaseStorageDao;
040import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
041import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser;
042import ca.uhn.fhir.jpa.dao.IResultIterator;
043import ca.uhn.fhir.jpa.dao.ISearchBuilder;
044import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
045import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
046import ca.uhn.fhir.jpa.dao.search.ResourceNotFoundInIndexException;
047import ca.uhn.fhir.jpa.entity.ResourceSearchView;
048import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
049import ca.uhn.fhir.jpa.model.config.PartitionSettings;
050import ca.uhn.fhir.jpa.model.dao.JpaPid;
051import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
052import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
053import ca.uhn.fhir.jpa.model.entity.ResourceTag;
054import ca.uhn.fhir.jpa.model.search.SearchBuilderLoadIncludesParameters;
055import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
056import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
057import ca.uhn.fhir.jpa.search.SearchConstants;
058import ca.uhn.fhir.jpa.search.builder.sql.GeneratedSql;
059import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
060import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryExecutor;
061import ca.uhn.fhir.jpa.search.builder.sql.SqlObjectFactory;
062import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
063import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
064import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper;
065import ca.uhn.fhir.jpa.searchparam.util.JpaParamUtil;
066import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
067import ca.uhn.fhir.jpa.util.BaseIterator;
068import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
069import ca.uhn.fhir.jpa.util.QueryChunker;
070import ca.uhn.fhir.jpa.util.SqlQueryList;
071import ca.uhn.fhir.model.api.IQueryParameterType;
072import ca.uhn.fhir.model.api.Include;
073import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
074import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
075import ca.uhn.fhir.rest.api.Constants;
076import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
077import ca.uhn.fhir.rest.api.SearchContainedModeEnum;
078import ca.uhn.fhir.rest.api.SortOrderEnum;
079import ca.uhn.fhir.rest.api.SortSpec;
080import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
081import ca.uhn.fhir.rest.api.server.RequestDetails;
082import ca.uhn.fhir.rest.param.DateRangeParam;
083import ca.uhn.fhir.rest.param.ParameterUtil;
084import ca.uhn.fhir.rest.param.ReferenceParam;
085import ca.uhn.fhir.rest.param.StringParam;
086import ca.uhn.fhir.rest.param.TokenParam;
087import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
088import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
089import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
090import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
091import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
092import ca.uhn.fhir.util.StopWatch;
093import ca.uhn.fhir.util.StringUtil;
094import ca.uhn.fhir.util.UrlUtil;
095import com.google.common.collect.Streams;
096import com.healthmarketscience.sqlbuilder.Condition;
097import org.apache.commons.lang3.StringUtils;
098import org.apache.commons.lang3.Validate;
099import org.apache.commons.lang3.math.NumberUtils;
100import org.apache.commons.lang3.tuple.Pair;
101import org.hl7.fhir.instance.model.api.IAnyResource;
102import org.hl7.fhir.instance.model.api.IBaseResource;
103import org.slf4j.Logger;
104import org.slf4j.LoggerFactory;
105import org.springframework.beans.factory.annotation.Autowired;
106import org.springframework.jdbc.core.JdbcTemplate;
107import org.springframework.jdbc.core.SingleColumnRowMapper;
108import org.springframework.transaction.support.TransactionSynchronizationManager;
109
110import java.util.ArrayList;
111import java.util.Collection;
112import java.util.Collections;
113import java.util.HashMap;
114import java.util.HashSet;
115import java.util.Iterator;
116import java.util.List;
117import java.util.Map;
118import java.util.Objects;
119import java.util.Optional;
120import java.util.Set;
121import java.util.stream.Collectors;
122import javax.annotation.Nonnull;
123import javax.annotation.Nullable;
124import javax.persistence.EntityManager;
125import javax.persistence.PersistenceContext;
126import javax.persistence.PersistenceContextType;
127import javax.persistence.Query;
128import javax.persistence.Tuple;
129import javax.persistence.TypedQuery;
130import javax.persistence.criteria.CriteriaBuilder;
131
132import static ca.uhn.fhir.jpa.search.builder.QueryStack.LOCATION_POSITION;
133import static org.apache.commons.lang3.StringUtils.defaultString;
134import static org.apache.commons.lang3.StringUtils.isBlank;
135import static org.apache.commons.lang3.StringUtils.isNotBlank;
136
137/**
138 * The SearchBuilder is responsible for actually forming the SQL query that handles
139 * searches for resources
140 */
141public class SearchBuilder implements ISearchBuilder<JpaPid> {
142
143        /**
144         * See loadResourcesByPid
145         * for an explanation of why we use the constant 800
146         */
147        // NB: keep public
148        @Deprecated
149        public static final int MAXIMUM_PAGE_SIZE = SearchConstants.MAX_PAGE_SIZE;
150
151        public static final int MAXIMUM_PAGE_SIZE_FOR_TESTING = 50;
152        public static final String RESOURCE_ID_ALIAS = "resource_id";
153        public static final String RESOURCE_VERSION_ALIAS = "resource_version";
154        private static final Logger ourLog = LoggerFactory.getLogger(SearchBuilder.class);
155        private static final JpaPid NO_MORE = JpaPid.fromId(-1L);
156        private static final String MY_TARGET_RESOURCE_PID = "myTargetResourcePid";
157        private static final String MY_SOURCE_RESOURCE_PID = "mySourceResourcePid";
158        private static final String MY_TARGET_RESOURCE_TYPE = "myTargetResourceType";
159        private static final String MY_SOURCE_RESOURCE_TYPE = "mySourceResourceType";
160        private static final String MY_TARGET_RESOURCE_VERSION = "myTargetResourceVersion";
161        public static boolean myUseMaxPageSize50ForTest = false;
162        protected final IInterceptorBroadcaster myInterceptorBroadcaster;
163        protected final IResourceTagDao myResourceTagDao;
164        private final String myResourceName;
165        private final Class<? extends IBaseResource> myResourceType;
166        private final HapiFhirLocalContainerEntityManagerFactoryBean myEntityManagerFactory;
167        private final SqlObjectFactory mySqlBuilderFactory;
168        private final HibernatePropertiesProvider myDialectProvider;
169        private final ISearchParamRegistry mySearchParamRegistry;
170        private final PartitionSettings myPartitionSettings;
171        private final DaoRegistry myDaoRegistry;
172        private final IResourceSearchViewDao myResourceSearchViewDao;
173        private final FhirContext myContext;
174        private final IIdHelperService<JpaPid> myIdHelperService;
175        private final JpaStorageSettings myStorageSettings;
176        private final IDao myCallingDao;
177
178        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
179        protected EntityManager myEntityManager;
180
181        private List<JpaPid> myAlsoIncludePids;
182        private CriteriaBuilder myCriteriaBuilder;
183        private SearchParameterMap myParams;
184        private String mySearchUuid;
185        private int myFetchSize;
186        private Integer myMaxResultsToFetch;
187        private Set<JpaPid> myPidSet;
188        private boolean myHasNextIteratorQuery = false;
189        private RequestPartitionId myRequestPartitionId;
190
191        @Autowired(required = false)
192        private IFulltextSearchSvc myFulltextSearchSvc;
193
194        @Autowired(required = false)
195        private IElasticsearchSvc myIElasticsearchSvc;
196
197        @Autowired
198        private IJpaStorageResourceParser myJpaStorageResourceParser;
199
200        /**
201         * Constructor
202         */
203        public SearchBuilder(
204                        IDao theDao,
205                        String theResourceName,
206                        JpaStorageSettings theStorageSettings,
207                        HapiFhirLocalContainerEntityManagerFactoryBean theEntityManagerFactory,
208                        SqlObjectFactory theSqlBuilderFactory,
209                        HibernatePropertiesProvider theDialectProvider,
210                        ISearchParamRegistry theSearchParamRegistry,
211                        PartitionSettings thePartitionSettings,
212                        IInterceptorBroadcaster theInterceptorBroadcaster,
213                        IResourceTagDao theResourceTagDao,
214                        DaoRegistry theDaoRegistry,
215                        IResourceSearchViewDao theResourceSearchViewDao,
216                        FhirContext theContext,
217                        IIdHelperService theIdHelperService,
218                        Class<? extends IBaseResource> theResourceType) {
219                myCallingDao = theDao;
220                myResourceName = theResourceName;
221                myResourceType = theResourceType;
222                myStorageSettings = theStorageSettings;
223
224                myEntityManagerFactory = theEntityManagerFactory;
225                mySqlBuilderFactory = theSqlBuilderFactory;
226                myDialectProvider = theDialectProvider;
227                mySearchParamRegistry = theSearchParamRegistry;
228                myPartitionSettings = thePartitionSettings;
229                myInterceptorBroadcaster = theInterceptorBroadcaster;
230                myResourceTagDao = theResourceTagDao;
231                myDaoRegistry = theDaoRegistry;
232                myResourceSearchViewDao = theResourceSearchViewDao;
233                myContext = theContext;
234                myIdHelperService = theIdHelperService;
235        }
236
237        @Override
238        public void setMaxResultsToFetch(Integer theMaxResultsToFetch) {
239                myMaxResultsToFetch = theMaxResultsToFetch;
240        }
241
242        private void searchForIdsWithAndOr(
243                        SearchQueryBuilder theSearchSqlBuilder,
244                        QueryStack theQueryStack,
245                        @Nonnull SearchParameterMap theParams,
246                        RequestDetails theRequest) {
247                myParams = theParams;
248
249                // Remove any empty parameters
250                theParams.clean();
251
252                // For DSTU3, pull out near-distance first so when it comes time to evaluate near, we already know the distance
253                if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) {
254                        Dstu3DistanceHelper.setNearDistance(myResourceType, theParams);
255                }
256
257                // Attempt to lookup via composite unique key.
258                if (isCompositeUniqueSpCandidate()) {
259                        attemptComboUniqueSpProcessing(theQueryStack, theParams, theRequest);
260                }
261
262                SearchContainedModeEnum searchContainedMode = theParams.getSearchContainedMode();
263
264                // Handle _id and _tag last, since they can typically be tacked onto a different parameter
265                List<String> paramNames = myParams.keySet().stream()
266                                .filter(t -> !t.equals(IAnyResource.SP_RES_ID))
267                                .filter(t -> !t.equals(Constants.PARAM_TAG))
268                                .collect(Collectors.toList());
269                if (myParams.containsKey(IAnyResource.SP_RES_ID)) {
270                        paramNames.add(IAnyResource.SP_RES_ID);
271                }
272                if (myParams.containsKey(Constants.PARAM_TAG)) {
273                        paramNames.add(Constants.PARAM_TAG);
274                }
275
276                // Handle each parameter
277                for (String nextParamName : paramNames) {
278                        if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) {
279                                // Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by
280                                // Elasticsearch
281                                continue;
282                        }
283                        List<List<IQueryParameterType>> andOrParams = myParams.get(nextParamName);
284                        Condition predicate = theQueryStack.searchForIdsWithAndOr(
285                                        null,
286                                        myResourceName,
287                                        nextParamName,
288                                        andOrParams,
289                                        theRequest,
290                                        myRequestPartitionId,
291                                        searchContainedMode);
292                        if (predicate != null) {
293                                theSearchSqlBuilder.addPredicate(predicate);
294                        }
295                }
296        }
297
298        /**
299         * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the
300         * parameters all have no modifiers.
301         */
302        private boolean isCompositeUniqueSpCandidate() {
303                return myStorageSettings.isUniqueIndexesEnabled()
304                                && myParams.getEverythingMode() == null
305                                && myParams.isAllParametersHaveNoModifier();
306        }
307
308        @SuppressWarnings("ConstantConditions")
309        @Override
310        public Long createCountQuery(
311                        SearchParameterMap theParams,
312                        String theSearchUuid,
313                        RequestDetails theRequest,
314                        @Nonnull RequestPartitionId theRequestPartitionId) {
315
316                assert theRequestPartitionId != null;
317                assert TransactionSynchronizationManager.isActualTransactionActive();
318
319                init(theParams, theSearchUuid, theRequestPartitionId);
320
321                if (checkUseHibernateSearch()) {
322                        long count = myFulltextSearchSvc.count(myResourceName, theParams.clone());
323                        return count;
324                }
325
326                List<ISearchQueryExecutor> queries = createQuery(theParams.clone(), null, null, null, true, theRequest, null);
327                if (queries.isEmpty()) {
328                        return 0L;
329                } else {
330                        return queries.get(0).next();
331                }
332        }
333
334        /**
335         * @param thePidSet May be null
336         */
337        @Override
338        public void setPreviouslyAddedResourcePids(@Nonnull List<JpaPid> thePidSet) {
339                myPidSet = new HashSet<>(thePidSet);
340        }
341
342        @SuppressWarnings("ConstantConditions")
343        @Override
344        public IResultIterator createQuery(
345                        SearchParameterMap theParams,
346                        SearchRuntimeDetails theSearchRuntimeDetails,
347                        RequestDetails theRequest,
348                        @Nonnull RequestPartitionId theRequestPartitionId) {
349                assert theRequestPartitionId != null;
350                assert TransactionSynchronizationManager.isActualTransactionActive();
351
352                init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId);
353
354                if (myPidSet == null) {
355                        myPidSet = new HashSet<>();
356                }
357
358                return new QueryIterator(theSearchRuntimeDetails, theRequest);
359        }
360
361        private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) {
362                myCriteriaBuilder = myEntityManager.getCriteriaBuilder();
363                // we mutate the params.  Make a private copy.
364                myParams = theParams.clone();
365                mySearchUuid = theSearchUuid;
366                myRequestPartitionId = theRequestPartitionId;
367        }
368
369        private List<ISearchQueryExecutor> createQuery(
370                        SearchParameterMap theParams,
371                        SortSpec sort,
372                        Integer theOffset,
373                        Integer theMaximumResults,
374                        boolean theCountOnlyFlag,
375                        RequestDetails theRequest,
376                        SearchRuntimeDetails theSearchRuntimeDetails) {
377
378                ArrayList<ISearchQueryExecutor> queries = new ArrayList<>();
379
380                if (checkUseHibernateSearch()) {
381                        // we're going to run at least part of the search against the Fulltext service.
382
383                        // Ugh - we have two different return types for now
384                        ISearchQueryExecutor fulltextExecutor = null;
385                        List<JpaPid> fulltextMatchIds = null;
386                        int resultCount = 0;
387                        if (myParams.isLastN()) {
388                                fulltextMatchIds = executeLastNAgainstIndex(theMaximumResults);
389                                resultCount = fulltextMatchIds.size();
390                        } else if (myParams.getEverythingMode() != null) {
391                                fulltextMatchIds = queryHibernateSearchForEverythingPids(theRequest);
392                                resultCount = fulltextMatchIds.size();
393                        } else {
394                                fulltextExecutor = myFulltextSearchSvc.searchNotScrolled(
395                                                myResourceName, myParams, myMaxResultsToFetch, theRequest);
396                        }
397
398                        if (fulltextExecutor == null) {
399                                fulltextExecutor = SearchQueryExecutors.from(fulltextMatchIds);
400                        }
401
402                        if (theSearchRuntimeDetails != null) {
403                                theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount);
404                                HookParams params = new HookParams()
405                                                .add(RequestDetails.class, theRequest)
406                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
407                                                .add(SearchRuntimeDetails.class, theSearchRuntimeDetails);
408                                CompositeInterceptorBroadcaster.doCallHooks(
409                                                myInterceptorBroadcaster,
410                                                theRequest,
411                                                Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE,
412                                                params);
413                        }
414
415                        // can we skip the database entirely and return the pid list from here?
416                        boolean canSkipDatabase =
417                                        // if we processed an AND clause, and it returned nothing, then nothing can match.
418                                        !fulltextExecutor.hasNext()
419                                                        ||
420                                                        // Our hibernate search query doesn't respect partitions yet
421                                                        (!myPartitionSettings.isPartitioningEnabled()
422                                                                        &&
423                                                                        // were there AND terms left?  Then we still need the db.
424                                                                        theParams.isEmpty()
425                                                                        &&
426                                                                        // not every param is a param. :-(
427                                                                        theParams.getNearDistanceParam() == null
428                                                                        &&
429                                                                        // todo MB don't we support _lastUpdated and _offset now?
430                                                                        theParams.getLastUpdated() == null
431                                                                        && theParams.getEverythingMode() == null
432                                                                        && theParams.getOffset() == null);
433
434                        if (canSkipDatabase) {
435                                ourLog.trace("Query finished after HSearch.  Skip db query phase");
436                                if (theMaximumResults != null) {
437                                        fulltextExecutor = SearchQueryExecutors.limited(fulltextExecutor, theMaximumResults);
438                                }
439                                queries.add(fulltextExecutor);
440                        } else {
441                                ourLog.trace("Query needs db after HSearch.  Chunking.");
442                                // Finish the query in the database for the rest of the search parameters, sorting, partitioning, etc.
443                                // We break the pids into chunks that fit in the 1k limit for jdbc bind params.
444                                new QueryChunker<Long>()
445                                                .chunk(
446                                                                Streams.stream(fulltextExecutor).collect(Collectors.toList()),
447                                                                t -> doCreateChunkedQueries(
448                                                                                theParams, t, theOffset, sort, theCountOnlyFlag, theRequest, queries));
449                        }
450                } else {
451                        // do everything in the database.
452                        Optional<SearchQueryExecutor> query = createChunkedQuery(
453                                        theParams, sort, theOffset, theMaximumResults, theCountOnlyFlag, theRequest, null);
454                        query.ifPresent(queries::add);
455                }
456
457                return queries;
458        }
459
460        /**
461         * Check to see if query should use Hibernate Search, and error if the query can't continue.
462         *
463         * @return true if the query should first be processed by Hibernate Search
464         * @throws InvalidRequestException if fulltext search is not enabled but the query requires it - _content or _text
465         */
466        private boolean checkUseHibernateSearch() {
467                boolean fulltextEnabled = (myFulltextSearchSvc != null) && !myFulltextSearchSvc.isDisabled();
468
469                if (!fulltextEnabled) {
470                        failIfUsed(Constants.PARAM_TEXT);
471                        failIfUsed(Constants.PARAM_CONTENT);
472                }
473
474                // someday we'll want a query planner to figure out if we _should_ or _must_ use the ft index, not just if we
475                // can.
476                return fulltextEnabled
477                                && myParams != null
478                                && myParams.getSearchContainedMode() == SearchContainedModeEnum.FALSE
479                                && myFulltextSearchSvc.supportsSomeOf(myParams);
480        }
481
482        private void failIfUsed(String theParamName) {
483                if (myParams.containsKey(theParamName)) {
484                        throw new InvalidRequestException(Msg.code(1192)
485                                        + "Fulltext search is not enabled on this service, can not process parameter: " + theParamName);
486                }
487        }
488
489        private List<JpaPid> executeLastNAgainstIndex(Integer theMaximumResults) {
490                // Can we use our hibernate search generated index on resource to support lastN?:
491                if (myStorageSettings.isAdvancedHSearchIndexing()) {
492                        if (myFulltextSearchSvc == null) {
493                                throw new InvalidRequestException(Msg.code(2027)
494                                                + "LastN operation is not enabled on this service, can not process this request");
495                        }
496                        return myFulltextSearchSvc.lastN(myParams, theMaximumResults).stream()
497                                        .map(lastNResourceId -> myIdHelperService.resolveResourcePersistentIds(
498                                                        myRequestPartitionId, myResourceName, String.valueOf(lastNResourceId)))
499                                        .collect(Collectors.toList());
500                } else {
501                        if (myIElasticsearchSvc == null) {
502                                throw new InvalidRequestException(Msg.code(2033)
503                                                + "LastN operation is not enabled on this service, can not process this request");
504                        }
505                        // use the dedicated observation ES/Lucene index to support lastN query
506                        return myIElasticsearchSvc.executeLastN(myParams, myContext, theMaximumResults).stream()
507                                        .map(lastnResourceId -> myIdHelperService.resolveResourcePersistentIds(
508                                                        myRequestPartitionId, myResourceName, lastnResourceId))
509                                        .collect(Collectors.toList());
510                }
511        }
512
513        private List<JpaPid> queryHibernateSearchForEverythingPids(RequestDetails theRequestDetails) {
514                JpaPid pid = null;
515                if (myParams.get(IAnyResource.SP_RES_ID) != null) {
516                        String idParamValue;
517                        IQueryParameterType idParam =
518                                        myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
519                        if (idParam instanceof TokenParam) {
520                                TokenParam idParm = (TokenParam) idParam;
521                                idParamValue = idParm.getValue();
522                        } else {
523                                StringParam idParm = (StringParam) idParam;
524                                idParamValue = idParm.getValue();
525                        }
526
527                        pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue);
528                }
529                List<JpaPid> pids = myFulltextSearchSvc.everything(myResourceName, myParams, pid, theRequestDetails);
530                return pids;
531        }
532
533        private void doCreateChunkedQueries(
534                        SearchParameterMap theParams,
535                        List<Long> thePids,
536                        Integer theOffset,
537                        SortSpec sort,
538                        boolean theCount,
539                        RequestDetails theRequest,
540                        ArrayList<ISearchQueryExecutor> theQueries) {
541                if (thePids.size() < getMaximumPageSize()) {
542                        normalizeIdListForLastNInClause(thePids);
543                }
544                Optional<SearchQueryExecutor> query =
545                                createChunkedQuery(theParams, sort, theOffset, thePids.size(), theCount, theRequest, thePids);
546                query.ifPresent(t -> theQueries.add(t));
547        }
548
549        /**
550         * Combs through the params for any _id parameters and extracts the PIDs for them
551         *
552         * @param theTargetPids
553         */
554        private void extractTargetPidsFromIdParams(HashSet<Long> theTargetPids) {
555                // get all the IQueryParameterType objects
556                // for _id -> these should all be StringParam values
557                HashSet<String> ids = new HashSet<>();
558                List<List<IQueryParameterType>> params = myParams.get(IAnyResource.SP_RES_ID);
559                for (List<IQueryParameterType> paramList : params) {
560                        for (IQueryParameterType param : paramList) {
561                                if (param instanceof StringParam) {
562                                        // we expect all _id values to be StringParams
563                                        ids.add(((StringParam) param).getValue());
564                                } else if (param instanceof TokenParam) {
565                                        ids.add(((TokenParam) param).getValue());
566                                } else {
567                                        // we do not expect the _id parameter to be a non-string value
568                                        throw new IllegalArgumentException(
569                                                        Msg.code(1193) + "_id parameter must be a StringParam or TokenParam");
570                                }
571                        }
572                }
573
574                // fetch our target Pids
575                // this will throw if an id is not found
576                Map<String, JpaPid> idToPid = myIdHelperService.resolveResourcePersistentIds(
577                                myRequestPartitionId, myResourceName, new ArrayList<>(ids));
578                if (myAlsoIncludePids == null) {
579                        myAlsoIncludePids = new ArrayList<>();
580                }
581
582                // add the pids to targetPids
583                for (JpaPid pid : idToPid.values()) {
584                        myAlsoIncludePids.add(pid);
585                        theTargetPids.add(pid.getId());
586                }
587        }
588
589        private Optional<SearchQueryExecutor> createChunkedQuery(
590                        SearchParameterMap theParams,
591                        SortSpec sort,
592                        Integer theOffset,
593                        Integer theMaximumResults,
594                        boolean theCountOnlyFlag,
595                        RequestDetails theRequest,
596                        List<Long> thePidList) {
597                String sqlBuilderResourceName = myParams.getEverythingMode() == null ? myResourceName : null;
598                SearchQueryBuilder sqlBuilder = new SearchQueryBuilder(
599                                myContext,
600                                myStorageSettings,
601                                myPartitionSettings,
602                                myRequestPartitionId,
603                                sqlBuilderResourceName,
604                                mySqlBuilderFactory,
605                                myDialectProvider,
606                                theCountOnlyFlag);
607                QueryStack queryStack3 = new QueryStack(
608                                theParams, myStorageSettings, myContext, sqlBuilder, mySearchParamRegistry, myPartitionSettings);
609
610                if (theParams.keySet().size() > 1
611                                || theParams.getSort() != null
612                                || theParams.keySet().contains(Constants.PARAM_HAS)
613                                || isPotentiallyContainedReferenceParameterExistsAtRoot(theParams)) {
614                        List<RuntimeSearchParam> activeComboParams =
615                                        mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet());
616                        if (activeComboParams.isEmpty()) {
617                                sqlBuilder.setNeedResourceTableRoot(true);
618                        }
619                }
620
621                JdbcTemplate jdbcTemplate = new JdbcTemplate(myEntityManagerFactory.getDataSource());
622                jdbcTemplate.setFetchSize(myFetchSize);
623                if (theMaximumResults != null) {
624                        jdbcTemplate.setMaxRows(theMaximumResults);
625                }
626
627                if (myParams.getEverythingMode() != null) {
628                        HashSet<Long> targetPids = new HashSet<>();
629                        if (myParams.get(IAnyResource.SP_RES_ID) != null) {
630                                extractTargetPidsFromIdParams(targetPids);
631                        } else {
632                                // For Everything queries, we make the query root by the ResourceLink table, since this query
633                                // is basically a reverse-include search. For type/Everything (as opposed to instance/Everything)
634                                // the one problem with this approach is that it doesn't catch Patients that have absolutely
635                                // nothing linked to them. So we do one additional query to make sure we catch those too.
636                                SearchQueryBuilder fetchPidsSqlBuilder = new SearchQueryBuilder(
637                                                myContext,
638                                                myStorageSettings,
639                                                myPartitionSettings,
640                                                myRequestPartitionId,
641                                                myResourceName,
642                                                mySqlBuilderFactory,
643                                                myDialectProvider,
644                                                theCountOnlyFlag);
645                                GeneratedSql allTargetsSql = fetchPidsSqlBuilder.generate(theOffset, myMaxResultsToFetch);
646                                String sql = allTargetsSql.getSql();
647                                Object[] args = allTargetsSql.getBindVariables().toArray(new Object[0]);
648                                List<Long> output = jdbcTemplate.query(sql, args, new SingleColumnRowMapper<>(Long.class));
649                                if (myAlsoIncludePids == null) {
650                                        myAlsoIncludePids = new ArrayList<>(output.size());
651                                }
652                                myAlsoIncludePids.addAll(JpaPid.fromLongList(output));
653                        }
654
655                        List<String> typeSourceResources = new ArrayList<>();
656                        if (myParams.get(Constants.PARAM_TYPE) != null) {
657                                typeSourceResources.addAll(extractTypeSourceResourcesFromParams());
658                        }
659
660                        queryStack3.addPredicateEverythingOperation(
661                                        myResourceName, typeSourceResources, targetPids.toArray(new Long[0]));
662                } else {
663                        /*
664                         * If we're doing a filter, always use the resource table as the root - This avoids the possibility of
665                         * specific filters with ORs as their root from working around the natural resource type / deletion
666                         * status / partition IDs built into queries.
667                         */
668                        if (theParams.containsKey(Constants.PARAM_FILTER)) {
669                                Condition partitionIdPredicate = sqlBuilder
670                                                .getOrCreateResourceTablePredicateBuilder()
671                                                .createPartitionIdPredicate(myRequestPartitionId);
672                                if (partitionIdPredicate != null) {
673                                        sqlBuilder.addPredicate(partitionIdPredicate);
674                                }
675                        }
676
677                        // Normal search
678                        searchForIdsWithAndOr(sqlBuilder, queryStack3, myParams, theRequest);
679                }
680
681                // If we haven't added any predicates yet, we're doing a search for all resources. Make sure we add the
682                // partition ID predicate in that case.
683                if (!sqlBuilder.haveAtLeastOnePredicate()) {
684                        Condition partitionIdPredicate = sqlBuilder
685                                        .getOrCreateResourceTablePredicateBuilder()
686                                        .createPartitionIdPredicate(myRequestPartitionId);
687                        if (partitionIdPredicate != null) {
688                                sqlBuilder.addPredicate(partitionIdPredicate);
689                        }
690                }
691
692                // Add PID list predicate for full text search and/or lastn operation
693                if (thePidList != null && thePidList.size() > 0) {
694                        sqlBuilder.addResourceIdsPredicate(thePidList);
695                }
696
697                // Last updated
698                DateRangeParam lu = myParams.getLastUpdated();
699                if (lu != null && !lu.isEmpty()) {
700                        Condition lastUpdatedPredicates = sqlBuilder.addPredicateLastUpdated(lu);
701                        sqlBuilder.addPredicate(lastUpdatedPredicates);
702                }
703
704                /*
705                 * Exclude the pids already in the previous iterator. This is an optimization, as opposed
706                 * to something needed to guarantee correct results.
707                 *
708                 * Why do we need it? Suppose for example, a query like:
709                 *    Observation?category=foo,bar,baz
710                 * And suppose you have many resources that have all 3 of these category codes. In this case
711                 * the SQL query will probably return the same PIDs multiple times, and if this happens enough
712                 * we may exhaust the query results without getting enough distinct results back. When that
713                 * happens we re-run the query with a larger limit. Excluding results we already know about
714                 * tries to ensure that we get new unique results.
715                 *
716                 * The challenge with that though is that lots of DBs have an issue with too many
717                 * parameters in one query. So we only do this optimization if there aren't too
718                 * many results.
719                 */
720                if (myHasNextIteratorQuery) {
721                        if (myPidSet.size() + sqlBuilder.countBindVariables() < 900) {
722                                sqlBuilder.excludeResourceIdsPredicate(myPidSet);
723                        }
724                }
725
726                /*
727                 * If offset is present, we want deduplicate the results by using GROUP BY
728                 */
729                if (theOffset != null) {
730                        queryStack3.addGrouping();
731                        queryStack3.setUseAggregate(true);
732                }
733
734                /*
735                 * Sort
736                 *
737                 * If we have a sort, we wrap the criteria search (the search that actually
738                 * finds the appropriate resources) in an outer search which is then sorted
739                 */
740                if (sort != null) {
741                        assert !theCountOnlyFlag;
742
743                        createSort(queryStack3, sort, theParams);
744                }
745
746                /*
747                 * Now perform the search
748                 */
749                GeneratedSql generatedSql = sqlBuilder.generate(theOffset, myMaxResultsToFetch);
750                if (generatedSql.isMatchNothing()) {
751                        return Optional.empty();
752                }
753
754                SearchQueryExecutor executor = mySqlBuilderFactory.newSearchQueryExecutor(generatedSql, myMaxResultsToFetch);
755                return Optional.of(executor);
756        }
757
758        private Collection<String> extractTypeSourceResourcesFromParams() {
759
760                List<List<IQueryParameterType>> listOfList = myParams.get(Constants.PARAM_TYPE);
761
762                // first off, let's flatten the list of list
763                List<IQueryParameterType> iQueryParameterTypesList =
764                                listOfList.stream().flatMap(List::stream).collect(Collectors.toList());
765
766                // then, extract all elements of each CSV into one big list
767                List<String> resourceTypes = iQueryParameterTypesList.stream()
768                                .map(param -> ((StringParam) param).getValue())
769                                .map(csvString -> List.of(csvString.split(",")))
770                                .flatMap(List::stream)
771                                .collect(Collectors.toList());
772
773                Set<String> knownResourceTypes = myContext.getResourceTypes();
774
775                // remove leading/trailing whitespaces if any and remove duplicates
776                Set<String> retVal = new HashSet<>();
777
778                for (String type : resourceTypes) {
779                        String trimmed = type.trim();
780                        if (!knownResourceTypes.contains(trimmed)) {
781                                throw new ResourceNotFoundException(
782                                                Msg.code(2197) + "Unknown resource type '" + trimmed + "' in _type parameter.");
783                        }
784                        retVal.add(trimmed);
785                }
786
787                return retVal;
788        }
789
790        private boolean isPotentiallyContainedReferenceParameterExistsAtRoot(SearchParameterMap theParams) {
791                return myStorageSettings.isIndexOnContainedResources()
792                                && theParams.values().stream()
793                                                .flatMap(Collection::stream)
794                                                .flatMap(Collection::stream)
795                                                .anyMatch(t -> t instanceof ReferenceParam);
796        }
797
798        private List<Long> normalizeIdListForLastNInClause(List<Long> lastnResourceIds) {
799                /*
800                The following is a workaround to a known issue involving Hibernate. If queries are used with "in" clauses with large and varying
801                numbers of parameters, this can overwhelm Hibernate's QueryPlanCache and deplete heap space. See the following link for more info:
802                https://stackoverflow.com/questions/31557076/spring-hibernate-query-plan-cache-memory-usage.
803
804                Normalizing the number of parameters in the "in" clause stabilizes the size of the QueryPlanCache, so long as the number of
805                arguments never exceeds the maximum specified below.
806                */
807                int listSize = lastnResourceIds.size();
808
809                if (listSize > 1 && listSize < 10) {
810                        padIdListWithPlaceholders(lastnResourceIds, 10);
811                } else if (listSize > 10 && listSize < 50) {
812                        padIdListWithPlaceholders(lastnResourceIds, 50);
813                } else if (listSize > 50 && listSize < 100) {
814                        padIdListWithPlaceholders(lastnResourceIds, 100);
815                } else if (listSize > 100 && listSize < 200) {
816                        padIdListWithPlaceholders(lastnResourceIds, 200);
817                } else if (listSize > 200 && listSize < 500) {
818                        padIdListWithPlaceholders(lastnResourceIds, 500);
819                } else if (listSize > 500 && listSize < 800) {
820                        padIdListWithPlaceholders(lastnResourceIds, 800);
821                }
822
823                return lastnResourceIds;
824        }
825
826        private void padIdListWithPlaceholders(List<Long> theIdList, int preferredListSize) {
827                while (theIdList.size() < preferredListSize) {
828                        theIdList.add(-1L);
829                }
830        }
831
832        private void createSort(QueryStack theQueryStack, SortSpec theSort, SearchParameterMap theParams) {
833                if (theSort == null || isBlank(theSort.getParamName())) {
834                        return;
835                }
836
837                boolean ascending = (theSort.getOrder() == null) || (theSort.getOrder() == SortOrderEnum.ASC);
838
839                if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) {
840
841                        theQueryStack.addSortOnResourceId(ascending);
842
843                } else if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) {
844
845                        theQueryStack.addSortOnLastUpdated(ascending);
846
847                } else {
848
849                        RuntimeSearchParam param = null;
850
851                        /*
852                         * If we have a sort like _sort=subject.name and we  have an
853                         * uplifted refchain for that combination we can do it more efficiently
854                         * by using the index associated with the uplifted refchain. In this case,
855                         * we need to find the actual target search parameter (corresponding
856                         * to "name" in this example) so that we know what datatype it is.
857                         */
858                        String paramName = theSort.getParamName();
859                        if (myStorageSettings.isIndexOnUpliftedRefchains()) {
860                                String[] chains = StringUtils.split(paramName, '.');
861                                if (chains.length == 2) {
862
863                                        // Given: Encounter?_sort=Patient:subject.name
864                                        String referenceParam = chains[0]; // subject
865                                        String referenceParamTargetType = null; // Patient
866                                        String targetParam = chains[1]; // name
867
868                                        int colonIdx = referenceParam.indexOf(':');
869                                        if (colonIdx > -1) {
870                                                referenceParamTargetType = referenceParam.substring(0, colonIdx);
871                                                referenceParam = referenceParam.substring(colonIdx + 1);
872                                        }
873                                        RuntimeSearchParam outerParam =
874                                                        mySearchParamRegistry.getActiveSearchParam(myResourceName, referenceParam);
875                                        if (outerParam == null) {
876                                                throwInvalidRequestExceptionForUnknownSortParameter(myResourceName, referenceParam);
877                                        }
878
879                                        if (outerParam.hasUpliftRefchain(targetParam)) {
880                                                for (String nextTargetType : outerParam.getTargets()) {
881                                                        if (referenceParamTargetType != null && !referenceParamTargetType.equals(nextTargetType)) {
882                                                                continue;
883                                                        }
884                                                        RuntimeSearchParam innerParam =
885                                                                        mySearchParamRegistry.getActiveSearchParam(nextTargetType, targetParam);
886                                                        if (innerParam != null) {
887                                                                param = innerParam;
888                                                                break;
889                                                        }
890                                                }
891                                        }
892                                }
893                        }
894
895                        int colonIdx = paramName.indexOf(':');
896                        String referenceTargetType = null;
897                        if (colonIdx > -1) {
898                                referenceTargetType = paramName.substring(0, colonIdx);
899                                paramName = paramName.substring(colonIdx + 1);
900                        }
901
902                        int dotIdx = paramName.indexOf('.');
903                        String chainName = null;
904                        if (param == null && dotIdx > -1) {
905                                chainName = paramName.substring(dotIdx + 1);
906                                paramName = paramName.substring(0, dotIdx);
907                                if (chainName.contains(".")) {
908                                        String msg = myContext
909                                                        .getLocalizer()
910                                                        .getMessageSanitized(
911                                                                        BaseStorageDao.class,
912                                                                        "invalidSortParameterTooManyChains",
913                                                                        paramName + "." + chainName);
914                                        throw new InvalidRequestException(Msg.code(2286) + msg);
915                                }
916                        }
917
918                        if (param == null) {
919                                param = mySearchParamRegistry.getActiveSearchParam(myResourceName, paramName);
920                        }
921
922                        if (param == null) {
923                                throwInvalidRequestExceptionForUnknownSortParameter(getResourceName(), paramName);
924                        }
925
926                        if (isNotBlank(chainName) && param.getParamType() != RestSearchParameterTypeEnum.REFERENCE) {
927                                throw new InvalidRequestException(
928                                                Msg.code(2285) + "Invalid chain, " + paramName + " is not a reference SearchParameter");
929                        }
930
931                        switch (param.getParamType()) {
932                                case STRING:
933                                        theQueryStack.addSortOnString(myResourceName, paramName, ascending);
934                                        break;
935                                case DATE:
936                                        theQueryStack.addSortOnDate(myResourceName, paramName, ascending);
937                                        break;
938                                case REFERENCE:
939                                        theQueryStack.addSortOnResourceLink(
940                                                        myResourceName, referenceTargetType, paramName, chainName, ascending);
941                                        break;
942                                case TOKEN:
943                                        theQueryStack.addSortOnToken(myResourceName, paramName, ascending);
944                                        break;
945                                case NUMBER:
946                                        theQueryStack.addSortOnNumber(myResourceName, paramName, ascending);
947                                        break;
948                                case URI:
949                                        theQueryStack.addSortOnUri(myResourceName, paramName, ascending);
950                                        break;
951                                case QUANTITY:
952                                        theQueryStack.addSortOnQuantity(myResourceName, paramName, ascending);
953                                        break;
954                                case COMPOSITE:
955                                        List<RuntimeSearchParam> compositeList =
956                                                        JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, param);
957                                        if (compositeList == null) {
958                                                throw new InvalidRequestException(Msg.code(1195) + "The composite _sort parameter " + paramName
959                                                                + " is not defined by the resource " + myResourceName);
960                                        }
961                                        if (compositeList.size() != 2) {
962                                                throw new InvalidRequestException(Msg.code(1196) + "The composite _sort parameter " + paramName
963                                                                + " must have 2 composite types declared in parameter annotation, found "
964                                                                + compositeList.size());
965                                        }
966                                        RuntimeSearchParam left = compositeList.get(0);
967                                        RuntimeSearchParam right = compositeList.get(1);
968
969                                        createCompositeSort(theQueryStack, left.getParamType(), left.getName(), ascending);
970                                        createCompositeSort(theQueryStack, right.getParamType(), right.getName(), ascending);
971
972                                        break;
973                                case SPECIAL:
974                                        if (LOCATION_POSITION.equals(param.getPath())) {
975                                                theQueryStack.addSortOnCoordsNear(paramName, ascending, theParams);
976                                                break;
977                                        }
978                                        throw new InvalidRequestException(
979                                                        Msg.code(2306) + "This server does not support _sort specifications of type "
980                                                                        + param.getParamType() + " - Can't serve _sort=" + paramName);
981
982                                case HAS:
983                                default:
984                                        throw new InvalidRequestException(
985                                                        Msg.code(1197) + "This server does not support _sort specifications of type "
986                                                                        + param.getParamType() + " - Can't serve _sort=" + paramName);
987                        }
988                }
989
990                // Recurse
991                createSort(theQueryStack, theSort.getChain(), theParams);
992        }
993
994        private void throwInvalidRequestExceptionForUnknownSortParameter(String theResourceName, String theParamName) {
995                Collection<String> validSearchParameterNames =
996                                mySearchParamRegistry.getValidSearchParameterNamesIncludingMeta(theResourceName);
997                String msg = myContext
998                                .getLocalizer()
999                                .getMessageSanitized(
1000                                                BaseStorageDao.class,
1001                                                "invalidSortParameter",
1002                                                theParamName,
1003                                                theResourceName,
1004                                                validSearchParameterNames);
1005                throw new InvalidRequestException(Msg.code(1194) + msg);
1006        }
1007
1008        private void createCompositeSort(
1009                        QueryStack theQueryStack,
1010                        RestSearchParameterTypeEnum theParamType,
1011                        String theParamName,
1012                        boolean theAscending) {
1013
1014                switch (theParamType) {
1015                        case STRING:
1016                                theQueryStack.addSortOnString(myResourceName, theParamName, theAscending);
1017                                break;
1018                        case DATE:
1019                                theQueryStack.addSortOnDate(myResourceName, theParamName, theAscending);
1020                                break;
1021                        case TOKEN:
1022                                theQueryStack.addSortOnToken(myResourceName, theParamName, theAscending);
1023                                break;
1024                        case QUANTITY:
1025                                theQueryStack.addSortOnQuantity(myResourceName, theParamName, theAscending);
1026                                break;
1027                        case NUMBER:
1028                        case REFERENCE:
1029                        case COMPOSITE:
1030                        case URI:
1031                        case HAS:
1032                        case SPECIAL:
1033                        default:
1034                                throw new InvalidRequestException(
1035                                                Msg.code(1198) + "Don't know how to handle composite parameter with type of " + theParamType
1036                                                                + " on _sort=" + theParamName);
1037                }
1038        }
1039
1040        private void doLoadPids(
1041                        Collection<JpaPid> thePids,
1042                        Collection<JpaPid> theIncludedPids,
1043                        List<IBaseResource> theResourceListToPopulate,
1044                        boolean theForHistoryOperation,
1045                        Map<JpaPid, Integer> thePosition) {
1046
1047                Map<Long, Long> resourcePidToVersion = null;
1048                for (JpaPid next : thePids) {
1049                        if (next.getVersion() != null && myStorageSettings.isRespectVersionsForSearchIncludes()) {
1050                                if (resourcePidToVersion == null) {
1051                                        resourcePidToVersion = new HashMap<>();
1052                                }
1053                                resourcePidToVersion.put((next).getId(), next.getVersion());
1054                        }
1055                }
1056
1057                List<Long> versionlessPids = JpaPid.toLongList(thePids);
1058                if (versionlessPids.size() < getMaximumPageSize()) {
1059                        versionlessPids = normalizeIdListForLastNInClause(versionlessPids);
1060                }
1061
1062                // -- get the resource from the searchView
1063                Collection<ResourceSearchView> resourceSearchViewList =
1064                                myResourceSearchViewDao.findByResourceIds(versionlessPids);
1065
1066                // -- preload all tags with tag definition if any
1067                Map<Long, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
1068
1069                for (IBaseResourceEntity next : resourceSearchViewList) {
1070                        if (next.getDeleted() != null) {
1071                                continue;
1072                        }
1073
1074                        Class<? extends IBaseResource> resourceType =
1075                                        myContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
1076
1077                        JpaPid resourceId = JpaPid.fromId(next.getResourceId());
1078
1079                        /*
1080                         * If a specific version is requested via an include, we'll replace the current version
1081                         * with the specific desired version. This is not the most efficient thing, given that
1082                         * we're loading the current version and then turning around and throwing it away again.
1083                         * This could be optimized and probably should be, but it's not critical given that
1084                         * this only applies to includes, which don't tend to be massive in numbers.
1085                         */
1086                        if (resourcePidToVersion != null) {
1087                                Long version = resourcePidToVersion.get(next.getResourceId());
1088                                resourceId.setVersion(version);
1089                                if (version != null && !version.equals(next.getVersion())) {
1090                                        IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(resourceType);
1091                                        next = (IBaseResourceEntity)
1092                                                        dao.readEntity(next.getIdDt().withVersion(Long.toString(version)), null);
1093                                }
1094                        }
1095
1096                        IBaseResource resource = null;
1097                        if (next != null) {
1098                                resource = myJpaStorageResourceParser.toResource(
1099                                                resourceType, next, tagMap.get(next.getId()), theForHistoryOperation);
1100                        }
1101                        if (resource == null) {
1102                                ourLog.warn(
1103                                                "Unable to find resource {}/{}/_history/{} in database",
1104                                                next.getResourceType(),
1105                                                next.getIdDt().getIdPart(),
1106                                                next.getVersion());
1107                                continue;
1108                        }
1109
1110                        Integer index = thePosition.get(resourceId);
1111                        if (index == null) {
1112                                ourLog.warn("Got back unexpected resource PID {}", resourceId);
1113                                continue;
1114                        }
1115
1116                        if (theIncludedPids.contains(resourceId)) {
1117                                ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.INCLUDE);
1118                        } else {
1119                                ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(resource, BundleEntrySearchModeEnum.MATCH);
1120                        }
1121
1122                        theResourceListToPopulate.set(index, resource);
1123                }
1124        }
1125
1126        private Map<Long, Collection<ResourceTag>> getResourceTagMap(
1127                        Collection<? extends IBaseResourceEntity> theResourceSearchViewList) {
1128
1129                List<Long> idList = new ArrayList<>(theResourceSearchViewList.size());
1130
1131                // -- find all resource has tags
1132                for (IBaseResourceEntity resource : theResourceSearchViewList) {
1133                        if (resource.isHasTags()) idList.add(resource.getId());
1134                }
1135
1136                return getPidToTagMap(idList);
1137        }
1138
1139        @Nonnull
1140        private Map<Long, Collection<ResourceTag>> getPidToTagMap(List<Long> thePidList) {
1141                Map<Long, Collection<ResourceTag>> tagMap = new HashMap<>();
1142
1143                // -- no tags
1144                if (thePidList.size() == 0) return tagMap;
1145
1146                // -- get all tags for the idList
1147                Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(thePidList);
1148
1149                // -- build the map, key = resourceId, value = list of ResourceTag
1150                JpaPid resourceId;
1151                Collection<ResourceTag> tagCol;
1152                for (ResourceTag tag : tagList) {
1153
1154                        resourceId = JpaPid.fromId(tag.getResourceId());
1155                        tagCol = tagMap.get(resourceId.getId());
1156                        if (tagCol == null) {
1157                                tagCol = new ArrayList<>();
1158                                tagCol.add(tag);
1159                                tagMap.put(resourceId.getId(), tagCol);
1160                        } else {
1161                                tagCol.add(tag);
1162                        }
1163                }
1164
1165                return tagMap;
1166        }
1167
1168        @Override
1169        public void loadResourcesByPid(
1170                        Collection<JpaPid> thePids,
1171                        Collection<JpaPid> theIncludedPids,
1172                        List<IBaseResource> theResourceListToPopulate,
1173                        boolean theForHistoryOperation,
1174                        RequestDetails theDetails) {
1175                if (thePids.isEmpty()) {
1176                        ourLog.debug("The include pids are empty");
1177                        // return;
1178                }
1179
1180                // Dupes will cause a crash later anyhow, but this is expensive so only do it
1181                // when running asserts
1182                assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids;
1183
1184                Map<JpaPid, Integer> position = new HashMap<>();
1185                for (JpaPid next : thePids) {
1186                        position.put(next, theResourceListToPopulate.size());
1187                        theResourceListToPopulate.add(null);
1188                }
1189
1190                // Can we fast track this loading by checking elastic search?
1191                if (isLoadingFromElasticSearchSupported(thePids)) {
1192                        try {
1193                                theResourceListToPopulate.addAll(loadResourcesFromElasticSearch(thePids));
1194                                return;
1195
1196                        } catch (ResourceNotFoundInIndexException theE) {
1197                                // some resources were not found in index, so we will inform this and resort to JPA search
1198                                ourLog.warn(
1199                                                "Some resources were not found in index. Make sure all resources were indexed. Resorting to database search.");
1200                        }
1201                }
1202
1203                // We only chunk because some jdbc drivers can't handle long param lists.
1204                new QueryChunker<JpaPid>()
1205                                .chunk(
1206                                                thePids,
1207                                                t -> doLoadPids(
1208                                                                t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position));
1209        }
1210
1211        /**
1212         * Check if we can load the resources from Hibernate Search instead of the database.
1213         * We assume this is faster.
1214         * <p>
1215         * Hibernate Search only stores the current version, and only if enabled.
1216         *
1217         * @param thePids the pids to check for versioned references
1218         * @return can we fetch from Hibernate Search?
1219         */
1220        private boolean isLoadingFromElasticSearchSupported(Collection<JpaPid> thePids) {
1221                // is storage enabled?
1222                return myStorageSettings.isStoreResourceInHSearchIndex()
1223                                && myStorageSettings.isAdvancedHSearchIndexing()
1224                                &&
1225                                // we don't support history
1226                                thePids.stream().noneMatch(p -> p.getVersion() != null)
1227                                &&
1228                                // skip the complexity for metadata in dstu2
1229                                myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3);
1230        }
1231
1232        private List<IBaseResource> loadResourcesFromElasticSearch(Collection<JpaPid> thePids) {
1233                // Do we use the fulltextsvc via hibernate-search to load resources or be backwards compatible with older ES
1234                // only impl
1235                // to handle lastN?
1236                if (myStorageSettings.isAdvancedHSearchIndexing() && myStorageSettings.isStoreResourceInHSearchIndex()) {
1237                        List<Long> pidList = thePids.stream().map(pid -> (pid).getId()).collect(Collectors.toList());
1238
1239                        List<IBaseResource> resources = myFulltextSearchSvc.getResources(pidList);
1240                        return resources;
1241                } else if (!Objects.isNull(myParams) && myParams.isLastN()) {
1242                        // legacy LastN implementation
1243                        return myIElasticsearchSvc.getObservationResources(thePids);
1244                } else {
1245                        return Collections.emptyList();
1246                }
1247        }
1248
1249        /**
1250         * THIS SHOULD RETURN HASHSET and not just Set because we add to it later
1251         * so it can't be Collections.emptySet() or some such thing.
1252         * The JpaPid returned will have resource type populated.
1253         */
1254        @Override
1255        public Set<JpaPid> loadIncludes(
1256                        FhirContext theContext,
1257                        EntityManager theEntityManager,
1258                        Collection<JpaPid> theMatches,
1259                        Collection<Include> theIncludes,
1260                        boolean theReverseMode,
1261                        DateRangeParam theLastUpdated,
1262                        String theSearchIdOrDescription,
1263                        RequestDetails theRequest,
1264                        Integer theMaxCount) {
1265                SearchBuilderLoadIncludesParameters<JpaPid> parameters = new SearchBuilderLoadIncludesParameters<>();
1266                parameters.setFhirContext(theContext);
1267                parameters.setEntityManager(theEntityManager);
1268                parameters.setMatches(theMatches);
1269                parameters.setIncludeFilters(theIncludes);
1270                parameters.setReverseMode(theReverseMode);
1271                parameters.setLastUpdated(theLastUpdated);
1272                parameters.setSearchIdOrDescription(theSearchIdOrDescription);
1273                parameters.setRequestDetails(theRequest);
1274                parameters.setMaxCount(theMaxCount);
1275                return loadIncludes(parameters);
1276        }
1277
1278        @Override
1279        public Set<JpaPid> loadIncludes(SearchBuilderLoadIncludesParameters<JpaPid> theParameters) {
1280                Collection<JpaPid> matches = theParameters.getMatches();
1281                Collection<Include> currentIncludes = theParameters.getIncludeFilters();
1282                boolean reverseMode = theParameters.isReverseMode();
1283                EntityManager entityManager = theParameters.getEntityManager();
1284                Integer maxCount = theParameters.getMaxCount();
1285                FhirContext fhirContext = theParameters.getFhirContext();
1286                DateRangeParam lastUpdated = theParameters.getLastUpdated();
1287                RequestDetails request = theParameters.getRequestDetails();
1288                String searchIdOrDescription = theParameters.getSearchIdOrDescription();
1289                List<String> desiredResourceTypes = theParameters.getDesiredResourceTypes();
1290                boolean hasDesiredResourceTypes = desiredResourceTypes != null && !desiredResourceTypes.isEmpty();
1291                if (CompositeInterceptorBroadcaster.hasHooks(
1292                                Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, theParameters.getRequestDetails())) {
1293                        CurrentThreadCaptureQueriesListener.startCapturing();
1294                }
1295                if (matches.size() == 0) {
1296                        return new HashSet<>();
1297                }
1298                if (currentIncludes == null || currentIncludes.isEmpty()) {
1299                        return new HashSet<>();
1300                }
1301                String searchPidFieldName = reverseMode ? MY_TARGET_RESOURCE_PID : MY_SOURCE_RESOURCE_PID;
1302                String findPidFieldName = reverseMode ? MY_SOURCE_RESOURCE_PID : MY_TARGET_RESOURCE_PID;
1303                String findResourceTypeFieldName = reverseMode ? MY_SOURCE_RESOURCE_TYPE : MY_TARGET_RESOURCE_TYPE;
1304                String findVersionFieldName = null;
1305                if (!reverseMode && myStorageSettings.isRespectVersionsForSearchIncludes()) {
1306                        findVersionFieldName = MY_TARGET_RESOURCE_VERSION;
1307                }
1308
1309                List<JpaPid> nextRoundMatches = new ArrayList<>(matches);
1310                HashSet<JpaPid> allAdded = new HashSet<>();
1311                HashSet<JpaPid> original = new HashSet<>(matches);
1312                ArrayList<Include> includes = new ArrayList<>(currentIncludes);
1313
1314                int roundCounts = 0;
1315                StopWatch w = new StopWatch();
1316
1317                boolean addedSomeThisRound;
1318                do {
1319                        roundCounts++;
1320
1321                        HashSet<JpaPid> pidsToInclude = new HashSet<>();
1322
1323                        for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
1324                                Include nextInclude = iter.next();
1325                                if (nextInclude.isRecurse() == false) {
1326                                        iter.remove();
1327                                }
1328
1329                                // Account for _include=*
1330                                boolean matchAll = "*".equals(nextInclude.getValue());
1331
1332                                // Account for _include=[resourceType]:*
1333                                String wantResourceType = null;
1334                                if (!matchAll) {
1335                                        if ("*".equals(nextInclude.getParamName())) {
1336                                                wantResourceType = nextInclude.getParamType();
1337                                                matchAll = true;
1338                                        }
1339                                }
1340
1341                                if (matchAll) {
1342                                        StringBuilder sqlBuilder = new StringBuilder();
1343                                        sqlBuilder.append("SELECT r.").append(findPidFieldName);
1344                                        sqlBuilder.append(", r.").append(findResourceTypeFieldName);
1345                                        if (findVersionFieldName != null) {
1346                                                sqlBuilder.append(", r.").append(findVersionFieldName);
1347                                        }
1348                                        sqlBuilder.append(" FROM ResourceLink r WHERE ");
1349
1350                                        sqlBuilder.append("r.");
1351                                        sqlBuilder.append(searchPidFieldName); // (rev mode) target_resource_id | source_resource_id
1352                                        sqlBuilder.append(" IN (:target_pids)");
1353
1354                                        /*
1355                                         * We need to set the resource type in 2 cases only:
1356                                         * 1) we are in $everything mode
1357                                         *              (where we only want to fetch specific resource types, regardless of what is
1358                                         *              available to fetch)
1359                                         * 2) we are doing revincludes
1360                                         *
1361                                         *      Technically if the request is a qualified star (e.g. _include=Observation:*) we
1362                                         * should always be checking the source resource type on the resource link. We don't
1363                                         * actually index that column though by default, so in order to try and be efficient
1364                                         * we don't actually include it for includes (but we do for revincludes). This is
1365                                         * because for an include, it doesn't really make sense to include a different
1366                                         * resource type than the one you are searching on.
1367                                         */
1368                                        if (wantResourceType != null
1369                                                        && (reverseMode || (myParams != null && myParams.getEverythingMode() != null))) {
1370                                                // because mySourceResourceType is not part of the HFJ_RES_LINK
1371                                                // index, this might not be the most optimal performance.
1372                                                // but it is for an $everything operation (and maybe we should update the index)
1373                                                sqlBuilder.append(" AND r.mySourceResourceType = :want_resource_type");
1374                                        } else {
1375                                                wantResourceType = null;
1376                                        }
1377
1378                                        // When calling $everything on a Patient instance, we don't want to recurse into new Patient
1379                                        // resources
1380                                        // (e.g. via Provenance, List, or Group) when in an $everything operation
1381                                        if (myParams != null
1382                                                        && myParams.getEverythingMode() == SearchParameterMap.EverythingModeEnum.PATIENT_INSTANCE) {
1383                                                sqlBuilder.append(" AND r.myTargetResourceType != 'Patient'");
1384                                                sqlBuilder.append(" AND r.mySourceResourceType != 'Provenance'");
1385                                        }
1386                                        if (hasDesiredResourceTypes) {
1387                                                sqlBuilder.append(" AND r.myTargetResourceType IN (:desired_target_resource_types)");
1388                                        }
1389
1390                                        String sql = sqlBuilder.toString();
1391                                        List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
1392                                        for (Collection<JpaPid> nextPartition : partitions) {
1393                                                TypedQuery<?> q = entityManager.createQuery(sql, Object[].class);
1394                                                q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
1395                                                if (wantResourceType != null) {
1396                                                        q.setParameter("want_resource_type", wantResourceType);
1397                                                }
1398                                                if (maxCount != null) {
1399                                                        q.setMaxResults(maxCount);
1400                                                }
1401                                                if (hasDesiredResourceTypes) {
1402                                                        q.setParameter("desired_target_resource_types", String.join(", ", desiredResourceTypes));
1403                                                }
1404                                                List<?> results = q.getResultList();
1405                                                for (Object nextRow : results) {
1406                                                        if (nextRow == null) {
1407                                                                // This can happen if there are outgoing references which are canonical or point to
1408                                                                // other servers
1409                                                                continue;
1410                                                        }
1411
1412                                                        Long version = null;
1413                                                        Long resourceLink = (Long) ((Object[]) nextRow)[0];
1414                                                        String resourceType = (String) ((Object[]) nextRow)[1];
1415                                                        if (findVersionFieldName != null) {
1416                                                                version = (Long) ((Object[]) nextRow)[2];
1417                                                        }
1418
1419                                                        if (resourceLink != null) {
1420                                                                JpaPid pid =
1421                                                                                JpaPid.fromIdAndVersionAndResourceType(resourceLink, version, resourceType);
1422                                                                pidsToInclude.add(pid);
1423                                                        }
1424                                                }
1425                                        }
1426                                } else {
1427                                        List<String> paths;
1428
1429                                        // Start replace
1430                                        RuntimeSearchParam param;
1431                                        String resType = nextInclude.getParamType();
1432                                        if (isBlank(resType)) {
1433                                                continue;
1434                                        }
1435                                        RuntimeResourceDefinition def = fhirContext.getResourceDefinition(resType);
1436                                        if (def == null) {
1437                                                ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
1438                                                continue;
1439                                        }
1440
1441                                        String paramName = nextInclude.getParamName();
1442                                        if (isNotBlank(paramName)) {
1443                                                param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
1444                                        } else {
1445                                                param = null;
1446                                        }
1447                                        if (param == null) {
1448                                                ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
1449                                                continue;
1450                                        }
1451
1452                                        paths = param.getPathsSplitForResourceType(resType);
1453                                        // end replace
1454
1455                                        Set<String> targetResourceTypes = computeTargetResourceTypes(nextInclude, param);
1456
1457                                        for (String nextPath : paths) {
1458                                                String findPidFieldSqlColumn = findPidFieldName.equals(MY_SOURCE_RESOURCE_PID)
1459                                                                ? "src_resource_id"
1460                                                                : "target_resource_id";
1461                                                String fieldsToLoad = "r." + findPidFieldSqlColumn + " AS " + RESOURCE_ID_ALIAS;
1462                                                if (findVersionFieldName != null) {
1463                                                        fieldsToLoad += ", r.target_resource_version AS " + RESOURCE_VERSION_ALIAS;
1464                                                }
1465
1466                                                // Query for includes lookup has 2 cases
1467                                                // Case 1: Where target_resource_id is available in hfj_res_link table for local references
1468                                                // Case 2: Where target_resource_id is null in hfj_res_link table and referred by a canonical
1469                                                // url in target_resource_url
1470
1471                                                // Case 1:
1472                                                Map<String, Object> localReferenceQueryParams = new HashMap<>();
1473
1474                                                String searchPidFieldSqlColumn = searchPidFieldName.equals(MY_TARGET_RESOURCE_PID)
1475                                                                ? "target_resource_id"
1476                                                                : "src_resource_id";
1477                                                StringBuilder localReferenceQuery =
1478                                                                new StringBuilder("SELECT " + fieldsToLoad + " FROM hfj_res_link r "
1479                                                                                + " WHERE r.src_path = :src_path AND "
1480                                                                                + " r.target_resource_id IS NOT NULL AND "
1481                                                                                + " r."
1482                                                                                + searchPidFieldSqlColumn + " IN (:target_pids) ");
1483                                                localReferenceQueryParams.put("src_path", nextPath);
1484                                                // we loop over target_pids later.
1485                                                if (targetResourceTypes != null) {
1486                                                        if (targetResourceTypes.size() == 1) {
1487                                                                localReferenceQuery.append(" AND r.target_resource_type = :target_resource_type ");
1488                                                                localReferenceQueryParams.put(
1489                                                                                "target_resource_type",
1490                                                                                targetResourceTypes.iterator().next());
1491                                                        } else {
1492                                                                localReferenceQuery.append(" AND r.target_resource_type in (:target_resource_types) ");
1493                                                                localReferenceQueryParams.put("target_resource_types", targetResourceTypes);
1494                                                        }
1495                                                }
1496
1497                                                // Case 2:
1498                                                Pair<String, Map<String, Object>> canonicalQuery = buildCanonicalUrlQuery(
1499                                                                findVersionFieldName, searchPidFieldSqlColumn, targetResourceTypes);
1500
1501                                                // @formatter:on
1502
1503                                                String sql = localReferenceQuery + " UNION " + canonicalQuery.getLeft();
1504
1505                                                List<Collection<JpaPid>> partitions = partition(nextRoundMatches, getMaximumPageSize());
1506                                                for (Collection<JpaPid> nextPartition : partitions) {
1507                                                        Query q = entityManager.createNativeQuery(sql, Tuple.class);
1508                                                        q.setParameter("target_pids", JpaPid.toLongList(nextPartition));
1509                                                        localReferenceQueryParams.forEach(q::setParameter);
1510                                                        canonicalQuery.getRight().forEach(q::setParameter);
1511
1512                                                        if (maxCount != null) {
1513                                                                q.setMaxResults(maxCount);
1514                                                        }
1515                                                        @SuppressWarnings("unchecked")
1516                                                        List<Tuple> results = q.getResultList();
1517                                                        for (Tuple result : results) {
1518                                                                if (result != null) {
1519                                                                        Long resourceId =
1520                                                                                        NumberUtils.createLong(String.valueOf(result.get(RESOURCE_ID_ALIAS)));
1521                                                                        Long resourceVersion = null;
1522                                                                        if (findVersionFieldName != null && result.get(RESOURCE_VERSION_ALIAS) != null) {
1523                                                                                resourceVersion = NumberUtils.createLong(
1524                                                                                                String.valueOf(result.get(RESOURCE_VERSION_ALIAS)));
1525                                                                        }
1526                                                                        pidsToInclude.add(JpaPid.fromIdAndVersion(resourceId, resourceVersion));
1527                                                                }
1528                                                        }
1529                                                }
1530                                        }
1531                                }
1532                        }
1533
1534                        nextRoundMatches.clear();
1535                        for (JpaPid next : pidsToInclude) {
1536                                if (!original.contains(next) && !allAdded.contains(next)) {
1537                                        nextRoundMatches.add(next);
1538                                }
1539                        }
1540
1541                        addedSomeThisRound = allAdded.addAll(pidsToInclude);
1542
1543                        if (maxCount != null && allAdded.size() >= maxCount) {
1544                                break;
1545                        }
1546
1547                } while (!includes.isEmpty() && !nextRoundMatches.isEmpty() && addedSomeThisRound);
1548
1549                allAdded.removeAll(original);
1550
1551                ourLog.info(
1552                                "Loaded {} {} in {} rounds and {} ms for search {}",
1553                                allAdded.size(),
1554                                reverseMode ? "_revincludes" : "_includes",
1555                                roundCounts,
1556                                w.getMillisAndRestart(),
1557                                searchIdOrDescription);
1558
1559                if (CompositeInterceptorBroadcaster.hasHooks(
1560                                Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, request)) {
1561                        callRawSqlHookWithCurrentThreadQueries(request);
1562                }
1563                // Interceptor call: STORAGE_PREACCESS_RESOURCES
1564                // This can be used to remove results from the search result details before
1565                // the user has a chance to know that they were in the results
1566                if (!allAdded.isEmpty()) {
1567
1568                        if (CompositeInterceptorBroadcaster.hasHooks(
1569                                        Pointcut.STORAGE_PREACCESS_RESOURCES, myInterceptorBroadcaster, request)) {
1570                                List<JpaPid> includedPidList = new ArrayList<>(allAdded);
1571                                JpaPreResourceAccessDetails accessDetails =
1572                                                new JpaPreResourceAccessDetails(includedPidList, () -> this);
1573                                HookParams params = new HookParams()
1574                                                .add(IPreResourceAccessDetails.class, accessDetails)
1575                                                .add(RequestDetails.class, request)
1576                                                .addIfMatchesType(ServletRequestDetails.class, request);
1577                                CompositeInterceptorBroadcaster.doCallHooks(
1578                                                myInterceptorBroadcaster, request, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
1579
1580                                for (int i = includedPidList.size() - 1; i >= 0; i--) {
1581                                        if (accessDetails.isDontReturnResourceAtIndex(i)) {
1582                                                JpaPid value = includedPidList.remove(i);
1583                                                if (value != null) {
1584                                                        allAdded.remove(value);
1585                                                }
1586                                        }
1587                                }
1588                        }
1589                }
1590
1591                return allAdded;
1592        }
1593
1594        /**
1595         * Given a
1596         * @param request
1597         */
1598        private void callRawSqlHookWithCurrentThreadQueries(RequestDetails request) {
1599                SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
1600                HookParams params = new HookParams()
1601                                .add(RequestDetails.class, request)
1602                                .addIfMatchesType(ServletRequestDetails.class, request)
1603                                .add(SqlQueryList.class, capturedQueries);
1604                CompositeInterceptorBroadcaster.doCallHooks(
1605                                myInterceptorBroadcaster, request, Pointcut.JPA_PERFTRACE_RAW_SQL, params);
1606        }
1607
1608        @Nullable
1609        private static Set<String> computeTargetResourceTypes(Include nextInclude, RuntimeSearchParam param) {
1610                String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
1611                boolean haveTargetTypesDefinedByParam = param.hasTargets();
1612                Set<String> targetResourceTypes;
1613                if (targetResourceType != null) {
1614                        targetResourceTypes = Set.of(targetResourceType);
1615                } else if (haveTargetTypesDefinedByParam) {
1616                        targetResourceTypes = param.getTargets();
1617                } else {
1618                        // all types!
1619                        targetResourceTypes = null;
1620                }
1621                return targetResourceTypes;
1622        }
1623
1624        @Nonnull
1625        private Pair<String, Map<String, Object>> buildCanonicalUrlQuery(
1626                        String theVersionFieldName, String thePidFieldSqlColumn, Set<String> theTargetResourceTypes) {
1627                String fieldsToLoadFromSpidxUriTable = "rUri.res_id";
1628                if (theVersionFieldName != null) {
1629                        // canonical-uri references aren't versioned, but we need to match the column count for the UNION
1630                        fieldsToLoadFromSpidxUriTable += ", NULL";
1631                }
1632                // The logical join will be by hfj_spidx_uri on sp_name='uri' and sp_uri=target_resource_url.
1633                // But sp_name isn't indexed, so we use hash_identity instead.
1634                if (theTargetResourceTypes == null) {
1635                        // hash_identity includes the resource type.  So a null wildcard must be replaced with a list of all types.
1636                        theTargetResourceTypes = myDaoRegistry.getRegisteredDaoTypes();
1637                }
1638                assert !theTargetResourceTypes.isEmpty();
1639
1640                Set<Long> identityHashesForTypes = theTargetResourceTypes.stream()
1641                                .map(type -> BaseResourceIndexedSearchParam.calculateHashIdentity(
1642                                                myPartitionSettings, myRequestPartitionId, type, "url"))
1643                                .collect(Collectors.toSet());
1644
1645                Map<String, Object> canonicalUriQueryParams = new HashMap<>();
1646                StringBuilder canonicalUrlQuery = new StringBuilder(
1647                                "SELECT " + fieldsToLoadFromSpidxUriTable + " FROM hfj_res_link r " + " JOIN hfj_spidx_uri rUri ON ( ");
1648                // join on hash_identity and sp_uri - indexed in IDX_SP_URI_HASH_IDENTITY_V2
1649                if (theTargetResourceTypes.size() == 1) {
1650                        canonicalUrlQuery.append("   rUri.hash_identity = :uri_identity_hash ");
1651                        canonicalUriQueryParams.put(
1652                                        "uri_identity_hash", identityHashesForTypes.iterator().next());
1653                } else {
1654                        canonicalUrlQuery.append("   rUri.hash_identity in (:uri_identity_hashes) ");
1655                        canonicalUriQueryParams.put("uri_identity_hashes", identityHashesForTypes);
1656                }
1657
1658                canonicalUrlQuery.append("  AND r.target_resource_url = rUri.sp_uri  )" + " WHERE r.src_path = :src_path AND "
1659                                + " r.target_resource_id IS NULL AND "
1660                                + " r."
1661                                + thePidFieldSqlColumn + " IN (:target_pids) ");
1662                return Pair.of(canonicalUrlQuery.toString(), canonicalUriQueryParams);
1663        }
1664
1665        private List<Collection<JpaPid>> partition(Collection<JpaPid> theNextRoundMatches, int theMaxLoad) {
1666                if (theNextRoundMatches.size() <= theMaxLoad) {
1667                        return Collections.singletonList(theNextRoundMatches);
1668                } else {
1669
1670                        List<Collection<JpaPid>> retVal = new ArrayList<>();
1671                        Collection<JpaPid> current = null;
1672                        for (JpaPid next : theNextRoundMatches) {
1673                                if (current == null) {
1674                                        current = new ArrayList<>(theMaxLoad);
1675                                        retVal.add(current);
1676                                }
1677
1678                                current.add(next);
1679
1680                                if (current.size() >= theMaxLoad) {
1681                                        current = null;
1682                                }
1683                        }
1684
1685                        return retVal;
1686                }
1687        }
1688
1689        private void attemptComboUniqueSpProcessing(
1690                        QueryStack theQueryStack3, @Nonnull SearchParameterMap theParams, RequestDetails theRequest) {
1691                RuntimeSearchParam comboParam = null;
1692                List<String> comboParamNames = null;
1693                List<RuntimeSearchParam> exactMatchParams =
1694                                mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet());
1695                if (exactMatchParams.size() > 0) {
1696                        comboParam = exactMatchParams.get(0);
1697                        comboParamNames = new ArrayList<>(theParams.keySet());
1698                }
1699
1700                if (comboParam == null) {
1701                        List<RuntimeSearchParam> candidateComboParams =
1702                                        mySearchParamRegistry.getActiveComboSearchParams(myResourceName);
1703                        for (RuntimeSearchParam nextCandidate : candidateComboParams) {
1704                                List<String> nextCandidateParamNames =
1705                                                JpaParamUtil.resolveComponentParameters(mySearchParamRegistry, nextCandidate).stream()
1706                                                                .map(t -> t.getName())
1707                                                                .collect(Collectors.toList());
1708                                if (theParams.keySet().containsAll(nextCandidateParamNames)) {
1709                                        comboParam = nextCandidate;
1710                                        comboParamNames = nextCandidateParamNames;
1711                                        break;
1712                                }
1713                        }
1714                }
1715
1716                if (comboParam != null) {
1717                        // Since we're going to remove elements below
1718                        theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList));
1719
1720                        StringBuilder sb = new StringBuilder();
1721                        sb.append(myResourceName);
1722                        sb.append("?");
1723
1724                        boolean first = true;
1725
1726                        Collections.sort(comboParamNames);
1727                        for (String nextParamName : comboParamNames) {
1728                                List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
1729
1730                                // TODO Hack to fix weird IOOB on the next stanza until James comes back and makes sense of this.
1731                                if (nextValues.isEmpty()) {
1732                                        ourLog.error(
1733                                                        "query parameter {} is unexpectedly empty. Encountered while considering {} index for {}",
1734                                                        nextParamName,
1735                                                        comboParam.getName(),
1736                                                        theRequest.getCompleteUrl());
1737                                        sb = null;
1738                                        break;
1739                                }
1740
1741                                if (nextValues.get(0).size() != 1) {
1742                                        sb = null;
1743                                        break;
1744                                }
1745
1746                                // Reference params are only eligible for using a composite index if they
1747                                // are qualified
1748                                RuntimeSearchParam nextParamDef =
1749                                                mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
1750                                if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
1751                                        ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0);
1752                                        if (isBlank(param.getResourceType())) {
1753                                                sb = null;
1754                                                break;
1755                                        }
1756                                }
1757
1758                                List<? extends IQueryParameterType> nextAnd = nextValues.remove(0);
1759                                IQueryParameterType nextOr = nextAnd.remove(0);
1760                                String nextOrValue = nextOr.getValueAsQueryToken(myContext);
1761
1762                                if (comboParam.getComboSearchParamType() == ComboSearchParamType.NON_UNIQUE) {
1763                                        if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.STRING) {
1764                                                nextOrValue = StringUtil.normalizeStringForSearchIndexing(nextOrValue);
1765                                        }
1766                                }
1767
1768                                if (first) {
1769                                        first = false;
1770                                } else {
1771                                        sb.append('&');
1772                                }
1773
1774                                nextParamName = UrlUtil.escapeUrlParam(nextParamName);
1775                                nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
1776
1777                                sb.append(nextParamName).append('=').append(nextOrValue);
1778                        }
1779
1780                        if (sb != null) {
1781                                String indexString = sb.toString();
1782                                ourLog.debug(
1783                                                "Checking for {} combo index for query: {}", comboParam.getComboSearchParamType(), indexString);
1784
1785                                // Interceptor broadcast: JPA_PERFTRACE_INFO
1786                                StorageProcessingMessage msg = new StorageProcessingMessage()
1787                                                .setMessage("Using " + comboParam.getComboSearchParamType() + " index for query for search: "
1788                                                                + indexString);
1789                                HookParams params = new HookParams()
1790                                                .add(RequestDetails.class, theRequest)
1791                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
1792                                                .add(StorageProcessingMessage.class, msg);
1793                                CompositeInterceptorBroadcaster.doCallHooks(
1794                                                myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
1795
1796                                switch (comboParam.getComboSearchParamType()) {
1797                                        case UNIQUE:
1798                                                theQueryStack3.addPredicateCompositeUnique(indexString, myRequestPartitionId);
1799                                                break;
1800                                        case NON_UNIQUE:
1801                                                theQueryStack3.addPredicateCompositeNonUnique(indexString, myRequestPartitionId);
1802                                                break;
1803                                }
1804
1805                                // Remove any empty parameters remaining after this
1806                                theParams.clean();
1807                        }
1808                }
1809        }
1810
1811        private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) {
1812                for (int i = 0; i < theListOfLists.size(); i++) {
1813                        List<T> oldSubList = theListOfLists.get(i);
1814                        if (!(oldSubList instanceof ArrayList)) {
1815                                List<T> newSubList = new ArrayList<>(oldSubList);
1816                                theListOfLists.set(i, newSubList);
1817                        }
1818                }
1819        }
1820
1821        @Override
1822        public void setFetchSize(int theFetchSize) {
1823                myFetchSize = theFetchSize;
1824        }
1825
1826        public SearchParameterMap getParams() {
1827                return myParams;
1828        }
1829
1830        public CriteriaBuilder getBuilder() {
1831                return myCriteriaBuilder;
1832        }
1833
1834        public Class<? extends IBaseResource> getResourceType() {
1835                return myResourceType;
1836        }
1837
1838        public String getResourceName() {
1839                return myResourceName;
1840        }
1841
1842        /**
1843         * IncludesIterator, used to recursively fetch resources from the provided list of PIDs
1844         */
1845        public class IncludesIterator extends BaseIterator<JpaPid> implements Iterator<JpaPid> {
1846
1847                private final RequestDetails myRequest;
1848                private final Set<JpaPid> myCurrentPids;
1849                private Iterator<JpaPid> myCurrentIterator;
1850                private JpaPid myNext;
1851
1852                IncludesIterator(Set<JpaPid> thePidSet, RequestDetails theRequest) {
1853                        myCurrentPids = new HashSet<>(thePidSet);
1854                        myCurrentIterator = null;
1855                        myRequest = theRequest;
1856                }
1857
1858                private void fetchNext() {
1859                        while (myNext == null) {
1860
1861                                if (myCurrentIterator == null) {
1862                                        Set<Include> includes = new HashSet<>();
1863                                        if (myParams.containsKey(Constants.PARAM_TYPE)) {
1864                                                for (List<IQueryParameterType> typeList : myParams.get(Constants.PARAM_TYPE)) {
1865                                                        for (IQueryParameterType type : typeList) {
1866                                                                String queryString = ParameterUtil.unescape(type.getValueAsQueryToken(myContext));
1867                                                                for (String resourceType : queryString.split(",")) {
1868                                                                        String rt = resourceType.trim();
1869                                                                        if (isNotBlank(rt)) {
1870                                                                                includes.add(new Include(rt + ":*", true));
1871                                                                        }
1872                                                                }
1873                                                        }
1874                                                }
1875                                        }
1876                                        if (includes.isEmpty()) {
1877                                                includes.add(new Include("*", true));
1878                                        }
1879                                        Set<JpaPid> newPids = loadIncludes(
1880                                                        myContext,
1881                                                        myEntityManager,
1882                                                        myCurrentPids,
1883                                                        includes,
1884                                                        false,
1885                                                        getParams().getLastUpdated(),
1886                                                        mySearchUuid,
1887                                                        myRequest,
1888                                                        null);
1889                                        myCurrentIterator = newPids.iterator();
1890                                }
1891
1892                                if (myCurrentIterator.hasNext()) {
1893                                        myNext = myCurrentIterator.next();
1894                                } else {
1895                                        myNext = NO_MORE;
1896                                }
1897                        }
1898                }
1899
1900                @Override
1901                public boolean hasNext() {
1902                        fetchNext();
1903                        return !NO_MORE.equals(myNext);
1904                }
1905
1906                @Override
1907                public JpaPid next() {
1908                        fetchNext();
1909                        JpaPid retVal = myNext;
1910                        myNext = null;
1911                        return retVal;
1912                }
1913        }
1914
1915        /**
1916         * Basic Query iterator, used to fetch the results of a query.
1917         */
1918        private final class QueryIterator extends BaseIterator<JpaPid> implements IResultIterator<JpaPid> {
1919
1920                private final SearchRuntimeDetails mySearchRuntimeDetails;
1921                private final RequestDetails myRequest;
1922                private final boolean myHaveRawSqlHooks;
1923                private final boolean myHavePerfTraceFoundIdHook;
1924                private final SortSpec mySort;
1925                private final Integer myOffset;
1926                private boolean myFirst = true;
1927                private IncludesIterator myIncludesIterator;
1928                private JpaPid myNext;
1929                private ISearchQueryExecutor myResultsIterator;
1930                private boolean myFetchIncludesForEverythingOperation;
1931                private int mySkipCount = 0;
1932                private int myNonSkipCount = 0;
1933                private List<ISearchQueryExecutor> myQueryList = new ArrayList<>();
1934
1935                private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
1936                        mySearchRuntimeDetails = theSearchRuntimeDetails;
1937                        mySort = myParams.getSort();
1938                        myOffset = myParams.getOffset();
1939                        myRequest = theRequest;
1940
1941                        // everything requires fetching recursively all related resources
1942                        if (myParams.getEverythingMode() != null) {
1943                                myFetchIncludesForEverythingOperation = true;
1944                        }
1945
1946                        myHavePerfTraceFoundIdHook = CompositeInterceptorBroadcaster.hasHooks(
1947                                        Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest);
1948                        myHaveRawSqlHooks = CompositeInterceptorBroadcaster.hasHooks(
1949                                        Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
1950                }
1951
1952                private void fetchNext() {
1953                        try {
1954                                if (myHaveRawSqlHooks) {
1955                                        CurrentThreadCaptureQueriesListener.startCapturing();
1956                                }
1957
1958                                // If we don't have a query yet, create one
1959                                if (myResultsIterator == null) {
1960                                        if (myMaxResultsToFetch == null) {
1961                                                if (myParams.getLoadSynchronousUpTo() != null) {
1962                                                        myMaxResultsToFetch = myParams.getLoadSynchronousUpTo();
1963                                                } else if (myParams.getOffset() != null && myParams.getCount() != null) {
1964                                                        myMaxResultsToFetch = myParams.getCount();
1965                                                } else {
1966                                                        myMaxResultsToFetch = myStorageSettings.getFetchSizeDefaultMaximum();
1967                                                }
1968                                        }
1969
1970                                        // assigns the results iterator
1971                                        initializeIteratorQuery(myOffset, myMaxResultsToFetch);
1972
1973                                        if (myAlsoIncludePids == null) {
1974                                                myAlsoIncludePids = new ArrayList<>();
1975                                        }
1976                                }
1977
1978                                if (myNext == null) {
1979                                        for (Iterator<JpaPid> myPreResultsIterator = myAlsoIncludePids.iterator();
1980                                                        myPreResultsIterator.hasNext(); ) {
1981                                                JpaPid next = myPreResultsIterator.next();
1982                                                if (next != null)
1983                                                        if (myPidSet.add(next)) {
1984                                                                myNext = next;
1985                                                                break;
1986                                                        }
1987                                        }
1988
1989                                        if (myNext == null) {
1990                                                while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) {
1991                                                        // Update iterator with next chunk if necessary.
1992                                                        if (!myResultsIterator.hasNext()) {
1993                                                                retrieveNextIteratorQuery();
1994                                                        }
1995
1996                                                        Long nextLong = myResultsIterator.next();
1997                                                        if (myHavePerfTraceFoundIdHook) {
1998                                                                HookParams params = new HookParams()
1999                                                                                .add(Integer.class, System.identityHashCode(this))
2000                                                                                .add(Object.class, nextLong);
2001                                                                CompositeInterceptorBroadcaster.doCallHooks(
2002                                                                                myInterceptorBroadcaster,
2003                                                                                myRequest,
2004                                                                                Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID,
2005                                                                                params);
2006                                                        }
2007
2008                                                        if (nextLong != null) {
2009                                                                JpaPid next = JpaPid.fromId(nextLong);
2010                                                                if (myPidSet.add(next)) {
2011                                                                        myNext = next;
2012                                                                        myNonSkipCount++;
2013                                                                        break;
2014                                                                } else {
2015                                                                        mySkipCount++;
2016                                                                }
2017                                                        }
2018
2019                                                        if (!myResultsIterator.hasNext()) {
2020                                                                if (myMaxResultsToFetch != null
2021                                                                                && (mySkipCount + myNonSkipCount == myMaxResultsToFetch)) {
2022                                                                        if (mySkipCount > 0 && myNonSkipCount == 0) {
2023
2024                                                                                StorageProcessingMessage message = new StorageProcessingMessage();
2025                                                                                String msg = "Pass completed with no matching results seeking rows "
2026                                                                                                + myPidSet.size() + "-" + mySkipCount
2027                                                                                                + ". This indicates an inefficient query! Retrying with new max count of "
2028                                                                                                + myMaxResultsToFetch;
2029                                                                                ourLog.warn(msg);
2030                                                                                message.setMessage(msg);
2031                                                                                HookParams params = new HookParams()
2032                                                                                                .add(RequestDetails.class, myRequest)
2033                                                                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
2034                                                                                                .add(StorageProcessingMessage.class, message);
2035                                                                                CompositeInterceptorBroadcaster.doCallHooks(
2036                                                                                                myInterceptorBroadcaster,
2037                                                                                                myRequest,
2038                                                                                                Pointcut.JPA_PERFTRACE_WARNING,
2039                                                                                                params);
2040
2041                                                                                myMaxResultsToFetch += 1000;
2042                                                                                initializeIteratorQuery(myOffset, myMaxResultsToFetch);
2043                                                                        }
2044                                                                }
2045                                                        }
2046                                                }
2047                                        }
2048
2049                                        if (myNext == null) {
2050                                                // if we got here, it means the current PjaPid has already been processed
2051                                                // and we will decide (here) if we need to fetch related resources recursively
2052                                                if (myFetchIncludesForEverythingOperation) {
2053                                                        myIncludesIterator = new IncludesIterator(myPidSet, myRequest);
2054                                                        myFetchIncludesForEverythingOperation = false;
2055                                                }
2056                                                if (myIncludesIterator != null) {
2057                                                        while (myIncludesIterator.hasNext()) {
2058                                                                JpaPid next = myIncludesIterator.next();
2059                                                                if (next != null)
2060                                                                        if (myPidSet.add(next)) {
2061                                                                                myNext = next;
2062                                                                                break;
2063                                                                        }
2064                                                        }
2065                                                        if (myNext == null) {
2066                                                                myNext = NO_MORE;
2067                                                        }
2068                                                } else {
2069                                                        myNext = NO_MORE;
2070                                                }
2071                                        }
2072                                } // if we need to fetch the next result
2073
2074                                mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
2075
2076                        } finally {
2077                                // search finished - fire hooks
2078                                if (myHaveRawSqlHooks) {
2079                                        callRawSqlHookWithCurrentThreadQueries(myRequest);
2080                                }
2081                        }
2082
2083                        if (myFirst) {
2084                                HookParams params = new HookParams()
2085                                                .add(RequestDetails.class, myRequest)
2086                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
2087                                                .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
2088                                CompositeInterceptorBroadcaster.doCallHooks(
2089                                                myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params);
2090                                myFirst = false;
2091                        }
2092
2093                        if (NO_MORE.equals(myNext)) {
2094                                HookParams params = new HookParams()
2095                                                .add(RequestDetails.class, myRequest)
2096                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
2097                                                .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
2098                                CompositeInterceptorBroadcaster.doCallHooks(
2099                                                myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params);
2100                        }
2101                }
2102
2103                private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) {
2104                        if (myQueryList.isEmpty()) {
2105                                // Capture times for Lucene/Elasticsearch queries as well
2106                                mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
2107                                myQueryList = createQuery(
2108                                                myParams, mySort, theOffset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails);
2109                        }
2110
2111                        mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
2112
2113                        retrieveNextIteratorQuery();
2114
2115                        mySkipCount = 0;
2116                        myNonSkipCount = 0;
2117                }
2118
2119                private void retrieveNextIteratorQuery() {
2120                        close();
2121                        if (myQueryList != null && myQueryList.size() > 0) {
2122                                myResultsIterator = myQueryList.remove(0);
2123                                myHasNextIteratorQuery = true;
2124                        } else {
2125                                myResultsIterator = SearchQueryExecutor.emptyExecutor();
2126                                myHasNextIteratorQuery = false;
2127                        }
2128                }
2129
2130                @Override
2131                public boolean hasNext() {
2132                        if (myNext == null) {
2133                                fetchNext();
2134                        }
2135                        return !NO_MORE.equals(myNext);
2136                }
2137
2138                @Override
2139                public JpaPid next() {
2140                        fetchNext();
2141                        JpaPid retVal = myNext;
2142                        myNext = null;
2143                        Validate.isTrue(!NO_MORE.equals(retVal), "No more elements");
2144                        return retVal;
2145                }
2146
2147                @Override
2148                public int getSkippedCount() {
2149                        return mySkipCount;
2150                }
2151
2152                @Override
2153                public int getNonSkippedCount() {
2154                        return myNonSkipCount;
2155                }
2156
2157                @Override
2158                public Collection<JpaPid> getNextResultBatch(long theBatchSize) {
2159                        Collection<JpaPid> batch = new ArrayList<>();
2160                        while (this.hasNext() && batch.size() < theBatchSize) {
2161                                batch.add(this.next());
2162                        }
2163                        return batch;
2164                }
2165
2166                @Override
2167                public void close() {
2168                        if (myResultsIterator != null) {
2169                                myResultsIterator.close();
2170                        }
2171                        myResultsIterator = null;
2172                }
2173        }
2174
2175        public static int getMaximumPageSize() {
2176                if (myUseMaxPageSize50ForTest) {
2177                        return MAXIMUM_PAGE_SIZE_FOR_TESTING;
2178                } else {
2179                        return MAXIMUM_PAGE_SIZE;
2180                }
2181        }
2182
2183        public static void setMaxPageSize50ForTest(boolean theIsTest) {
2184                myUseMaxPageSize50ForTest = theIsTest;
2185        }
2186}