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 * 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.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.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<>(); 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 if (getResultsCount() < 0) { 178 // additional info to handle next page when results count 179 // is unknown 180 if (currentPageDocuments != null && currentPageDocuments.size() > 0) { 181 int higherNonEmptyPage = getCurrentHigherNonEmptyPageIndex(); 182 int currentFilledPage = (int) getCurrentPageIndex(); 183 if ((docs.size() >= getPageSize()) && (currentFilledPage > higherNonEmptyPage)) { 184 setCurrentHigherNonEmptyPageIndex(currentFilledPage); 185 } 186 } 187 } 188 } catch (NuxeoException e) { 189 error = e; 190 errorMessage = e.getMessage(); 191 log.warn(e.getMessage(), e); 192 } 193 } 194 195 // send event for statistics ! 196 fireSearchEvent(getCoreSession().getPrincipal(), query, currentPageDocuments, System.currentTimeMillis() - t0); 197 198 return currentPageDocuments; 199 } 200 201 protected void buildQuery(CoreSession coreSession) { 202 List<SortInfo> sort = null; 203 List<QuickFilter> quickFilters = getQuickFilters(); 204 String quickFiltersClause = ""; 205 206 if (quickFilters != null && !quickFilters.isEmpty()) { 207 sort = new ArrayList<>(); 208 for (QuickFilter quickFilter : quickFilters) { 209 String clause = quickFilter.getClause(); 210 if (clause != null) { 211 if (!quickFiltersClause.isEmpty()) { 212 quickFiltersClause = NXQLQueryBuilder.appendClause(quickFiltersClause, clause); 213 } else { 214 quickFiltersClause = clause; 215 } 216 } 217 sort.addAll(quickFilter.getSortInfos()); 218 } 219 } else if (sortInfos != null) { 220 sort = sortInfos; 221 } 222 223 SortInfo[] sortArray = null; 224 if (sort != null) { 225 sortArray = sort.toArray(new SortInfo[] {}); 226 } 227 228 String newQuery; 229 PageProviderDefinition def = getDefinition(); 230 WhereClauseDefinition whereClause = def.getWhereClause(); 231 if (whereClause == null) { 232 233 String originalPattern = def.getPattern(); 234 String pattern = quickFiltersClause.isEmpty() ? originalPattern 235 : StringUtils.containsIgnoreCase(originalPattern, " WHERE ") 236 ? NXQLQueryBuilder.appendClause(originalPattern, quickFiltersClause) 237 : originalPattern + " WHERE " + quickFiltersClause; 238 239 newQuery = NXQLQueryBuilder.getQuery(pattern, getParameters(), def.getQuotePatternParameters(), 240 def.getEscapePatternParameters(), getSearchDocumentModel(), sortArray); 241 } else { 242 243 DocumentModel searchDocumentModel = getSearchDocumentModel(); 244 if (searchDocumentModel == null) { 245 throw new NuxeoException(String.format( 246 "Cannot build query of provider '%s': " + "no search document model is set", getName())); 247 } 248 newQuery = NXQLQueryBuilder.getQuery(searchDocumentModel, whereClause, quickFiltersClause, getParameters(), 249 sortArray); 250 } 251 252 if (query != null && newQuery != null && !newQuery.equals(query)) { 253 // query has changed => refresh 254 refresh(); 255 } 256 query = newQuery; 257 } 258 259 protected void checkQueryCache() { 260 // maybe handle refresh of select page according to query 261 if (getBooleanProperty(CHECK_QUERY_CACHE_PROPERTY, false)) { 262 CoreSession coreSession = getCoreSession(); 263 buildQuery(coreSession); 264 } 265 } 266 267 protected boolean useUnrestrictedSession() { 268 return getBooleanProperty(USE_UNRESTRICTED_SESSION_PROPERTY, false); 269 } 270 271 protected boolean detachDocuments() { 272 return getBooleanProperty(DETACH_DOCUMENTS_PROPERTY, true); 273 } 274 275 protected CoreSession getCoreSession() { 276 Map<String, Serializable> props = getProperties(); 277 CoreSession coreSession = (CoreSession) props.get(CORE_SESSION_PROPERTY); 278 if (coreSession == null) { 279 throw new NuxeoException("cannot find core session"); 280 } 281 return coreSession; 282 } 283 284 /** 285 * Returns the maximum number of results or <code>0<code> if there is no limit. 286 * 287 * @since 5.6 288 */ 289 public long getMaxResults() { 290 if (maxResults == null) { 291 maxResults = Long.valueOf(0); 292 String maxResultsStr = (String) getProperties().get(MAX_RESULTS_PROPERTY); 293 if (maxResultsStr != null) { 294 if (DEFAULT_NAVIGATION_RESULTS_KEY.equals(maxResultsStr)) { 295 ConfigurationService cs = Framework.getService(ConfigurationService.class); 296 maxResultsStr = cs.getProperty(DEFAULT_NAVIGATION_RESULTS_PROPERTY, 297 DEFAULT_NAVIGATION_RESULTS_VALUE); 298 } else if (PAGE_SIZE_RESULTS_KEY.equals(maxResultsStr)) { 299 maxResultsStr = Long.valueOf(getPageSize()).toString(); 300 } 301 try { 302 maxResults = Long.valueOf(maxResultsStr); 303 } catch (NumberFormatException e) { 304 log.warn(String.format( 305 "Invalid maxResults property value: %s for page provider: %s, fallback to unlimited.", 306 maxResultsStr, getName())); 307 } 308 } 309 } 310 return maxResults.longValue(); 311 } 312 313 @Override 314 public long getResultsCountLimit() { 315 return getMaxResults(); 316 } 317 318 /** 319 * Returns the page limit. The n first page we know they exist. We don't compute the number of page beyond this 320 * limit. 321 * 322 * @since 5.8 323 */ 324 @Override 325 public long getPageLimit() { 326 long pageSize = getPageSize(); 327 if (pageSize == 0) { 328 return 0; 329 } 330 return getMaxResults() / pageSize; 331 } 332 333 /** 334 * Sets the maximum number of result elements. 335 * 336 * @since 5.6 337 */ 338 public void setMaxResults(long maxResults) { 339 this.maxResults = Long.valueOf(maxResults); 340 } 341 342 @Override 343 public PageSelections<DocumentModel> getCurrentSelectPage() { 344 checkQueryCache(); 345 // fetch last page if current page index is beyond the last page or if there are no results to display 346 rewindSelectablePage(); 347 return super.getCurrentSelectPage(); 348 } 349 350 public String getCurrentQuery() { 351 return query; 352 } 353 354 /** 355 * Fetch a page that can be selected. It loads the last page if we're targeting a page beyond the last one or the 356 * first page if there are no results to show and we're targeting anything other than the first page. Fix for 357 * NXP-8564. 358 */ 359 protected void rewindSelectablePage() { 360 long pageSize = getPageSize(); 361 if (pageSize != 0) { 362 if (offset != 0 && currentPageDocuments != null && currentPageDocuments.size() == 0) { 363 if (resultsCount == 0) { 364 // fetch first page directly 365 if (log.isDebugEnabled()) { 366 log.debug( 367 String.format( 368 "Current page %s is not the first one but " + "shows no result and there are " 369 + "no results => rewind to first page", 370 Long.valueOf(getCurrentPageIndex()))); 371 } 372 firstPage(); 373 } else { 374 // fetch last page 375 if (log.isDebugEnabled()) { 376 log.debug(String.format( 377 "Current page %s is not the first one but " + "shows no result and there are " 378 + "%s results => fetch last page", 379 Long.valueOf(getCurrentPageIndex()), Long.valueOf(resultsCount))); 380 } 381 lastPage(); 382 } 383 // fetch current page again 384 getCurrentPage(); 385 } 386 } 387 } 388 389 /** 390 * Filter to use when processing results. 391 * <p> 392 * Defaults to null (no filter applied), method to be overridden by subclasses. 393 * 394 * @since 6.0 395 */ 396 protected Filter getFilter() { 397 return null; 398 } 399 400 @Override 401 protected void pageChanged() { 402 currentPageDocuments = null; 403 super.pageChanged(); 404 } 405 406 @Override 407 public void refresh() { 408 query = null; 409 currentPageDocuments = null; 410 super.refresh(); 411 } 412 413}