001package ca.uhn.fhir.jpa.dao;
002
003/*
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 * http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.ComboSearchParamType;
024import ca.uhn.fhir.context.FhirContext;
025import ca.uhn.fhir.context.FhirVersionEnum;
026import ca.uhn.fhir.context.RuntimeResourceDefinition;
027import ca.uhn.fhir.context.RuntimeSearchParam;
028import ca.uhn.fhir.i18n.Msg;
029import ca.uhn.fhir.interceptor.api.HookParams;
030import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
031import ca.uhn.fhir.interceptor.api.Pointcut;
032import ca.uhn.fhir.interceptor.model.RequestPartitionId;
033import ca.uhn.fhir.jpa.api.config.DaoConfig;
034import ca.uhn.fhir.jpa.api.dao.IDao;
035import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
036import ca.uhn.fhir.jpa.dao.data.IResourceSearchViewDao;
037import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
038import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilder;
039import ca.uhn.fhir.jpa.dao.predicate.PredicateBuilderFactory;
040import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinEnum;
041import ca.uhn.fhir.jpa.dao.predicate.SearchBuilderJoinKey;
042import ca.uhn.fhir.jpa.dao.predicate.querystack.QueryStack;
043import ca.uhn.fhir.jpa.entity.ResourceSearchView;
044import ca.uhn.fhir.jpa.interceptor.JpaPreResourceAccessDetails;
045import ca.uhn.fhir.jpa.model.config.PartitionSettings;
046import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
047import ca.uhn.fhir.jpa.model.entity.ResourceIndexedComboStringUnique;
048import ca.uhn.fhir.jpa.model.entity.ResourceLink;
049import ca.uhn.fhir.jpa.model.entity.ResourceTable;
050import ca.uhn.fhir.jpa.model.entity.ResourceTag;
051import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails;
052import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
053import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
054import ca.uhn.fhir.jpa.search.lastn.IElasticsearchSvc;
055import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
056import ca.uhn.fhir.jpa.searchparam.util.Dstu3DistanceHelper;
057import ca.uhn.fhir.jpa.searchparam.util.LastNParameterHelper;
058import ca.uhn.fhir.jpa.util.BaseIterator;
059import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
060import ca.uhn.fhir.jpa.util.QueryChunker;
061import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
062import ca.uhn.fhir.jpa.util.SqlQueryList;
063import ca.uhn.fhir.model.api.IQueryParameterType;
064import ca.uhn.fhir.model.api.IResource;
065import ca.uhn.fhir.model.api.Include;
066import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
067import ca.uhn.fhir.model.primitive.InstantDt;
068import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
069import ca.uhn.fhir.rest.api.Constants;
070import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
071import ca.uhn.fhir.rest.api.SortOrderEnum;
072import ca.uhn.fhir.rest.api.SortSpec;
073import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
074import ca.uhn.fhir.rest.api.server.RequestDetails;
075import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
076import ca.uhn.fhir.rest.param.DateRangeParam;
077import ca.uhn.fhir.rest.param.ReferenceParam;
078import ca.uhn.fhir.rest.param.StringParam;
079import ca.uhn.fhir.rest.param.TokenParam;
080import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
081import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
082import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
083import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
084import ca.uhn.fhir.util.StopWatch;
085import ca.uhn.fhir.util.UrlUtil;
086import com.google.common.annotations.VisibleForTesting;
087import org.apache.commons.lang3.Validate;
088import org.hibernate.ScrollMode;
089import org.hibernate.ScrollableResults;
090import org.hibernate.query.Query;
091import org.hl7.fhir.instance.model.api.IAnyResource;
092import org.hl7.fhir.instance.model.api.IBaseResource;
093import org.slf4j.Logger;
094import org.slf4j.LoggerFactory;
095import org.springframework.beans.factory.annotation.Autowired;
096import org.springframework.transaction.support.TransactionSynchronizationManager;
097
098import javax.annotation.Nonnull;
099import javax.persistence.EntityManager;
100import javax.persistence.PersistenceContext;
101import javax.persistence.PersistenceContextType;
102import javax.persistence.TypedQuery;
103import javax.persistence.criteria.CriteriaBuilder;
104import javax.persistence.criteria.CriteriaQuery;
105import javax.persistence.criteria.From;
106import javax.persistence.criteria.Join;
107import javax.persistence.criteria.Order;
108import javax.persistence.criteria.Predicate;
109import javax.persistence.criteria.Root;
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.Optional;
119import java.util.Set;
120
121import static ca.uhn.fhir.jpa.search.builder.SearchBuilder.getMaximumPageSize;
122import static org.apache.commons.lang3.StringUtils.defaultString;
123import static org.apache.commons.lang3.StringUtils.isBlank;
124import static org.apache.commons.lang3.StringUtils.isNotBlank;
125
126/**
127 * The SearchBuilder is responsible for actually forming the SQL query that handles
128 * searches for resources
129 */
130public class LegacySearchBuilder implements ISearchBuilder {
131
132        private static final List<ResourcePersistentId> EMPTY_LONG_LIST = Collections.unmodifiableList(new ArrayList<>());
133        private static final Logger ourLog = LoggerFactory.getLogger(LegacySearchBuilder.class);
134        private static final ResourcePersistentId NO_MORE = new ResourcePersistentId(-1L);
135        private final String myResourceName;
136        private final Class<? extends IBaseResource> myResourceType;
137        private final IDao myCallingDao;
138        @Autowired
139        protected IInterceptorBroadcaster myInterceptorBroadcaster;
140        @Autowired
141        protected IResourceTagDao myResourceTagDao;
142        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
143        protected EntityManager myEntityManager;
144        private QueryStack myQueryStack;
145        @Autowired
146        private DaoConfig myDaoConfig;
147        @Autowired
148        private IResourceSearchViewDao myResourceSearchViewDao;
149        @Autowired
150        private FhirContext myContext;
151        @Autowired
152        private IIdHelperService myIdHelperService;
153        @Autowired(required = false)
154        private IFulltextSearchSvc myFulltextSearchSvc;
155        @Autowired(required = false)
156        private IElasticsearchSvc myIElasticsearchSvc;
157        @Autowired
158        private ISearchParamRegistry mySearchParamRegistry;
159        @Autowired
160        private PredicateBuilderFactory myPredicateBuilderFactory;
161        private List<ResourcePersistentId> myAlsoIncludePids;
162        private CriteriaBuilder myCriteriaBuilder;
163        private SearchParameterMap myParams;
164        private String mySearchUuid;
165        private int myFetchSize;
166        private Integer myMaxResultsToFetch;
167        private Set<ResourcePersistentId> myPidSet;
168        private PredicateBuilder myPredicateBuilder;
169        private RequestPartitionId myRequestPartitionId;
170        @Autowired
171        private PartitionSettings myPartitionSettings;
172
173        /**
174         * Constructor
175         */
176        public LegacySearchBuilder(IDao theDao, String theResourceName, Class<? extends IBaseResource> theResourceType) {
177                myCallingDao = theDao;
178                myResourceName = theResourceName;
179                myResourceType = theResourceType;
180        }
181
182        @Override
183        public void setMaxResultsToFetch(Integer theMaxResultsToFetch) {
184                myMaxResultsToFetch = theMaxResultsToFetch;
185        }
186
187        private void searchForIdsWithAndOr(String theResourceName, String theNextParamName, List<List<IQueryParameterType>> theAndOrParams, RequestDetails theRequest) {
188                myPredicateBuilder.searchForIdsWithAndOr(theResourceName, theNextParamName, theAndOrParams, theRequest, myRequestPartitionId);
189        }
190
191        private void searchForIdsWithAndOr(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) {
192                myParams = theParams;
193
194                // Remove any empty parameters
195                theParams.clean();
196
197                // For DSTU3, pull out near-distance first so when it comes time to evaluate near, we already know the distance
198                if (myContext.getVersion().getVersion() == FhirVersionEnum.DSTU3) {
199                        Dstu3DistanceHelper.setNearDistance(myResourceType, theParams);
200                }
201
202                // Attempt to lookup via composite unique key.
203                if (isCompositeUniqueSpCandidate()) {
204                        attemptCompositeUniqueSpProcessing(theParams, theRequest);
205                }
206
207                // Handle each parameter
208                for (Map.Entry<String, List<List<IQueryParameterType>>> nextParamEntry : myParams.entrySet()) {
209                        String nextParamName = nextParamEntry.getKey();
210                        if (myParams.isLastN() && LastNParameterHelper.isLastNParameter(nextParamName, myContext)) {
211                                // Skip parameters for Subject, Patient, Code and Category for LastN as these will be filtered by Elasticsearch
212                                continue;
213                        }
214                        List<List<IQueryParameterType>> andOrParams = nextParamEntry.getValue();
215                        searchForIdsWithAndOr(myResourceName, nextParamName, andOrParams, theRequest);
216                }
217        }
218
219        /**
220         * A search is a candidate for Composite Unique SP if unique indexes are enabled, there is no EverythingMode, and the
221         * parameters all have no modifiers.
222         */
223        private boolean isCompositeUniqueSpCandidate() {
224                return myDaoConfig.isUniqueIndexesEnabled() &&
225                        myParams.getEverythingMode() == null &&
226                        myParams.isAllParametersHaveNoModifier();
227        }
228
229        @Override
230        public Long createCountQuery(SearchParameterMap theParams, String theSearchUuid, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) {
231                assert theRequestPartitionId != null;
232                assert TransactionSynchronizationManager.isActualTransactionActive();
233
234                init(theParams, theSearchUuid, theRequestPartitionId);
235
236                List<TypedQuery<Long>> queries = createQuery(null, null, null, true, theRequest, null);
237                return new CountQueryIterator(queries.get(0)).next();
238        }
239
240        /**
241         * @param thePidSet May be null
242         */
243        @Override
244        public void setPreviouslyAddedResourcePids(@Nonnull List<ResourcePersistentId> thePidSet) {
245                myPidSet = new HashSet<>(thePidSet);
246        }
247
248        @Override
249        public IResultIterator createQuery(SearchParameterMap theParams, SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest, @Nonnull RequestPartitionId theRequestPartitionId) {
250                assert theRequestPartitionId != null;
251                assert TransactionSynchronizationManager.isActualTransactionActive();
252
253                init(theParams, theSearchRuntimeDetails.getSearchUuid(), theRequestPartitionId);
254
255                if (myPidSet == null) {
256                        myPidSet = new HashSet<>();
257                }
258
259                return new QueryIterator(theSearchRuntimeDetails, theRequest);
260        }
261
262        private void init(SearchParameterMap theParams, String theSearchUuid, RequestPartitionId theRequestPartitionId) {
263                myCriteriaBuilder = myEntityManager.getCriteriaBuilder();
264                myQueryStack = new QueryStack(myCriteriaBuilder, myResourceName, theParams, theRequestPartitionId);
265                myParams = theParams;
266                mySearchUuid = theSearchUuid;
267                myPredicateBuilder = new PredicateBuilder(this, myPredicateBuilderFactory);
268                myRequestPartitionId = theRequestPartitionId;
269        }
270
271        private List<TypedQuery<Long>> createQuery(SortSpec sort, Integer theOffset, Integer theMaximumResults, boolean theCount, RequestDetails theRequest,
272                                                                                                                         SearchRuntimeDetails theSearchRuntimeDetails) {
273
274                List<ResourcePersistentId> pids = new ArrayList<>();
275
276                /*
277                 * Fulltext or lastn search
278                 */
279                if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT) || myParams.isLastN()) {
280                        if (myParams.containsKey(Constants.PARAM_CONTENT) || myParams.containsKey(Constants.PARAM_TEXT)) {
281                                if (myFulltextSearchSvc == null || myFulltextSearchSvc.isDisabled()) {
282                                        if (myParams.containsKey(Constants.PARAM_TEXT)) {
283                                                throw new InvalidRequestException(Msg.code(937) + "Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_TEXT);
284                                        } else if (myParams.containsKey(Constants.PARAM_CONTENT)) {
285                                                throw new InvalidRequestException(Msg.code(938) + "Fulltext search is not enabled on this service, can not process parameter: " + Constants.PARAM_CONTENT);
286                                        }
287                                }
288
289                                if (myParams.getEverythingMode() != null) {
290                                        pids = queryHibernateSearchForEverythingPids();
291                                } else {
292                                        pids = myFulltextSearchSvc.search(myResourceName, myParams);
293                                }
294                        } else if (myParams.isLastN()) {
295                                if (myIElasticsearchSvc == null) {
296                                        if (myParams.isLastN()) {
297                                                throw new InvalidRequestException(Msg.code(939) + "LastN operation is not enabled on this service, can not process this request");
298                                        }
299                                }
300                                List<String> lastnResourceIds = myIElasticsearchSvc.executeLastN(myParams, myContext, theMaximumResults);
301                                for (String lastnResourceId : lastnResourceIds) {
302                                        pids.add(myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, lastnResourceId));
303                                }
304                        }
305                        if (theSearchRuntimeDetails != null) {
306                                theSearchRuntimeDetails.setFoundIndexMatchesCount(pids.size());
307                                HookParams params = new HookParams()
308                                        .add(RequestDetails.class, theRequest)
309                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
310                                        .add(SearchRuntimeDetails.class, theSearchRuntimeDetails);
311                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, params);
312                        }
313
314                        if (pids.isEmpty()) {
315                                // Will never match
316                                pids = Collections.singletonList(new ResourcePersistentId(-1L));
317                        }
318
319                }
320
321                ArrayList<TypedQuery<Long>> myQueries = new ArrayList<>();
322
323                if (!pids.isEmpty()) {
324                        if (theMaximumResults != null && pids.size() > theMaximumResults) {
325                                pids.subList(0,theMaximumResults-1);
326                        }
327                        new QueryChunker<Long>().chunk(ResourcePersistentId.toLongList(pids), t-> doCreateChunkedQueries(t, sort, theOffset, theCount, theRequest, myQueries));
328                } else {
329                        myQueries.add(createChunkedQuery(sort, theOffset, theMaximumResults, theCount, theRequest, null));
330                }
331
332                return myQueries;
333        }
334
335        private List<ResourcePersistentId> queryHibernateSearchForEverythingPids() {
336                ResourcePersistentId pid = null;
337                if (myParams.get(IAnyResource.SP_RES_ID) != null) {
338                        String idParamValue;
339                        IQueryParameterType idParam = myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
340                        if (idParam instanceof TokenParam) {
341                                TokenParam idParm = (TokenParam) idParam;
342                                idParamValue = idParm.getValue();
343                        } else {
344                                StringParam idParm = (StringParam) idParam;
345                                idParamValue = idParm.getValue();
346                        }
347
348                        pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParamValue);
349                }
350                List<ResourcePersistentId> pids = myFulltextSearchSvc.everything(myResourceName, myParams, pid);
351                return pids;
352        }
353
354        private void doCreateChunkedQueries(List<Long> thePids, SortSpec sort, Integer theOffset, boolean theCount, RequestDetails theRequest, ArrayList<TypedQuery<Long>> theQueries) {
355                if(thePids.size() < getMaximumPageSize()) {
356                        normalizeIdListForLastNInClause(thePids);
357                }
358                theQueries.add(createChunkedQuery(sort, theOffset, thePids.size(), theCount, theRequest, thePids));
359        }
360
361        private TypedQuery<Long> createChunkedQuery(SortSpec sort, Integer theOffset, Integer theMaximumResults, boolean theCount, RequestDetails theRequest, List<Long> thePidList) {
362                /*
363                 * Sort
364                 *
365                 * If we have a sort, we wrap the criteria search (the search that actually
366                 * finds the appropriate resources) in an outer search which is then sorted
367                 */
368                if (sort != null) {
369                        assert !theCount;
370
371                        myQueryStack.pushResourceTableQuery();
372
373                        List<Order> orders = createSort(myCriteriaBuilder, myQueryStack, sort);
374                        if (orders.size() > 0) {
375                                myQueryStack.orderBy(orders);
376                        }
377
378                } else {
379
380                        if (theCount) {
381                                myQueryStack.pushResourceTableCountQuery();
382                        } else if (myParams.getEverythingMode() != null && myParams.isLoadSynchronous()) {
383                                myQueryStack.pushResourceTableDistinctQuery();
384                        } else {
385                                myQueryStack.pushResourceTableQuery();
386                        }
387                }
388
389                if (myParams.getEverythingMode() != null) {
390                        From<?, ResourceLink> join = myQueryStack.createJoin(SearchBuilderJoinEnum.REFERENCE, null);
391
392                        if (myParams.get(IAnyResource.SP_RES_ID) != null) {
393                                ResourcePersistentId pid = null;
394                                if (myParams.get(IAnyResource.SP_RES_ID) instanceof StringParam) {
395                                        StringParam idParam = (StringParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
396                                        pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, idParam.getValue());
397                                } else {
398                                        TokenParam tokenParam = (TokenParam) myParams.get(IAnyResource.SP_RES_ID).get(0).get(0);
399                                        pid = myIdHelperService.resolveResourcePersistentIds(myRequestPartitionId, myResourceName, tokenParam.getValue());
400                                }
401                                if (myAlsoIncludePids == null) {
402                                        myAlsoIncludePids = new ArrayList<>(1);
403                                }
404                                myAlsoIncludePids.add(pid);
405                                myQueryStack.addPredicate(myCriteriaBuilder.equal(join.get("myTargetResourcePid").as(Long.class), pid.getIdAsLong()));
406                        } else {
407                                Predicate targetTypePredicate = myCriteriaBuilder.equal(join.get("myTargetResourceType").as(String.class), myResourceName);
408                                Predicate sourceTypePredicate = myCriteriaBuilder.equal(myQueryStack.get("myResourceType").as(String.class), myResourceName);
409                                myQueryStack.addPredicate(myCriteriaBuilder.or(sourceTypePredicate, targetTypePredicate));
410                        }
411
412                } else {
413                        // Normal search
414                        searchForIdsWithAndOr(myParams, theRequest);
415                }
416
417                // Add PID list predicate for full text search and/or lastn operation
418                if (thePidList != null && thePidList.size() > 0) {
419                        myQueryStack.addPredicate(myQueryStack.get("myId").as(Long.class).in(thePidList));
420                }
421
422                // Last updated
423                DateRangeParam lu = myParams.getLastUpdated();
424                List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(lu, myCriteriaBuilder);
425                myQueryStack.addPredicates(lastUpdatedPredicates);
426
427                /*
428                 * Now perform the search
429                 */
430                CriteriaQuery<Long> outerQuery = (CriteriaQuery<Long>) myQueryStack.pop();
431                final TypedQuery<Long> query = myEntityManager.createQuery(outerQuery);
432                assert myQueryStack.isEmpty();
433                if (!theCount && theOffset != null) {
434                        query.setFirstResult(theOffset);
435                }
436                if (theMaximumResults != null) {
437                        query.setMaxResults(theMaximumResults);
438                }
439
440                return query;
441        }
442
443        private List<Long> normalizeIdListForLastNInClause(List<Long> lastnResourceIds) {
444                /*
445                        The following is a workaround to a known issue involving Hibernate. If queries are used with "in" clauses with large and varying
446                        numbers of parameters, this can overwhelm Hibernate's QueryPlanCache and deplete heap space. See the following link for more info:
447                        https://stackoverflow.com/questions/31557076/spring-hibernate-query-plan-cache-memory-usage.
448
449                        Normalizing the number of parameters in the "in" clause stabilizes the size of the QueryPlanCache, so long as the number of
450                        arguments never exceeds the maximum specified below.
451                 */
452                int listSize = lastnResourceIds.size();
453
454                if(listSize > 1 && listSize < 10) {
455                        padIdListWithPlaceholders(lastnResourceIds, 10);
456                } else if (listSize > 10 && listSize < 50) {
457                        padIdListWithPlaceholders(lastnResourceIds, 50);
458                } else if (listSize > 50 && listSize < 100) {
459                        padIdListWithPlaceholders(lastnResourceIds, 100);
460                } else if (listSize > 100 && listSize < 200) {
461                        padIdListWithPlaceholders(lastnResourceIds, 200);
462                } else if (listSize > 200 && listSize < 500) {
463                        padIdListWithPlaceholders(lastnResourceIds, 500);
464                } else if (listSize > 500 && listSize < 800) {
465                        padIdListWithPlaceholders(lastnResourceIds, 800);
466                }
467
468                return lastnResourceIds;
469        }
470
471        private void padIdListWithPlaceholders(List<Long> theIdList, int preferredListSize) {
472                while(theIdList.size() < preferredListSize) {
473                        theIdList.add(-1L);
474                }
475        }
476
477        /**
478         * @return Returns {@literal true} if any search parameter sorts were found, or false if
479         * no sorts were found, or only non-search parameters ones (e.g. _id, _lastUpdated)
480         */
481        private List<Order> createSort(CriteriaBuilder theBuilder, QueryStack theQueryStack, SortSpec theSort) {
482                if (theSort == null || isBlank(theSort.getParamName())) {
483                        return Collections.emptyList();
484                }
485
486                List<Order> orders = new ArrayList<>(1);
487                if (IAnyResource.SP_RES_ID.equals(theSort.getParamName())) {
488                        From<?, ?> forcedIdJoin = theQueryStack.createJoin(SearchBuilderJoinEnum.FORCED_ID, null);
489                        if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
490                                orders.add(theBuilder.asc(forcedIdJoin.get("myForcedId")));
491                                orders.add(theBuilder.asc(theQueryStack.get("myId")));
492                        } else {
493                                orders.add(theBuilder.desc(forcedIdJoin.get("myForcedId")));
494                                orders.add(theBuilder.desc(theQueryStack.get("myId")));
495                        }
496
497                        orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain()));
498                        return orders;
499                }
500
501                if (Constants.PARAM_LASTUPDATED.equals(theSort.getParamName())) {
502                        if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
503                                orders.add(theBuilder.asc(theQueryStack.get("myUpdated")));
504                        } else {
505                                orders.add(theBuilder.desc(theQueryStack.get("myUpdated")));
506                        }
507
508                        orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain()));
509                        return orders;
510                }
511
512                RuntimeSearchParam param = mySearchParamRegistry.getActiveSearchParam(myResourceName, theSort.getParamName());
513                if (param == null) {
514                        throw new InvalidRequestException(Msg.code(940) + "Unknown sort parameter '" + theSort.getParamName() + "'");
515                }
516
517                String[] sortAttrName;
518                SearchBuilderJoinEnum joinType;
519
520                switch (param.getParamType()) {
521                        case STRING:
522                                sortAttrName = new String[]{"myValueExact"};
523                                joinType = SearchBuilderJoinEnum.STRING;
524                                break;
525                        case DATE:
526                                sortAttrName = new String[]{"myValueLow"};
527                                joinType = SearchBuilderJoinEnum.DATE;
528                                break;
529                        case REFERENCE:
530                                sortAttrName = new String[]{"myTargetResourcePid"};
531                                joinType = SearchBuilderJoinEnum.REFERENCE;
532                                break;
533                        case TOKEN:
534                                sortAttrName = new String[]{"mySystem", "myValue"};
535                                joinType = SearchBuilderJoinEnum.TOKEN;
536                                break;
537                        case NUMBER:
538                                sortAttrName = new String[]{"myValue"};
539                                joinType = SearchBuilderJoinEnum.NUMBER;
540                                break;
541                        case URI:
542                                sortAttrName = new String[]{"myUri"};
543                                joinType = SearchBuilderJoinEnum.URI;
544                                break;
545                        case QUANTITY:
546                                sortAttrName = new String[]{"myValue"};
547                                joinType = SearchBuilderJoinEnum.QUANTITY;
548                                break;
549                        case SPECIAL:
550                        case COMPOSITE:
551                        case HAS:
552                        default:
553                                throw new InvalidRequestException(Msg.code(941) + "This server does not support _sort specifications of type " + param.getParamType() + " - Can't serve _sort=" + theSort.getParamName());
554                }
555
556                /*
557                 * If we've already got a join for the specific parameter we're
558                 * sorting on, we'll also sort with it. Otherwise we need a new join.
559                 */
560                SearchBuilderJoinKey key = new SearchBuilderJoinKey(theSort.getParamName(), joinType);
561                Optional<Join<?, ?>> joinOpt = theQueryStack.getExistingJoin(key);
562
563                From<?, ?> join;
564                if (!joinOpt.isPresent()) {
565                        join = theQueryStack.createJoin(joinType, theSort.getParamName());
566
567                        if (param.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
568                                theQueryStack.addPredicate(join.get("mySourcePath").as(String.class).in(param.getPathsSplit()));
569                        } else {
570                                if (myDaoConfig.getDisableHashBasedSearches()) {
571                                        Predicate joinParam1 = theBuilder.equal(join.get("myParamName"), theSort.getParamName());
572                                        theQueryStack.addPredicate(joinParam1);
573                                } else {
574                                        Long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(myPartitionSettings, myRequestPartitionId, myResourceName, theSort.getParamName());
575                                        Predicate joinParam1 = theBuilder.equal(join.get("myHashIdentity"), hashIdentity);
576                                        theQueryStack.addPredicate(joinParam1);
577                                }
578                        }
579                } else {
580                        ourLog.debug("Reusing join for {}", theSort.getParamName());
581                        join = joinOpt.get();
582                }
583
584                for (String next : sortAttrName) {
585                        if (theSort.getOrder() == null || theSort.getOrder() == SortOrderEnum.ASC) {
586                                orders.add(theBuilder.asc(join.get(next)));
587                        } else {
588                                orders.add(theBuilder.desc(join.get(next)));
589                        }
590                }
591
592                orders.addAll(createSort(theBuilder, theQueryStack, theSort.getChain()));
593
594                return orders;
595        }
596
597
598        private void doLoadPids(Collection<ResourcePersistentId> thePids, Collection<ResourcePersistentId> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation,
599                                                                        Map<ResourcePersistentId, Integer> thePosition) {
600
601                List<Long> myLongPersistentIds;
602                if(thePids.size() < getMaximumPageSize()) {
603                        myLongPersistentIds = normalizeIdListForLastNInClause(ResourcePersistentId.toLongList(thePids));
604                } else {
605                        myLongPersistentIds = ResourcePersistentId.toLongList(thePids);
606                }
607
608                // -- get the resource from the searchView
609                Collection<ResourceSearchView> resourceSearchViewList = myResourceSearchViewDao.findByResourceIds(myLongPersistentIds);
610
611                //-- preload all tags with tag definition if any
612                Map<ResourcePersistentId, Collection<ResourceTag>> tagMap = getResourceTagMap(resourceSearchViewList);
613
614                ResourcePersistentId resourceId;
615                for (ResourceSearchView next : resourceSearchViewList) {
616                        if (next.getDeleted() != null) {
617                                continue;
618                        }
619
620                        Class<? extends IBaseResource> resourceType = myContext.getResourceDefinition(next.getResourceType()).getImplementingClass();
621
622                        resourceId = new ResourcePersistentId(next.getId());
623
624                        IBaseResource resource = myCallingDao.toResource(resourceType, next, tagMap.get(resourceId), theForHistoryOperation);
625                        if (resource == null) {
626                                ourLog.warn("Unable to find resource {}/{}/_history/{} in database", next.getResourceType(), next.getIdDt().getIdPart(), next.getVersion());
627                                continue;
628                        }
629                        Integer index = thePosition.get(resourceId);
630                        if (index == null) {
631                                ourLog.warn("Got back unexpected resource PID {}", resourceId);
632                                continue;
633                        }
634
635                        if (resource instanceof IResource) {
636                                if (theIncludedPids.contains(resourceId)) {
637                                        ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.INCLUDE);
638                                } else {
639                                        ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IResource) resource, BundleEntrySearchModeEnum.MATCH);
640                                }
641                        } else {
642                                if (theIncludedPids.contains(resourceId)) {
643                                        ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.INCLUDE.getCode());
644                                } else {
645                                        ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put((IAnyResource) resource, BundleEntrySearchModeEnum.MATCH.getCode());
646                                }
647                        }
648
649                        theResourceListToPopulate.set(index, resource);
650                }
651        }
652
653        private Map<ResourcePersistentId, Collection<ResourceTag>> getResourceTagMap(Collection<ResourceSearchView> theResourceSearchViewList) {
654
655                List<Long> idList = new ArrayList<>(theResourceSearchViewList.size());
656
657                //-- find all resource has tags
658                for (ResourceSearchView resource : theResourceSearchViewList) {
659                        if (resource.isHasTags())
660                                idList.add(resource.getId());
661                }
662
663                Map<ResourcePersistentId, Collection<ResourceTag>> tagMap = new HashMap<>();
664
665                //-- no tags
666                if (idList.size() == 0)
667                        return tagMap;
668
669                //-- get all tags for the idList
670                Collection<ResourceTag> tagList = myResourceTagDao.findByResourceIds(idList);
671
672                //-- build the map, key = resourceId, value = list of ResourceTag
673                ResourcePersistentId resourceId;
674                Collection<ResourceTag> tagCol;
675                for (ResourceTag tag : tagList) {
676
677                        resourceId = new ResourcePersistentId(tag.getResourceId());
678                        tagCol = tagMap.get(resourceId);
679                        if (tagCol == null) {
680                                tagCol = new ArrayList<>();
681                                tagCol.add(tag);
682                                tagMap.put(resourceId, tagCol);
683                        } else {
684                                tagCol.add(tag);
685                        }
686                }
687
688                return tagMap;
689        }
690
691        @Override
692        public void loadResourcesByPid(Collection<ResourcePersistentId> thePids, Collection<ResourcePersistentId> theIncludedPids, List<IBaseResource> theResourceListToPopulate, boolean theForHistoryOperation, RequestDetails theDetails) {
693                if (thePids.isEmpty()) {
694                        ourLog.debug("The include pids are empty");
695                        // return;
696                }
697
698                // Dupes will cause a crash later anyhow, but this is expensive so only do it
699                // when running asserts
700                assert new HashSet<>(thePids).size() == thePids.size() : "PID list contains duplicates: " + thePids;
701
702                Map<ResourcePersistentId, Integer> position = new HashMap<>();
703                for (ResourcePersistentId next : thePids) {
704                        position.put(next, theResourceListToPopulate.size());
705                        theResourceListToPopulate.add(null);
706                }
707
708                List<ResourcePersistentId> pids = new ArrayList<>(thePids);
709                new QueryChunker<ResourcePersistentId>().chunk(pids, t -> doLoadPids(t, theIncludedPids, theResourceListToPopulate, theForHistoryOperation, position));
710
711        }
712
713        /**
714         * THIS SHOULD RETURN HASHSET and not just Set because we add to it later
715         * so it can't be Collections.emptySet() or some such thing
716         */
717        @Override
718        public HashSet<ResourcePersistentId> loadIncludes(FhirContext theContext, EntityManager theEntityManager, Collection<ResourcePersistentId> theMatches, Set<Include> theRevIncludes,
719                                                                                                                                          boolean theReverseMode, DateRangeParam theLastUpdated, String theSearchIdOrDescription, RequestDetails theRequest, Integer theMaxCount) {
720                if (theMatches.size() == 0) {
721                        return new HashSet<>();
722                }
723                if (theRevIncludes == null || theRevIncludes.isEmpty()) {
724                        return new HashSet<>();
725                }
726                String searchFieldName = theReverseMode ? "myTargetResourcePid" : "mySourceResourcePid";
727                String findFieldName = theReverseMode ? "mySourceResourcePid" : "myTargetResourcePid";
728
729                Collection<ResourcePersistentId> nextRoundMatches = theMatches;
730                HashSet<ResourcePersistentId> allAdded = new HashSet<>();
731                HashSet<ResourcePersistentId> original = new HashSet<>(theMatches);
732                ArrayList<Include> includes = new ArrayList<>(theRevIncludes);
733
734                int roundCounts = 0;
735                StopWatch w = new StopWatch();
736
737                boolean addedSomeThisRound;
738                do {
739                        roundCounts++;
740
741                        HashSet<ResourcePersistentId> pidsToInclude = new HashSet<>();
742
743                        for (Iterator<Include> iter = includes.iterator(); iter.hasNext(); ) {
744                                Include nextInclude = iter.next();
745                                if (nextInclude.isRecurse() == false) {
746                                        iter.remove();
747                                }
748
749                                boolean matchAll = "*".equals(nextInclude.getValue());
750                                if (matchAll) {
751                                        String sql;
752                                        sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r." + searchFieldName + " IN (:target_pids) ";
753                                        List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
754                                        for (Collection<ResourcePersistentId> nextPartition : partitions) {
755                                                TypedQuery<Long> q = theEntityManager.createQuery(sql, Long.class);
756                                                q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
757                                                List<Long> results = q.getResultList();
758                                                for (Long resourceLink : results) {
759                                                        if (resourceLink == null) {
760                                                                continue;
761                                                        }
762                                                        if (theReverseMode) {
763                                                                pidsToInclude.add(new ResourcePersistentId(resourceLink));
764                                                        } else {
765                                                                pidsToInclude.add(new ResourcePersistentId(resourceLink));
766                                                        }
767                                                }
768                                        }
769                                } else {
770
771                                        List<String> paths;
772                                        RuntimeSearchParam param;
773                                        String resType = nextInclude.getParamType();
774                                        if (isBlank(resType)) {
775                                                continue;
776                                        }
777                                        RuntimeResourceDefinition def = theContext.getResourceDefinition(resType);
778                                        if (def == null) {
779                                                ourLog.warn("Unknown resource type in include/revinclude=" + nextInclude.getValue());
780                                                continue;
781                                        }
782
783                                        String paramName = nextInclude.getParamName();
784                                        if (isNotBlank(paramName)) {
785                                                param = mySearchParamRegistry.getActiveSearchParam(resType, paramName);
786                                        } else {
787                                                param = null;
788                                        }
789                                        if (param == null) {
790                                                ourLog.warn("Unknown param name in include/revinclude=" + nextInclude.getValue());
791                                                continue;
792                                        }
793
794                                        paths = param.getPathsSplit();
795
796                                        String targetResourceType = defaultString(nextInclude.getParamTargetType(), null);
797                                        for (String nextPath : paths) {
798                                                String sql;
799
800                                                boolean haveTargetTypesDefinedByParam = param.hasTargets();
801                                                if (targetResourceType != null) {
802                                                        sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType = :target_resource_type";
803                                                } else if (haveTargetTypesDefinedByParam) {
804                                                        sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids) AND r.myTargetResourceType in (:target_resource_types)";
805                                                } else {
806                                                        sql = "SELECT r." + findFieldName + " FROM ResourceLink r WHERE r.mySourcePath = :src_path AND r." + searchFieldName + " IN (:target_pids)";
807                                                }
808
809                                                List<Collection<ResourcePersistentId>> partitions = partition(nextRoundMatches, getMaximumPageSize());
810                                                for (Collection<ResourcePersistentId> nextPartition : partitions) {
811                                                        TypedQuery<Long> q = theEntityManager.createQuery(sql, Long.class);
812                                                        q.setParameter("src_path", nextPath);
813                                                        q.setParameter("target_pids", ResourcePersistentId.toLongList(nextPartition));
814                                                        if (targetResourceType != null) {
815                                                                q.setParameter("target_resource_type", targetResourceType);
816                                                        } else if (haveTargetTypesDefinedByParam) {
817                                                                q.setParameter("target_resource_types", param.getTargets());
818                                                        }
819                                                        List<Long> results = q.getResultList();
820                                                        for (Long resourceLink : results) {
821                                                                if (resourceLink != null) {
822                                                                        pidsToInclude.add(new ResourcePersistentId(resourceLink));
823                                                                }
824                                                        }
825                                                }
826                                        }
827                                }
828                        }
829
830                        if (theReverseMode) {
831                                if (theLastUpdated != null && (theLastUpdated.getLowerBoundAsInstant() != null || theLastUpdated.getUpperBoundAsInstant() != null)) {
832                                        pidsToInclude = new HashSet<>(filterResourceIdsByLastUpdated(theEntityManager, theLastUpdated, pidsToInclude));
833                                }
834                        }
835
836                        addedSomeThisRound = allAdded.addAll(pidsToInclude);
837                        nextRoundMatches = pidsToInclude;
838                } while (includes.size() > 0 && nextRoundMatches.size() > 0 && addedSomeThisRound);
839
840                allAdded.removeAll(original);
841
842                ourLog.info("Loaded {} {} in {} rounds and {} ms for search {}", allAdded.size(), theReverseMode ? "_revincludes" : "_includes", roundCounts, w.getMillisAndRestart(), theSearchIdOrDescription);
843
844                // Interceptor call: STORAGE_PREACCESS_RESOURCES
845                // This can be used to remove results from the search result details before
846                // the user has a chance to know that they were in the results
847                if (allAdded.size() > 0) {
848                        List<ResourcePersistentId> includedPidList = new ArrayList<>(allAdded);
849                        JpaPreResourceAccessDetails accessDetails = new JpaPreResourceAccessDetails(includedPidList, () -> this);
850                        HookParams params = new HookParams()
851                                .add(IPreResourceAccessDetails.class, accessDetails)
852                                .add(RequestDetails.class, theRequest)
853                                .addIfMatchesType(ServletRequestDetails.class, theRequest);
854                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PREACCESS_RESOURCES, params);
855
856                        allAdded = new HashSet<>(includedPidList);
857
858                        for (int i = includedPidList.size() - 1; i >= 0; i--) {
859                                if (accessDetails.isDontReturnResourceAtIndex(i)) {
860                                        ResourcePersistentId value = includedPidList.remove(i);
861                                        if (value != null) {
862                                                allAdded.remove(value);
863                                        }
864                                }
865                        }
866                }
867
868                return allAdded;
869        }
870
871        private List<Collection<ResourcePersistentId>> partition(Collection<ResourcePersistentId> theNextRoundMatches, int theMaxLoad) {
872                if (theNextRoundMatches.size() <= theMaxLoad) {
873                        return Collections.singletonList(theNextRoundMatches);
874                } else {
875
876                        List<Collection<ResourcePersistentId>> retVal = new ArrayList<>();
877                        Collection<ResourcePersistentId> current = null;
878                        for (ResourcePersistentId next : theNextRoundMatches) {
879                                if (current == null) {
880                                        current = new ArrayList<>(theMaxLoad);
881                                        retVal.add(current);
882                                }
883
884                                current.add(next);
885
886                                if (current.size() >= theMaxLoad) {
887                                        current = null;
888                                }
889                        }
890
891                        return retVal;
892                }
893        }
894
895        private void attemptCompositeUniqueSpProcessing(@Nonnull SearchParameterMap theParams, RequestDetails theRequest) {
896                // Since we're going to remove elements below
897                theParams.values().forEach(nextAndList -> ensureSubListsAreWritable(nextAndList));
898
899                List<RuntimeSearchParam> activeUniqueSearchParams = mySearchParamRegistry.getActiveComboSearchParams(myResourceName, theParams.keySet());
900                if (activeUniqueSearchParams.size() > 0) {
901
902                        Validate.isTrue(activeUniqueSearchParams.get(0).getComboSearchParamType()== ComboSearchParamType.UNIQUE, "Non unique combo parameters are not supported with the legacy search builder");
903
904                        StringBuilder sb = new StringBuilder();
905                        sb.append(myResourceName);
906                        sb.append("?");
907
908                        boolean first = true;
909
910                        ArrayList<String> keys = new ArrayList<>(theParams.keySet());
911                        Collections.sort(keys);
912                        for (String nextParamName : keys) {
913                                List<List<IQueryParameterType>> nextValues = theParams.get(nextParamName);
914
915                                nextParamName = UrlUtil.escapeUrlParam(nextParamName);
916                                if (nextValues.get(0).size() != 1) {
917                                        sb = null;
918                                        break;
919                                }
920
921                                // Reference params are only eligible for using a composite index if they
922                                // are qualified
923                                RuntimeSearchParam nextParamDef = mySearchParamRegistry.getActiveSearchParam(myResourceName, nextParamName);
924                                if (nextParamDef.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
925                                        ReferenceParam param = (ReferenceParam) nextValues.get(0).get(0);
926                                        if (isBlank(param.getResourceType())) {
927                                                sb = null;
928                                                break;
929                                        }
930                                }
931
932                                List<? extends IQueryParameterType> nextAnd = nextValues.remove(0);
933                                IQueryParameterType nextOr = nextAnd.remove(0);
934                                String nextOrValue = nextOr.getValueAsQueryToken(myContext);
935                                nextOrValue = UrlUtil.escapeUrlParam(nextOrValue);
936
937                                if (first) {
938                                        first = false;
939                                } else {
940                                        sb.append('&');
941                                }
942
943                                sb.append(nextParamName).append('=').append(nextOrValue);
944
945                        }
946
947                        if (sb != null) {
948                                String indexString = sb.toString();
949                                ourLog.debug("Checking for unique index for query: {}", indexString);
950
951                                // Interceptor broadcast: JPA_PERFTRACE_INFO
952                                StorageProcessingMessage msg = new StorageProcessingMessage()
953                                        .setMessage("Using unique index for query for search: " + indexString);
954                                HookParams params = new HookParams()
955                                        .add(RequestDetails.class, theRequest)
956                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
957                                        .add(StorageProcessingMessage.class, msg);
958                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
959
960                                addPredicateCompositeStringUnique(theParams, indexString, myRequestPartitionId);
961                        }
962                }
963        }
964
965        private <T> void ensureSubListsAreWritable(List<List<T>> theListOfLists) {
966                for (int i = 0; i < theListOfLists.size(); i++) {
967                        List<T> oldSubList = theListOfLists.get(i);
968                        if (!(oldSubList instanceof ArrayList)) {
969                                List<T> newSubList = new ArrayList<>(oldSubList);
970                                theListOfLists.set(i, newSubList);
971                        }
972                }
973        }
974
975        private void addPredicateCompositeStringUnique(@Nonnull SearchParameterMap theParams, String theIndexedString, RequestPartitionId theRequestPartitionId) {
976                From<?, ResourceIndexedComboStringUnique> join = myQueryStack.createJoin(SearchBuilderJoinEnum.COMPOSITE_UNIQUE, null);
977
978                if (!theRequestPartitionId.isAllPartitions()) {
979                        Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull();
980                        Predicate predicate = myCriteriaBuilder.equal(join.get("myPartitionIdValue").as(Integer.class), partitionId);
981                        myQueryStack.addPredicate(predicate);
982                }
983
984                Predicate predicate = myCriteriaBuilder.equal(join.get("myIndexString"), theIndexedString);
985                myQueryStack.addPredicateWithImplicitTypeSelection(predicate);
986
987                // Remove any empty parameters remaining after this
988                theParams.clean();
989        }
990
991        @Override
992        public void setFetchSize(int theFetchSize) {
993                myFetchSize = theFetchSize;
994        }
995
996        @VisibleForTesting
997        void setParamsForUnitTest(SearchParameterMap theParams) {
998                myParams = theParams;
999        }
1000
1001        public SearchParameterMap getParams() {
1002                return myParams;
1003        }
1004
1005        @VisibleForTesting
1006        void setEntityManagerForUnitTest(EntityManager theEntityManager) {
1007                myEntityManager = theEntityManager;
1008        }
1009
1010        public CriteriaBuilder getBuilder() {
1011                return myCriteriaBuilder;
1012        }
1013
1014        public QueryStack getQueryStack() {
1015                return myQueryStack;
1016        }
1017
1018        public Class<? extends IBaseResource> getResourceType() {
1019                return myResourceType;
1020        }
1021
1022        public String getResourceName() {
1023                return myResourceName;
1024        }
1025
1026        @VisibleForTesting
1027        public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
1028                myDaoConfig = theDaoConfig;
1029        }
1030
1031        private List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder) {
1032                List<Predicate> lastUpdatedPredicates = new ArrayList<>();
1033                if (theLastUpdated != null) {
1034                        if (theLastUpdated.getLowerBoundAsInstant() != null) {
1035                                ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
1036                                Predicate predicateLower = builder.greaterThanOrEqualTo(myQueryStack.getLastUpdatedColumn(), theLastUpdated.getLowerBoundAsInstant());
1037                                lastUpdatedPredicates.add(predicateLower);
1038                        }
1039                        if (theLastUpdated.getUpperBoundAsInstant() != null) {
1040                                ourLog.debug("LastUpdated upper bound: {}", new InstantDt(theLastUpdated.getUpperBoundAsInstant()));
1041                                Predicate predicateUpper = builder.lessThanOrEqualTo(myQueryStack.getLastUpdatedColumn(), theLastUpdated.getUpperBoundAsInstant());
1042                                lastUpdatedPredicates.add(predicateUpper);
1043                        }
1044                }
1045                return lastUpdatedPredicates;
1046        }
1047
1048        public class IncludesIterator extends BaseIterator<ResourcePersistentId> implements Iterator<ResourcePersistentId> {
1049
1050                private final RequestDetails myRequest;
1051                private Iterator<ResourcePersistentId> myCurrentIterator;
1052                private final Set<ResourcePersistentId> myCurrentPids;
1053                private ResourcePersistentId myNext;
1054
1055                IncludesIterator(Set<ResourcePersistentId> thePidSet, RequestDetails theRequest) {
1056                        myCurrentPids = new HashSet<>(thePidSet);
1057                        myCurrentIterator = EMPTY_LONG_LIST.iterator();
1058                        myRequest = theRequest;
1059                }
1060
1061                private void fetchNext() {
1062                        while (myNext == null) {
1063
1064                                if (myCurrentIterator.hasNext()) {
1065                                        myNext = myCurrentIterator.next();
1066                                        break;
1067                                }
1068
1069                                Set<Include> includes = Collections.singleton(new Include("*", true));
1070                                Set<ResourcePersistentId> newPids = loadIncludes(myContext, myEntityManager, myCurrentPids, includes, false, getParams().getLastUpdated(), mySearchUuid, myRequest, null);
1071                                if (newPids.isEmpty()) {
1072                                        myNext = NO_MORE;
1073                                        break;
1074                                }
1075                                myCurrentPids.addAll(newPids);
1076                                myCurrentIterator = newPids.iterator();
1077                        }
1078                }
1079
1080                @Override
1081                public boolean hasNext() {
1082                        fetchNext();
1083                        return !NO_MORE.equals(myNext);
1084                }
1085
1086                @Override
1087                public ResourcePersistentId next() {
1088                        fetchNext();
1089                        ResourcePersistentId retVal = myNext;
1090                        myNext = null;
1091                        return retVal;
1092                }
1093
1094        }
1095
1096        private final class QueryIterator extends BaseIterator<ResourcePersistentId> implements IResultIterator {
1097
1098                private final SearchRuntimeDetails mySearchRuntimeDetails;
1099                private final RequestDetails myRequest;
1100                private final boolean myHaveRawSqlHooks;
1101                private final boolean myHavePerfTraceFoundIdHook;
1102                private boolean myFirst = true;
1103                private IncludesIterator myIncludesIterator;
1104                private ResourcePersistentId myNext;
1105                private ScrollableResultsIterator<Long> myResultsIterator;
1106                private Integer myOffset;
1107                private final SortSpec mySort;
1108                private boolean myStillNeedToFetchIncludes;
1109                private int mySkipCount = 0;
1110                private int myNonSkipCount = 0;
1111
1112                private List<TypedQuery<Long>> myQueryList = new ArrayList<>();
1113
1114                private QueryIterator(SearchRuntimeDetails theSearchRuntimeDetails, RequestDetails theRequest) {
1115                        mySearchRuntimeDetails = theSearchRuntimeDetails;
1116                        mySort = myParams.getSort();
1117                        myOffset = myParams.getOffset();
1118                        myRequest = theRequest;
1119
1120                        // Includes are processed inline for $everything query
1121                        if (myParams.getEverythingMode() != null) {
1122                                myStillNeedToFetchIncludes = true;
1123                        }
1124
1125                        myHavePerfTraceFoundIdHook = CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, myInterceptorBroadcaster, myRequest);
1126                        myHaveRawSqlHooks = CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_RAW_SQL, myInterceptorBroadcaster, myRequest);
1127
1128                }
1129
1130                private boolean isPagingProviderDatabaseBacked() {
1131                        if (myRequest == null || myRequest.getServer() == null) {
1132                                return false;
1133                        }
1134                        return myRequest.getServer().getPagingProvider() instanceof DatabaseBackedPagingProvider;
1135                }
1136
1137                private void fetchNext() {
1138
1139                        try {
1140                                if (myHaveRawSqlHooks) {
1141                                        CurrentThreadCaptureQueriesListener.startCapturing();
1142                                }
1143
1144                                // If we don't have a query yet, create one
1145                                if (myResultsIterator == null) {
1146                                        if (myMaxResultsToFetch == null) {
1147                                                if (myParams.getLoadSynchronousUpTo() != null) {
1148                                                        myMaxResultsToFetch = myParams.getLoadSynchronousUpTo();
1149                                                } else if (myParams.getCount() != null) {
1150                                                        myMaxResultsToFetch = myParams.getCount();
1151                                                } else {
1152                                                        myMaxResultsToFetch = myDaoConfig.getFetchSizeDefaultMaximum();
1153                                                }
1154                                        }
1155
1156                                        initializeIteratorQuery(myOffset, myMaxResultsToFetch);
1157
1158                                        if (myAlsoIncludePids == null) {
1159                                                myAlsoIncludePids = new ArrayList<>();
1160                                        }
1161                                }
1162
1163                                if (myNext == null) {
1164
1165                                        for (Iterator<ResourcePersistentId> myPreResultsIterator = myAlsoIncludePids.iterator(); myPreResultsIterator.hasNext();) {
1166                                                        ResourcePersistentId next = myPreResultsIterator.next();
1167                                                        if (next != null)
1168                                                                if (myPidSet.add(next)) {
1169                                                                        myNext = next;
1170                                                                        break;
1171                                                                }
1172                                                }
1173
1174                                        if (myNext == null) {
1175                                                while (myResultsIterator.hasNext() || !myQueryList.isEmpty()) {
1176                                                        // Update iterator with next chunk if necessary.
1177                                                        if (!myResultsIterator.hasNext()) {
1178                                                                retrieveNextIteratorQuery();
1179                                                        }
1180
1181                                                        Long nextLong = myResultsIterator.next();
1182                                                        if (myHavePerfTraceFoundIdHook) {
1183                                                                HookParams params = new HookParams()
1184                                                                        .add(Integer.class, System.identityHashCode(this))
1185                                                                        .add(Object.class, nextLong);
1186                                                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FOUND_ID, params);
1187                                                        }
1188
1189                                                        if (nextLong != null) {
1190                                                                ResourcePersistentId next = new ResourcePersistentId(nextLong);
1191                                                                if (myPidSet.add(next)) {
1192                                                                        myNext = next;
1193                                                                        myNonSkipCount++;
1194                                                                        break;
1195                                                                } else {
1196                                                                        mySkipCount++;
1197                                                                }
1198                                                        }
1199
1200                                                        if (!myResultsIterator.hasNext()) {
1201                                                                if (myMaxResultsToFetch != null && (mySkipCount + myNonSkipCount == myMaxResultsToFetch)) {
1202                                                                        if (mySkipCount > 0 && myNonSkipCount == 0) {
1203                                                                                myMaxResultsToFetch += 1000;
1204
1205                                                                                StorageProcessingMessage message = new StorageProcessingMessage();
1206                                                                                String msg = "Pass completed with no matching results. This indicates an inefficient query! Retrying with new max count of " + myMaxResultsToFetch;
1207                                                                                ourLog.warn(msg);
1208                                                                                message.setMessage(msg);
1209                                                                                HookParams params = new HookParams()
1210                                                                                        .add(RequestDetails.class, myRequest)
1211                                                                                        .addIfMatchesType(ServletRequestDetails.class, myRequest)
1212                                                                                        .add(StorageProcessingMessage.class, message);
1213                                                                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_WARNING, params);
1214
1215                                                                                initializeIteratorQuery(null, myMaxResultsToFetch);
1216                                                                        }
1217                                                                }
1218                                                        }
1219                                                }
1220                                        }
1221
1222                                        if (myNext == null) {
1223                                                if (myStillNeedToFetchIncludes) {
1224                                                        myIncludesIterator = new IncludesIterator(myPidSet, myRequest);
1225                                                        myStillNeedToFetchIncludes = false;
1226                                                }
1227                                                if (myIncludesIterator != null) {
1228                                                        while (myIncludesIterator.hasNext()) {
1229                                                                ResourcePersistentId next = myIncludesIterator.next();
1230                                                                if (next != null)
1231                                                                        if (myPidSet.add(next)) {
1232                                                                                myNext = next;
1233                                                                                break;
1234                                                                        }
1235                                                        }
1236                                                        if (myNext == null) {
1237                                                                myNext = NO_MORE;
1238                                                        }
1239                                                } else {
1240                                                        myNext = NO_MORE;
1241                                                }
1242                                        }
1243
1244                                } // if we need to fetch the next result
1245
1246                                mySearchRuntimeDetails.setFoundMatchesCount(myPidSet.size());
1247
1248                        } finally {
1249                                if (myHaveRawSqlHooks) {
1250                                        SqlQueryList capturedQueries = CurrentThreadCaptureQueriesListener.getCurrentQueueAndStopCapturing();
1251                                        HookParams params = new HookParams()
1252                                                .add(RequestDetails.class, myRequest)
1253                                                .addIfMatchesType(ServletRequestDetails.class, myRequest)
1254                                                .add(SqlQueryList.class, capturedQueries);
1255                                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_RAW_SQL, params);
1256                                }
1257                        }
1258
1259                        if (myFirst) {
1260                                HookParams params = new HookParams()
1261                                        .add(RequestDetails.class, myRequest)
1262                                        .addIfMatchesType(ServletRequestDetails.class, myRequest)
1263                                        .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
1264                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_FIRST_RESULT_LOADED, params);
1265                                myFirst = false;
1266                        }
1267
1268                        if (NO_MORE.equals(myNext)) {
1269                                HookParams params = new HookParams()
1270                                        .add(RequestDetails.class, myRequest)
1271                                        .addIfMatchesType(ServletRequestDetails.class, myRequest)
1272                                        .add(SearchRuntimeDetails.class, mySearchRuntimeDetails);
1273                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, myRequest, Pointcut.JPA_PERFTRACE_SEARCH_SELECT_COMPLETE, params);
1274                        }
1275
1276                }
1277
1278                private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) {
1279                        if (myQueryList.isEmpty()) {
1280                                // Capture times for Lucene/Elasticsearch queries as well
1281                                mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
1282                                myQueryList = createQuery(mySort, theOffset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails);
1283                        }
1284
1285                        mySearchRuntimeDetails.setQueryStopwatch(new StopWatch());
1286
1287                        retrieveNextIteratorQuery();
1288
1289                        mySkipCount = 0;
1290                        myNonSkipCount = 0;
1291                }
1292
1293                private void retrieveNextIteratorQuery() {
1294                        if (myQueryList != null && myQueryList.size() > 0) {
1295                                final TypedQuery<Long> query = myQueryList.remove(0);
1296                                Query<Long> hibernateQuery = (Query<Long>) (query);
1297                                hibernateQuery.setFetchSize(myFetchSize);
1298                                ScrollableResults scroll = hibernateQuery.scroll(ScrollMode.FORWARD_ONLY);
1299                                myResultsIterator = new ScrollableResultsIterator<>(scroll);
1300                        } else {
1301                                myResultsIterator = null;
1302                        }
1303
1304                }
1305
1306                @Override
1307                public boolean hasNext() {
1308                        if (myNext == null) {
1309                                fetchNext();
1310                        }
1311                        return !NO_MORE.equals(myNext);
1312                }
1313
1314                @Override
1315                public ResourcePersistentId next() {
1316                        fetchNext();
1317                        ResourcePersistentId retVal = myNext;
1318                        myNext = null;
1319                        Validate.isTrue(!NO_MORE.equals(retVal), "No more elements");
1320                        return retVal;
1321                }
1322
1323                @Override
1324                public int getSkippedCount() {
1325                        return mySkipCount;
1326                }
1327
1328                @Override
1329                public int getNonSkippedCount() {
1330                        return myNonSkipCount;
1331                }
1332
1333                @Override
1334                public Collection<ResourcePersistentId> getNextResultBatch(long theBatchSize) {
1335                        Collection<ResourcePersistentId> batch = new ArrayList<>();
1336                        while (this.hasNext() && batch.size() < theBatchSize) {
1337                                batch.add(this.next());
1338                        }
1339                        return batch;
1340                }
1341
1342                @Override
1343                public void close() {
1344                        if (myResultsIterator != null) {
1345                                myResultsIterator.close();
1346                        }
1347                }
1348
1349        }
1350
1351        private static class CountQueryIterator implements Iterator<Long> {
1352                private final TypedQuery<Long> myQuery;
1353                private boolean myCountLoaded;
1354                private Long myCount;
1355
1356                CountQueryIterator(TypedQuery<Long> theQuery) {
1357                        myQuery = theQuery;
1358                }
1359
1360                @Override
1361                public boolean hasNext() {
1362                        boolean retVal = myCount != null;
1363                        if (!retVal) {
1364                                if (myCountLoaded == false) {
1365                                        myCount = myQuery.getSingleResult();
1366                                        retVal = true;
1367                                        myCountLoaded = true;
1368                                }
1369                        }
1370                        return retVal;
1371                }
1372
1373                @Override
1374                public Long next() {
1375                        Validate.isTrue(hasNext());
1376                        Validate.isTrue(myCount != null);
1377                        Long retVal = myCount;
1378                        myCount = null;
1379                        return retVal;
1380                }
1381        }
1382
1383        private static List<Predicate> createLastUpdatedPredicates(final DateRangeParam theLastUpdated, CriteriaBuilder builder, From<?, ResourceTable> from) {
1384                List<Predicate> lastUpdatedPredicates = new ArrayList<>();
1385                if (theLastUpdated != null) {
1386                        if (theLastUpdated.getLowerBoundAsInstant() != null) {
1387                                ourLog.debug("LastUpdated lower bound: {}", new InstantDt(theLastUpdated.getLowerBoundAsInstant()));
1388                                Predicate predicateLower = builder.greaterThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getLowerBoundAsInstant());
1389                                lastUpdatedPredicates.add(predicateLower);
1390                        }
1391                        if (theLastUpdated.getUpperBoundAsInstant() != null) {
1392                                Predicate predicateUpper = builder.lessThanOrEqualTo(from.get("myUpdated"), theLastUpdated.getUpperBoundAsInstant());
1393                                lastUpdatedPredicates.add(predicateUpper);
1394                        }
1395                }
1396                return lastUpdatedPredicates;
1397        }
1398
1399        private static List<ResourcePersistentId> filterResourceIdsByLastUpdated(EntityManager theEntityManager, final DateRangeParam theLastUpdated, Collection<ResourcePersistentId> thePids) {
1400                if (thePids.isEmpty()) {
1401                        return Collections.emptyList();
1402                }
1403                CriteriaBuilder builder = theEntityManager.getCriteriaBuilder();
1404                CriteriaQuery<Long> cq = builder.createQuery(Long.class);
1405                Root<ResourceTable> from = cq.from(ResourceTable.class);
1406                cq.select(from.get("myId").as(Long.class));
1407
1408                List<Predicate> lastUpdatedPredicates = createLastUpdatedPredicates(theLastUpdated, builder, from);
1409                lastUpdatedPredicates.add(from.get("myId").as(Long.class).in(ResourcePersistentId.toLongList(thePids)));
1410
1411                cq.where(LegacySearchBuilder.toPredicateArray(lastUpdatedPredicates));
1412                TypedQuery<Long> query = theEntityManager.createQuery(cq);
1413
1414                return ResourcePersistentId.fromLongList(query.getResultList());
1415        }
1416
1417        public static Predicate[] toPredicateArray(List<Predicate> thePredicates) {
1418                return thePredicates.toArray(new Predicate[0]);
1419        }
1420}