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