001/*
002 * (C) Copyright 2010 Nuxeo SA (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.lang.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<DocumentModel>();
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                // refresh may have triggered display of an empty page => go
178                // back to first page or forward to last page depending on
179                // results count and page size
180                long pageSize = getPageSize();
181                if (pageSize != 0) {
182                    if (offset != 0 && currentPageDocuments.size() == 0) {
183                        if (resultsCount == 0) {
184                            // fetch first page directly
185                            if (log.isDebugEnabled()) {
186                                log.debug(String.format(
187                                        "Current page %s is not the first one but " + "shows no result and there are "
188                                                + "no results => rewind to first page",
189                                        Long.valueOf(getCurrentPageIndex())));
190                            }
191                            firstPage();
192                        } else {
193                            // fetch last page
194                            if (log.isDebugEnabled()) {
195                                log.debug(String.format(
196                                        "Current page %s is not the first one but " + "shows no result and there are "
197                                                + "%s results => fetch last page",
198                                        Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
199                            }
200                            lastPage();
201                        }
202                        // fetch current page again
203                        getCurrentPage();
204                    }
205                }
206
207                if (getResultsCount() < 0) {
208                    // additional info to handle next page when results count
209                    // is unknown
210                    if (currentPageDocuments != null && currentPageDocuments.size() > 0) {
211                        int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex();
212                        int currentFilledPage = Long.valueOf(getCurrentPageIndex()).intValue();
213                        if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) {
214                            setCurrentHigherNonEmptyPageIndex(currentFilledPage);
215                        }
216                    }
217                }
218            } catch (NuxeoException e) {
219                error = e;
220                errorMessage = e.getMessage();
221                log.warn(e.getMessage(), e);
222            }
223        }
224
225        // send event for statistics !
226        fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis() - t0);
227
228        return currentPageDocuments;
229    }
230
231    protected void buildQuery(CoreSession coreSession) {
232        List<SortInfo> sort = null;
233        List<QuickFilter> quickFilters = getQuickFilters();
234        String quickFiltersClause = "";
235
236        if (quickFilters != null && !quickFilters.isEmpty()) {
237            sort = new ArrayList<>();
238            for (QuickFilter quickFilter : quickFilters) {
239                String clause = quickFilter.getClause();
240                if (clause != null) {
241                    if (!quickFiltersClause.isEmpty()) {
242                        quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
243                    } else {
244                        quickFiltersClause = clause;
245                    }
246                }
247                sort.addAll(quickFilter.getSortInfos());
248            }
249        } else if (sortInfos != null) {
250            sort = sortInfos;
251        }
252
253        SortInfo[] sortArray = null;
254        if (sort != null) {
255            sortArray = sort.toArray(new SortInfo[] {});
256        }
257
258        String newQuery;
259        PageProviderDefinition def = getDefinition();
260        WhereClauseDefinition whereClause = def.getWhereClause();
261        if (whereClause == null) {
262
263            String originalPattern = def.getPattern();
264            String pattern = quickFiltersClause.isEmpty() ? originalPattern
265                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
266                            ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
267                            : originalPattern + " WHERE " + quickFiltersClause;
268
269            newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(),
270                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
271        } else {
272
273            DocumentModel searchDocumentModel = getSearchDocumentModel();
274            if (searchDocumentModel == null) {
275                throw new NuxeoException(String.format(
276                        "Cannot build query of provider '%s': " + "no search document model is set", getName()));
277            }
278            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(),
279                    sortArray);
280        }
281
282        if (query != null && newQuery != null && !newQuery.equals(query)) {
283            // query has changed => refresh
284            refresh();
285        }
286        query = newQuery;
287    }
288
289    protected void checkQueryCache() {
290        // maybe handle refresh of select page according to query
291        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
292            CoreSession coreSession = getCoreSession();
293            buildQuery(coreSession);
294        }
295    }
296
297    protected boolean useUnrestrictedSession() {
298        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
299    }
300
301    protected boolean detachDocuments() {
302        return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true);
303    }
304
305    protected CoreSession getCoreSession() {
306        Map<String, Serializable> props = getProperties();
307        CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY);
308        if (coreSession == null) {
309            throw new NuxeoException("cannot find core session");
310        }
311        return coreSession;
312    }
313
314    /**
315     * Returns the maximum number of results or <code>0<code> if there is no limit.
316     *
317     * @since 5.6
318     */
319    public long getMaxResults() {
320        if (maxResults == null) {
321            maxResults = Long.valueOf(0);
322            String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY);
323            if (maxResultsStr != null) {
324                if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) {
325                    ConfigurationService cs = Framework.getService(ConfigurationService.class);
326                    maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY,
327                            DEFAULT_NAVIGATION_RESULTS_VALUE);
328                } else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) {
329                    maxResultsStr = Long.valueOf(getPageSize()).toString();
330                }
331                try {
332                    maxResults = Long.valueOf(maxResultsStr);
333                } catch (NumberFormatException e) {
334                    log.warn(String.format(
335                            "Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.",
336                            maxResultsStr, getName()));
337                }
338            }
339        }
340        return maxResults.longValue();
341    }
342
343    /**
344     * Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this
345     * limit.
346     *
347     * @since 5.8
348     */
349    @Override
350    public long getPageLimit() {
351        long pageSize = getPageSize();
352        if (pageSize == 0) {
353            return 0;
354        }
355        return getMaxResults() / pageSize;
356    }
357
358    /**
359     * Sets the maximum number of result elements.
360     *
361     * @since 5.6
362     */
363    public void setMaxResults(long maxResults) {
364        this.maxResults = Long.valueOf(maxResults);
365    }
366
367    @Override
368    public PageSelections<DocumentModel> getCurrentSelectPage() {
369        checkQueryCache();
370        return super.getCurrentSelectPage();
371    }
372
373    public String getCurrentQuery() {
374        return query;
375    }
376
377    /**
378     * Filter to use when processing results.
379     * <p>
380     * Defaults to null (no filter applied), method to be overridden by subclasses.
381     *
382     * @since 6.0
383     */
384    protected Filter getFilter() {
385        return null;
386    }
387
388    @Override
389    protected void pageChanged() {
390        currentPageDocuments = null;
391        super.pageChanged();
392    }
393
394    @Override
395    public void refresh() {
396        query = null;
397        currentPageDocuments = null;
398        super.refresh();
399    }
400
401}