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 */ 019package org.nuxeo.ecm.platform.query.api; 020 021import java.io.IOException; 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029 030import org.apache.commons.lang3.ArrayUtils; 031import org.apache.commons.lang3.NotImplementedException; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.nuxeo.ecm.core.api.DocumentModel; 035import org.nuxeo.ecm.core.api.NuxeoException; 036import org.nuxeo.ecm.core.api.NuxeoPrincipal; 037import org.nuxeo.ecm.core.api.SortInfo; 038import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel; 039import org.nuxeo.ecm.core.api.model.Property; 040import org.nuxeo.ecm.core.event.EventContext; 041import org.nuxeo.ecm.core.event.EventService; 042import org.nuxeo.ecm.core.event.impl.UnboundEventContext; 043import org.nuxeo.ecm.core.io.registry.MarshallerHelper; 044import org.nuxeo.ecm.core.io.registry.context.RenderingContext; 045import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; 046import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider; 047import org.nuxeo.runtime.api.Framework; 048import org.nuxeo.runtime.services.config.ConfigurationService; 049 050/** 051 * Basic implementation for a {@link PageProvider}. 052 * <p> 053 * Provides next/prev standard logics, and helper methods for retrieval of items and first/next/prev/last buttons 054 * display as well as other display information (number of pages for instance). 055 * <p> 056 * Also handles selection by providing a default implementation of {@link #getCurrentSelectPage()} working in 057 * conjunction with {@link #setSelectedEntries(List)}. 058 * 059 * @author Anahide Tchertchian 060 */ 061public abstract class AbstractPageProvider<T> implements PageProvider<T> { 062 063 public static final Log log = LogFactory.getLog(AbstractPageProvider.class); 064 065 private static final long serialVersionUID = 1L; 066 067 /** 068 * property used to enable globally tracking : property should contains the list of pageproviders to be tracked 069 * 070 * @since 7.4 071 */ 072 public static final String PAGEPROVIDER_TRACK_PROPERTY_NAME = "nuxeo.pageprovider.track"; 073 074 /** 075 * lists schemas prefixes that should be skipped when extracting "search fields" (tracking) from searchDocumentModel 076 * 077 * @since 7.4 078 */ 079 protected static final List<String> SKIPPED_SCHEMAS_FOR_SEARCHFIELD = Collections.singletonList("cvd"); 080 081 protected String name; 082 083 protected long offset = 0; 084 085 protected long pageSize = 0; 086 087 protected List<Long> pageSizeOptions; 088 089 protected long maxPageSize = getDefaultMaxPageSize(); 090 091 protected long resultsCount = UNKNOWN_SIZE; 092 093 protected int currentEntryIndex = 0; 094 095 /** 096 * Integer keeping track of the higher page index giving results. Useful for enabling or disabling the nextPage 097 * action when number of results cannot be known. 098 * 099 * @since 5.5 100 */ 101 protected int currentHigherNonEmptyPageIndex = 0; 102 103 protected List<SortInfo> sortInfos; 104 105 protected boolean sortable = false; 106 107 protected List<T> selectedEntries; 108 109 protected PageSelections<T> currentSelectPage; 110 111 protected Map<String, Serializable> properties; 112 113 protected Object[] parameters; 114 115 protected DocumentModel searchDocumentModel; 116 117 /** 118 * @since 8.4 119 */ 120 protected List<QuickFilter> quickFilters; 121 122 protected List<String> highlights; 123 124 protected String errorMessage; 125 126 protected Throwable error; 127 128 protected PageProviderDefinition definition; 129 130 protected PageProviderChangedListener pageProviderChangedListener; 131 132 /** 133 * Returns the list of current page items. 134 * <p> 135 * Custom implementation can be added here, based on the page provider properties, parameters and 136 * {@link WhereClauseDefinition} on the {@link PageProviderDefinition}, as well as search document, sort 137 * information, etc... 138 * <p> 139 * Implementation of this method usually consists in setting a non-null value to a field caching current items, and 140 * nullifying this field by overriding {@link #pageChanged()} and {@link #refresh()}. 141 * <p> 142 * Fields {@link #errorMessage} and {@link #error} can also be filled to provide accurate feedback in case an error 143 * occurs during the search. 144 * <p> 145 * When items are retrieved, a call to {@link #setResultsCount(long)} should be made to ensure proper pagination as 146 * implemented in this abstract class. The implementation in {@link CoreQueryAndFetchPageProvider} is a good example 147 * when the total results count is known. 148 * <p> 149 * If for performance reasons, for instance, the number of results cannot be known, a fall-back strategy can be 150 * applied to provide the "next" button but not the "last" one, by calling 151 * {@link #getCurrentHigherNonEmptyPageIndex()} and {@link #setCurrentHigherNonEmptyPageIndex(int)}. In this case, 152 * {@link CoreQueryDocumentPageProvider} is a good example. 153 */ 154 @Override 155 public abstract List<T> getCurrentPage(); 156 157 /** 158 * Page change hook, to override for custom behavior 159 * <p> 160 * When overriding it, call {@code super.pageChanged()} as last statement to make sure that the 161 * {@link PageProviderChangedListener} is called with the up-to-date @{code PageProvider} state. 162 */ 163 protected void pageChanged() { 164 currentEntryIndex = 0; 165 currentSelectPage = null; 166 notifyPageChanged(); 167 } 168 169 @Override 170 public void firstPage() { 171 long pageSize = getPageSize(); 172 if (pageSize == 0) { 173 // do nothing 174 return; 175 } 176 long offset = getCurrentPageOffset(); 177 if (offset != 0) { 178 setCurrentPageOffset(0); 179 pageChanged(); 180 } 181 } 182 183 @Override 184 public long getCurrentPageIndex() { 185 long pageSize = getPageSize(); 186 if (pageSize == 0) { 187 return 0; 188 } 189 long offset = getCurrentPageOffset(); 190 return offset / pageSize; 191 } 192 193 @Override 194 public long getCurrentPageOffset() { 195 return offset; 196 } 197 198 @Override 199 public void setCurrentPageOffset(long offset) { 200 this.offset = offset; 201 } 202 203 @Override 204 public long getCurrentPageSize() { 205 List<T> currentItems = getCurrentPage(); 206 if (currentItems != null) { 207 return currentItems.size(); 208 } 209 return 0; 210 } 211 212 @Override 213 public String getName() { 214 return name; 215 } 216 217 @Override 218 public long getNumberOfPages() { 219 long pageSize = getPageSize(); 220 // ensure 1 if no pagination 221 if (pageSize == 0) { 222 return 1; 223 } 224 // take max page size into into account 225 pageSize = getMinMaxPageSize(); 226 if (pageSize == 0) { 227 return 1; 228 } 229 long resultsCount = getResultsCount(); 230 if (resultsCount < 0) { 231 return 0; 232 } else { 233 return (1 + (resultsCount - 1) / pageSize); 234 } 235 } 236 237 @Override 238 public void setCurrentPageIndex(long currentPageIndex) { 239 long pageSize = getPageSize(); 240 long offset = currentPageIndex * pageSize; 241 setCurrentPageOffset(offset); 242 pageChanged(); 243 } 244 245 @Override 246 public List<T> setCurrentPage(long page) { 247 setCurrentPageIndex(page); 248 return getCurrentPage(); 249 } 250 251 @Override 252 public long getPageSize() { 253 return pageSize; 254 } 255 256 @Override 257 public void setPageSize(long pageSize) { 258 long localPageSize = getPageSize(); 259 if (localPageSize != pageSize) { 260 this.pageSize = pageSize; 261 // reset offset too 262 setCurrentPageOffset(0); 263 refresh(); 264 } 265 } 266 267 @Override 268 public List<Long> getPageSizeOptions() { 269 List<Long> res = new ArrayList<>(); 270 if (pageSizeOptions != null) { 271 res.addAll(pageSizeOptions); 272 } 273 // include the actual page size of page provider if not present 274 long ppsize = getPageSize(); 275 if (ppsize > 0 && !res.contains(ppsize)) { 276 res.add(Long.valueOf(ppsize)); 277 } 278 Collections.sort(res); 279 return res; 280 } 281 282 @Override 283 public void setPageSizeOptions(List<Long> options) { 284 pageSizeOptions = options; 285 } 286 287 @Override 288 public List<SortInfo> getSortInfos() { 289 // break reference 290 List<SortInfo> res = new ArrayList<>(); 291 if (sortInfos != null) { 292 res.addAll(sortInfos); 293 } 294 return res; 295 } 296 297 @Override 298 public SortInfo getSortInfo() { 299 if (sortInfos != null && !sortInfos.isEmpty()) { 300 return sortInfos.get(0); 301 } 302 return null; 303 } 304 305 protected boolean sortInfoChanged(List<SortInfo> oldSortInfos, List<SortInfo> newSortInfos) { 306 if (oldSortInfos == null && newSortInfos == null) { 307 return false; 308 } else if (oldSortInfos == null) { 309 oldSortInfos = Collections.emptyList(); 310 } else if (newSortInfos == null) { 311 newSortInfos = Collections.emptyList(); 312 } 313 if (oldSortInfos.size() != newSortInfos.size()) { 314 return true; 315 } 316 for (int i = 0; i < oldSortInfos.size(); i++) { 317 SortInfo oldSort = oldSortInfos.get(i); 318 SortInfo newSort = newSortInfos.get(i); 319 if (oldSort == null && newSort == null) { 320 continue; 321 } else if (oldSort == null || newSort == null) { 322 return true; 323 } 324 if (!oldSort.equals(newSort)) { 325 return true; 326 } 327 } 328 return false; 329 } 330 331 @Override 332 public void setQuickFilters(List<QuickFilter> quickFilters) { 333 this.quickFilters = quickFilters; 334 } 335 336 @Override 337 public List<QuickFilter> getQuickFilters() { 338 return quickFilters; 339 } 340 341 @Override 342 public List<QuickFilter> getAvailableQuickFilters() { 343 return definition != null ? definition.getQuickFilters() : null; 344 } 345 346 @Override 347 public void addQuickFilter(QuickFilter quickFilter) { 348 if (quickFilters == null) { 349 quickFilters = new ArrayList<>(); 350 } 351 quickFilters.add(quickFilter); 352 } 353 354 @Override 355 public void setSortInfos(List<SortInfo> sortInfo) { 356 if (sortInfoChanged(this.sortInfos, sortInfo)) { 357 this.sortInfos = sortInfo; 358 refresh(); 359 } 360 } 361 362 @Override 363 public void setSortInfo(SortInfo sortInfo) { 364 List<SortInfo> newSortInfos = new ArrayList<>(); 365 if (sortInfo != null) { 366 newSortInfos.add(sortInfo); 367 } 368 setSortInfos(newSortInfos); 369 } 370 371 @Override 372 public void setSortInfo(String sortColumn, boolean sortAscending, boolean removeOtherSortInfos) { 373 if (removeOtherSortInfos) { 374 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 375 setSortInfo(sortInfo); 376 } else { 377 if (getSortInfoIndex(sortColumn, sortAscending) != -1) { 378 // do nothing: sort on this column is not set 379 } else if (getSortInfoIndex(sortColumn, !sortAscending) != -1) { 380 // change direction 381 List<SortInfo> newSortInfos = new ArrayList<>(); 382 for (SortInfo sortInfo : getSortInfos()) { 383 if (sortColumn.equals(sortInfo.getSortColumn())) { 384 newSortInfos.add(new SortInfo(sortColumn, sortAscending)); 385 } else { 386 newSortInfos.add(sortInfo); 387 } 388 } 389 setSortInfos(newSortInfos); 390 } else { 391 // just add it 392 addSortInfo(sortColumn, sortAscending); 393 } 394 } 395 } 396 397 @Override 398 public void addSortInfo(String sortColumn, boolean sortAscending) { 399 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 400 List<SortInfo> sortInfos = getSortInfos(); 401 if (sortInfos == null) { 402 setSortInfo(sortInfo); 403 } else { 404 sortInfos.add(sortInfo); 405 setSortInfos(sortInfos); 406 } 407 } 408 409 @Override 410 public int getSortInfoIndex(String sortColumn, boolean sortAscending) { 411 List<SortInfo> sortInfos = getSortInfos(); 412 if (sortInfos == null || sortInfos.isEmpty()) { 413 return -1; 414 } else { 415 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 416 return sortInfos.indexOf(sortInfo); 417 } 418 } 419 420 @Override 421 public List<String> getHighlights() { 422 return highlights; 423 } 424 425 @Override 426 public void setHighlights(List<String> highlights) { 427 this.highlights = highlights; 428 } 429 430 @Override 431 public boolean isNextPageAvailable() { 432 long pageSize = getPageSize(); 433 if (pageSize == 0) { 434 return false; 435 } 436 long resultsCount = getResultsCount(); 437 if (resultsCount < 0) { 438 long currentPageIndex = getCurrentPageIndex(); 439 return currentPageIndex < getCurrentHigherNonEmptyPageIndex() + getMaxNumberOfEmptyPages(); 440 } else { 441 long offset = getCurrentPageOffset(); 442 return resultsCount > pageSize + offset; 443 } 444 } 445 446 @Override 447 public boolean isLastPageAvailable() { 448 long resultsCount = getResultsCount(); 449 if (resultsCount < 0) { 450 return false; 451 } 452 return isNextPageAvailable(); 453 } 454 455 @Override 456 public boolean isPreviousPageAvailable() { 457 long offset = getCurrentPageOffset(); 458 return offset > 0; 459 } 460 461 @Override 462 public void lastPage() { 463 long pageSize = getPageSize(); 464 long resultsCount = getResultsCount(); 465 if (pageSize == 0 || resultsCount < 0) { 466 // do nothing 467 return; 468 } 469 if (resultsCount % pageSize == 0) { 470 setCurrentPageOffset(resultsCount - pageSize); 471 } else { 472 setCurrentPageOffset(resultsCount - resultsCount % pageSize); 473 } 474 pageChanged(); 475 } 476 477 @Override 478 public void nextPage() { 479 long pageSize = getPageSize(); 480 if (pageSize == 0) { 481 // do nothing 482 return; 483 } 484 long offset = getCurrentPageOffset(); 485 offset += pageSize; 486 setCurrentPageOffset(offset); 487 pageChanged(); 488 } 489 490 @Override 491 public void previousPage() { 492 long pageSize = getPageSize(); 493 if (pageSize == 0) { 494 // do nothing 495 return; 496 } 497 long offset = getCurrentPageOffset(); 498 if (offset >= pageSize) { 499 offset -= pageSize; 500 setCurrentPageOffset(offset); 501 pageChanged(); 502 } 503 } 504 505 /** 506 * Refresh hook, to override for custom behavior 507 * <p> 508 * When overriding it, call {@code super.refresh()} as last statement to make sure that the 509 * {@link PageProviderChangedListener} is called with the up-to-date @{code PageProvider} state. 510 */ 511 @Override 512 public void refresh() { 513 setResultsCount(UNKNOWN_SIZE); 514 setCurrentHigherNonEmptyPageIndex(-1); 515 currentSelectPage = null; 516 errorMessage = null; 517 error = null; 518 notifyRefresh(); 519 520 } 521 522 @Override 523 public void setName(String name) { 524 this.name = name; 525 } 526 527 @Override 528 public String getCurrentPageStatus() { 529 long total = getNumberOfPages(); 530 long current = getCurrentPageIndex() + 1; 531 if (total <= 0) { 532 // number of pages unknown or there is only one page 533 return String.format("%d", Long.valueOf(current)); 534 } else { 535 return String.format("%d/%d", Long.valueOf(current), Long.valueOf(total)); 536 } 537 } 538 539 @Override 540 public boolean isNextEntryAvailable() { 541 long pageSize = getPageSize(); 542 long resultsCount = getResultsCount(); 543 if (pageSize == 0) { 544 if (resultsCount < 0) { 545 // results count unknown 546 long currentPageSize = getCurrentPageSize(); 547 return currentEntryIndex < currentPageSize - 1; 548 } else { 549 return currentEntryIndex < resultsCount - 1; 550 } 551 } else { 552 long currentPageSize = getCurrentPageSize(); 553 if (currentEntryIndex < currentPageSize - 1) { 554 return true; 555 } 556 if (resultsCount < 0) { 557 // results count unknown => do not look for entry in next page 558 return false; 559 } else { 560 return isNextPageAvailable(); 561 } 562 } 563 } 564 565 @Override 566 public boolean isPreviousEntryAvailable() { 567 return (currentEntryIndex != 0 || isPreviousPageAvailable()); 568 } 569 570 @Override 571 public void nextEntry() { 572 long pageSize = getPageSize(); 573 long resultsCount = getResultsCount(); 574 if (pageSize == 0) { 575 if (resultsCount < 0) { 576 // results count unknown 577 long currentPageSize = getCurrentPageSize(); 578 if (currentEntryIndex < currentPageSize - 1) { 579 currentEntryIndex++; 580 return; 581 } 582 } else { 583 if (currentEntryIndex < resultsCount - 1) { 584 currentEntryIndex++; 585 return; 586 } 587 } 588 } else { 589 long currentPageSize = getCurrentPageSize(); 590 if (currentEntryIndex < currentPageSize - 1) { 591 currentEntryIndex++; 592 return; 593 } 594 if (resultsCount >= 0) { 595 // if results count is unknown, do not look for entry in next 596 // page 597 if (isNextPageAvailable()) { 598 nextPage(); 599 currentEntryIndex = 0; 600 return; 601 } 602 } 603 } 604 605 } 606 607 @Override 608 public void previousEntry() { 609 if (currentEntryIndex > 0) { 610 currentEntryIndex--; 611 return; 612 } 613 if (!isPreviousPageAvailable()) { 614 return; 615 } 616 617 previousPage(); 618 List<T> currentPage = getCurrentPage(); 619 if (currentPage == null || currentPage.isEmpty()) { 620 // things may have changed since last query 621 currentEntryIndex = 0; 622 } else { 623 currentEntryIndex = (Long.valueOf(getPageSize() - 1)).intValue(); 624 } 625 } 626 627 @Override 628 public T getCurrentEntry() { 629 List<T> currentPage = getCurrentPage(); 630 if (currentPage == null || currentPage.isEmpty()) { 631 return null; 632 } 633 return currentPage.get(currentEntryIndex); 634 } 635 636 @Override 637 public void setCurrentEntry(T entry) { 638 List<T> currentPage = getCurrentPage(); 639 if (currentPage == null || currentPage.isEmpty()) { 640 throw new NuxeoException(String.format("Entry '%s' not found in current page", entry)); 641 } 642 int i = currentPage.indexOf(entry); 643 if (i == -1) { 644 throw new NuxeoException(String.format("Entry '%s' not found in current page", entry)); 645 } 646 currentEntryIndex = i; 647 } 648 649 @Override 650 public void setCurrentEntryIndex(long index) { 651 int intIndex = (int) index; 652 List<T> currentPage = getCurrentPage(); 653 if (currentPage == null || currentPage.isEmpty()) { 654 throw new NuxeoException(String.format("Index %s not found in current page", intIndex)); 655 } 656 if (index >= currentPage.size()) { 657 throw new NuxeoException(String.format("Index %s not found in current page", intIndex)); 658 } 659 currentEntryIndex = intIndex; 660 } 661 662 @Override 663 public long getResultsCount() { 664 return resultsCount; 665 } 666 667 @Override 668 public Map<String, Serializable> getProperties() { 669 // break reference 670 return new HashMap<>(properties); 671 } 672 673 @Override 674 public void setProperties(Map<String, Serializable> properties) { 675 this.properties = properties; 676 } 677 678 /** 679 * @since 6.0 680 */ 681 protected boolean getBooleanProperty(String propName, boolean defaultValue) { 682 Map<String, Serializable> props = getProperties(); 683 if (props.containsKey(propName)) { 684 Serializable prop = props.get(propName); 685 if (prop instanceof String) { 686 return Boolean.parseBoolean((String) prop); 687 } else { 688 return Boolean.TRUE.equals(prop); 689 } 690 } 691 return defaultValue; 692 } 693 694 @Override 695 public void setResultsCount(long resultsCount) { 696 this.resultsCount = resultsCount; 697 setCurrentHigherNonEmptyPageIndex(-1); 698 } 699 700 @Override 701 public void setSortable(boolean sortable) { 702 this.sortable = sortable; 703 } 704 705 @Override 706 public boolean isSortable() { 707 return sortable; 708 } 709 710 @Override 711 public PageSelections<T> getCurrentSelectPage() { 712 if (currentSelectPage == null) { 713 List<PageSelection<T>> entries = new ArrayList<>(); 714 List<T> currentPage = getCurrentPage(); 715 currentSelectPage = new PageSelections<>(); 716 currentSelectPage.setName(name); 717 if (currentPage != null && !currentPage.isEmpty()) { 718 if (selectedEntries == null || selectedEntries.isEmpty()) { 719 // no selection at all 720 for (T entry : currentPage) { 721 entries.add(new PageSelection<>(entry, false)); 722 } 723 } else { 724 boolean allSelected = true; 725 for (T entry : currentPage) { 726 Boolean selected = Boolean.valueOf(selectedEntries.contains(entry)); 727 if (!Boolean.TRUE.equals(selected)) { 728 allSelected = false; 729 } 730 entries.add(new PageSelection<>(entry, selected.booleanValue())); // NOSONAR 731 } 732 if (allSelected) { 733 currentSelectPage.setSelected(true); 734 } 735 } 736 } 737 currentSelectPage.setEntries(entries); 738 } 739 return currentSelectPage; 740 } 741 742 @Override 743 public void setSelectedEntries(List<T> entries) { 744 this.selectedEntries = entries; 745 // reset current select page so that it's rebuilt 746 currentSelectPage = null; 747 } 748 749 @Override 750 public Object[] getParameters() { 751 return parameters; 752 } 753 754 @Override 755 public void setParameters(Object[] parameters) { 756 this.parameters = parameters; 757 } 758 759 @Override 760 public DocumentModel getSearchDocumentModel() { 761 return searchDocumentModel; 762 } 763 764 protected boolean searchDocumentModelChanged(DocumentModel oldDoc, DocumentModel newDoc) { 765 if (oldDoc == null && newDoc == null) { 766 return false; 767 } else if (oldDoc == null || newDoc == null) { 768 return true; 769 } 770 // do not compare properties and assume it's changed 771 return true; 772 } 773 774 @Override 775 public void setSearchDocumentModel(DocumentModel searchDocumentModel) { 776 if (searchDocumentModelChanged(this.searchDocumentModel, searchDocumentModel)) { 777 refresh(); 778 } 779 this.searchDocumentModel = searchDocumentModel; 780 } 781 782 @Override 783 public String getErrorMessage() { 784 return errorMessage; 785 } 786 787 @Override 788 public Throwable getError() { 789 return error; 790 } 791 792 @Override 793 public boolean hasError() { 794 return error != null; 795 } 796 797 @Override 798 public PageProviderDefinition getDefinition() { 799 return definition; 800 } 801 802 @Override 803 public void setDefinition(PageProviderDefinition providerDefinition) { 804 this.definition = providerDefinition; 805 } 806 807 @Override 808 public long getMaxPageSize() { 809 return maxPageSize; 810 } 811 812 @Override 813 public void setMaxPageSize(long maxPageSize) { 814 this.maxPageSize = maxPageSize; 815 } 816 817 /** 818 * Returns the minimal value for the max page size, taking the lower value between the requested page size and the 819 * maximum accepted page size. 820 * 821 * @since 5.4.2 822 */ 823 public long getMinMaxPageSize() { 824 long pageSize = getPageSize(); 825 long maxPageSize = getMaxPageSize(); 826 if (maxPageSize < 0) { 827 maxPageSize = getDefaultMaxPageSize(); 828 } 829 if (pageSize <= 0) { 830 return maxPageSize; 831 } 832 if (maxPageSize > 0 && maxPageSize < pageSize) { 833 return maxPageSize; 834 } 835 return pageSize; 836 } 837 838 /** 839 * Returns an integer keeping track of the higher page index giving results. Useful for enabling or disabling the 840 * nextPage action when number of results cannot be known. 841 * 842 * @since 5.5 843 */ 844 public int getCurrentHigherNonEmptyPageIndex() { 845 return currentHigherNonEmptyPageIndex; 846 } 847 848 /** 849 * Returns the page limit. The n first page we know they exist. 850 * 851 * @since 5.8 852 */ 853 @Override 854 public long getPageLimit() { 855 return PAGE_LIMIT_UNKNOWN; 856 } 857 858 public void setCurrentHigherNonEmptyPageIndex(int higherFilledPageIndex) { 859 this.currentHigherNonEmptyPageIndex = higherFilledPageIndex; 860 } 861 862 /** 863 * Returns the maximum number of empty pages that can be fetched empty (defaults to 1). Can be useful for displaying 864 * pages of a provider without results count. 865 * 866 * @since 5.5 867 */ 868 public int getMaxNumberOfEmptyPages() { 869 return 1; 870 } 871 872 protected long getDefaultMaxPageSize() { 873 long res = DEFAULT_MAX_PAGE_SIZE; 874 if (Framework.isInitialized()) { 875 ConfigurationService cs = Framework.getService(ConfigurationService.class); 876 res = cs.getLong(DEFAULT_MAX_PAGE_SIZE_RUNTIME_PROP, res); 877 } 878 return res; 879 } 880 881 @Override 882 public void setPageProviderChangedListener(PageProviderChangedListener listener) { 883 pageProviderChangedListener = listener; 884 } 885 886 /** 887 * Call the registered {@code PageProviderChangedListener}, if any, to notify that the page provider current page 888 * has changed. 889 * 890 * @since 5.7 891 */ 892 protected void notifyPageChanged() { 893 if (pageProviderChangedListener != null) { 894 pageProviderChangedListener.pageChanged(this); 895 } 896 } 897 898 /** 899 * Call the registered {@code PageProviderChangedListener}, if any, to notify that the page provider has refreshed. 900 * 901 * @since 5.7 902 */ 903 protected void notifyRefresh() { 904 if (pageProviderChangedListener != null) { 905 pageProviderChangedListener.refreshed(this); 906 } 907 } 908 909 @Override 910 public boolean hasChangedParameters(Object[] parameters) { 911 return getParametersChanged(getParameters(), parameters); 912 } 913 914 protected boolean getParametersChanged(Object[] oldParams, Object[] newParams) { 915 if (oldParams == null && newParams == null) { 916 return true; 917 } else if (oldParams != null && newParams != null) { 918 if (oldParams.length != newParams.length) { 919 return true; 920 } 921 for (int i = 0; i < oldParams.length; i++) { 922 if (oldParams[i] == null && newParams[i] == null) { 923 continue; 924 } else if (newParams[i] instanceof String[] && oldParams[i] instanceof String[] 925 && Arrays.equals((String[]) oldParams[i], (String[]) newParams[i])) { 926 continue; 927 } else if (oldParams[i] != null && !oldParams[i].equals(newParams[i])) { 928 return true; 929 } else if (newParams[i] != null && !newParams[i].equals(oldParams[i])) { 930 return true; 931 } 932 } 933 return false; 934 } 935 return true; 936 } 937 938 @Override 939 public List<AggregateDefinition> getAggregateDefinitions() { 940 return definition.getAggregates(); 941 } 942 943 @Override 944 public Map<String, Aggregate<? extends Bucket>> getAggregates() { 945 throw new NotImplementedException("No aggregates on basic page provider"); 946 } 947 948 @Override 949 public boolean hasAggregateSupport() { 950 return false; 951 } 952 953 protected Boolean tracking = null; 954 955 /** 956 * @since 7.4 957 */ 958 protected boolean isTrackingEnabled() { 959 960 if (tracking != null) { 961 return tracking; 962 } 963 964 if (getDefinition().isUsageTrackingEnabled()) { 965 tracking = true; 966 } else { 967 String trackedPageProviders = Framework.getProperty(PAGEPROVIDER_TRACK_PROPERTY_NAME, ""); 968 if ("*".equals(trackedPageProviders)) { 969 tracking = true; 970 } else { 971 List<String> pps = Arrays.asList(trackedPageProviders.split(",")); 972 if (pps.contains(getDefinition().getName())) { 973 tracking = true; 974 } else { 975 tracking = false; 976 } 977 } 978 } 979 return tracking; 980 } 981 982 /** 983 * Send a search event so that PageProvider calls can be tracked by Audit or other statistic gathering process 984 * 985 * @since 7.4 986 */ 987 protected void fireSearchEvent(NuxeoPrincipal principal, String query, List<T> entries, Long executionTimeMs) { 988 989 if (!isTrackingEnabled()) { 990 return; 991 } 992 993 Map<String, Serializable> props = new HashMap<>(); 994 995 props.put("pageProviderName", getDefinition().getName()); 996 997 props.put("effectiveQuery", query); 998 props.put("searchPattern", getDefinition().getPattern()); 999 props.put("queryParams", getDefinition().getQueryParameters()); 1000 props.put("params", getParameters()); 1001 WhereClauseDefinition wc = getDefinition().getWhereClause(); 1002 if (wc != null) { 1003 props.put("whereClause_fixedPart", wc.getFixedPart()); 1004 props.put("whereClause_select", wc.getSelectStatement()); 1005 } 1006 1007 DocumentModel searchDocumentModel = getSearchDocumentModel(); 1008 if (searchDocumentModel != null && !(searchDocumentModel instanceof SimpleDocumentModel)) { 1009 RenderingContext rCtx = RenderingContext.CtxBuilder.properties("*").get(); 1010 try { 1011 // the SearchDocumentModel is not a Document bound to the repository 1012 // - it may not survive the Event Stacking (ShallowDocumentModel) 1013 // - it may take too much space in memory 1014 // => let's use JSON 1015 String searchDocumentModelAsJson = MarshallerHelper.objectToJson(DocumentModel.class, 1016 searchDocumentModel, rCtx); 1017 props.put("searchDocumentModelAsJson", searchDocumentModelAsJson); 1018 } catch (IOException e) { 1019 log.error("Unable to Marshall SearchDocumentModel as JSON", e); 1020 } 1021 1022 ArrayList<String> searchFields = new ArrayList<>(); 1023 // searchFields collects the non- null fields inside the SearchDocumentModel 1024 // some schemas are skipped because they contains ContentView related info 1025 for (String schema : searchDocumentModel.getSchemas()) { 1026 for (Property prop : searchDocumentModel.getPropertyObjects(schema)) { 1027 if (prop.getValue() != null 1028 && !SKIPPED_SCHEMAS_FOR_SEARCHFIELD.contains(prop.getSchema().getNamespace().prefix)) { 1029 if (prop.isList()) { 1030 if (ArrayUtils.isNotEmpty(prop.getValue(Object[].class))) { 1031 searchFields.add(prop.getXPath()); 1032 } 1033 } else { 1034 searchFields.add(prop.getXPath()); 1035 } 1036 } 1037 } 1038 } 1039 props.put("searchFields", searchFields); 1040 } 1041 1042 if (entries != null) { 1043 props.put("resultsCountInPage", entries.size()); 1044 } 1045 props.put("resultsCount", getResultsCount()); 1046 props.put("pageSize", getPageSize()); 1047 props.put("pageIndex", getCurrentPageIndex()); 1048 props.put("principal", principal.getName()); 1049 1050 if (executionTimeMs != null) { 1051 props.put("executionTimeMs", executionTimeMs); 1052 } 1053 1054 incorporateAggregates(props); 1055 1056 EventService es = Framework.getService(EventService.class); 1057 EventContext ctx = new UnboundEventContext(principal, props); 1058 es.fireEvent(ctx.newEvent("search")); 1059 } 1060 1061 /** 1062 * Default (dummy) implementation that should be overridden by PageProvider actually dealing with Aggregates 1063 * 1064 * @since 7.4 1065 */ 1066 protected void incorporateAggregates(Map<String, Serializable> eventProps) { 1067 1068 List<AggregateDefinition> ags = getDefinition().getAggregates(); 1069 if (ags != null) { 1070 ArrayList<HashMap<String, Serializable>> aggregates = new ArrayList<>(); 1071 for (AggregateDefinition ag : ags) { 1072 HashMap<String, Serializable> agData = new HashMap<>(); 1073 agData.put("type", ag.getType()); 1074 agData.put("id", ag.getId()); 1075 agData.put("field", ag.getDocumentField()); 1076 agData.putAll(ag.getProperties()); 1077 ArrayList<HashMap<String, Serializable>> rangesData = new ArrayList<>(); 1078 if (ag.getDateRanges() != null) { 1079 for (AggregateRangeDateDefinition range : ag.getDateRanges()) { 1080 HashMap<String, Serializable> rangeData = new HashMap<>(); 1081 rangeData.put("from", range.getFromAsString()); 1082 rangeData.put("to", range.getToAsString()); 1083 rangesData.add(rangeData); 1084 } 1085 for (AggregateRangeDefinition range : ag.getRanges()) { 1086 HashMap<String, Serializable> rangeData = new HashMap<>(); 1087 rangeData.put("from-dbl", range.getFrom()); 1088 rangeData.put("to-dbl", range.getTo()); 1089 rangesData.add(rangeData); 1090 } 1091 } 1092 agData.put("ranges", rangesData); 1093 aggregates.add(agData); 1094 } 1095 eventProps.put("aggregates", aggregates); 1096 } 1097 1098 } 1099 1100 @Override 1101 public long getResultsCountLimit() { 1102 return 0; 1103 } 1104 1105 @Override 1106 public boolean isSkipAggregates() { 1107 return (boolean) getProperties().getOrDefault(SKIP_AGGREGATES_PROP, false); 1108 } 1109 1110}