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