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.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.nuxeo.ecm.core.api.CoreSession;
030import org.nuxeo.ecm.core.api.DocumentModel;
031import org.nuxeo.ecm.core.api.DocumentModelList;
032import org.nuxeo.ecm.core.api.Filter;
033import org.nuxeo.ecm.core.api.NuxeoException;
034import org.nuxeo.ecm.core.api.SortInfo;
035import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
036import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
037import org.nuxeo.ecm.platform.query.api.PageSelections;
038import org.nuxeo.runtime.api.Framework;
039import org.nuxeo.runtime.services.config.ConfigurationService;
040
041/**
042 * Page provider performing a query on a core session.
043 * <p>
044 * It builds the query at each call so that it can refresh itself when the query changes.
045 * <p>
046 * The page provider property named {@link #CORE_SESSION_PROPERTY} is used to pass the {@link CoreSession} instance that
047 * will perform the query. The optional property {@link #CHECK_QUERY_CACHE_PROPERTY} can be set to "true" to avoid
048 * performing the query again if it did not change.
049 * <p>
050 * Since 6.0, the page provider property named {@link #USE_UNRESTRICTED_SESSION_PROPERTY} allows specifying whether the
051 * query should be run as unrestricted. When such a property is set to "true", the additional property
052 * {@link #DETACH_DOCUMENTS_PROPERTY} is used to detach documents (defaults to true when session is unrestricted).
053 *
054 * @author Anahide Tchertchian
055 * @since 5.4
056 */
057public class CoreQueryDocumentPageProvider extends AbstractPageProvider<DocumentModel> {
058
059    public static final String CORE_SESSION_PROPERTY = "coreSession";
060
061    public static final String MAX_RESULTS_PROPERTY = "maxResults";
062
063    // Special maxResults value used for navigation, can be tuned
064    public static final String DEFAULT_NAVIGATION_RESULTS_KEY = "DEFAULT_NAVIGATION_RESULTS";
065
066    // Special maxResults value that means same as the page size
067    public static final String PAGE_SIZE_RESULTS_KEY = "PAGE_SIZE";
068
069    public static final String DEFAULT_NAVIGATION_RESULTS_PROPERTY = "org.nuxeo.ecm.platform.query.nxql.defaultNavigationResults";
070
071    public static final String DEFAULT_NAVIGATION_RESULTS_VALUE = "200";
072
073    public static final String CHECK_QUERY_CACHE_PROPERTY = "checkQueryCache";
074
075    /**
076     * Boolean property stating that query should be unrestricted.
077     *
078     * @since 6.0
079     */
080    public static final String USE_UNRESTRICTED_SESSION_PROPERTY = "useUnrestrictedSession";
081
082    /**
083     * Boolean property stating that documents should be detached, only useful when property
084     * {@link #USE_UNRESTRICTED_SESSION_PROPERTY} is set to true.
085     * <p>
086     * When an unrestricted session is used, this property defaults to true.
087     *
088     * @since 6.0
089     */
090    public static final String DETACH_DOCUMENTS_PROPERTY = "detachDocuments";
091
092    private static final Log log = LogFactory.getLog(CoreQueryDocumentPageProvider.class);
093
094    private static final long serialVersionUID = 1L;
095
096    protected String query;
097
098    protected List<DocumentModel> currentPageDocuments;
099
100    protected Long maxResults;
101
102    @Override
103    public List<DocumentModel> getCurrentPage() {
104
105        long t0 = System.currentTimeMillis();
106
107        checkQueryCache();
108        if (currentPageDocuments == null) {
109            error = null;
110            errorMessage = null;
111
112            CoreSession coreSession = getCoreSession();
113            if (query == null) {
114                buildQuery(coreSession);
115            }
116            if (query == null) {
117                throw new NuxeoException(String.format("Cannot perform null query: check provider '%s'", getName()));
118            }
119
120            currentPageDocuments = new ArrayList<DocumentModel>();
121
122            try {
123
124                final long minMaxPageSize = getMinMaxPageSize();
125
126                final long offset = getCurrentPageOffset();
127                if (log.isDebugEnabled()) {
128                    log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s",
129                            getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset)));
130                }
131
132                final DocumentModelList docs;
133                final long maxResults = getMaxResults();
134                final Filter filter = getFilter();
135                final boolean useUnrestricted = useUnrestrictedSession();
136
137                final boolean detachDocs = detachDocuments();
138                if (maxResults > 0) {
139                    if (useUnrestricted) {
140                        CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
141                                query, filter, minMaxPageSize, offset, false, maxResults, detachDocs);
142                        r.runUnrestricted();
143                        docs = r.getDocs();
144                    } else {
145                        docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, maxResults);
146                    }
147                } else {
148                    // use a totalCount=true instead of countUpTo=-1 to
149                    // enable global limitation described in NXP-9381
150                    if (useUnrestricted) {
151                        CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
152                                query, filter, minMaxPageSize, offset, true, maxResults, detachDocs);
153                        r.runUnrestricted();
154                        docs = r.getDocs();
155                    } else {
156                        docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, true);
157                    }
158                }
159
160                long resultsCount = docs.totalSize();
161                if (resultsCount < 0) {
162                    // results count is truncated
163                    setResultsCount(UNKNOWN_SIZE_AFTER_QUERY);
164                } else {
165                    setResultsCount(resultsCount);
166                }
167                currentPageDocuments = docs;
168
169                if (log.isDebugEnabled()) {
170                    log.debug(String.format("Performed query for provider '%s': got %s hits (limit %s)", getName(),
171                            Long.valueOf(resultsCount), Long.valueOf(getMaxResults())));
172                }
173
174                // refresh may have triggered display of an empty page => go
175                // back to first page or forward to last page depending on
176                // results count and page size
177                long pageSize = getPageSize();
178                if (pageSize != 0) {
179                    if (offset != 0 && currentPageDocuments.size() == 0) {
180                        if (resultsCount == 0) {
181                            // fetch first page directly
182                            if (log.isDebugEnabled()) {
183                                log.debug(String.format("Current page %s is not the first one but "
184                                        + "shows no result and there are " + "no results => rewind to first page",
185                                        Long.valueOf(getCurrentPageIndex())));
186                            }
187                            firstPage();
188                        } else {
189                            // fetch last page
190                            if (log.isDebugEnabled()) {
191                                log.debug(String.format("Current page %s is not the first one but "
192                                        + "shows no result and there are " + "%s results => fetch last page",
193                                        Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
194                            }
195                            lastPage();
196                        }
197                        // fetch current page again
198                        getCurrentPage();
199                    }
200                }
201
202                if (getResultsCount() < 0) {
203                    // additional info to handle next page when results count
204                    // is unknown
205                    if (currentPageDocuments != null && currentPageDocuments.size() > 0) {
206                        int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex();
207                        int currentFilledPage = Long.valueOf(getCurrentPageIndex()).intValue();
208                        if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) {
209                            setCurrentHigherNonEmptyPageIndex(currentFilledPage);
210                        }
211                    }
212                }
213            } catch (NuxeoException e) {
214                error = e;
215                errorMessage = e.getMessage();
216                log.warn(e.getMessage(), e);
217            }
218        }
219
220        // send event for statistics !
221        fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis()-t0);
222
223        return currentPageDocuments;
224    }
225
226    protected void buildQuery(CoreSession coreSession) {
227        SortInfo[] sortArray = null;
228        if (sortInfos != null) {
229            sortArray = sortInfos.toArray(new SortInfo[] {});
230        }
231        String newQuery;
232        PageProviderDefinition def = getDefinition();
233        if (def.getWhereClause() == null) {
234            newQuery = NXQLQueryBuilder.getQuery(def.getPattern(), getParameters(), def.getQuotePatternParameters(),
235                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
236        } else {
237            DocumentModel searchDocumentModel = getSearchDocumentModel();
238            if (searchDocumentModel == null) {
239                throw new NuxeoException(String.format(
240                        "Cannot build query of provider '%s': " + "no search document model is set", getName()));
241            }
242            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, def.getWhereClause(), getParameters(), sortArray);
243        }
244
245        if (query != null && newQuery != null && !newQuery.equals(query)) {
246            // query has changed => refresh
247            refresh();
248        }
249        query = newQuery;
250    }
251
252    protected void checkQueryCache() {
253        // maybe handle refresh of select page according to query
254        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
255            CoreSession coreSession = getCoreSession();
256            buildQuery(coreSession);
257        }
258    }
259
260    protected boolean useUnrestrictedSession() {
261        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
262    }
263
264    protected boolean detachDocuments() {
265        return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true);
266    }
267
268    protected CoreSession getCoreSession() {
269        Map<String, Serializable> props = getProperties();
270        CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY);
271        if (coreSession == null) {
272            throw new NuxeoException("cannot find core session");
273        }
274        return coreSession;
275    }
276
277    /**
278     * Returns the maximum number of results or <code>0<code> if there is no limit.
279     *
280     * @since 5.6
281     */
282    public long getMaxResults() {
283        if (maxResults == null) {
284            maxResults = Long.valueOf(0);
285            String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY);
286            if (maxResultsStr != null) {
287                if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) {
288                    ConfigurationService cs = Framework.getService(ConfigurationService.class);
289                    maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY,
290                            DEFAULT_NAVIGATION_RESULTS_VALUE);
291                } else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) {
292                    maxResultsStr = Long.valueOf(getPageSize()).toString();
293                }
294                try {
295                    maxResults = Long.valueOf(maxResultsStr);
296                } catch (NumberFormatException e) {
297                    log.warn(String.format(
298                            "Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.",
299                            maxResultsStr, getName()));
300                }
301            }
302        }
303        return maxResults.longValue();
304    }
305
306    /**
307     * Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this
308     * limit.
309     *
310     * @since 5.8
311     */
312    @Override
313    public long getPageLimit() {
314        long pageSize = getPageSize();
315        if (pageSize == 0) {
316            return 0;
317        }
318        return getMaxResults() / pageSize;
319    }
320
321    /**
322     * Sets the maximum number of result elements.
323     *
324     * @since 5.6
325     */
326    public void setMaxResults(long maxResults) {
327        this.maxResults = Long.valueOf(maxResults);
328    }
329
330    @Override
331    public PageSelections<DocumentModel> getCurrentSelectPage() {
332        checkQueryCache();
333        return super.getCurrentSelectPage();
334    }
335
336    public String getCurrentQuery() {
337        return query;
338    }
339
340    /**
341     * Filter to use when processing results.
342     * <p>
343     * Defaults to null (no filter applied), method to be overridden by subclasses.
344     *
345     * @since 6.0
346     */
347    protected Filter getFilter() {
348        return null;
349    }
350
351    @Override
352    protected void pageChanged() {
353        currentPageDocuments = null;
354        super.pageChanged();
355    }
356
357    @Override
358    public void refresh() {
359        query = null;
360        currentPageDocuments = null;
361        super.refresh();
362    }
363
364}