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