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