001/* 002 * (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and contributors. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the GNU Lesser General Public License 006 * (LGPL) version 2.1 which accompanies this distribution, and is available at 007 * http://www.gnu.org/licenses/lgpl.html 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * Contributors: 015 * Anahide Tchertchian 016 */ 017package org.nuxeo.ecm.platform.query.api; 018 019import java.io.IOException; 020import java.io.Serializable; 021import java.security.Principal; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.commons.lang.ArrayUtils; 030import org.apache.commons.lang.NotImplementedException; 031import org.apache.commons.lang.StringUtils; 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.SortInfo; 037import org.nuxeo.ecm.core.api.model.DocumentPart; 038import org.nuxeo.ecm.core.api.model.Property; 039import org.nuxeo.ecm.core.event.EventContext; 040import org.nuxeo.ecm.core.event.EventService; 041import org.nuxeo.ecm.core.event.impl.UnboundEventContext; 042import org.nuxeo.ecm.core.io.registry.MarshallerHelper; 043import org.nuxeo.ecm.core.io.registry.context.RenderingContext; 044import org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider; 045import org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider; 046import org.nuxeo.runtime.api.Framework; 047import org.nuxeo.runtime.services.config.ConfigurationService; 048 049/** 050 * Basic implementation for a {@link PageProvider}. 051 * <p> 052 * Provides next/prev standard logics, and helper methods for retrieval of items and first/next/prev/last buttons 053 * display as well as other display information (number of pages for instance). 054 * <p> 055 * Also handles selection by providing a default implementation of {@link #getCurrentSelectPage()} working in 056 * conjunction with {@link #setSelectedEntries(List)}. 057 * 058 * @author Anahide Tchertchian 059 */ 060public abstract class AbstractPageProvider<T> implements PageProvider<T> { 061 062 public static final Log log = LogFactory.getLog(AbstractPageProvider.class); 063 064 private static final long serialVersionUID = 1L; 065 066 /** 067 * property used to enable globally tracking : property should contains the list of pageproviders to be tracked 068 * 069 * @since 7.4 070 */ 071 public static final String PAGEPROVIDER_TRACK_PROPERTY_NAME = "nuxeo.pageprovider.track"; 072 073 /** 074 * lists schemas prefixes that should be skipped when extracting "search fields" (tracking) from searchDocumentModel 075 * 076 * @since 7.4 077 */ 078 protected static final List<String> SKIPPED_SCHEMAS_FOR_SEARCHFIELD = Arrays.asList(new String[] { "cvd" }); 079 080 protected String name; 081 082 protected long offset = 0; 083 084 protected long pageSize = 0; 085 086 protected List<Long> pageSizeOptions; 087 088 protected long maxPageSize = getDefaultMaxPageSize(); 089 090 protected long resultsCount = UNKNOWN_SIZE; 091 092 protected int currentEntryIndex = 0; 093 094 /** 095 * Integer keeping track of the higher page index giving results. Useful for enabling or disabling the nextPage 096 * action when number of results cannot be known. 097 * 098 * @since 5.5 099 */ 100 protected int currentHigherNonEmptyPageIndex = 0; 101 102 protected List<SortInfo> sortInfos; 103 104 protected boolean sortable = false; 105 106 protected List<T> selectedEntries; 107 108 protected PageSelections<T> currentSelectPage; 109 110 protected Map<String, Serializable> properties; 111 112 protected Object[] parameters; 113 114 protected DocumentModel searchDocumentModel; 115 116 protected String errorMessage; 117 118 protected Throwable error; 119 120 protected PageProviderDefinition definition; 121 122 protected PageProviderChangedListener pageProviderChangedListener; 123 124 /** 125 * Returns the list of current page items. 126 * <p> 127 * Custom implementation can be added here, based on the page provider properties, parameters and 128 * {@link WhereClauseDefinition} on the {@link PageProviderDefinition}, as well as search document, sort 129 * information, etc... 130 * <p> 131 * Implementation of this method usually consists in setting a non-null value to a field caching current items, and 132 * nullifying this field by overriding {@link #pageChanged()} and {@link #refresh()}. 133 * <p> 134 * Fields {@link #errorMessage} and {@link #error} can also be filled to provide accurate feedback in case an error 135 * occurs during the search. 136 * <p> 137 * When items are retrieved, a call to {@link #setResultsCount(long)} should be made to ensure proper pagination as 138 * implemented in this abstract class. The implementation in {@link CoreQueryAndFetchPageProvider} is a good example 139 * when the total results count is known. 140 * <p> 141 * If for performance reasons, for instance, the number of results cannot be known, a fall-back strategy can be 142 * applied to provide the "next" button but not the "last" one, by calling 143 * {@link #getCurrentHigherNonEmptyPageIndex()} and {@link #setCurrentHigherNonEmptyPageIndex(int)}. In this case, 144 * {@link CoreQueryDocumentPageProvider} is a good example. 145 */ 146 @Override 147 public abstract List<T> getCurrentPage(); 148 149 /** 150 * Page change hook, to override for custom behavior 151 * <p> 152 * When overriding it, call {@code super.pageChanged()} as last statement to make sure that the 153 * {@link PageProviderChangedListener} is called with the up-to-date @{code PageProvider} state. 154 */ 155 protected void pageChanged() { 156 currentEntryIndex = 0; 157 currentSelectPage = null; 158 notifyPageChanged(); 159 } 160 161 @Override 162 public void firstPage() { 163 long pageSize = getPageSize(); 164 if (pageSize == 0) { 165 // do nothing 166 return; 167 } 168 long offset = getCurrentPageOffset(); 169 if (offset != 0) { 170 setCurrentPageOffset(0); 171 pageChanged(); 172 } 173 } 174 175 /** 176 * @deprecated: use {@link #firstPage()} instead 177 */ 178 @Deprecated 179 public void rewind() { 180 firstPage(); 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<Long>(); 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<SortInfo>(); 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 setSortInfos(List<SortInfo> sortInfo) { 333 if (sortInfoChanged(this.sortInfos, sortInfo)) { 334 this.sortInfos = sortInfo; 335 refresh(); 336 } 337 } 338 339 @Override 340 public void setSortInfo(SortInfo sortInfo) { 341 List<SortInfo> newSortInfos = new ArrayList<SortInfo>(); 342 if (sortInfo != null) { 343 newSortInfos.add(sortInfo); 344 } 345 setSortInfos(newSortInfos); 346 } 347 348 @Override 349 public void setSortInfo(String sortColumn, boolean sortAscending, boolean removeOtherSortInfos) { 350 if (removeOtherSortInfos) { 351 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 352 setSortInfo(sortInfo); 353 } else { 354 if (getSortInfoIndex(sortColumn, sortAscending) != -1) { 355 // do nothing: sort on this column is not set 356 } else if (getSortInfoIndex(sortColumn, !sortAscending) != -1) { 357 // change direction 358 List<SortInfo> newSortInfos = new ArrayList<SortInfo>(); 359 for (SortInfo sortInfo : getSortInfos()) { 360 if (sortColumn.equals(sortInfo.getSortColumn())) { 361 newSortInfos.add(new SortInfo(sortColumn, sortAscending)); 362 } else { 363 newSortInfos.add(sortInfo); 364 } 365 } 366 setSortInfos(newSortInfos); 367 } else { 368 // just add it 369 addSortInfo(sortColumn, sortAscending); 370 } 371 } 372 } 373 374 @Override 375 public void addSortInfo(String sortColumn, boolean sortAscending) { 376 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 377 List<SortInfo> sortInfos = getSortInfos(); 378 if (sortInfos == null) { 379 setSortInfo(sortInfo); 380 } else { 381 sortInfos.add(sortInfo); 382 setSortInfos(sortInfos); 383 } 384 } 385 386 @Override 387 public int getSortInfoIndex(String sortColumn, boolean sortAscending) { 388 List<SortInfo> sortInfos = getSortInfos(); 389 if (sortInfos == null || sortInfos.isEmpty()) { 390 return -1; 391 } else { 392 SortInfo sortInfo = new SortInfo(sortColumn, sortAscending); 393 return sortInfos.indexOf(sortInfo); 394 } 395 } 396 397 @Override 398 public boolean isNextPageAvailable() { 399 long pageSize = getPageSize(); 400 if (pageSize == 0) { 401 return false; 402 } 403 long resultsCount = getResultsCount(); 404 if (resultsCount < 0) { 405 long currentPageIndex = getCurrentPageIndex(); 406 return currentPageIndex < getCurrentHigherNonEmptyPageIndex() + getMaxNumberOfEmptyPages(); 407 } else { 408 long offset = getCurrentPageOffset(); 409 return resultsCount > pageSize + offset; 410 } 411 } 412 413 @Override 414 public boolean isLastPageAvailable() { 415 long resultsCount = getResultsCount(); 416 if (resultsCount < 0) { 417 return false; 418 } 419 return isNextPageAvailable(); 420 } 421 422 @Override 423 public boolean isPreviousPageAvailable() { 424 long offset = getCurrentPageOffset(); 425 return offset > 0; 426 } 427 428 @Override 429 public void lastPage() { 430 long pageSize = getPageSize(); 431 long resultsCount = getResultsCount(); 432 if (pageSize == 0 || resultsCount < 0) { 433 // do nothing 434 return; 435 } 436 if (resultsCount % pageSize == 0) { 437 setCurrentPageOffset(resultsCount - pageSize); 438 } else { 439 setCurrentPageOffset(resultsCount - resultsCount % pageSize); 440 } 441 pageChanged(); 442 } 443 444 /** 445 * @deprecated: use {@link #lastPage()} instead 446 */ 447 @Deprecated 448 public void last() { 449 lastPage(); 450 } 451 452 @Override 453 public void nextPage() { 454 long pageSize = getPageSize(); 455 if (pageSize == 0) { 456 // do nothing 457 return; 458 } 459 long offset = getCurrentPageOffset(); 460 offset += pageSize; 461 setCurrentPageOffset(offset); 462 pageChanged(); 463 } 464 465 /** 466 * @deprecated: use {@link #nextPage()} instead 467 */ 468 @Deprecated 469 public void next() { 470 nextPage(); 471 } 472 473 @Override 474 public void previousPage() { 475 long pageSize = getPageSize(); 476 if (pageSize == 0) { 477 // do nothing 478 return; 479 } 480 long offset = getCurrentPageOffset(); 481 if (offset >= pageSize) { 482 offset -= pageSize; 483 setCurrentPageOffset(offset); 484 pageChanged(); 485 } 486 } 487 488 /** 489 * @deprecated: use {@link #previousPage()} instead 490 */ 491 @Deprecated 492 public void previous() { 493 previousPage(); 494 } 495 496 /** 497 * Refresh hook, to override for custom behavior 498 * <p> 499 * When overriding it, call {@code super.refresh()} as last statement to make sure that the 500 * {@link PageProviderChangedListener} is called with the up-to-date @{code PageProvider} state. 501 */ 502 @Override 503 public void refresh() { 504 setResultsCount(UNKNOWN_SIZE); 505 setCurrentHigherNonEmptyPageIndex(-1); 506 currentSelectPage = null; 507 errorMessage = null; 508 error = null; 509 notifyRefresh(); 510 511 } 512 513 @Override 514 public void setName(String name) { 515 this.name = name; 516 } 517 518 @Override 519 public String getCurrentPageStatus() { 520 long total = getNumberOfPages(); 521 long current = getCurrentPageIndex() + 1; 522 if (total <= 0) { 523 // number of pages unknown or there is only one page 524 return String.format("%d", Long.valueOf(current)); 525 } else { 526 return String.format("%d/%d", Long.valueOf(current), Long.valueOf(total)); 527 } 528 } 529 530 @Override 531 public boolean isNextEntryAvailable() { 532 long pageSize = getPageSize(); 533 long resultsCount = getResultsCount(); 534 if (pageSize == 0) { 535 if (resultsCount < 0) { 536 // results count unknown 537 long currentPageSize = getCurrentPageSize(); 538 return currentEntryIndex < currentPageSize - 1; 539 } else { 540 return currentEntryIndex < resultsCount - 1; 541 } 542 } else { 543 long currentPageSize = getCurrentPageSize(); 544 if (currentEntryIndex < currentPageSize - 1) { 545 return true; 546 } 547 if (resultsCount < 0) { 548 // results count unknown => do not look for entry in next page 549 return false; 550 } else { 551 return isNextPageAvailable(); 552 } 553 } 554 } 555 556 @Override 557 public boolean isPreviousEntryAvailable() { 558 return (currentEntryIndex != 0 || isPreviousPageAvailable()); 559 } 560 561 @Override 562 public void nextEntry() { 563 long pageSize = getPageSize(); 564 long resultsCount = getResultsCount(); 565 if (pageSize == 0) { 566 if (resultsCount < 0) { 567 // results count unknown 568 long currentPageSize = getCurrentPageSize(); 569 if (currentEntryIndex < currentPageSize - 1) { 570 currentEntryIndex++; 571 return; 572 } 573 } else { 574 if (currentEntryIndex < resultsCount - 1) { 575 currentEntryIndex++; 576 return; 577 } 578 } 579 } else { 580 long currentPageSize = getCurrentPageSize(); 581 if (currentEntryIndex < currentPageSize - 1) { 582 currentEntryIndex++; 583 return; 584 } 585 if (resultsCount >= 0) { 586 // if results count is unknown, do not look for entry in next 587 // page 588 if (isNextPageAvailable()) { 589 nextPage(); 590 currentEntryIndex = 0; 591 return; 592 } 593 } 594 } 595 596 } 597 598 @Override 599 public void previousEntry() { 600 if (currentEntryIndex > 0) { 601 currentEntryIndex--; 602 return; 603 } 604 if (!isPreviousPageAvailable()) { 605 return; 606 } 607 608 previousPage(); 609 List<T> currentPage = getCurrentPage(); 610 if (currentPage == null || currentPage.isEmpty()) { 611 // things may have changed since last query 612 currentEntryIndex = 0; 613 } else { 614 currentEntryIndex = (new Long(getPageSize() - 1)).intValue(); 615 } 616 } 617 618 @Override 619 public T getCurrentEntry() { 620 List<T> currentPage = getCurrentPage(); 621 if (currentPage == null || currentPage.isEmpty()) { 622 return null; 623 } 624 return currentPage.get(currentEntryIndex); 625 } 626 627 @Override 628 public void setCurrentEntry(T entry) { 629 List<T> currentPage = getCurrentPage(); 630 if (currentPage == null || currentPage.isEmpty()) { 631 throw new NuxeoException(String.format("Entry '%s' not found in current page", entry)); 632 } 633 int i = currentPage.indexOf(entry); 634 if (i == -1) { 635 throw new NuxeoException(String.format("Entry '%s' not found in current page", entry)); 636 } 637 currentEntryIndex = i; 638 } 639 640 @Override 641 public void setCurrentEntryIndex(long index) { 642 int intIndex = new Long(index).intValue(); 643 List<T> currentPage = getCurrentPage(); 644 if (currentPage == null || currentPage.isEmpty()) { 645 throw new NuxeoException(String.format("Index %s not found in current page", new Integer(intIndex))); 646 } 647 if (index >= currentPage.size()) { 648 throw new NuxeoException(String.format("Index %s not found in current page", new Integer(intIndex))); 649 } 650 currentEntryIndex = intIndex; 651 } 652 653 @Override 654 public long getResultsCount() { 655 return resultsCount; 656 } 657 658 @Override 659 public Map<String, Serializable> getProperties() { 660 // break reference 661 return new HashMap<String, Serializable>(properties); 662 } 663 664 @Override 665 public void setProperties(Map<String, Serializable> properties) { 666 this.properties = properties; 667 } 668 669 /** 670 * @since 6.0 671 */ 672 protected boolean getBooleanProperty(String propName, boolean defaultValue) { 673 Map<String, Serializable> props = getProperties(); 674 if (props.containsKey(propName)) { 675 Serializable prop = props.get(propName); 676 if (prop instanceof String) { 677 return Boolean.parseBoolean((String) prop); 678 } else { 679 return Boolean.TRUE.equals(prop); 680 } 681 } 682 return defaultValue; 683 } 684 685 @Override 686 public void setResultsCount(long resultsCount) { 687 this.resultsCount = resultsCount; 688 setCurrentHigherNonEmptyPageIndex(-1); 689 } 690 691 @Override 692 public void setSortable(boolean sortable) { 693 this.sortable = sortable; 694 } 695 696 @Override 697 public boolean isSortable() { 698 return sortable; 699 } 700 701 @Override 702 public PageSelections<T> getCurrentSelectPage() { 703 if (currentSelectPage == null) { 704 List<PageSelection<T>> entries = new ArrayList<PageSelection<T>>(); 705 List<T> currentPage = getCurrentPage(); 706 currentSelectPage = new PageSelections<T>(); 707 currentSelectPage.setName(name); 708 if (currentPage != null && !currentPage.isEmpty()) { 709 if (selectedEntries == null || selectedEntries.isEmpty()) { 710 // no selection at all 711 for (int i = 0; i < currentPage.size(); i++) { 712 entries.add(new PageSelection<T>(currentPage.get(i), false)); 713 } 714 } else { 715 boolean allSelected = true; 716 for (int i = 0; i < currentPage.size(); i++) { 717 T entry = currentPage.get(i); 718 Boolean selected = Boolean.valueOf(selectedEntries.contains(entry)); 719 if (!Boolean.TRUE.equals(selected)) { 720 allSelected = false; 721 } 722 entries.add(new PageSelection<T>(entry, selected.booleanValue())); 723 } 724 if (allSelected) { 725 currentSelectPage.setSelected(true); 726 } 727 } 728 } 729 currentSelectPage.setEntries(entries); 730 } 731 return currentSelectPage; 732 } 733 734 @Override 735 public void setSelectedEntries(List<T> entries) { 736 this.selectedEntries = entries; 737 // reset current select page so that it's rebuilt 738 currentSelectPage = null; 739 } 740 741 @Override 742 public Object[] getParameters() { 743 return parameters; 744 } 745 746 @Override 747 public void setParameters(Object[] parameters) { 748 this.parameters = parameters; 749 } 750 751 @Override 752 public DocumentModel getSearchDocumentModel() { 753 return searchDocumentModel; 754 } 755 756 protected boolean searchDocumentModelChanged(DocumentModel oldDoc, DocumentModel newDoc) { 757 if (oldDoc == null && newDoc == null) { 758 return false; 759 } else if (oldDoc == null || newDoc == null) { 760 return true; 761 } 762 // do not compare properties and assume it's changed 763 return true; 764 } 765 766 @Override 767 public void setSearchDocumentModel(DocumentModel searchDocumentModel) { 768 if (searchDocumentModelChanged(this.searchDocumentModel, searchDocumentModel)) { 769 refresh(); 770 } 771 this.searchDocumentModel = searchDocumentModel; 772 } 773 774 @Override 775 public String getErrorMessage() { 776 return errorMessage; 777 } 778 779 @Override 780 public Throwable getError() { 781 return error; 782 } 783 784 @Override 785 public boolean hasError() { 786 return error != null; 787 } 788 789 @Override 790 public PageProviderDefinition getDefinition() { 791 return definition; 792 } 793 794 @Override 795 public void setDefinition(PageProviderDefinition providerDefinition) { 796 this.definition = providerDefinition; 797 } 798 799 @Override 800 public long getMaxPageSize() { 801 return maxPageSize; 802 } 803 804 @Override 805 public void setMaxPageSize(long maxPageSize) { 806 this.maxPageSize = maxPageSize; 807 } 808 809 /** 810 * Returns the minimal value for the max page size, taking the lower value between the requested page size and the 811 * maximum accepted page size. 812 * 813 * @since 5.4.2 814 */ 815 public long getMinMaxPageSize() { 816 long pageSize = getPageSize(); 817 long maxPageSize = getMaxPageSize(); 818 if (maxPageSize < 0) { 819 maxPageSize = getDefaultMaxPageSize(); 820 } 821 if (pageSize <= 0) { 822 return maxPageSize; 823 } 824 if (maxPageSize > 0 && maxPageSize < pageSize) { 825 return maxPageSize; 826 } 827 return pageSize; 828 } 829 830 /** 831 * Returns an integer keeping track of the higher page index giving results. Useful for enabling or disabling the 832 * nextPage action when number of results cannot be known. 833 * 834 * @since 5.5 835 */ 836 public int getCurrentHigherNonEmptyPageIndex() { 837 return currentHigherNonEmptyPageIndex; 838 } 839 840 /** 841 * Returns the page limit. The n first page we know they exist. 842 * 843 * @since 5.8 844 */ 845 @Override 846 public long getPageLimit() { 847 return PAGE_LIMIT_UNKNOWN; 848 } 849 850 public void setCurrentHigherNonEmptyPageIndex(int higherFilledPageIndex) { 851 this.currentHigherNonEmptyPageIndex = higherFilledPageIndex; 852 } 853 854 /** 855 * Returns the maximum number of empty pages that can be fetched empty (defaults to 1). Can be useful for displaying 856 * pages of a provider without results count. 857 * 858 * @since 5.5 859 */ 860 public int getMaxNumberOfEmptyPages() { 861 return 1; 862 } 863 864 protected long getDefaultMaxPageSize() { 865 long res = DEFAULT_MAX_PAGE_SIZE; 866 if (Framework.isInitialized()) { 867 ConfigurationService cs = Framework.getService(ConfigurationService.class); 868 String maxPageSize = cs.getProperty(DEFAULT_MAX_PAGE_SIZE_RUNTIME_PROP); 869 if (!StringUtils.isBlank(maxPageSize)) { 870 try { 871 res = Long.parseLong(maxPageSize.trim()); 872 } catch (NumberFormatException e) { 873 log.warn(String.format("Invalid max page size defined for property " 874 + "\"%s\": %s (waiting for a long value)", DEFAULT_MAX_PAGE_SIZE_RUNTIME_PROP, maxPageSize)); 875 } 876 } 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(); 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 * @param principal 986 * @param query 987 * @param entries 988 * @since 7.4 989 */ 990 protected void fireSearchEvent(Principal principal, String query, List<T> entries, Long executionTimeMs) { 991 992 if (!isTrackingEnabled()) { 993 return; 994 } 995 996 Map<String, Serializable> props = new HashMap<String, Serializable>(); 997 998 props.put("pageProviderName", getDefinition().getName()); 999 1000 props.put("effectiveQuery", query); 1001 props.put("searchPattern", getDefinition().getPattern()); 1002 props.put("queryParams", getDefinition().getQueryParameters()); 1003 props.put("params", getParameters()); 1004 WhereClauseDefinition wc = getDefinition().getWhereClause(); 1005 if (wc != null) { 1006 props.put("whereClause_fixedPart", wc.getFixedPart()); 1007 props.put("whereClause_select", wc.getSelectStatement()); 1008 } 1009 1010 DocumentModel searchDocumentModel = getSearchDocumentModel(); 1011 if (searchDocumentModel != null) { 1012 RenderingContext rCtx = RenderingContext.CtxBuilder.properties("*").get(); 1013 try { 1014 // the SearchDocumentModel is not a Document bound to the repository 1015 // - it may not survive the Event Stacking (ShallowDocumentModel) 1016 // - it may take too much space in memory 1017 // => let's use JSON 1018 String searchDocumentModelAsJson = MarshallerHelper.objectToJson(DocumentModel.class, 1019 searchDocumentModel, rCtx); 1020 props.put("searchDocumentModelAsJson", searchDocumentModelAsJson); 1021 } catch (IOException e) { 1022 log.error("Unable to Marshall SearchDocumentModel as JSON", e); 1023 } 1024 1025 ArrayList<String> searchFields = new ArrayList<String>(); 1026 // searchFields collects the non- null fields inside the SearchDocumentModel 1027 // some schemas are skipped because they contains ContentView related info 1028 for (DocumentPart part : searchDocumentModel.getParts()) { 1029 for (Property prop : part.getChildren()) { 1030 if (prop.getValue() != null 1031 && !SKIPPED_SCHEMAS_FOR_SEARCHFIELD.contains(prop.getSchema().getNamespace().prefix)) { 1032 if (prop.isList()) { 1033 if (ArrayUtils.isNotEmpty(prop.getValue(Object[].class))) { 1034 searchFields.add(prop.getPath()); 1035 } 1036 } else { 1037 searchFields.add(prop.getPath()); 1038 } 1039 } 1040 } 1041 } 1042 props.put("searchFields", searchFields); 1043 } 1044 1045 if (entries != null) { 1046 props.put("resultsCountInPage", entries.size()); 1047 } 1048 props.put("resultsCount", getResultsCount()); 1049 props.put("pageSize", getPageSize()); 1050 props.put("pageIndex", getCurrentPageIndex()); 1051 props.put("principal", principal.getName()); 1052 1053 if (executionTimeMs != null) { 1054 props.put("executionTimeMs", executionTimeMs); 1055 } 1056 1057 incorporateAggregates(props); 1058 1059 EventService es = Framework.getService(EventService.class); 1060 EventContext ctx = new UnboundEventContext(principal, props); 1061 es.fireEvent(ctx.newEvent("search")); 1062 } 1063 1064 /** 1065 * Default (dummy) implementation that should be overridden by PageProvider actually dealing with Aggregates 1066 * 1067 * @param eventProps 1068 * @since 7.4 1069 */ 1070 protected void incorporateAggregates(Map<String, Serializable> eventProps) { 1071 1072 List<AggregateDefinition> ags = getDefinition().getAggregates(); 1073 if (ags != null) { 1074 ArrayList<HashMap<String, Serializable>> aggregates = new ArrayList<HashMap<String, Serializable>>(); 1075 for (AggregateDefinition ag : ags) { 1076 HashMap<String, Serializable> agData = new HashMap<String, Serializable>(); 1077 agData.put("type", ag.getType()); 1078 agData.put("id", ag.getId()); 1079 agData.put("field", ag.getDocumentField()); 1080 agData.putAll(ag.getProperties()); 1081 ArrayList<HashMap<String, Serializable>> rangesData = new ArrayList<HashMap<String, Serializable>>(); 1082 if (ag.getDateRanges() != null) { 1083 for (AggregateRangeDateDefinition range : ag.getDateRanges()) { 1084 HashMap<String, Serializable> rangeData = new HashMap<String, Serializable>(); 1085 rangeData.put("from", range.getFromAsString()); 1086 rangeData.put("to", range.getToAsString()); 1087 rangesData.add(rangeData); 1088 } 1089 for (AggregateRangeDefinition range : ag.getRanges()) { 1090 HashMap<String, Serializable> rangeData = new HashMap<String, Serializable>(); 1091 rangeData.put("from-dbl", range.getFrom()); 1092 rangeData.put("to-dbl", range.getTo()); 1093 rangesData.add(rangeData); 1094 } 1095 } 1096 agData.put("ranges", rangesData); 1097 aggregates.add(agData); 1098 } 1099 eventProps.put("aggregates", aggregates); 1100 } 1101 1102 } 1103}