001/*
002 * (C) Copyright 2010-2018 Nuxeo (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.lang3.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;
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<>();
111
112            coreSession = getCoreSession();
113            if (coreSession == null) {
114                throw new NuxeoException("cannot find core session");
115            }
116
117            IterableQueryResult result = null;
118            try {
119
120                long minMaxPageSize = getMinMaxPageSize();
121
122                long offset = getCurrentPageOffset();
123                if (log.isDebugEnabled()) {
124                    log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s",
125                            getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset)));
126                }
127
128                final String language = getQueryLanguage();
129                final boolean useUnrestricted = useUnrestrictedSession();
130                if (useUnrestricted) {
131                    CoreQueryAndFetchUnrestrictedSessionRunner r = new CoreQueryAndFetchUnrestrictedSessionRunner(
132                            coreSession, query, language);
133                    r.runUnrestricted();
134                    result = r.getResult();
135                } else {
136                    result = coreSession.queryAndFetch(query, language);
137                }
138                long resultsCount = result.size();
139                setResultsCount(resultsCount);
140                if (offset < resultsCount) {
141                    result.skipTo(offset);
142                }
143
144                Iterator<Map<String, Serializable>> it = result.iterator();
145                int pos = 0;
146                while (it.hasNext() && (maxPageSize == 0 || pos < minMaxPageSize)) {
147                    pos += 1;
148                    Map<String, Serializable> item = it.next();
149                    currentItems.add(item);
150                }
151
152                if (log.isDebugEnabled()) {
153                    log.debug(String.format("Performed query for provider '%s': got %s hits", getName(),
154                            Long.valueOf(resultsCount)));
155                }
156
157            } catch (NuxeoException e) {
158                errorMessage = e.getMessage();
159                error = e;
160                log.warn(e.getMessage(), e);
161            } finally {
162                if (result != null) {
163                    result.close();
164                }
165            }
166        }
167
168        if (coreSession == null) {
169            coreSession = getCoreSession();
170        }
171
172        // send event for statistics !
173        fireSearchEvent(coreSession.getPrincipal(), query, currentItems, System.currentTimeMillis() - t0);
174
175        return currentItems;
176    }
177
178    protected void buildQuery() {
179        List<SortInfo> sort = null;
180        List<QuickFilter> quickFilters = getQuickFilters();
181        String quickFiltersClause = "";
182
183        if (quickFilters != null && !quickFilters.isEmpty()) {
184            sort = new ArrayList<>();
185            for (QuickFilter quickFilter : quickFilters) {
186                String clause = quickFilter.getClause();
187                if (!quickFiltersClause.isEmpty() && clause != null) {
188                    quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
189                } else {
190                    quickFiltersClause = clause != null ? clause : "";
191                }
192                sort.addAll(quickFilter.getSortInfos());
193            }
194        } else if (sortInfos != null) {
195            sort = sortInfos;
196        }
197
198        SortInfo[] sortArray = null;
199        if (sort != null) {
200            sortArray = sort.toArray(new SortInfo[] {});
201        }
202
203        String newQuery;
204        PageProviderDefinition def = getDefinition();
205        WhereClauseDefinition whereClause = def.getWhereClause();
206        if (whereClause == null) {
207
208            String originalPattern = def.getPattern();
209            String pattern = quickFiltersClause.isEmpty() ? originalPattern
210                    : StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
211                    ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
212                    : originalPattern + " WHERE " + quickFiltersClause;
213
214            newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(),
215                    def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
216        } else {
217
218            DocumentModel searchDocumentModel = getSearchDocumentModel();
219            if (searchDocumentModel == null) {
220                throw new NuxeoException(String.format(
221                        "Cannot build query of provider '%s': " + "no search document model is set", getName()));
222            }
223            newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(),
224                    sortArray);
225        }
226
227        if (query != null && newQuery != null && !newQuery.equals(query)) {
228            // query has changed => refresh
229            refresh();
230        }
231        query = newQuery;
232    }
233
234    @Override
235    public PageSelections<Map<String, Serializable>> getCurrentSelectPage() {
236        checkQueryCache();
237        // fetch last page if current page index is beyond the last page or if there are no results to display
238        rewindSelectablePage();
239        return super.getCurrentSelectPage();
240    }
241
242    /**
243     * Fetch a page that can be selected. It loads the last page if we're targeting a page beyond the last one or
244     * the first page if there are no results to show and we're targeting anything other than the first page.
245     *
246     * Fix for NXP-8564.
247     */
248    protected void rewindSelectablePage() {
249        long pageSize = getPageSize();
250        if (pageSize != 0) {
251            if (offset != 0 && currentItems != null && currentItems.size() == 0) {
252                if (resultsCount == 0) {
253                    // fetch first page directly
254                    if (log.isDebugEnabled()) {
255                        log.debug(String.format(
256                            "Current page %s is not the first one but " + "shows no result and there are "
257                                + "no results => rewind to first page",
258                            Long.valueOf(getCurrentPageIndex())));
259                    }
260                    firstPage();
261                } else {
262                    // fetch last page
263                    if (log.isDebugEnabled()) {
264                        log.debug(String.format(
265                            "Current page %s is not the first one but " + "shows no result and there are "
266                                + "%s results => fetch last page",
267                            Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
268                    }
269                    lastPage();
270                }
271                // fetch current page again
272                getCurrentPage();
273            }
274        }
275    }
276
277    protected void checkQueryCache() {
278        // maybe handle refresh of select page according to query
279        if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
280            buildQuery();
281        }
282    }
283
284    protected boolean useUnrestrictedSession() {
285        return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
286    }
287
288    protected String getQueryLanguage() {
289        Map<String, Serializable> props = getProperties();
290        if (props.containsKey(LANGUAGE_PROPERTY)) {
291            return (String) props.get(LANGUAGE_PROPERTY);
292        }
293        return NXQL.NXQL;
294    }
295
296    public String getCurrentQuery() {
297        return query;
298    }
299
300    @Override
301    protected void pageChanged() {
302        currentItems = null;
303        super.pageChanged();
304    }
305
306    @Override
307    public void refresh() {
308        query = null;
309        currentItems = null;
310        super.refresh();
311    }
312
313}