/*
* (C) Copyright 2006-2007 Nuxeo SAS (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:
* <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
*
* $Id: LayoutTagHandler.java 30553 2008-02-24 15:51:31Z atchertchian $
*/
package org.nuxeo.ecm.platform.forms.layout.facelets;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.el.ELException;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.FaceletHandler;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagAttributes;
import javax.faces.view.facelets.TagConfig;
import javax.faces.view.facelets.TagException;
import javax.faces.view.facelets.TagHandler;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.forms.layout.api.Layout;
import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition;
import org.nuxeo.ecm.platform.forms.layout.facelets.dev.DevTagHandler;
import org.nuxeo.ecm.platform.forms.layout.facelets.dev.LayoutDevTagHandler;
import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory;
import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
import org.nuxeo.runtime.api.Framework;
import com.sun.faces.facelets.el.VariableMapperWrapper;
import com.sun.faces.facelets.tag.ui.DecorateHandler;
/**
* Layout tag handler.
* <p>
* Computes a layout in given facelet context, for given mode and value
* attributes. The layout can either be computed from a layout definition, or
* by a layout name, where the layout service will lookup the corresponding
* definition.
* <p>
* If a template is found for this layout, include the corresponding facelet
* and use facelet template features to iterate over rows and widgets.
* <p>
* Since 5.6, the layout name attribute also accepts a comma separated list of
* layout names.
*
* @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
*/
public class LayoutTagHandler extends TagHandler {
private static final Log log = LogFactory.getLog(LayoutTagHandler.class);
protected final TagConfig config;
/**
* The layout instance to render, instead of resolving it from a name or
* definition
*
* @since 5.7
*/
protected final TagAttribute layout;
protected final TagAttribute name;
/**
* @since 5.5.
*/
protected final TagAttribute category;
/**
* @since 5.4.2
*/
protected final TagAttribute definition;
protected final TagAttribute mode;
protected final TagAttribute value;
protected final TagAttribute template;
protected final TagAttribute selectedRows;
protected final TagAttribute selectedColumns;
protected final TagAttribute selectAllByDefault;
/**
* Parameter used to specify that layout should not be rendered, only
* resolved and exposed to the context.
*
* @since 5.7
*/
protected final TagAttribute resolveOnly;
protected final TagAttribute[] vars;
protected final String[] reservedVarsArray = { "id", "layout", "name",
"category", "definition", "mode", "value", "template",
"selectedRows", "selectedColumns", "selectAllByDefault",
"resolveOnly" };
public LayoutTagHandler(TagConfig config) {
super(config);
this.config = config;
name = getAttribute("name");
category = getAttribute("category");
definition = getAttribute("definition");
layout = getAttribute("layout");
if (name == null && definition == null && layout == null) {
throw new TagException(this.tag,
"At least one of attributes 'name', 'layout' or 'definition'"
+ " is required");
}
mode = getAttribute("mode");
value = getRequiredAttribute("value");
if (layout == null && (name != null || definition != null)) {
if (mode == null) {
throw new TagException(this.tag,
"Attribute 'mode' is required when using attribute"
+ " 'name' or 'definition' so that the "
+ "layout instance can be resolved");
}
}
template = getAttribute("template");
selectedRows = getAttribute("selectedRows");
selectedColumns = getAttribute("selectedColumns");
if (selectedRows != null && selectedColumns != null) {
throw new TagException(this.tag, "Attributes 'selectedRows' "
+ "and 'selectedColumns' are aliases: only one of "
+ "them should be filled");
}
selectAllByDefault = getAttribute("selectAllByDefault");
resolveOnly = getAttribute("resolveOnly");
vars = tag.getAttributes().getAll();
}
@SuppressWarnings("unchecked")
// TODO: add javadoc about variables exposed
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException, FacesException, ELException {
WebLayoutManager layoutService;
try {
layoutService = Framework.getService(WebLayoutManager.class);
} catch (Exception e) {
throw new FacesException(e);
}
if (layoutService == null) {
throw new FacesException("Layout service not found");
}
// add additional properties put on tag
Map<String, Serializable> additionalProps = new HashMap<String, Serializable>();
List<String> reservedVars = Arrays.asList(reservedVarsArray);
for (TagAttribute var : vars) {
String localName = var.getLocalName();
if (!reservedVars.contains(localName)) {
// resolve value as there's no alias value expression exposed
// for layout properties
additionalProps.put(localName,
(Serializable) var.getObject(ctx));
}
}
// expose some layout variables before layout creation so that they
// can be used in mode expressions
VariableMapper orig = ctx.getVariableMapper();
VariableMapper vm = new VariableMapperWrapper(orig);
ctx.setVariableMapper(vm);
FaceletHandlerHelper helper = new FaceletHandlerHelper(ctx, config);
try {
Layout layoutInstance = null;
String valueName = value.getValue();
if (ComponentTagUtils.isStrictValueReference(valueName)) {
valueName = ComponentTagUtils.getBareValueName(valueName);
}
String templateValue = null;
if (template != null) {
templateValue = template.getValue(ctx);
}
boolean resolveOnlyValue = false;
if (resolveOnly != null) {
resolveOnlyValue = resolveOnly.getBoolean(ctx);
}
if (layout != null) {
// resolve layout instance given as attribute
layoutInstance = (Layout) layout.getObject(ctx, Layout.class);
if (layoutInstance == null) {
String errMsg = String.format("Layout not found");
applyErrorHandler(ctx, parent, helper, errMsg);
} else {
Map<String, ValueExpression> vars = getVariablesForLayoutBuild(
ctx, layoutInstance.getMode());
for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
vm.setVariable(var.getKey(), var.getValue());
}
layoutInstance.setValueName(valueName);
applyLayoutHandler(ctx, parent, helper, layoutService,
layoutInstance, templateValue, additionalProps,
vars, resolveOnlyValue);
}
} else {
// build layout instance from other attributes
String modeValue = mode.getValue(ctx);
List<String> selectedRowsValue = null;
boolean selectAllByDefaultValue = false;
Map<String, ValueExpression> vars = getVariablesForLayoutBuild(
ctx, modeValue);
for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
vm.setVariable(var.getKey(), var.getValue());
}
if (selectedRows != null || selectedColumns != null) {
if (selectedRows != null) {
selectedRowsValue = (List<String>) selectedRows.getObject(
ctx, List.class);
} else if (selectedColumns != null) {
List<String> selectedColumnsList = (List<String>) selectedColumns.getObject(
ctx, List.class);
// Handle empty selected columns list as null to
// display all columns.
if (selectedColumnsList != null
&& selectedColumnsList.isEmpty()) {
selectedColumnsList = null;
}
selectedRowsValue = selectedColumnsList;
}
}
if (selectAllByDefault != null) {
selectAllByDefaultValue = selectAllByDefault.getBoolean(ctx);
}
if (name != null) {
String layoutCategory = null;
if (category != null) {
layoutCategory = category.getValue(ctx);
}
String nameValue = name.getValue(ctx);
List<String> layoutNames = resolveLayoutNames(nameValue);
for (String layoutName : layoutNames) {
layoutInstance = layoutService.getLayout(ctx,
layoutName, layoutCategory, modeValue,
valueName, selectedRowsValue,
selectAllByDefaultValue);
if (layoutInstance == null) {
String errMsg = String.format(
"Layout '%s' not found", layoutName);
applyErrorHandler(ctx, parent, helper, errMsg);
} else {
applyLayoutHandler(ctx, parent, helper,
layoutService, layoutInstance,
templateValue, additionalProps, vars,
resolveOnlyValue);
}
}
}
if (definition != null) {
LayoutDefinition layoutDef = (LayoutDefinition) definition.getObject(
ctx, LayoutDefinition.class);
if (layoutDef == null) {
String errMsg = "Layout definition resolved to null";
applyErrorHandler(ctx, parent, helper, errMsg);
} else {
layoutInstance = layoutService.getLayout(ctx,
layoutDef, modeValue, valueName,
selectedRowsValue, selectAllByDefaultValue);
applyLayoutHandler(ctx, parent, helper, layoutService,
layoutInstance, templateValue, additionalProps,
vars, resolveOnlyValue);
}
}
}
} finally {
// layout resolved => cleanup variable mapper
ctx.setVariableMapper(orig);
}
}
/**
* Resolves layouts names, splitting on character "," and trimming
* resulting names, and allowing empty strings if the whole string is not
* empty to ease up rendering of layout names using variables.
* <p>
* For instance, if value is null or empty, will return a single empty
* layout name "". If value is "," it will return an empty list, triggering
* no error for usage like <nxl:layout name="#{myLayout}, #{myOtherLayout}"
* [...] />
*/
protected List<String> resolveLayoutNames(String nameValue) {
List<String> res = new ArrayList<String>();
if (nameValue != null) {
String[] split = nameValue.split(",|\\s");
if (split != null) {
for (String item : split) {
if (!StringUtils.isBlank(item)) {
res.add(item.trim());
}
}
}
}
return res;
}
protected void applyLayoutHandler(FaceletContext ctx, UIComponent parent,
FaceletHandlerHelper helper, WebLayoutManager layoutService,
Layout layoutInstance, String templateValue,
Map<String, Serializable> additionalProps,
Map<String, ValueExpression> vars, boolean resolveOnly)
throws IOException, FacesException, ELException {
// set unique id on layout, unless layout is only resolved
if (!resolveOnly) {
layoutInstance.setId(helper.generateLayoutId(layoutInstance.getName()));
}
// add additional properties put on tag
Map<String, Serializable> layoutProps = layoutInstance.getProperties();
if (additionalProps != null && !additionalProps.isEmpty()) {
for (Map.Entry<String, Serializable> entry : additionalProps.entrySet()) {
// XXX: do not override with empty property values if already
// set on the layout properties
String key = entry.getKey();
Serializable value = entry.getValue();
if (layoutProps.containsKey(key)
&& (value == null || ((value instanceof String) && StringUtils.isBlank((String) value)))) {
// do not override property on layout
if (log.isDebugEnabled()) {
log.debug(String.format(
"Do not override property '%s' with "
+ "empty value on layout named '%s'",
key, layoutInstance.getName()));
}
} else {
layoutInstance.setProperty(key, value);
}
}
}
if (StringUtils.isBlank(templateValue)) {
templateValue = layoutInstance.getTemplate();
}
// expose layout instance to variable mapper to ensure good
// resolution of properties
ExpressionFactory eFactory = ctx.getExpressionFactory();
ValueExpression layoutVe = eFactory.createValueExpression(
layoutInstance, Layout.class);
ctx.getVariableMapper().setVariable(
RenderVariables.layoutVariables.layout.name(), layoutVe);
// expose all variables through an alias tag handler
vars.putAll(getVariablesForLayoutRendering(ctx, layoutService,
layoutInstance));
List<String> blockedPatterns = new ArrayList<String>();
blockedPatterns.add(RenderVariables.layoutVariables.layout.name());
blockedPatterns.add(RenderVariables.layoutVariables.layoutProperty.name()
+ "_*");
final String layoutTagConfigId = layoutInstance.getTagConfigId();
if (resolveOnly) {
FaceletHandler handler = helper.getAliasTagHandler(
layoutTagConfigId, vars, blockedPatterns, nextHandler);
// apply
handler.apply(ctx, parent);
} else {
if (!StringUtils.isBlank(templateValue)) {
TagAttribute srcAttr = helper.createAttribute("template",
templateValue);
TagConfig config = TagConfigFactory.createTagConfig(
this.config, layoutTagConfigId,
FaceletHandlerHelper.getTagAttributes(srcAttr),
nextHandler);
FaceletHandler includeHandler = new DecorateHandler(config);
FaceletHandler handler;
if (FaceletHandlerHelper.isDevModeEnabled(ctx)) {
// decorate handler with dev handler
FaceletHandler devHandler = getDevFaceletHandler(ctx,
helper, config, layoutInstance);
FaceletHandler nextHandler;
if (devHandler == null) {
nextHandler = includeHandler;
} else {
nextHandler = new DevTagHandler(config,
layoutInstance.getName(), includeHandler,
devHandler);
}
handler = helper.getAliasTagHandler(layoutTagConfigId,
vars, blockedPatterns, nextHandler);
} else {
handler = helper.getAliasTagHandler(layoutTagConfigId,
vars, blockedPatterns, includeHandler);
}
// apply
handler.apply(ctx, parent);
} else {
String errMsg = String.format(
"Missing template property for layout '%s'",
layoutInstance.getName());
applyErrorHandler(ctx, parent, helper, errMsg);
}
}
}
protected Map<String, ValueExpression> getVariablesForLayoutBuild(
FaceletContext ctx, String modeValue) {
Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
ValueExpression valueExpr = value.getValueExpression(ctx, Object.class);
vars.put(RenderVariables.globalVariables.value.name(), valueExpr);
// vars.put(RenderVariables.globalVariables.document.name(),
// valueExpr);
vars.put(RenderVariables.globalVariables.layoutValue.name(), valueExpr);
ExpressionFactory eFactory = ctx.getExpressionFactory();
ValueExpression modeVe = eFactory.createValueExpression(modeValue,
String.class);
vars.put(RenderVariables.globalVariables.layoutMode.name(), modeVe);
// mode as alias to layoutMode
vars.put(RenderVariables.globalVariables.mode.name(), modeVe);
return vars;
}
/**
* Computes variables for rendering, making available the layout instance
* and its properties to the context.
*/
protected Map<String, ValueExpression> getVariablesForLayoutRendering(
FaceletContext ctx, WebLayoutManager layoutService,
Layout layoutInstance) {
Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
ExpressionFactory eFactory = ctx.getExpressionFactory();
// expose layout value
ValueExpression layoutVe = eFactory.createValueExpression(
layoutInstance, Layout.class);
vars.put(RenderVariables.layoutVariables.layout.name(), layoutVe);
// expose layout properties too
for (Map.Entry<String, Serializable> prop : layoutInstance.getProperties().entrySet()) {
String key = prop.getKey();
String name = String.format("%s_%s",
RenderVariables.layoutVariables.layoutProperty.name(), key);
String value;
Serializable valueInstance = prop.getValue();
if (!layoutService.referencePropertyAsExpression(key,
valueInstance, null, null, null, null)) {
// FIXME: this will not be updated correctly using ajax
value = (String) valueInstance;
} else {
// create a reference so that it's a real expression and it's
// not kept (cached) in a component value on ajax refresh
value = String.format("#{%s.properties.%s}",
RenderVariables.layoutVariables.layout.name(), key);
}
vars.put(name,
eFactory.createValueExpression(ctx, value, Object.class));
}
return vars;
}
protected void applyErrorHandler(FaceletContext ctx, UIComponent parent,
FaceletHandlerHelper helper, String message) throws IOException {
log.error(message);
ComponentHandler output = helper.getErrorComponentHandler(null, message);
output.apply(ctx, parent);
}
protected FaceletHandler getDevFaceletHandler(FaceletContext ctx,
FaceletHandlerHelper helper, TagConfig config, Layout layout) {
if (StringUtils.isBlank(layout.getDevTemplate())) {
return null;
}
// use the default dev handler for widget types
TagAttribute attr = helper.createAttribute(
"layout",
String.format("#{%s}",
RenderVariables.layoutVariables.layout.name()));
TagAttributes devWidgetAttributes = FaceletHandlerHelper.getTagAttributes(attr);
TagConfig devWidgetConfig = TagConfigFactory.createTagConfig(config,
layout.getTagConfigId(), devWidgetAttributes,
new LeafFaceletHandler());
return new LayoutDevTagHandler(devWidgetConfig);
}
}