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