/*
* (C) Copyright 2010 Nuxeo SA (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Anahide Tchertchian
*/
package org.nuxeo.ecm.platform.contentview.jsf;
import static org.apache.commons.lang.StringUtils.isBlank;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.core.Events;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelFactory;
import org.nuxeo.ecm.core.api.SortInfo;
import org.nuxeo.ecm.core.api.model.PropertyException;
import org.nuxeo.ecm.core.api.model.impl.MapProperty;
import org.nuxeo.ecm.platform.query.api.Aggregate;
import org.nuxeo.ecm.platform.query.api.PageProvider;
import org.nuxeo.ecm.platform.query.api.PageProviderChangedListener;
import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
import org.nuxeo.runtime.api.Framework;
import com.google.common.base.Function;
/**
* Default implementation for the content view object.
* <p>
* Provides simple getters for attributes defined in the XMap descriptor,
* except cache key which is computed from currrent {@link FacesContext}
* instance if cache key is an EL expression.
* <p>
* The page provider is initialized calling
* {@link ContentViewService#getPageProvider}.
*
* @author Anahide Tchertchian
* @since 5.4
*/
public class ContentViewImpl implements ContentView,
PageProviderChangedListener {
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(ContentViewImpl.class);
protected String name;
protected PageProvider<?> pageProvider;
protected String title;
protected boolean translateTitle;
protected String emptySentence;
protected boolean translateEmptySentence;
protected String iconPath;
protected boolean showTitle;
protected String selectionList;
protected String pagination;
protected List<String> actionCategories;
protected ContentViewLayout searchLayout;
protected List<ContentViewLayout> resultLayouts;
protected List<String> flags;
protected boolean currentResultLayoutSet = false;
protected ContentViewLayout currentResultLayout;
protected List<String> currentResultLayoutColumns;
protected String cacheKey;
protected Integer cacheSize;
protected List<String> refreshEventNames;
protected List<String> resetEventNames;
protected boolean useGlobalPageSize;
protected boolean showPageSizeSelector;
protected boolean showRefreshCommand;
protected boolean showFilterForm;
protected Long currentPageSize;
protected String[] queryParameters;
protected DocumentModel searchDocumentModel;
protected String searchDocumentModelBinding;
protected String searchDocumentModelType;
protected String resultColumnsBinding;
protected String resultLayoutBinding;
protected String pageSizeBinding;
protected String sortInfosBinding;
public ContentViewImpl(String name, String title, boolean translateTitle,
String iconPath, String selectionList, String pagination,
List<String> actionCategories, ContentViewLayout searchLayout,
List<ContentViewLayout> resultLayouts, List<String> flags,
String cacheKey, Integer cacheSize, List<String> refreshEventNames,
List<String> resetEventNames, boolean useGlobalPageSize,
String[] queryParameters, String searchDocumentModelBinding,
String searchDocumentModelType, String resultColumnsBinding,
String resultLayoutBinding, String sortInfosBinding,
String pageSizeBinding, boolean showTitle,
boolean showPageSizeSelector, boolean showRefreshCommand,
boolean showFilterForm, String emptySentence,
boolean translateEmptySentence) {
this.name = name;
this.title = title;
this.translateTitle = translateTitle;
this.iconPath = iconPath;
this.selectionList = selectionList;
this.pagination = pagination;
this.actionCategories = actionCategories;
this.searchLayout = searchLayout;
this.resultLayouts = resultLayouts;
this.flags = flags;
this.cacheKey = cacheKey;
this.cacheSize = cacheSize;
if (cacheSize != null && cacheSize.intValue() <= 0) {
// force a static cache key
this.cacheKey = "static_key_no_cache";
}
this.refreshEventNames = refreshEventNames;
this.resetEventNames = resetEventNames;
this.useGlobalPageSize = useGlobalPageSize;
this.queryParameters = queryParameters;
this.searchDocumentModelBinding = searchDocumentModelBinding;
this.searchDocumentModelType = searchDocumentModelType;
this.resultColumnsBinding = resultColumnsBinding;
this.resultLayoutBinding = resultLayoutBinding;
this.pageSizeBinding = pageSizeBinding;
this.sortInfosBinding = sortInfosBinding;
this.showTitle = showTitle;
this.showPageSizeSelector = showPageSizeSelector;
this.showRefreshCommand = showRefreshCommand;
this.showFilterForm = showFilterForm;
this.emptySentence = emptySentence;
this.translateEmptySentence = translateEmptySentence;
}
public String getName() {
return name;
}
public String getTitle() {
return title;
}
public boolean getTranslateTitle() {
return translateTitle;
}
public String getIconPath() {
return iconPath;
}
public String getSelectionListName() {
return selectionList;
}
public String getPagination() {
return pagination;
}
public List<String> getActionsCategories() {
return actionCategories;
}
public ContentViewLayout getSearchLayout() {
return searchLayout;
}
public List<ContentViewLayout> getResultLayouts() {
return resultLayouts;
}
public ContentViewLayout getCurrentResultLayout() {
// resolve binding if it is set
if (!currentResultLayoutSet
&& !StringUtils.isBlank(resultLayoutBinding)) {
Object res = resolveWithSearchDocument(new Function<FacesContext, Object>() {
@Override
public Object apply(FacesContext ctx) {
return ComponentTagUtils.resolveElExpression(ctx,
resultLayoutBinding);
}
});
if (res != null && res instanceof String) {
setCurrentResultLayout((String) res);
currentResultLayoutSet = true;
}
}
if (currentResultLayout == null && resultLayouts != null
&& !resultLayouts.isEmpty()) {
// resolve first current result layout
return resultLayouts.get(0);
}
return currentResultLayout;
}
public void setCurrentResultLayout(final ContentViewLayout layout) {
setCurrentResultLayout(layout, true);
}
public void setCurrentResultLayout(final ContentViewLayout layout, boolean resetLayoutColumn) {
if (!isBlank(resultLayoutBinding)
&& ComponentTagUtils.isStrictValueReference(resultLayoutBinding)) {
resolveWithSearchDocument(new Function<FacesContext, Object>() {
@Override
public Object apply(FacesContext ctx) {
ComponentTagUtils.applyValueExpression(ctx,
resultLayoutBinding,
layout == null ? null : layout.getName());
return null;
}
});
}
// still set current result layout value
currentResultLayoutSet = true;
currentResultLayout = layout;
if (resetLayoutColumn) {
// reset corresponding columns
setCurrentResultLayoutColumns(null);
}
}
protected Object resolveWithSearchDocument(
Function<FacesContext, Object> func) {
FacesContext ctx = FacesContext.getCurrentInstance();
if (getSearchDocumentModel() == null) {
return func.apply(ctx);
} else {
Object previousSearchDocValue = addSearchDocumentToELContext(ctx);
try {
return func.apply(ctx);
} finally {
removeSearchDocumentFromELContext(ctx, previousSearchDocValue);
}
}
}
public void setCurrentResultLayout(String resultLayoutName) {
if (resultLayoutName != null) {
for (ContentViewLayout layout : resultLayouts) {
if (resultLayoutName.equals(layout.getName())) {
setCurrentResultLayout(layout, false);
}
}
}
}
@Override
public boolean hasResultLayoutBinding() {
return !isBlank(resultLayoutBinding);
}
/**
* Returns cached page provider if it exists or build a new one if
* parameters have changed.
* <p>
* The search document, current page and page size are set on the page
* provider anyway. Sort infos are not set again if page provider was not
* built again (e.g if parameters did not change) to avoid erasing sort
* infos already held by it.
*/
public PageProvider<?> getPageProvider(DocumentModel searchDocument,
List<SortInfo> sortInfos, Long pageSize, Long currentPage,
Object... params) throws ClientException {
// resolve search doc so that it can be used in EL expressions defined
// in XML configuration
boolean setSearchDoc = false;
DocumentModel finalSearchDocument = null;
if (searchDocument != null) {
setSearchDoc = true;
finalSearchDocument = searchDocument;
} else if (this.searchDocumentModel == null) {
setSearchDoc = true;
if (pageProvider != null) {
// try to retrieve it on current page provider
finalSearchDocument = pageProvider.getSearchDocumentModel();
}
if (finalSearchDocument == null) {
// initialize it and set it => do not need to set it again
finalSearchDocument = getSearchDocumentModel();
setSearchDoc = false;
}
} else {
finalSearchDocument = this.searchDocumentModel;
}
if (setSearchDoc) {
// set it on content view so that it can be used when resolving EL
// expressions
setSearchDocumentModel(finalSearchDocument);
}
// fallback on local parameters if defined in the XML configuration
if (params == null) {
params = getQueryParameters();
}
if (sortInfos == null) {
sortInfos = resolveSortInfos();
}
// allow to pass negative integers instead of null: EL transforms
// numbers into value 0 for numbers
if (pageSize != null && pageSize.longValue() < 0) {
pageSize = null;
}
if (currentPage != null && currentPage.longValue() < 0) {
currentPage = null;
}
if (pageSize == null) {
if (currentPageSize != null && currentPageSize.longValue() >= 0) {
pageSize = currentPageSize;
}
if (pageSize == null) {
pageSize = resolvePageSize();
}
}
// parameters changed => reset provider.
// do not force setting of sort infos as they can be set directly on
// the page provider and this method will be called after so they could
// be lost.
if (pageProvider == null || pageProvider.hasChangedParameters(params)) {
try {
// make the service build the provider
ContentViewService service = Framework.getLocalService(ContentViewService.class);
if (service == null) {
throw new ClientException(
"Could not resolve ContentViewService");
}
pageProvider = service.getPageProvider(getName(), sortInfos,
pageSize, currentPage, finalSearchDocument, params);
} catch (ClientException e) {
throw e;
}
} else {
if (pageSize != null) {
pageProvider.setPageSize(pageSize.longValue());
}
if (currentPage != null) {
pageProvider.setCurrentPage(currentPage.longValue());
}
}
// Register listener to be notified when the page has changed on the
// page provider
pageProvider.setPageProviderChangedListener(this);
return pageProvider;
}
public PageProvider<?> getPageProviderWithParams(Object... params)
throws ClientException {
return getPageProvider(null, null, null, null, params);
}
public PageProvider<?> getPageProvider() throws ClientException {
return getPageProviderWithParams((Object[]) null);
}
public PageProvider<?> getCurrentPageProvider() {
return pageProvider;
}
public void resetPageProvider() {
pageProvider = null;
}
public void refreshPageProvider() {
if (pageProvider != null) {
pageProvider.refresh();
}
}
public void refreshAndRewindPageProvider() {
if (pageProvider != null) {
pageProvider.refresh();
pageProvider.firstPage();
}
}
public String getCacheKey() {
FacesContext context = FacesContext.getCurrentInstance();
Object value = ComponentTagUtils.resolveElExpression(context, cacheKey);
if (value != null && !(value instanceof String)) {
log.error(String.format("Error processing expression '%s', "
+ "result is not a String: %s", cacheKey, value));
}
return (String) value;
}
public Integer getCacheSize() {
return cacheSize;
}
public Object[] getQueryParameters() {
if (queryParameters == null) {
return null;
}
FacesContext context = FacesContext.getCurrentInstance();
Object previousSearchDocValue = addSearchDocumentToELContext(context);
try {
Object[] res = new Object[queryParameters.length];
for (int i = 0; i < queryParameters.length; i++) {
res[i] = ComponentTagUtils.resolveElExpression(context,
queryParameters[i]);
}
return res;
} finally {
removeSearchDocumentFromELContext(context, previousSearchDocValue);
}
}
public List<String> getRefreshEventNames() {
return refreshEventNames;
}
public List<String> getResetEventNames() {
return resetEventNames;
}
public boolean getUseGlobalPageSize() {
return useGlobalPageSize;
}
public Long getCurrentPageSize() {
// take actual value on page provider first in case it's reached its
// max page size
if (pageProvider != null) {
long pageSize = pageProvider.getPageSize();
long maxPageSize = pageProvider.getMaxPageSize();
if (pageSize > 0 && maxPageSize > 0 && maxPageSize < pageSize) {
return Long.valueOf(maxPageSize);
}
return Long.valueOf(pageSize);
}
if (currentPageSize != null && currentPageSize.longValue() >= 0) {
return currentPageSize;
}
return null;
}
@Override
public void setCurrentPageSize(Long pageSize) {
this.currentPageSize = pageSize;
raiseEvent(CONTENT_VIEW_PAGE_SIZE_CHANGED_EVENT);
}
public DocumentModel getSearchDocumentModel() {
if (searchDocumentModel == null) {
if (searchDocumentModelBinding != null) {
// initialize from binding
FacesContext context = FacesContext.getCurrentInstance();
Object value = ComponentTagUtils.resolveElExpression(context,
searchDocumentModelBinding);
if (value != null && !(value instanceof DocumentModel)) {
log.error(String.format(
"Error processing expression '%s', "
+ "result is not a DocumentModel: %s",
searchDocumentModelBinding, value));
} else {
setSearchDocumentModel((DocumentModel) value);
}
}
if (searchDocumentModel == null) {
// generate a bare document model of given type
String docType = getSearchDocumentModelType();
if (docType != null) {
DocumentModel bareDoc = DocumentModelFactory.createDocumentModel(docType);
setSearchDocumentModel(bareDoc);
}
}
}
return searchDocumentModel;
}
public void setSearchDocumentModel(DocumentModel searchDocumentModel) {
this.searchDocumentModel = searchDocumentModel;
if (pageProvider != null) {
pageProvider.setSearchDocumentModel(searchDocumentModel);
}
}
public void resetSearchDocumentModel() {
searchDocumentModel = null;
if (pageProvider != null) {
pageProvider.setSearchDocumentModel(null);
}
}
public String getSearchDocumentModelType() {
return searchDocumentModelType;
}
public List<String> getFlags() {
return flags;
}
@Override
public List<String> getResultLayoutColumns() {
return getCurrentResultLayoutColumns();
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public List<String> getCurrentResultLayoutColumns() {
// always resolve binding if it is set
if (!StringUtils.isBlank(resultColumnsBinding)) {
Object res = resolveWithSearchDocument(new Function<FacesContext, Object>() {
@Override
public Object apply(FacesContext ctx) {
Object value = ComponentTagUtils.resolveElExpression(ctx,
resultColumnsBinding);
if (value != null && !(value instanceof List)) {
log.error(String.format(
"Error processing expression '%s', "
+ "result is not a List: %s",
resultColumnsBinding, value));
}
return value;
}
});
if (res != null && res instanceof List) {
return ((List) res).isEmpty() ? null : (List) res;
}
}
return currentResultLayoutColumns;
}
@Override
public void setCurrentResultLayoutColumns(final List<String> resultColumns) {
if (isBlank(resultColumnsBinding)
|| !ComponentTagUtils.isStrictValueReference(resultColumnsBinding)) {
// set local values
currentResultLayoutColumns = resultColumns;
} else {
resolveWithSearchDocument(new Function<FacesContext, Object>() {
@Override
public Object apply(FacesContext ctx) {
ComponentTagUtils.applyValueExpression(ctx,
resultColumnsBinding, resultColumns);
return null;
}
});
}
}
@Override
public boolean hasResultLayoutColumnsBinding() {
return !isBlank(resultColumnsBinding);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected List<SortInfo> resolveSortInfos() {
if (sortInfosBinding == null) {
return null;
}
FacesContext context = FacesContext.getCurrentInstance();
Object previousSearchDocValue = addSearchDocumentToELContext(context);
try {
Object value = ComponentTagUtils.resolveElExpression(context,
sortInfosBinding);
if (value != null && !(value instanceof List)) {
log.error(String.format("Error processing expression '%s', "
+ "result is not a List: %s", sortInfosBinding, value));
}
if (value == null) {
return null;
}
List<SortInfo> res = new ArrayList<SortInfo>();
List listValue = (List) value;
for (Object listItem : listValue) {
if (listItem instanceof SortInfo) {
res.add((SortInfo) listItem);
} else if (listItem instanceof Map) {
// XXX: MapProperty does not implement containsKey, so
// resolve
// value instead
if (listItem instanceof MapProperty) {
try {
listItem = ((MapProperty) listItem).getValue();
} catch (ClassCastException | PropertyException e) {
log.error("Cannot resolve sort info item: "
+ listItem, e);
}
}
Map map = (Map) listItem;
SortInfo sortInfo = SortInfo.asSortInfo(map);
if (sortInfo != null) {
res.add(sortInfo);
} else {
log.error("Cannot resolve sort info item: " + listItem);
}
} else {
log.error("Cannot resolve sort info item: " + listItem);
}
}
if (res.isEmpty()) {
return null;
}
return res;
} finally {
removeSearchDocumentFromELContext(context, previousSearchDocValue);
}
}
protected Long resolvePageSize() {
if (pageSizeBinding == null) {
return null;
}
FacesContext context = FacesContext.getCurrentInstance();
Object previousSearchDocValue = addSearchDocumentToELContext(context);
try {
Object value = ComponentTagUtils.resolveElExpression(context,
pageSizeBinding);
if (value == null) {
return null;
}
if (value instanceof String) {
try {
return Long.valueOf((String) value);
} catch (NumberFormatException e) {
log.error(String.format(
"Error processing expression '%s', "
+ "result is not a Long: %s",
pageSizeBinding, value));
}
} else if (value instanceof Number) {
return Long.valueOf(((Number) value).longValue());
}
return null;
} finally {
removeSearchDocumentFromELContext(context, previousSearchDocValue);
}
}
protected Object addSearchDocumentToELContext(FacesContext facesContext) {
if (facesContext == null) {
log.error(String.format(
"Faces context is null: cannot expose variable '%s' "
+ "for content view '%s'",
SEARCH_DOCUMENT_EL_VARIABLE, getName()));
return null;
}
ExternalContext econtext = facesContext.getExternalContext();
if (econtext != null) {
Map<String, Object> requestMap = econtext.getRequestMap();
Object previousValue = requestMap.get(SEARCH_DOCUMENT_EL_VARIABLE);
requestMap.put(SEARCH_DOCUMENT_EL_VARIABLE, searchDocumentModel);
return previousValue;
} else {
log.error(String.format(
"External context is null: cannot expose variable '%s' "
+ "for content view '%s'",
SEARCH_DOCUMENT_EL_VARIABLE, getName()));
return null;
}
}
protected void removeSearchDocumentFromELContext(FacesContext facesContext,
Object previousValue) {
if (facesContext == null) {
// ignore
return;
}
ExternalContext econtext = facesContext.getExternalContext();
if (econtext != null) {
Map<String, Object> requestMap = econtext.getRequestMap();
requestMap.remove(SEARCH_DOCUMENT_EL_VARIABLE);
if (previousValue != null) {
requestMap.put(SEARCH_DOCUMENT_EL_VARIABLE, previousValue);
}
} else {
log.error(String.format(
"External context is null: cannot dispose variable '%s' "
+ "for content view '%s'",
SEARCH_DOCUMENT_EL_VARIABLE, getName()));
}
}
@Override
public boolean getShowPageSizeSelector() {
return showPageSizeSelector;
}
@Override
public boolean getShowRefreshCommand() {
return showRefreshCommand;
}
@Override
public boolean getShowFilterForm() {
return showFilterForm;
}
@Override
public boolean getShowTitle() {
return showTitle;
}
public String getEmptySentence() {
return emptySentence;
}
public boolean getTranslateEmptySentence() {
return translateEmptySentence;
}
@Override
public String toString() {
return String.format("ContentViewImpl [name=%s, title=%s, "
+ "translateTitle=%s, iconPath=%s, "
+ "selectionList=%s, pagination=%s, "
+ "actionCategories=%s, searchLayout=%s, "
+ "resultLayouts=%s, currentResultLayout=%s, "
+ "flags=%s, cacheKey=%s, cacheSize=%s, currentPageSize=%s"
+ "refreshEventNames=%s, resetEventNames=%s,"
+ "useGlobalPageSize=%s, searchDocumentModel=%s]", name, title,
Boolean.valueOf(translateTitle), iconPath, selectionList,
pagination, actionCategories, searchLayout, resultLayouts,
currentResultLayout, flags, cacheKey, cacheSize,
currentPageSize, refreshEventNames, resetEventNames,
Boolean.valueOf(useGlobalPageSize), searchDocumentModel);
}
/*
* ----- PageProviderChangedListener -----
*/
protected void raiseEvent(String eventName, Object... params) {
if (Events.exists()) {
Events.instance().raiseEvent(eventName, params);
}
}
protected void raiseEvent(String eventName) {
raiseEvent(eventName, name);
}
@Override
public void pageChanged(PageProvider<?> pageProvider) {
raiseEvent(CONTENT_VIEW_PAGE_CHANGED_EVENT);
}
@Override
public void refreshed(PageProvider<?> pageProvider) {
raiseEvent(CONTENT_VIEW_REFRESH_EVENT);
}
@SuppressWarnings("rawtypes")
@Override
public void resetPageProviderAggregates() {
if (pageProvider != null && pageProvider.hasAggregateSupport()) {
Map<String, Aggregate> aggs = pageProvider.getAggregates();
for (Aggregate agg : aggs.values()) {
agg.resetSelection();
}
}
}
}