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