001/*
002 * (C) Copyright 2010-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Anahide Tchertchian
018 *     Benoit Delbosc
019 */
020package org.nuxeo.ecm.platform.query.nxql;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026
027import org.apache.commons.lang3.StringUtils;
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030import org.nuxeo.ecm.core.api.CoreSession;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.DocumentModelList;
033import org.nuxeo.ecm.core.api.Filter;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.core.api.SortInfo;
036import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
037import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
038import org.nuxeo.ecm.platform.query.api.PageSelections;
039import org.nuxeo.ecm.platform.query.api.QuickFilter;
040import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition;
041import org.nuxeo.runtime.api.Framework;
042import org.nuxeo.runtime.services.config.ConfigurationService;
043
044/**
045 * Page provider performing a query on a core session.
046 * <p>
047 * It builds the query at each call so that it can refresh itself when the query changes.
048 * <p>
049 * The page provider property named {@link #CORE_SESSION_PROPERTY} is used to pass the {@link CoreSession} instance that
050 * will perform the query. The optional property {@link #CHECK_QUERY_CACHE_PROPERTY} can be set to "true" to avoid
051 * performing the query again if it did not change.
052 * <p>
053 * Since 6.0, the page provider property named {@link #USE_UNRESTRICTED_SESSION_PROPERTY} allows specifying whether the
054 * query should be run as unrestricted. When such a property is set to "true", the additional property
055 * {@link #DETACH_DOCUMENTS_PROPERTY} is used to detach documents (defaults to true when session is unrestricted).
056 *
057 * @author Anahide Tchertchian
058 * @since 5.4
059 */
060public class CoreQueryDocumentPageProvider extends AbstractPageProvider<DocumentModel> {
061
062    public static final String CORE_SESSION_PROPERTY = "coreSession";
063
064    public static final String MAX_RESULTS_PROPERTY = "maxResults";
065
066    // Special maxResults value used for navigation, can be tuned
067    public static final String DEFAULT_NAVIGATION_RESULTS_KEY = "DEFAULT_NAVIGATION_RESULTS";
068
069    // Special maxResults value that means same as the page size
070    public static final String PAGE_SIZE_RESULTS_KEY = "PAGE_SIZE";
071
072    public static final String DEFAULT_NAVIGATION_RESULTS_PROPERTY = "org.nuxeo.ecm.platform.query.nxql.defaultNavigationResults";
073
074    public static final String DEFAULT_NAVIGATION_RESULTS_VALUE = "200";
075
076    public static final String CHECK_QUERY_CACHE_PROPERTY = "checkQueryCache";
077
078    /**
079     * Boolean property stating that query should be unrestricted.
080     *
081     * @since 6.0
082     */
083    public static final String USE_UNRESTRICTED_SESSION_PROPERTY = "useUnrestrictedSession";
084
085    /**
086     * Boolean property stating that documents should be detached, only useful when property
087     * {@link #USE_UNRESTRICTED_SESSION_PROPERTY} is set to true.
088     * <p>
089     * When an unrestricted session is used, this property defaults to true.
090     *
091     * @since 6.0
092     */
093    public static final String DETACH_DOCUMENTS_PROPERTY = "detachDocuments";
094
095    private static final Log log = LogFactory.getLog(CoreQueryDocumentPageProvider.class);
096
097    private static final long serialVersionUID = 1L;
098
099    protected String query;
100
101    protected List<DocumentModel> currentPageDocuments;
102
103    protected Long maxResults;
104
105    @Override
106    public List<DocumentModel> getCurrentPage() {
107
108        long t0 = System.currentTimeMillis();
109
110        checkQueryCache();
111        if (currentPageDocuments == null) {
112            error = null;
113            errorMessage = null;
114
115            CoreSession coreSession = getCoreSession();
116            if (query == null) {
117                buildQuery(coreSession);
118            }
119            if (query == null) {
120                throw new NuxeoException(String.format("Cannot perform null query: check provider '%s'", getName()));
121            }
122
123            currentPageDocuments = new ArrayList<>();
124
125            try {
126
127                final long minMaxPageSize = getMinMaxPageSize();
128
129                final long offset = getCurrentPageOffset();
130                if (log.isDebugEnabled()) {
131                    log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s",
132                            getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset)));
133                }
134
135                final DocumentModelList docs;
136                final long maxResults = getMaxResults();
137                final Filter filter = getFilter();
138                final boolean useUnrestricted = useUnrestrictedSession();
139
140                final boolean detachDocs = detachDocuments();
141                if (maxResults > 0) {
142                    if (useUnrestricted) {
143                        CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
144                                query, filter, minMaxPageSize, offset, false, maxResults, detachDocs);
145                        r.runUnrestricted();
146                        docs = r.getDocs();
147                    } else {
148                        docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, maxResults);
149                    }
150                } else {
151                    // use a totalCount=true instead of countUpTo=-1 to
152                    // enable global limitation described in NXP-9381
153                    if (useUnrestricted) {
154                        CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
155                                query, filter, minMaxPageSize, offset, true, maxResults, detachDocs);
156                        r.runUnrestricted();
157                        docs = r.getDocs();
158                    } else {
159                        docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, true);
160                    }
161                }
162
163                long resultsCount = docs.totalSize();
164                if (resultsCount < 0) {
165                    // results count is truncated
166                    setResultsCount(UNKNOWN_SIZE_AFTER_QUERY);
167                } else {
168                    setResultsCount(resultsCount);
169                }
170                currentPageDocuments = docs;
171
172                if (log.isDebugEnabled()) {
173                    log.debug(String.format("Performed query for provider '%s': got %s hits (limit %s)", getName(),
174                            Long.valueOf(resultsCount), Long.valueOf(getMaxResults())));
175                }
176
177                if (getResultsCount() < 0) {
178                    // additional info to handle next page when results count
179                    // is unknown
180                    if (currentPageDocuments != null && currentPageDocuments.size() > 0) {
181                        int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex();
182                        int currentFilledPage = (int) getCurrentPageIndex();
183                        if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) {
184                            setCurrentHigherNonEmptyPageIndex(currentFilledPage);
185                        }
186                    }
187                }
188            } catch (NuxeoException e) {
189                error = e;
190                errorMessage = e.getMessage();
191                log.warn(e.getMessage(), e);
192            }
193        }
194
195        // send event for statistics !
196        fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis() - t0);
197
198        return currentPageDocuments;
199    }
200
201    protected void buildQuery(CoreSession coreSession) {
202        List<SortInfo> sort = null;
203        List<QuickFilter> quickFilters = getQuickFilters();
204        String quickFiltersClause = "";
205
206        if (quickFilters != null && !quickFilters.isEmpty()) {
207            sort = new ArrayList<>();
208            for (QuickFilter quickFilter : quickFilters) {
209                String clause = quickFilter.getClause();
210                if (clause != null) {
211                    if (!quickFiltersClause.isEmpty()) {
212                        quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
213                    } else {
214                        quickFiltersClause = clause;
215                    }
216                }
217                sort.addAll(quickFilter.getSortInfos());
218            }
219        } else if (sortInfos != null) {
220            sort = sortInfos;
221        }
222
223        SortInfo[] sortArray = null;
224        if (sort != null) {
225            sortArray = sort.toArray(new SortInfo[] {});
226        }
227
228        String newQuery;
229        PageProviderDefinition def = getDefinition();
230        WhereClauseDefinition whereClause = def.getWhereClause();
231        if (whereClause == null) {
232
233            String originalPattern = def.getPattern();
234            String pattern = quickFiltersClause.isEmpty() ? originalPattern
235                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
236                            ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
237                            : originalPattern + " WHERE " + quickFiltersClause;
238
239            newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(),
240                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
241        } else {
242
243            DocumentModel searchDocumentModel = getSearchDocumentModel();
244            if (searchDocumentModel == null) {
245                throw new NuxeoException(String.format(
246                        "Cannot build query of provider '%s': " + "no search document model is set", getName()));
247            }
248            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(),
249                    sortArray);
250        }
251
252        if (query != null && newQuery != null && !newQuery.equals(query)) {
253            // query has changed => refresh
254            refresh();
255        }
256        query = newQuery;
257    }
258
259    protected void checkQueryCache() {
260        // maybe handle refresh of select page according to query
261        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
262            CoreSession coreSession = getCoreSession();
263            buildQuery(coreSession);
264        }
265    }
266
267    protected boolean useUnrestrictedSession() {
268        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
269    }
270
271    protected boolean detachDocuments() {
272        return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true);
273    }
274
275    protected CoreSession getCoreSession() {
276        Map<String, Serializable> props = getProperties();
277        CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY);
278        if (coreSession == null) {
279            throw new NuxeoException("cannot find core session");
280        }
281        return coreSession;
282    }
283
284    /**
285     * Returns the maximum number of results or <code>0<code> if there is no limit.
286     *
287     * @since 5.6
288     */
289    public long getMaxResults() {
290        if (maxResults == null) {
291            maxResults = Long.valueOf(0);
292            String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY);
293            if (maxResultsStr != null) {
294                if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) {
295                    ConfigurationService cs = Framework.getService(ConfigurationService.class);
296                    maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY,
297                            DEFAULT_NAVIGATION_RESULTS_VALUE);
298                } else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) {
299                    maxResultsStr = Long.valueOf(getPageSize()).toString();
300                }
301                try {
302                    maxResults = Long.valueOf(maxResultsStr);
303                } catch (NumberFormatException e) {
304                    log.warn(String.format(
305                            "Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.",
306                            maxResultsStr, getName()));
307                }
308            }
309        }
310        return maxResults.longValue();
311    }
312
313    @Override
314    public long getResultsCountLimit() {
315        return getMaxResults();
316    }
317
318    /**
319     * Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this
320     * limit.
321     *
322     * @since 5.8
323     */
324    @Override
325    public long getPageLimit() {
326        long pageSize = getPageSize();
327        if (pageSize == 0) {
328            return 0;
329        }
330        return getMaxResults() / pageSize;
331    }
332
333    /**
334     * Sets the maximum number of result elements.
335     *
336     * @since 5.6
337     */
338    public void setMaxResults(long maxResults) {
339        this.maxResults = Long.valueOf(maxResults);
340    }
341
342    @Override
343    public PageSelections<DocumentModel> getCurrentSelectPage() {
344        checkQueryCache();
345        // fetch last page if current page index is beyond the last page or if there are no results to display
346        rewindSelectablePage();
347        return super.getCurrentSelectPage();
348    }
349
350    public String getCurrentQuery() {
351        return query;
352    }
353
354    /**
355     * Fetch a page that can be selected. It loads the last page if we're targeting a page beyond the last one or the
356     * first page if there are no results to show and we're targeting anything other than the first page. Fix for
357     * NXP-8564.
358     */
359    protected void rewindSelectablePage() {
360        long pageSize = getPageSize();
361        if (pageSize != 0) {
362            if (offset != 0 && currentPageDocuments != null && currentPageDocuments.size() == 0) {
363                if (resultsCount == 0) {
364                    // fetch first page directly
365                    if (log.isDebugEnabled()) {
366                        log.debug(
367                                String.format(
368                                        "Current page %s is not the first one but " + "shows no result and there are "
369                                                + "no results => rewind to first page",
370                                                Long.valueOf(getCurrentPageIndex())));
371                    }
372                    firstPage();
373                } else {
374                    // fetch last page
375                    if (log.isDebugEnabled()) {
376                        log.debug(String.format(
377                                "Current page %s is not the first one but " + "shows no result and there are "
378                                        + "%s results => fetch last page",
379                                Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
380                    }
381                    lastPage();
382                }
383                // fetch current page again
384                getCurrentPage();
385            }
386        }
387    }
388
389    /**
390     * Filter to use when processing results.
391     * <p>
392     * Defaults to null (no filter applied), method to be overridden by subclasses.
393     *
394     * @since 6.0
395     */
396    protected Filter getFilter() {
397        return null;
398    }
399
400    @Override
401    protected void pageChanged() {
402        currentPageDocuments = null;
403        super.pageChanged();
404    }
405
406    @Override
407    public void refresh() {
408        query = null;
409        currentPageDocuments = null;
410        super.refresh();
411    }
412
413}