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 */
017package org.nuxeo.ecm.platform.query.nxql;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.Iterator;
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.IterableQueryResult;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.api.SortInfo;
032import org.nuxeo.ecm.core.query.sql.NXQL;
033import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
034import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
035import org.nuxeo.ecm.platform.query.api.PageSelections;
036
037/**
038 * Page provider performing a queryAndFetch on a core session.
039 * <p>
040 * It builds the query at each call so that it can refresh itself when the query changes.
041 * <p>
042 * <p>
043 * The page provider property named {@link #CORE_SESSION_PROPERTY} is used to pass the {@link CoreSession} instance that
044 * will perform the query. The optional property {@link #CHECK_QUERY_CACHE_PROPERTY} can be set to "true" to avoid
045 * performing the query again if it did not change.
046 * <p>
047 * Since 6.0, the page provider property named {@link #LANGUAGE_PROPERTY} allows specifying the query language (NXQL,
048 * NXTAG,...).
049 * <p>
050 * Also since 6.0, the page provider property named {@link #USE_UNRESTRICTED_SESSION_PROPERTY} allows specifying whether
051 * the query should be run as unrestricted.
052 *
053 * @author Anahide Tchertchian
054 * @since 5.4
055 */
056public class CoreQueryAndFetchPageProvider extends AbstractPageProvider<Map<String, Serializable>> {
057
058    public static final String CORE_SESSION_PROPERTY = "coreSession";
059
060    public static final String CHECK_QUERY_CACHE_PROPERTY = "checkQueryCache";
061
062    /**
063     * Boolean property stating that query should be unrestricted.
064     *
065     * @since 6.0
066     */
067    public static final String USE_UNRESTRICTED_SESSION_PROPERTY = "useUnrestrictedSession";
068
069    /**
070     * @since 6.0: alow specifying the query language (NXQL, NXTAG,...)
071     */
072    public static final String LANGUAGE_PROPERTY = "language";
073
074    private static final long serialVersionUID = 1L;
075
076    private static final Log log = LogFactory.getLog(CoreQueryDocumentPageProvider.class);
077
078    protected String query;
079
080    protected List<Map<String, Serializable>> currentItems;
081
082    protected CoreSession getCoreSession() {
083        CoreSession coreSession = null;
084        Map<String, Serializable> props = getProperties();
085        coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY);
086        return coreSession;
087    }
088
089    @Override
090    public List<Map<String, Serializable>> getCurrentPage() {
091        checkQueryCache();
092        CoreSession coreSession = null;
093        long t0 = System.currentTimeMillis();
094        if (currentItems == null) {
095            errorMessage = null;
096            error = null;
097
098            if (query == null) {
099                buildQuery();
100            }
101            if (query == null) {
102                throw new NuxeoException(String.format("Cannot perform null query: check provider '%s'", getName()));
103            }
104
105            currentItems = new ArrayList<Map<String, Serializable>>();
106
107            Map<String, Serializable> props = getProperties();
108            coreSession = getCoreSession();
109            if (coreSession == null) {
110                throw new NuxeoException("cannot find core session");
111            }
112
113            IterableQueryResult result = null;
114            try {
115
116                long minMaxPageSize = getMinMaxPageSize();
117
118                long offset = getCurrentPageOffset();
119                if (log.isDebugEnabled()) {
120                    log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s",
121                            getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset)));
122                }
123
124                final String language = getQueryLanguage();
125                final boolean useUnrestricted = useUnrestrictedSession();
126                if (useUnrestricted) {
127                    CoreQueryAndFetchUnrestrictedSessionRunner r = new CoreQueryAndFetchUnrestrictedSessionRunner(
128                            coreSession, query, language);
129                    r.runUnrestricted();
130                    result = r.getResult();
131                } else {
132                    result = coreSession.queryAndFetch(query, language);
133                }
134                long resultsCount = result.size();
135                setResultsCount(resultsCount);
136                if (offset < resultsCount) {
137                    result.skipTo(offset);
138                }
139
140                Iterator<Map<String, Serializable>> it = result.iterator();
141                int pos = 0;
142                while (it.hasNext() && (maxPageSize == 0 || pos < minMaxPageSize)) {
143                    pos += 1;
144                    Map<String, Serializable> item = it.next();
145                    currentItems.add(item);
146                }
147
148                if (log.isDebugEnabled()) {
149                    log.debug(String.format("Performed query for provider '%s': got %s hits", getName(),
150                            Long.valueOf(resultsCount)));
151                }
152
153                // refresh may have triggered display of an empty page => go
154                // back to first page or forward to last page depending on
155                // results count and page size
156                long pageSize = getPageSize();
157                if (pageSize != 0) {
158                    if (offset != 0 && currentItems.size() == 0) {
159                        if (resultsCount == 0) {
160                            // fetch first page directly
161                            if (log.isDebugEnabled()) {
162                                log.debug(String.format("Current page %s is not the first one but "
163                                        + "shows no result and there are " + "no results => rewind to first page",
164                                        Long.valueOf(getCurrentPageIndex())));
165                            }
166                            firstPage();
167                        } else {
168                            // fetch last page
169                            if (log.isDebugEnabled()) {
170                                log.debug(String.format("Current page %s is not the first one but "
171                                        + "shows no result and there are " + "%s results => fetch last page",
172                                        Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
173                            }
174                            lastPage();
175                        }
176                        // fetch current page again
177                        getCurrentPage();
178                    }
179                }
180
181            } catch (NuxeoException e) {
182                errorMessage = e.getMessage();
183                error = e;
184                log.warn(e.getMessage(), e);
185            } finally {
186                if (result != null) {
187                    result.close();
188                }
189            }
190        }
191
192        if (coreSession == null) {
193            coreSession = getCoreSession();
194        }
195
196        // send event for statistics !
197        fireSearchEvent(coreSession.getPrincipal(), query, currentItems, System.currentTimeMillis() - t0);
198
199        return currentItems;
200    }
201
202    protected void buildQuery() {
203        SortInfo[] sortArray = null;
204        if (sortInfos != null) {
205            sortArray = sortInfos.toArray(new SortInfo[] {});
206        }
207        String newQuery;
208        PageProviderDefinition def = getDefinition();
209        if (def.getWhereClause() == null) {
210            newQuery = NXQLQueryBuilder.getQuery(def.getPattern(), getParameters(), def.getQuotePatternParameters(),
211                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
212        } else {
213            DocumentModel searchDocumentModel = getSearchDocumentModel();
214            if (searchDocumentModel == null) {
215                throw new NuxeoException(String.format("Cannot build query of provider '%s': "
216                        + "no search document model is set", getName()));
217            }
218            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, def.getWhereClause(), getParameters(), sortArray);
219        }
220
221        if (query != null && newQuery != null && !newQuery.equals(query)) {
222            // query has changed => refresh
223            refresh();
224        }
225        query = newQuery;
226    }
227
228    @Override
229    public PageSelections<Map<String, Serializable>> getCurrentSelectPage() {
230        checkQueryCache();
231        return super.getCurrentSelectPage();
232    }
233
234    protected void checkQueryCache() {
235        // maybe handle refresh of select page according to query
236        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
237            buildQuery();
238        }
239    }
240
241    protected boolean useUnrestrictedSession() {
242        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
243    }
244
245    protected String getQueryLanguage() {
246        Map<String, Serializable> props = getProperties();
247        if (props.containsKey(LANGUAGE_PROPERTY)) {
248            return (String) props.get(LANGUAGE_PROPERTY);
249        }
250        return NXQL.NXQL;
251    }
252
253    public String getCurrentQuery() {
254        return query;
255    }
256
257    @Override
258    protected void pageChanged() {
259        currentItems = null;
260        super.pageChanged();
261    }
262
263    @Override
264    public void refresh() {
265        query = null;
266        currentItems = null;
267        super.refresh();
268    }
269
270}