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