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}