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