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            } catch (NuxeoException e) {
159                errorMessage = e.getMessage();
160                error = e;
161                log.warn(e.getMessage(), e);
162            } finally {
163                if (result != null) {
164                    result.close();
165                }
166            }
167        }
168
169        if (coreSession == null) {
170            coreSession = getCoreSession();
171        }
172
173        // send event for statistics !
174        fireSearchEvent(coreSession.getPrincipal(), query, currentItems, System.currentTimeMillis() - t0);
175
176        return currentItems;
177    }
178
179    protected void buildQuery() {
180        List<SortInfo> sort = null;
181        List<QuickFilter> quickFilters = getQuickFilters();
182        String quickFiltersClause = "";
183
184        if (quickFilters != null && !quickFilters.isEmpty()) {
185            sort = new ArrayList<>();
186            for (QuickFilter quickFilter : quickFilters) {
187                String clause = quickFilter.getClause();
188                if (!quickFiltersClause.isEmpty() && clause != null) {
189                    quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
190                } else {
191                    quickFiltersClause = clause != null ? clause : "";
192                }
193                sort.addAll(quickFilter.getSortInfos());
194            }
195        } else if (sortInfos != null) {
196            sort = sortInfos;
197        }
198
199        SortInfo[] sortArray = null;
200        if (sort != null) {
201            sortArray = sort.toArray(new SortInfo[] {});
202        }
203
204        String newQuery;
205        PageProviderDefinition def = getDefinition();
206        WhereClauseDefinition whereClause = def.getWhereClause();
207        if (whereClause == null) {
208
209            String originalPattern = def.getPattern();
210            String pattern = quickFiltersClause.isEmpty() ? originalPattern
211                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
212                    ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
213                    : originalPattern + " WHERE " + quickFiltersClause;
214
215            newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(),
216                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
217        } else {
218
219            DocumentModel searchDocumentModel = getSearchDocumentModel();
220            if (searchDocumentModel == null) {
221                throw new NuxeoException(String.format(
222                        "Cannot build query of provider '%s': " + "no search document model is set", getName()));
223            }
224            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(),
225                    sortArray);
226        }
227
228        if (query != null && newQuery != null && !newQuery.equals(query)) {
229            // query has changed => refresh
230            refresh();
231        }
232        query = newQuery;
233    }
234
235    @Override
236    public PageSelections<Map<String, Serializable>> getCurrentSelectPage() {
237        checkQueryCache();
238        // fetch last page if current page index is beyond the last page or if there are no results to display
239        rewindSelectablePage();
240        return super.getCurrentSelectPage();
241    }
242
243    /**
244     * Fetch a page that can be selected. It loads the last page if we're targeting a page beyond the last one or
245     * the first page if there are no results to show and we're targeting anything other than the first page.
246     *
247     * Fix for NXP-8564.
248     */
249    protected void rewindSelectablePage() {
250        long pageSize = getPageSize();
251        if (pageSize != 0) {
252            if (offset != 0 && currentItems != null && currentItems.size() == 0) {
253                if (resultsCount == 0) {
254                    // fetch first page directly
255                    if (log.isDebugEnabled()) {
256                        log.debug(String.format(
257                            "Current page %s is not the first one but " + "shows no result and there are "
258                                + "no results => rewind to first page",
259                            Long.valueOf(getCurrentPageIndex())));
260                    }
261                    firstPage();
262                } else {
263                    // fetch last page
264                    if (log.isDebugEnabled()) {
265                        log.debug(String.format(
266                            "Current page %s is not the first one but " + "shows no result and there are "
267                                + "%s results => fetch last page",
268                            Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
269                    }
270                    lastPage();
271                }
272                // fetch current page again
273                getCurrentPage();
274            }
275        }
276    }
277
278    protected void checkQueryCache() {
279        // maybe handle refresh of select page according to query
280        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
281            buildQuery();
282        }
283    }
284
285    protected boolean useUnrestrictedSession() {
286        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
287    }
288
289    protected String getQueryLanguage() {
290        Map<String, Serializable> props = getProperties();
291        if (props.containsKey(LANGUAGE_PROPERTY)) {
292            return (String) props.get(LANGUAGE_PROPERTY);
293        }
294        return NXQL.NXQL;
295    }
296
297    public String getCurrentQuery() {
298        return query;
299    }
300
301    @Override
302    protected void pageChanged() {
303        currentItems = null;
304        super.pageChanged();
305    }
306
307    @Override
308    public void refresh() {
309        query = null;
310        currentItems = null;
311        super.refresh();
312    }
313
314}