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.contentview.jsf;
020
021import static org.apache.commons.lang.StringUtils.isBlank;
022
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026
027import javax.faces.context.ExternalContext;
028import javax.faces.context.FacesContext;
029
030import org.apache.commons.lang.StringUtils;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.jboss.seam.core.Events;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.DocumentModelFactory;
036import org.nuxeo.ecm.core.api.PropertyException;
037import org.nuxeo.ecm.core.api.SortInfo;
038import org.nuxeo.ecm.core.api.model.impl.MapProperty;
039import org.nuxeo.ecm.platform.query.api.Aggregate;
040import org.nuxeo.ecm.platform.query.api.PageProvider;
041import org.nuxeo.ecm.platform.query.api.PageProviderChangedListener;
042import org.nuxeo.ecm.platform.query.api.QuickFilter;
043import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
044import org.nuxeo.runtime.api.Framework;
045
046import com.google.common.base.Function;
047
048/**
049 * Default implementation for the content view object.
050 * <p>
051 * Provides simple getters for attributes defined in the XMap descriptor, except cache key which is computed from
052 * currrent {@link FacesContext} instance if cache key is an EL expression.
053 * <p>
054 * The page provider is initialized calling {@link ContentViewService#getPageProvider}.
055 *
056 * @author Anahide Tchertchian
057 * @since 5.4
058 */
059public class ContentViewImpl implements ContentView, PageProviderChangedListener {
060
061    private static final long serialVersionUID = 1L;
062
063    private static final Log log = LogFactory.getLog(ContentViewImpl.class);
064
065    protected String name;
066
067    protected PageProvider<?> pageProvider;
068
069    protected String title;
070
071    protected boolean translateTitle;
072
073    protected String emptySentence;
074
075    protected boolean translateEmptySentence;
076
077    protected String iconPath;
078
079    protected boolean showTitle;
080
081    protected String selectionList;
082
083    protected String pagination;
084
085    protected List<String> actionCategories;
086
087    protected ContentViewLayout searchLayout;
088
089    protected List<ContentViewLayout> resultLayouts;
090
091    protected List<String> flags;
092
093    protected boolean currentResultLayoutSet = false;
094
095    protected ContentViewLayout currentResultLayout;
096
097    protected List<String> currentResultLayoutColumns;
098
099    protected String cacheKey;
100
101    protected Integer cacheSize;
102
103    protected List<String> refreshEventNames;
104
105    protected List<String> resetEventNames;
106
107    protected boolean useGlobalPageSize;
108
109    protected boolean showPageSizeSelector;
110
111    protected boolean showRefreshCommand;
112
113    protected boolean showFilterForm;
114
115    protected Long currentPageSize;
116
117    protected String[] queryParameters;
118
119    protected DocumentModel searchDocumentModel;
120
121    protected String searchDocumentModelBinding;
122
123    protected String searchDocumentModelType;
124
125    protected String resultColumnsBinding;
126
127    protected String resultLayoutBinding;
128
129    protected String pageSizeBinding;
130
131    protected String sortInfosBinding;
132
133    protected boolean waitForExecution = false;
134
135    protected String waitForExecutionSentence;
136
137    protected boolean executed = false;
138
139    /**
140     * @since 8.4
141     */
142    protected List<QuickFilter> quickFilters;
143
144    public ContentViewImpl(String name, String title, boolean translateTitle, String iconPath, String selectionList,
145            String pagination, List<String> actionCategories, ContentViewLayout searchLayout,
146            List<ContentViewLayout> resultLayouts, List<String> flags, String cacheKey, Integer cacheSize,
147            List<String> refreshEventNames, List<String> resetEventNames, boolean useGlobalPageSize,
148            String[] queryParameters, String searchDocumentModelBinding, String searchDocumentModelType,
149            String resultColumnsBinding, String resultLayoutBinding, String sortInfosBinding, String pageSizeBinding,
150            boolean showTitle, boolean showPageSizeSelector, boolean showRefreshCommand, boolean showFilterForm,
151            String emptySentence, boolean translateEmptySentence) {
152        this.name = name;
153        this.title = title;
154        this.translateTitle = translateTitle;
155        this.iconPath = iconPath;
156        this.selectionList = selectionList;
157        this.pagination = pagination;
158        this.actionCategories = actionCategories;
159        this.searchLayout = searchLayout;
160        this.resultLayouts = resultLayouts;
161        this.flags = flags;
162        this.cacheKey = cacheKey;
163        this.cacheSize = cacheSize;
164        if (cacheSize != null && cacheSize.intValue() <= 0) {
165            // force a static cache key
166            this.cacheKey = "static_key_no_cache";
167        }
168        this.refreshEventNames = refreshEventNames;
169        this.resetEventNames = resetEventNames;
170        this.useGlobalPageSize = useGlobalPageSize;
171        this.queryParameters = queryParameters;
172        this.searchDocumentModelBinding = searchDocumentModelBinding;
173        this.searchDocumentModelType = searchDocumentModelType;
174        this.resultColumnsBinding = resultColumnsBinding;
175        this.resultLayoutBinding = resultLayoutBinding;
176        this.pageSizeBinding = pageSizeBinding;
177        this.sortInfosBinding = sortInfosBinding;
178        this.showTitle = showTitle;
179        this.showPageSizeSelector = showPageSizeSelector;
180        this.showRefreshCommand = showRefreshCommand;
181        this.showFilterForm = showFilterForm;
182        this.emptySentence = emptySentence;
183        this.translateEmptySentence = translateEmptySentence;
184    }
185
186    @Override
187    public String getName() {
188        return name;
189    }
190
191    @Override
192    public String getTitle() {
193        return title;
194    }
195
196    @Override
197    public boolean getTranslateTitle() {
198        return translateTitle;
199    }
200
201    @Override
202    public String getIconPath() {
203        return iconPath;
204    }
205
206    @Override
207    public String getSelectionListName() {
208        return selectionList;
209    }
210
211    @Override
212    public String getPagination() {
213        return pagination;
214    }
215
216    @Override
217    public List<String> getActionsCategories() {
218        return actionCategories;
219    }
220
221    @Override
222    public ContentViewLayout getSearchLayout() {
223        return searchLayout;
224    }
225
226    @Override
227    public List<ContentViewLayout> getResultLayouts() {
228        return resultLayouts;
229    }
230
231    @Override
232    public ContentViewLayout getCurrentResultLayout() {
233        // resolve binding if it is set
234        if (!currentResultLayoutSet && !StringUtils.isBlank(resultLayoutBinding)) {
235            Object res = resolveWithSearchDocument(new Function<FacesContext, Object>() {
236                @Override
237                public Object apply(FacesContext ctx) {
238                    return ComponentTagUtils.resolveElExpression(ctx, resultLayoutBinding);
239                }
240            });
241            if (res != null && res instanceof String) {
242                setCurrentResultLayout((String) res);
243                currentResultLayoutSet = true;
244            }
245        }
246        if (currentResultLayout == null && resultLayouts != null && !resultLayouts.isEmpty()) {
247            // resolve first current result layout
248            return resultLayouts.get(0);
249        }
250        return currentResultLayout;
251    }
252
253    @Override
254    public void setCurrentResultLayout(final ContentViewLayout layout) {
255        setCurrentResultLayout(layout, true);
256    }
257
258    public void setCurrentResultLayout(final ContentViewLayout layout, boolean resetLayoutColumn) {
259        if (!isBlank(resultLayoutBinding) && ComponentTagUtils.isStrictValueReference(resultLayoutBinding)) {
260            resolveWithSearchDocument(new Function<FacesContext, Object>() {
261                @Override
262                public Object apply(FacesContext ctx) {
263                    ComponentTagUtils.applyValueExpression(ctx, resultLayoutBinding,
264                            layout == null ? null : layout.getName());
265                    return null;
266                }
267            });
268        }
269        // still set current result layout value
270        currentResultLayoutSet = true;
271        currentResultLayout = layout;
272
273        if (resetLayoutColumn) {
274            // reset corresponding columns
275            setCurrentResultLayoutColumns(null);
276        }
277    }
278
279    protected Object resolveWithSearchDocument(Function<FacesContext, Object> func) {
280        FacesContext ctx = FacesContext.getCurrentInstance();
281        if (getSearchDocumentModel() == null) {
282            return func.apply(ctx);
283        } else {
284            Object previousSearchDocValue = addSearchDocumentToELContext(ctx);
285            try {
286                return func.apply(ctx);
287            } finally {
288                removeSearchDocumentFromELContext(ctx, previousSearchDocValue);
289            }
290        }
291    }
292
293    @Override
294    public void setCurrentResultLayout(String resultLayoutName) {
295        if (resultLayoutName != null) {
296            for (ContentViewLayout layout : resultLayouts) {
297                if (resultLayoutName.equals(layout.getName())) {
298                    setCurrentResultLayout(layout, false);
299                }
300            }
301        }
302    }
303
304    @Override
305    public boolean hasResultLayoutBinding() {
306        return !isBlank(resultLayoutBinding);
307    }
308
309    /**
310     * Returns cached page provider if it exists or build a new one if parameters have changed.
311     * <p>
312     * The search document, current page and page size are set on the page provider anyway. Sort infos are not set again
313     * if page provider was not built again (e.g if parameters did not change) to avoid erasing sort infos already held
314     * by it.
315     */
316    @Override
317    public PageProvider<?> getPageProvider(DocumentModel searchDocument, List<SortInfo> sortInfos, Long pageSize,
318            Long currentPage, Object... params) {
319        // do not return any page provider if filter has not been done yet
320        if (isWaitForExecution() && !isExecuted()) {
321            return null;
322        }
323
324        // resolve search doc so that it can be used in EL expressions defined
325        // in XML configuration
326        boolean setSearchDoc = false;
327        DocumentModel finalSearchDocument = null;
328        if (searchDocument != null) {
329            setSearchDoc = true;
330            finalSearchDocument = searchDocument;
331        } else if (searchDocumentModel == null) {
332            setSearchDoc = true;
333            if (pageProvider != null) {
334                // try to retrieve it on current page provider
335                finalSearchDocument = pageProvider.getSearchDocumentModel();
336            }
337            if (finalSearchDocument == null) {
338                // initialize it and set it => do not need to set it again
339                finalSearchDocument = getSearchDocumentModel();
340                setSearchDoc = false;
341            }
342        } else {
343            finalSearchDocument = searchDocumentModel;
344        }
345        if (setSearchDoc) {
346            // set it on content view so that it can be used when resolving EL
347            // expressions
348            setSearchDocumentModel(finalSearchDocument);
349        }
350
351        // fallback on local parameters if defined in the XML configuration
352        if (params == null) {
353            params = getQueryParameters();
354        }
355        if (sortInfos == null) {
356            sortInfos = resolveSortInfos();
357        }
358        // allow to pass negative integers instead of null: EL transforms
359        // numbers into value 0 for numbers
360        if (pageSize != null && pageSize.longValue() < 0) {
361            pageSize = null;
362        }
363        if (currentPage != null && currentPage.longValue() < 0) {
364            currentPage = null;
365        }
366        if (pageSize == null) {
367            if (currentPageSize != null && currentPageSize.longValue() >= 0) {
368                pageSize = currentPageSize;
369            }
370            if (pageSize == null) {
371                pageSize = resolvePageSize();
372            }
373        }
374
375        // parameters changed => reset provider.
376        // do not force setting of sort infos as they can be set directly on
377        // the page provider and this method will be called after so they could
378        // be lost.
379        if (pageProvider == null || pageProvider.hasChangedParameters(params)) {
380            // make the service build the provider
381            ContentViewService service = Framework.getLocalService(ContentViewService.class);
382            pageProvider = service.getPageProvider(getName(), sortInfos, pageSize, currentPage, finalSearchDocument,
383                    params);
384        } else {
385            if (pageSize != null) {
386                pageProvider.setPageSize(pageSize.longValue());
387            }
388            if (currentPage != null) {
389                pageProvider.setCurrentPage(currentPage.longValue());
390            }
391        }
392
393        pageProvider.setQuickFilters(new ArrayList<>());
394
395        // Register listener to be notified when the page has changed on the
396        // page provider
397        pageProvider.setPageProviderChangedListener(this);
398        return pageProvider;
399    }
400
401    @Override
402    public PageProvider<?> getPageProviderWithParams(Object... params) {
403        return getPageProvider(null, null, null, null, params);
404    }
405
406    @Override
407    public PageProvider<?> getPageProvider() {
408        return getPageProviderWithParams((Object[]) null);
409    }
410
411    @Override
412    public PageProvider<?> getCurrentPageProvider() {
413        return pageProvider;
414    }
415
416    @Override
417    public void resetPageProvider() {
418        pageProvider = null;
419    }
420
421    @Override
422    public void refreshPageProvider() {
423        if (pageProvider != null) {
424            pageProvider.refresh();
425        }
426        setExecuted(true);
427    }
428
429    @Override
430    public void refreshAndRewindPageProvider() {
431        if (pageProvider != null) {
432            pageProvider.refresh();
433            pageProvider.firstPage();
434        }
435        setExecuted(true);
436    }
437
438    @Override
439    public String getCacheKey() {
440        FacesContext context = FacesContext.getCurrentInstance();
441        Object value = ComponentTagUtils.resolveElExpression(context, cacheKey);
442        if (value != null && !(value instanceof String)) {
443            log.error("Error processing expression '" + cacheKey + "', result is not a String: " + value);
444        }
445        return (String) value;
446    }
447
448    @Override
449    public Integer getCacheSize() {
450        return cacheSize;
451    }
452
453    @Override
454    public Object[] getQueryParameters() {
455        if (queryParameters == null) {
456            return null;
457        }
458        FacesContext context = FacesContext.getCurrentInstance();
459        Object previousSearchDocValue = addSearchDocumentToELContext(context);
460        try {
461            Object[] res = new Object[queryParameters.length];
462            for (int i = 0; i < queryParameters.length; i++) {
463                res[i] = ComponentTagUtils.resolveElExpression(context, queryParameters[i]);
464            }
465            return res;
466        } finally {
467            removeSearchDocumentFromELContext(context, previousSearchDocValue);
468        }
469    }
470
471    @Override
472    public List<String> getRefreshEventNames() {
473        return refreshEventNames;
474    }
475
476    @Override
477    public List<String> getResetEventNames() {
478        return resetEventNames;
479    }
480
481    @Override
482    public boolean getUseGlobalPageSize() {
483        return useGlobalPageSize;
484    }
485
486    @Override
487    public Long getCurrentPageSize() {
488        // take actual value on page provider first in case it's reached its
489        // max page size
490        if (pageProvider != null) {
491            long pageSize = pageProvider.getPageSize();
492            long maxPageSize = pageProvider.getMaxPageSize();
493            if (pageSize > 0 && maxPageSize > 0 && maxPageSize < pageSize) {
494                return Long.valueOf(maxPageSize);
495            }
496            return Long.valueOf(pageSize);
497        }
498        if (currentPageSize != null && currentPageSize.longValue() >= 0) {
499            return currentPageSize;
500        }
501        return null;
502    }
503
504    @Override
505    public void setCurrentPageSize(Long pageSize) {
506        currentPageSize = pageSize;
507        raiseEvent(CONTENT_VIEW_PAGE_SIZE_CHANGED_EVENT);
508    }
509
510    @Override
511    public DocumentModel getSearchDocumentModel() {
512        if (searchDocumentModel == null) {
513            if (searchDocumentModelBinding != null) {
514                // initialize from binding
515                FacesContext context = FacesContext.getCurrentInstance();
516                Object value = ComponentTagUtils.resolveElExpression(context, searchDocumentModelBinding);
517                if (value != null && !(value instanceof DocumentModel)) {
518                    log.error("Error processing expression '" + searchDocumentModelBinding
519                            + "', result is not a DocumentModel: " + value);
520                } else {
521                    setSearchDocumentModel((DocumentModel) value);
522                }
523            }
524            if (searchDocumentModel == null) {
525                // generate a bare document model of given type
526                String docType = getSearchDocumentModelType();
527                if (docType != null) {
528                    DocumentModel bareDoc = DocumentModelFactory.createDocumentModel(docType);
529                    setSearchDocumentModel(bareDoc);
530                }
531            }
532        }
533        return searchDocumentModel;
534    }
535
536    @Override
537    public void setSearchDocumentModel(DocumentModel searchDocumentModel) {
538        this.searchDocumentModel = searchDocumentModel;
539        if (pageProvider != null) {
540            pageProvider.setSearchDocumentModel(searchDocumentModel);
541        }
542    }
543
544    @Override
545    public void resetSearchDocumentModel() {
546        searchDocumentModel = null;
547        if (pageProvider != null) {
548            pageProvider.setSearchDocumentModel(null);
549        }
550    }
551
552    @Override
553    public String getSearchDocumentModelType() {
554        return searchDocumentModelType;
555    }
556
557    @Override
558    public List<String> getFlags() {
559        return flags;
560    }
561
562    @Override
563    public List<String> getResultLayoutColumns() {
564        return getCurrentResultLayoutColumns();
565    }
566
567    @Override
568    @SuppressWarnings({ "unchecked", "rawtypes" })
569    public List<String> getCurrentResultLayoutColumns() {
570        // always resolve binding if it is set
571        if (!StringUtils.isBlank(resultColumnsBinding)) {
572            Object res = resolveWithSearchDocument(new Function<FacesContext, Object>() {
573                @Override
574                public Object apply(FacesContext ctx) {
575                    Object value = ComponentTagUtils.resolveElExpression(ctx, resultColumnsBinding);
576                    if (value != null && !(value instanceof List)) {
577                        log.error("Error processing expression '" + resultColumnsBinding + "', result is not a List: "
578                                + value);
579                    }
580                    return value;
581                }
582            });
583            if (res != null && res instanceof List) {
584                return ((List) res).isEmpty() ? null : (List) res;
585            }
586        }
587        return currentResultLayoutColumns;
588    }
589
590    @Override
591    public void setCurrentResultLayoutColumns(final List<String> resultColumns) {
592        if (isBlank(resultColumnsBinding) || !ComponentTagUtils.isStrictValueReference(resultColumnsBinding)) {
593            // set local values
594            currentResultLayoutColumns = resultColumns;
595        } else {
596            resolveWithSearchDocument(new Function<FacesContext, Object>() {
597                @Override
598                public Object apply(FacesContext ctx) {
599                    ComponentTagUtils.applyValueExpression(ctx, resultColumnsBinding, resultColumns);
600                    return null;
601                }
602            });
603        }
604    }
605
606    @Override
607    public boolean hasResultLayoutColumnsBinding() {
608        return !isBlank(resultColumnsBinding);
609    }
610
611    @SuppressWarnings({ "unchecked", "rawtypes" })
612    protected List<SortInfo> resolveSortInfos() {
613        if (sortInfosBinding == null) {
614            return null;
615        }
616        FacesContext context = FacesContext.getCurrentInstance();
617        Object previousSearchDocValue = addSearchDocumentToELContext(context);
618        try {
619            Object value = ComponentTagUtils.resolveElExpression(context, sortInfosBinding);
620            if (value != null && !(value instanceof List)) {
621                log.error("Error processing expression '" + sortInfosBinding + "', result is not a List: '" + value
622                        + "'");
623            }
624            if (value == null) {
625                return null;
626            }
627            List<SortInfo> res = new ArrayList<SortInfo>();
628            List listValue = (List) value;
629            for (Object listItem : listValue) {
630                if (listItem instanceof SortInfo) {
631                    res.add((SortInfo) listItem);
632                } else if (listItem instanceof Map) {
633                    // XXX: MapProperty does not implement containsKey, so
634                    // resolve
635                    // value instead
636                    if (listItem instanceof MapProperty) {
637                        try {
638                            listItem = ((MapProperty) listItem).getValue();
639                        } catch (ClassCastException | PropertyException e) {
640                            log.error("Cannot resolve sort info item: " + listItem, e);
641                        }
642                    }
643                    Map map = (Map) listItem;
644                    SortInfo sortInfo = SortInfo.asSortInfo(map);
645                    if (sortInfo != null) {
646                        res.add(sortInfo);
647                    } else {
648                        log.error("Cannot resolve sort info item: " + listItem);
649                    }
650                } else {
651                    log.error("Cannot resolve sort info item: " + listItem);
652                }
653            }
654            if (res.isEmpty()) {
655                return null;
656            }
657            return res;
658        } finally {
659            removeSearchDocumentFromELContext(context, previousSearchDocValue);
660        }
661    }
662
663    protected Long resolvePageSize() {
664        if (pageSizeBinding == null) {
665            return null;
666        }
667        FacesContext context = FacesContext.getCurrentInstance();
668        Object previousSearchDocValue = addSearchDocumentToELContext(context);
669        try {
670            Object value = ComponentTagUtils.resolveElExpression(context, pageSizeBinding);
671            if (value == null) {
672                return null;
673            }
674            if (value instanceof String) {
675                try {
676                    return Long.valueOf((String) value);
677                } catch (NumberFormatException e) {
678                    log.error("Error processing expression '" + pageSizeBinding + "', result is not a Long: '" + value
679                            + "'");
680                }
681            } else if (value instanceof Number) {
682                return Long.valueOf(((Number) value).longValue());
683            }
684            return null;
685        } finally {
686            removeSearchDocumentFromELContext(context, previousSearchDocValue);
687        }
688    }
689
690    protected Object addSearchDocumentToELContext(FacesContext facesContext) {
691        ExternalContext econtext = null;
692        if (facesContext != null) {
693            econtext = facesContext.getExternalContext();
694        }
695        if (facesContext == null || econtext == null) {
696            log.error("JSF context is null: cannot expose variable '" + SEARCH_DOCUMENT_EL_VARIABLE
697                    + "' for content view '" + getName() + "'");
698            return null;
699        }
700        Map<String, Object> requestMap = econtext.getRequestMap();
701        Object previousValue = requestMap.get(SEARCH_DOCUMENT_EL_VARIABLE);
702        requestMap.put(SEARCH_DOCUMENT_EL_VARIABLE, searchDocumentModel);
703        return previousValue;
704    }
705
706    protected void removeSearchDocumentFromELContext(FacesContext facesContext, Object previousValue) {
707        if (facesContext == null) {
708            // ignore
709            return;
710        }
711        ExternalContext econtext = facesContext.getExternalContext();
712        if (econtext != null) {
713            Map<String, Object> requestMap = econtext.getRequestMap();
714            requestMap.remove(SEARCH_DOCUMENT_EL_VARIABLE);
715            if (previousValue != null) {
716                requestMap.put(SEARCH_DOCUMENT_EL_VARIABLE, previousValue);
717            }
718        } else {
719            log.error("External context is null: cannot dispose variable '" + SEARCH_DOCUMENT_EL_VARIABLE
720                    + "' for content view '" + getName() + "'");
721        }
722    }
723
724    @Override
725    public boolean getShowPageSizeSelector() {
726        return showPageSizeSelector;
727    }
728
729    @Override
730    public boolean getShowRefreshCommand() {
731        return showRefreshCommand;
732    }
733
734    @Override
735    public boolean getShowFilterForm() {
736        return showFilterForm;
737    }
738
739    @Override
740    public boolean getShowTitle() {
741        return showTitle;
742    }
743
744    @Override
745    public String getEmptySentence() {
746        return emptySentence;
747    }
748
749    @Override
750    public boolean getTranslateEmptySentence() {
751        return translateEmptySentence;
752    }
753
754    @Override
755    public String toString() {
756        final StringBuilder buf = new StringBuilder();
757        buf.append("ContentViewImpl")
758           .append(" {")
759           .append(" name=")
760           .append(name)
761           .append(", title=")
762           .append(title)
763           .append(", translateTitle=")
764           .append(translateTitle)
765           .append(", iconPath=")
766           .append(iconPath)
767           .append(", selectionList=")
768           .append(selectionList)
769           .append(", pagination=")
770           .append(pagination)
771           .append(", actionCategories=")
772           .append(actionCategories)
773           .append(", searchLayout=")
774           .append(searchLayout)
775           .append(", resultLayouts=")
776           .append(resultLayouts)
777           .append(", currentResultLayout=")
778           .append(currentResultLayout)
779           .append(", flags=")
780           .append(flags)
781           .append(", cacheKey=")
782           .append(cacheKey)
783           .append(", cacheSize=")
784           .append(cacheSize)
785           .append(", currentPageSize=")
786           .append(currentPageSize)
787           .append(", refreshEventNames=")
788           .append(refreshEventNames)
789           .append(", resetEventNames=")
790           .append(resetEventNames)
791           .append(", useGlobalPageSize=")
792           .append(useGlobalPageSize)
793           .append(", searchDocumentModel=")
794           .append(searchDocumentModel)
795           .append('}');
796        return buf.toString();
797    }
798
799    /*
800     * ----- PageProviderChangedListener -----
801     */
802
803    protected void raiseEvent(String eventName, Object... params) {
804        if (Events.exists()) {
805            Events.instance().raiseEvent(eventName, params);
806        }
807    }
808
809    protected void raiseEvent(String eventName) {
810        raiseEvent(eventName, name);
811    }
812
813    @Override
814    public void pageChanged(PageProvider<?> pageProvider) {
815        raiseEvent(CONTENT_VIEW_PAGE_CHANGED_EVENT);
816    }
817
818    @Override
819    public void refreshed(PageProvider<?> pageProvider) {
820        raiseEvent(CONTENT_VIEW_REFRESH_EVENT);
821    }
822
823    @SuppressWarnings("rawtypes")
824    @Override
825    public void resetPageProviderAggregates() {
826        if (pageProvider != null && pageProvider.hasAggregateSupport()) {
827            Map<String, ? extends Aggregate> aggs = pageProvider.getAggregates();
828            for (Aggregate agg : aggs.values()) {
829                agg.resetSelection();
830            }
831        }
832    }
833
834    @Override
835    public String getWaitForExecutionSentence() {
836        return waitForExecutionSentence;
837    }
838
839    /**
840     * @since 7.4
841     */
842    public void setWaitForExecutionSentence(String waitForExecutionSentence) {
843        this.waitForExecutionSentence = waitForExecutionSentence;
844    }
845
846    @Override
847    public boolean isWaitForExecution() {
848        return waitForExecution;
849    }
850
851    /**
852     * @since 7.4
853     */
854    public void setWaitForExecution(boolean waitForExecution) {
855        this.waitForExecution = waitForExecution;
856    }
857
858    @Override
859    public boolean isExecuted() {
860        return executed;
861    }
862
863    @Override
864    public void setExecuted(boolean executed) {
865        this.executed = executed;
866    }
867
868    /**
869     * @since 8.4
870     */
871    public void setQuickFilters(List<QuickFilter> quickFilters) {
872        this.quickFilters = quickFilters;
873    }
874
875    /**
876     * @since 8.4
877     */
878    public List<QuickFilter> getQuickFilters() {
879        return this.quickFilters;
880    }
881}