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