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