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