/*
* (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Anahide Tchertchian
* Benoit Delbosc
*/
package org.nuxeo.ecm.platform.query.nxql;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.Filter;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.SortInfo;
import org.nuxeo.ecm.platform.query.api.AbstractPageProvider;
import org.nuxeo.ecm.platform.query.api.PageProviderDefinition;
import org.nuxeo.ecm.platform.query.api.PageSelections;
import org.nuxeo.ecm.platform.query.api.QuickFilter;
import org.nuxeo.ecm.platform.query.api.WhereClauseDefinition;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.services.config.ConfigurationService;
/**
* Page provider performing a query on a core session.
* <p>
* It builds the query at each call so that it can refresh itself when the query changes.
* <p>
* The page provider property named {@link #CORE_SESSION_PROPERTY} is used to pass the {@link CoreSession} instance that
* will perform the query. The optional property {@link #CHECK_QUERY_CACHE_PROPERTY} can be set to "true" to avoid
* performing the query again if it did not change.
* <p>
* Since 6.0, the page provider property named {@link #USE_UNRESTRICTED_SESSION_PROPERTY} allows specifying whether the
* query should be run as unrestricted. When such a property is set to "true", the additional property
* {@link #DETACH_DOCUMENTS_PROPERTY} is used to detach documents (defaults to true when session is unrestricted).
*
* @author Anahide Tchertchian
* @since 5.4
*/
public class CoreQueryDocumentPageProvider extends AbstractPageProvider<DocumentModel> {
public static final String CORE_SESSION_PROPERTY = "coreSession";
public static final String MAX_RESULTS_PROPERTY = "maxResults";
// Special maxResults value used for navigation, can be tuned
public static final String DEFAULT_NAVIGATION_RESULTS_KEY = "DEFAULT_NAVIGATION_RESULTS";
// Special maxResults value that means same as the page size
public static final String PAGE_SIZE_RESULTS_KEY = "PAGE_SIZE";
public static final String DEFAULT_NAVIGATION_RESULTS_PROPERTY = "org.nuxeo.ecm.platform.query.nxql.defaultNavigationResults";
public static final String DEFAULT_NAVIGATION_RESULTS_VALUE = "200";
public static final String CHECK_QUERY_CACHE_PROPERTY = "checkQueryCache";
/**
* Boolean property stating that query should be unrestricted.
*
* @since 6.0
*/
public static final String USE_UNRESTRICTED_SESSION_PROPERTY = "useUnrestrictedSession";
/**
* Boolean property stating that documents should be detached, only useful when property
* {@link #USE_UNRESTRICTED_SESSION_PROPERTY} is set to true.
* <p>
* When an unrestricted session is used, this property defaults to true.
*
* @since 6.0
*/
public static final String DETACH_DOCUMENTS_PROPERTY = "detachDocuments";
private static final Log log = LogFactory.getLog(CoreQueryDocumentPageProvider.class);
private static final long serialVersionUID = 1L;
protected String query;
protected List<DocumentModel> currentPageDocuments;
protected Long maxResults;
@Override
public List<DocumentModel> getCurrentPage() {
long t0 = System.currentTimeMillis();
checkQueryCache();
if (currentPageDocuments == null) {
error = null;
errorMessage = null;
CoreSession coreSession = getCoreSession();
if (query == null) {
buildQuery(coreSession);
}
if (query == null) {
throw new NuxeoException(String.format("Cannot perform null query: check provider '%s'", getName()));
}
currentPageDocuments = new ArrayList<DocumentModel>();
try {
final long minMaxPageSize = getMinMaxPageSize();
final long offset = getCurrentPageOffset();
if (log.isDebugEnabled()) {
log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s",
getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset)));
}
final DocumentModelList docs;
final long maxResults = getMaxResults();
final Filter filter = getFilter();
final boolean useUnrestricted = useUnrestrictedSession();
final boolean detachDocs = detachDocuments();
if (maxResults > 0) {
if (useUnrestricted) {
CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
query, filter, minMaxPageSize, offset, false, maxResults, detachDocs);
r.runUnrestricted();
docs = r.getDocs();
} else {
docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, maxResults);
}
} else {
// use a totalCount=true instead of countUpTo=-1 to
// enable global limitation described in NXP-9381
if (useUnrestricted) {
CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession,
query, filter, minMaxPageSize, offset, true, maxResults, detachDocs);
r.runUnrestricted();
docs = r.getDocs();
} else {
docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, true);
}
}
long resultsCount = docs.totalSize();
if (resultsCount < 0) {
// results count is truncated
setResultsCount(UNKNOWN_SIZE_AFTER_QUERY);
} else {
setResultsCount(resultsCount);
}
currentPageDocuments = docs;
if (log.isDebugEnabled()) {
log.debug(String.format("Performed query for provider '%s': got %s hits (limit %s)", getName(),
Long.valueOf(resultsCount), Long.valueOf(getMaxResults())));
}
// refresh may have triggered display of an empty page => go
// back to first page or forward to last page depending on
// results count and page size
long pageSize = getPageSize();
if (pageSize != 0) {
if (offset != 0 && currentPageDocuments.size() == 0) {
if (resultsCount == 0) {
// fetch first page directly
if (log.isDebugEnabled()) {
log.debug(String.format(
"Current page %s is not the first one but " + "shows no result and there are "
+ "no results => rewind to first page",
Long.valueOf(getCurrentPageIndex())));
}
firstPage();
} else {
// fetch last page
if (log.isDebugEnabled()) {
log.debug(String.format(
"Current page %s is not the first one but " + "shows no result and there are "
+ "%s results => fetch last page",
Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount)));
}
lastPage();
}
// fetch current page again
getCurrentPage();
}
}
if (getResultsCount() < 0) {
// additional info to handle next page when results count
// is unknown
if (currentPageDocuments != null && currentPageDocuments.size() > 0) {
int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex();
int currentFilledPage = Long.valueOf(getCurrentPageIndex()).intValue();
if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) {
setCurrentHigherNonEmptyPageIndex(currentFilledPage);
}
}
}
} catch (NuxeoException e) {
error = e;
errorMessage = e.getMessage();
log.warn(e.getMessage(), e);
}
}
// send event for statistics !
fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis() - t0);
return currentPageDocuments;
}
protected void buildQuery(CoreSession coreSession) {
List<SortInfo> sort = null;
List<QuickFilter> quickFilters = getQuickFilters();
String quickFiltersClause = "";
if (quickFilters != null && !quickFilters.isEmpty()) {
sort = new ArrayList<>();
for (QuickFilter quickFilter : quickFilters) {
String clause = quickFilter.getClause();
if (clause != null) {
if (!quickFiltersClause.isEmpty()) {
quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause);
} else {
quickFiltersClause = clause;
}
}
sort.addAll(quickFilter.getSortInfos());
}
} else if (sortInfos != null) {
sort = sortInfos;
}
SortInfo[] sortArray = null;
if (sort != null) {
sortArray = sort.toArray(new SortInfo[] {});
}
String newQuery;
PageProviderDefinition def = getDefinition();
WhereClauseDefinition whereClause = def.getWhereClause();
if (whereClause == null) {
String originalPattern = def.getPattern();
String pattern = quickFiltersClause.isEmpty() ? originalPattern
: StringUtils.containsIgnoreCase(originalPattern, " WHERE ")
? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause)
: originalPattern + " WHERE " + quickFiltersClause;
newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(),
def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray);
} else {
DocumentModel searchDocumentModel = getSearchDocumentModel();
if (searchDocumentModel == null) {
throw new NuxeoException(String.format(
"Cannot build query of provider '%s': " + "no search document model is set", getName()));
}
newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(),
sortArray);
}
if (query != null && newQuery != null && !newQuery.equals(query)) {
// query has changed => refresh
refresh();
}
query = newQuery;
}
protected void checkQueryCache() {
// maybe handle refresh of select page according to query
if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) {
CoreSession coreSession = getCoreSession();
buildQuery(coreSession);
}
}
protected boolean useUnrestrictedSession() {
return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false);
}
protected boolean detachDocuments() {
return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true);
}
protected CoreSession getCoreSession() {
Map<String, Serializable> props = getProperties();
CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY);
if (coreSession == null) {
throw new NuxeoException("cannot find core session");
}
return coreSession;
}
/**
* Returns the maximum number of results or <code>0<code> if there is no limit.
*
* @since 5.6
*/
public long getMaxResults() {
if (maxResults == null) {
maxResults = Long.valueOf(0);
String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY);
if (maxResultsStr != null) {
if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) {
ConfigurationService cs = Framework.getService(ConfigurationService.class);
maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY,
DEFAULT_NAVIGATION_RESULTS_VALUE);
} else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) {
maxResultsStr = Long.valueOf(getPageSize()).toString();
}
try {
maxResults = Long.valueOf(maxResultsStr);
} catch (NumberFormatException e) {
log.warn(String.format(
"Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.",
maxResultsStr, getName()));
}
}
}
return maxResults.longValue();
}
/**
* Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this
* limit.
*
* @since 5.8
*/
@Override
public long getPageLimit() {
long pageSize = getPageSize();
if (pageSize == 0) {
return 0;
}
return getMaxResults() / pageSize;
}
/**
* Sets the maximum number of result elements.
*
* @since 5.6
*/
public void setMaxResults(long maxResults) {
this.maxResults = Long.valueOf(maxResults);
}
@Override
public PageSelections<DocumentModel> getCurrentSelectPage() {
checkQueryCache();
return super.getCurrentSelectPage();
}
public String getCurrentQuery() {
return query;
}
/**
* Filter to use when processing results.
* <p>
* Defaults to null (no filter applied), method to be overridden by subclasses.
*
* @since 6.0
*/
protected Filter getFilter() {
return null;
}
@Override
protected void pageChanged() {
currentPageDocuments = null;
super.pageChanged();
}
@Override
public void refresh() {
query = null;
currentPageDocuments = null;
super.refresh();
}
}