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