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