001/* 002 * (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Anahide Tchertchian 016 * Benoit Delbosc 017 */ 018package org.nuxeo.ecm.platform.query.nxql; 019 020import java.io.Serializable; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.Map; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.nuxeo.ecm.core.api.CoreSession; 028import org.nuxeo.ecm.core.api.DocumentModel; 029import org.nuxeo.ecm.core.api.DocumentModelList; 030import org.nuxeo.ecm.core.api.Filter; 031import org.nuxeo.ecm.core.api.NuxeoException; 032import org.nuxeo.ecm.core.api.SortInfo; 033import org.nuxeo.ecm.platform.query.api.AbstractPageProvider; 034import org.nuxeo.ecm.platform.query.api.PageProviderDefinition; 035import org.nuxeo.ecm.platform.query.api.PageSelections; 036import org.nuxeo.runtime.api.Framework; 037import org.nuxeo.runtime.services.config.ConfigurationService; 038 039/** 040 * Page provider performing a query on a core session. 041 * <p> 042 * It builds the query at each call so that it can refresh itself when the query changes. 043 * <p> 044 * The page provider property named {@link #CORE_SESSION_PROPERTY} is used to pass the {@link CoreSession} instance that 045 * will perform the query. The optional property {@link #CHECK_QUERY_CACHE_PROPERTY} can be set to "true" to avoid 046 * performing the query again if it did not change. 047 * <p> 048 * Since 6.0, the page provider property named {@link #USE_UNRESTRICTED_SESSION_PROPERTY} allows specifying whether the 049 * query should be run as unrestricted. When such a property is set to "true", the additional property 050 * {@link #DETACH_DOCUMENTS_PROPERTY} is used to detach documents (defaults to true when session is unrestricted). 051 * 052 * @author Anahide Tchertchian 053 * @since 5.4 054 */ 055public class CoreQueryDocumentPageProvider extends AbstractPageProvider<DocumentModel> { 056 057 public static final String CORE_SESSION_PROPERTY = "coreSession"; 058 059 public static final String MAX_RESULTS_PROPERTY = "maxResults"; 060 061 // Special maxResults value used for navigation, can be tuned 062 public static final String DEFAULT_NAVIGATION_RESULTS_KEY = "DEFAULT_NAVIGATION_RESULTS"; 063 064 // Special maxResults value that means same as the page size 065 public static final String PAGE_SIZE_RESULTS_KEY = "PAGE_SIZE"; 066 067 public static final String DEFAULT_NAVIGATION_RESULTS_PROPERTY = "org.nuxeo.ecm.platform.query.nxql.defaultNavigationResults"; 068 069 public static final String DEFAULT_NAVIGATION_RESULTS_VALUE = "200"; 070 071 public static final String CHECK_QUERY_CACHE_PROPERTY = "checkQueryCache"; 072 073 /** 074 * Boolean property stating that query should be unrestricted. 075 * 076 * @since 6.0 077 */ 078 public static final String USE_UNRESTRICTED_SESSION_PROPERTY = "useUnrestrictedSession"; 079 080 /** 081 * Boolean property stating that documents should be detached, only useful when property 082 * {@link #USE_UNRESTRICTED_SESSION_PROPERTY} is set to true. 083 * <p> 084 * When an unrestricted session is used, this property defaults to true. 085 * 086 * @since 6.0 087 */ 088 public static final String DETACH_DOCUMENTS_PROPERTY = "detachDocuments"; 089 090 private static final Log log = LogFactory.getLog(CoreQueryDocumentPageProvider.class); 091 092 private static final long serialVersionUID = 1L; 093 094 protected String query; 095 096 protected List<DocumentModel> currentPageDocuments; 097 098 protected Long maxResults; 099 100 @Override 101 public List<DocumentModel> getCurrentPage() { 102 103 long t0 = System.currentTimeMillis(); 104 105 checkQueryCache(); 106 if (currentPageDocuments == null) { 107 error = null; 108 errorMessage = null; 109 110 CoreSession coreSession = getCoreSession(); 111 if (query == null) { 112 buildQuery(coreSession); 113 } 114 if (query == null) { 115 throw new NuxeoException(String.format("Cannot perform null query: check provider '%s'", getName())); 116 } 117 118 currentPageDocuments = new ArrayList<DocumentModel>(); 119 120 try { 121 122 final long minMaxPageSize = getMinMaxPageSize(); 123 124 final long offset = getCurrentPageOffset(); 125 if (log.isDebugEnabled()) { 126 log.debug(String.format("Perform query for provider '%s': '%s' with pageSize=%s, offset=%s", 127 getName(), query, Long.valueOf(minMaxPageSize), Long.valueOf(offset))); 128 } 129 130 final DocumentModelList docs; 131 final long maxResults = getMaxResults(); 132 final Filter filter = getFilter(); 133 final boolean useUnrestricted = useUnrestrictedSession(); 134 135 final boolean detachDocs = detachDocuments(); 136 if (maxResults > 0) { 137 if (useUnrestricted) { 138 CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession, 139 query, filter, minMaxPageSize, offset, false, maxResults, detachDocs); 140 r.runUnrestricted(); 141 docs = r.getDocs(); 142 } else { 143 docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, maxResults); 144 } 145 } else { 146 // use a totalCount=true instead of countUpTo=-1 to 147 // enable global limitation described in NXP-9381 148 if (useUnrestricted) { 149 CoreQueryUnrestrictedSessionRunner r = new CoreQueryUnrestrictedSessionRunner(coreSession, 150 query, filter, minMaxPageSize, offset, true, maxResults, detachDocs); 151 r.runUnrestricted(); 152 docs = r.getDocs(); 153 } else { 154 docs = coreSession.query(query, getFilter(), minMaxPageSize, offset, true); 155 } 156 } 157 158 long resultsCount = docs.totalSize(); 159 if (resultsCount < 0) { 160 // results count is truncated 161 setResultsCount(UNKNOWN_SIZE_AFTER_QUERY); 162 } else { 163 setResultsCount(resultsCount); 164 } 165 currentPageDocuments = docs; 166 167 if (log.isDebugEnabled()) { 168 log.debug(String.format("Performed query for provider '%s': got %s hits (limit %s)", getName(), 169 Long.valueOf(resultsCount), Long.valueOf(getMaxResults()))); 170 } 171 172 // refresh may have triggered display of an empty page => go 173 // back to first page or forward to last page depending on 174 // results count and page size 175 long pageSize = getPageSize(); 176 if (pageSize != 0) { 177 if (offset != 0 && currentPageDocuments.size() == 0) { 178 if (resultsCount == 0) { 179 // fetch first page directly 180 if (log.isDebugEnabled()) { 181 log.debug(String.format("Current page %s is not the first one but " 182 + "shows no result and there are " + "no results => rewind to first page", 183 Long.valueOf(getCurrentPageIndex()))); 184 } 185 firstPage(); 186 } else { 187 // fetch last page 188 if (log.isDebugEnabled()) { 189 log.debug(String.format("Current page %s is not the first one but " 190 + "shows no result and there are " + "%s results => fetch last page", 191 Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount))); 192 } 193 lastPage(); 194 } 195 // fetch current page again 196 getCurrentPage(); 197 } 198 } 199 200 if (getResultsCount() < 0) { 201 // additional info to handle next page when results count 202 // is unknown 203 if (currentPageDocuments != null && currentPageDocuments.size() > 0) { 204 int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex(); 205 int currentFilledPage = Long.valueOf(getCurrentPageIndex()).intValue(); 206 if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) { 207 setCurrentHigherNonEmptyPageIndex(currentFilledPage); 208 } 209 } 210 } 211 } catch (NuxeoException e) { 212 error = e; 213 errorMessage = e.getMessage(); 214 log.warn(e.getMessage(), e); 215 } 216 } 217 218 // send event for statistics ! 219 fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis()-t0); 220 221 return currentPageDocuments; 222 } 223 224 protected void buildQuery(CoreSession coreSession) { 225 SortInfo[] sortArray = null; 226 if (sortInfos != null) { 227 sortArray = sortInfos.toArray(new SortInfo[] {}); 228 } 229 String newQuery; 230 PageProviderDefinition def = getDefinition(); 231 if (def.getWhereClause() == null) { 232 newQuery = NXQLQueryBuilder.getQuery(def.getPattern(), getParameters(), def.getQuotePatternParameters(), 233 def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray); 234 } else { 235 DocumentModel searchDocumentModel = getSearchDocumentModel(); 236 if (searchDocumentModel == null) { 237 throw new NuxeoException(String.format( 238 "Cannot build query of provider '%s': " + "no search document model is set", getName())); 239 } 240 newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, def.getWhereClause(), getParameters(), sortArray); 241 } 242 243 if (query != null && newQuery != null && !newQuery.equals(query)) { 244 // query has changed => refresh 245 refresh(); 246 } 247 query = newQuery; 248 } 249 250 protected void checkQueryCache() { 251 // maybe handle refresh of select page according to query 252 if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) { 253 CoreSession coreSession = getCoreSession(); 254 buildQuery(coreSession); 255 } 256 } 257 258 protected boolean useUnrestrictedSession() { 259 return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false); 260 } 261 262 protected boolean detachDocuments() { 263 return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true); 264 } 265 266 protected CoreSession getCoreSession() { 267 Map<String, Serializable> props = getProperties(); 268 CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY); 269 if (coreSession == null) { 270 throw new NuxeoException("cannot find core session"); 271 } 272 return coreSession; 273 } 274 275 /** 276 * Returns the maximum number of results or <code>0<code> if there is no limit. 277 * 278 * @since 5.6 279 */ 280 public long getMaxResults() { 281 if (maxResults == null) { 282 maxResults = Long.valueOf(0); 283 String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY); 284 if (maxResultsStr != null) { 285 if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) { 286 ConfigurationService cs = Framework.getService(ConfigurationService.class); 287 maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY, 288 DEFAULT_NAVIGATION_RESULTS_VALUE); 289 } else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) { 290 maxResultsStr = Long.valueOf(getPageSize()).toString(); 291 } 292 try { 293 maxResults = Long.valueOf(maxResultsStr); 294 } catch (NumberFormatException e) { 295 log.warn(String.format( 296 "Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.", 297 maxResultsStr, getName())); 298 } 299 } 300 } 301 return maxResults.longValue(); 302 } 303 304 /** 305 * Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this 306 * limit. 307 * 308 * @since 5.8 309 */ 310 @Override 311 public long getPageLimit() { 312 long pageSize = getPageSize(); 313 if (pageSize == 0) { 314 return 0; 315 } 316 return getMaxResults() / pageSize; 317 } 318 319 /** 320 * Sets the maximum number of result elements. 321 * 322 * @since 5.6 323 */ 324 public void setMaxResults(long maxResults) { 325 this.maxResults = Long.valueOf(maxResults); 326 } 327 328 @Override 329 public PageSelections<DocumentModel> getCurrentSelectPage() { 330 checkQueryCache(); 331 return super.getCurrentSelectPage(); 332 } 333 334 public String getCurrentQuery() { 335 return query; 336 } 337 338 /** 339 * Filter to use when processing results. 340 * <p> 341 * Defaults to null (no filter applied), method to be overridden by subclasses. 342 * 343 * @since 6.0 344 */ 345 protected Filter getFilter() { 346 return null; 347 } 348 349 @Override 350 protected void pageChanged() { 351 currentPageDocuments = null; 352 super.pageChanged(); 353 } 354 355 @Override 356 public void refresh() { 357 query = null; 358 currentPageDocuments = null; 359 super.refresh(); 360 } 361 362}