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