/*
* (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: FaceletHandlerHelper.java 30553 2008-02-24 15:51:31Z atchertchian $
*/
package org.nuxeo.ecm.platform.forms.layout.facelets;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.component.html.HtmlMessage;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.validator.Validator;
import javax.faces.view.facelets.ComponentConfig;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.ConverterConfig;
import javax.faces.view.facelets.ConverterHandler;
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.ValidatorConfig;
import javax.faces.view.facelets.ValidatorHandler;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.platform.forms.layout.actions.NuxeoLayoutManagerBean;
import org.nuxeo.ecm.platform.forms.layout.api.FieldDefinition;
import org.nuxeo.ecm.platform.forms.layout.api.Widget;
import org.nuxeo.ecm.platform.forms.layout.api.WidgetSelectOption;
import org.nuxeo.ecm.platform.forms.layout.api.WidgetSelectOptions;
import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
import org.nuxeo.ecm.platform.ui.web.binding.alias.AliasTagHandler;
import org.nuxeo.ecm.platform.ui.web.tag.fn.Functions;
import org.nuxeo.ecm.platform.ui.web.tag.handler.GenericHtmlComponentHandler;
import org.nuxeo.ecm.platform.ui.web.tag.handler.SetTagHandler;
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.tag.TagAttributeImpl;
import com.sun.faces.facelets.tag.TagAttributesImpl;
/**
* Helpers for layout/widget handlers.
* <p>
* Helps generating custom tag handlers and custom tag attributes.
*
* @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
*/
public final class FaceletHandlerHelper {
private static final Log log = LogFactory.getLog(FaceletHandlerHelper.class);
public static final String LAYOUT_ID_PREFIX = "nxl_";
public static final String WIDGET_ID_PREFIX = "nxw_";
public static final String MESSAGE_ID_SUFFIX = "_message";
/**
* @since 6.0
*/
public static final String DEV_CONTAINER_ID_SUFFIX = "_dev_container";
/**
* @since 6.0
*/
public static final String DEV_REGION_ID_SUFFIX = "_dev_region";
/**
* @since 6.0
*/
public static String DEV_MODE_DISABLED_VARIABLE = "nuxeoLayoutDevModeDisabled";
static final String LAYOUT_ID_COUNTERS = "org.nuxeo.ecm.platform.layouts.LAYOUT_ID_COUNTERS";
private static final Pattern UNIQUE_ID_STRIP_PATTERN = Pattern.compile("(.*)(_[0-9]+)");
/**
* @since 5.7
*/
public static final String DIR_PROPERTY = "dir";
/**
* @since 5.7
*/
public static final String DIR_AUTO = "auto";
final FaceletContext context;
final TagConfig tagConfig;
public FaceletHandlerHelper(FaceletContext context, TagConfig tagConfig) {
this.context = context;
this.tagConfig = tagConfig;
}
/**
* Returns a id unique within the facelet context.
*/
public String generateUniqueId() {
String id;
TagAttribute idAttr = tagConfig.getTag().getAttributes().get("id");
if (idAttr != null) {
id = idAttr.getValue(context);
} else {
id = context.getFacesContext().getViewRoot().createUniqueId();
}
return generateUniqueId(id);
}
/**
* Returns a id unique within the facelet context using given id as base.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public String generateUniqueId(String base) {
Map<String, Object> requestMap = context.getFacesContext().getExternalContext().getRequestMap();
Map<String, Integer> counters = (Map) requestMap.get(LAYOUT_ID_COUNTERS);
if (counters == null) {
counters = new HashMap<String, Integer>();
}
String generatedId = generateUniqueId(generateValidIdString(base),
counters);
requestMap.put(LAYOUT_ID_COUNTERS, counters);
return generatedId;
}
/**
* Strips given base of any ending counter that would conflict with
* potential already generated unique ids
*
* @since 5.7
*/
protected static String stripUniqueIdBase(String base) {
if (base != null) {
Matcher m = UNIQUE_ID_STRIP_PATTERN.matcher(base);
if (m.matches()) {
base = m.group(1);
return stripUniqueIdBase(base);
}
}
return base;
}
/**
* Generates a unique id from counters persisted in given map
*
* @since 5.7
*/
public static String generateUniqueId(String base,
Map<String, Integer> counters) {
// strip base of any remnant counter name
base = stripUniqueIdBase(base);
// increment in map
Integer cnt = counters.get(base);
if (cnt == null) {
counters.put(base, new Integer(0));
return base;
} else {
int i = cnt.intValue() + 1;
counters.put(base, new Integer(i));
return base + "_" + i;
}
}
/**
* @throws IllegalArgumentException if the given string is null or empty.
*/
protected static String generateValidIdString(String base) {
if (base == null) {
throw new IllegalArgumentException(base);
}
int n = base.length();
if (n < 1) {
throw new IllegalArgumentException(base);
}
return Functions.jsfTagIdEscape(base);
}
public String generateWidgetId(String widgetName) {
return generateUniqueId(WIDGET_ID_PREFIX + widgetName);
}
public String generateLayoutId(String layoutName) {
return generateUniqueId(LAYOUT_ID_PREFIX + layoutName);
}
public String generateMessageId(String widgetName) {
return generateUniqueId(WIDGET_ID_PREFIX + widgetName
+ MESSAGE_ID_SUFFIX);
}
/**
* @since 6.0
*/
public String generateDevRegionId(String widgetName) {
return generateUniqueId(WIDGET_ID_PREFIX + widgetName
+ DEV_REGION_ID_SUFFIX);
}
/**
* @since 6.0
*/
public String generateDevContainerId(String widgetName) {
return generateUniqueId(WIDGET_ID_PREFIX + widgetName
+ DEV_CONTAINER_ID_SUFFIX);
}
/**
* Creates a unique id and returns corresponding attribute, using given
* string id as base.
*/
public TagAttribute createIdAttribute(String base) {
String value = generateUniqueId(base);
return new TagAttributeImpl(tagConfig.getTag().getLocation(), "", "id",
"id", value);
}
/**
* Creates an attribute with given name and value.
* <p>
* The attribute namespace is assumed to be empty.
*/
public TagAttribute createAttribute(String name, String value) {
if (value == null || value instanceof String) {
return new TagAttributeImpl(tagConfig.getTag().getLocation(), "",
name, name, value);
}
return null;
}
/**
* Returns true if a reference tag attribute should be created for given
* property value.
* <p>
* Reference tag attributes are using a non-literal EL expression so that
* this property value is not kept (cached) in the component on ajax
* refresh.
* <p>
* Of course property values already representing an expression cannot be
* mapped as is because they would need to be resolved twice.
* <p>
* Converters and validators cannot be referenced either because components
* expect corresponding value expressions to resolve to a {@link Converter}
* or {@link Validator} instance (instead of the converter of validator
* id).
*/
public boolean shouldCreateReferenceAttribute(String key, Serializable value) {
// FIXME: NXP-7004: make this configurable per widget type and mode or
// JSF component
if ((value instanceof String)
&& (ComponentTagUtils.isValueReference((String) value)
|| "converter".equals(key) || "validator".equals(key)
// size is mistaken for the properties map size because
// of jboss el resolvers
|| "size".equals(key)
// richfaces calendar does not resolve EL expressions
// correctly
|| "showApplyButton".equals(key) || "defaultTime".equals(key))) {
return false;
}
return true;
}
public static TagAttributes getTagAttributes(TagAttribute... attributes) {
if (attributes == null || attributes.length == 0) {
return new TagAttributesImpl(new TagAttribute[0]);
}
return new TagAttributesImpl(attributes);
}
public static TagAttributes getTagAttributes(List<TagAttribute> attributes) {
return getTagAttributes(attributes.toArray(new TagAttribute[0]));
}
public static TagAttributes addTagAttribute(TagAttributes orig,
TagAttribute newAttr) {
if (orig == null) {
return new TagAttributesImpl(new TagAttribute[] { newAttr });
}
List<TagAttribute> allAttrs = new ArrayList<TagAttribute>(
Arrays.asList(orig.getAll()));
allAttrs.add(newAttr);
return getTagAttributes(allAttrs);
}
/**
* Copies tag attributes with given names from the tag config, using given
* id as base for the id attribute.
*/
public TagAttributes copyTagAttributes(String id, String... names) {
List<TagAttribute> list = new ArrayList<TagAttribute>();
list.add(createIdAttribute(id));
for (String name : names) {
if ("id".equals(name)) {
// ignore
continue;
}
TagAttribute attr = tagConfig.getTag().getAttributes().get(name);
if (attr != null) {
list.add(attr);
}
}
TagAttribute[] attrs = list.toArray(new TagAttribute[list.size()]);
return new TagAttributesImpl(attrs);
}
/**
* Creates tag attributes using given widget properties and field
* definitions.
* <p>
* Assumes the "value" attribute has to be computed from the first field
* definition, using the "value" expression (see widget type tag handler
* exposed values).
*/
public TagAttributes getTagAttributes(String id, Widget widget) {
// add id and value computed from fields
TagAttributes widgetAttrs = getTagAttributes(widget);
return addTagAttribute(widgetAttrs, createAttribute("id", id));
}
public TagAttributes getTagAttributes(Widget widget) {
return getTagAttributes(widget, null, true);
}
/**
* @since 5.5
*/
public TagAttributes getTagAttributes(Widget widget,
List<String> excludedProperties, boolean bindFirstFieldDefinition) {
return getTagAttributes(widget, excludedProperties,
bindFirstFieldDefinition, false);
}
/**
* Return tag attributes for this widget, including value mapping from
* field definitions and properties
*
* @since 5.6
* @param widget the widget to generate tag attributes for
* @param excludedProperties the properties to exclude from tag attributes
* @param bindFirstFieldDefinition if true, the first field definition will
* be bound to the tag attribute named "value"
* @param defaultToValue if true, and there are no field definitions, tag
* attribute named "value" will be mapped to the current widget
* value name (e.g the layout value in most cases, or the parent
* widget value if widget is a sub widget)
*/
public TagAttributes getTagAttributes(Widget widget,
List<String> excludedProperties, boolean bindFirstFieldDefinition,
boolean defaultToValue) {
List<TagAttribute> attrs = new ArrayList<TagAttribute>();
if (bindFirstFieldDefinition) {
FieldDefinition field = null;
FieldDefinition[] fields = widget.getFieldDefinitions();
if (fields != null && fields.length > 0) {
field = fields[0];
}
if (field != null || defaultToValue) {
// bind value to first field definition or current value name
TagAttribute valueAttr = createAttribute(
"value",
ValueExpressionHelper.createExpressionString(
widget.getValueName(), field));
attrs.add(valueAttr);
}
}
// fill with widget properties
List<TagAttribute> propertyAttrs = getTagAttributes(
widget.getProperties(), excludedProperties, true,
widget.getType(), widget.getTypeCategory(), widget.getMode());
if (propertyAttrs != null) {
attrs.addAll(propertyAttrs);
}
return getTagAttributes(attrs);
}
/**
* @since 5.5, signature changed on 5.6 to include parameters widgetType
* and widgetMode.
*/
public List<TagAttribute> getTagAttributes(
Map<String, Serializable> properties,
List<String> excludedProperties, boolean useReferenceProperties,
String widgetType, String widgetTypeCategory, String widgetMode) {
WebLayoutManager service = null;
try {
service = Framework.getService(WebLayoutManager.class);
} catch (Exception e) {
throw new FacesException(e);
}
if (service == null) {
throw new FacesException("Layout service not found");
}
List<TagAttribute> attrs = new ArrayList<TagAttribute>();
if (properties != null) {
for (Map.Entry<String, Serializable> prop : properties.entrySet()) {
TagAttribute attr;
String key = prop.getKey();
if (excludedProperties != null
&& excludedProperties.contains(key)) {
continue;
}
Serializable valueInstance = prop.getValue();
if (!useReferenceProperties
|| !service.referencePropertyAsExpression(key,
valueInstance, widgetType, widgetTypeCategory,
widgetMode, null)) {
if (valueInstance == null
|| valueInstance instanceof String) {
// FIXME: this will not be updated correctly using ajax
attr = createAttribute(key, (String) valueInstance);
} else {
attr = createAttribute(key, valueInstance.toString());
}
} else {
// create a reference so that it's a real expression
// and it's not kept (cached) in a component value on
// ajax refresh
attr = createAttribute(key, String.format(
"#{%s.properties.%s}",
RenderVariables.widgetVariables.widget.name(), key));
}
attrs.add(attr);
}
}
return attrs;
}
/**
* @since 6.0
*/
public TagAttributes getTagAttributes(WidgetSelectOption selectOption,
Map<String, Serializable> additionalProps) {
Map<String, Serializable> props = getSelectOptionProperties(selectOption);
if (additionalProps != null) {
props.putAll(additionalProps);
}
List<TagAttribute> attrs = getTagAttributes(props, null, false, null,
null, null);
if (attrs == null) {
attrs = Collections.emptyList();
}
return getTagAttributes(attrs);
}
public TagAttributes getTagAttributes(WidgetSelectOption selectOption) {
return getTagAttributes(selectOption, null);
}
public Map<String, Serializable> getSelectOptionProperties(
WidgetSelectOption selectOption) {
Map<String, Serializable> map = new HashMap<String, Serializable>();
if (selectOption != null) {
Serializable value = selectOption.getValue();
if (value != null) {
map.put("value", value);
}
String var = selectOption.getVar();
if (var != null) {
map.put("var", var);
}
String itemLabel = selectOption.getItemLabel();
if (itemLabel != null) {
map.put("itemLabel", itemLabel);
}
String itemValue = selectOption.getItemValue();
if (itemValue != null) {
map.put("itemValue", itemValue);
}
Serializable itemDisabled = selectOption.getItemDisabled();
if (itemDisabled != null) {
map.put("itemDisabled", itemDisabled);
}
Serializable itemRendered = selectOption.getItemRendered();
if (itemRendered != null) {
map.put("itemRendered", itemRendered);
}
if (selectOption instanceof WidgetSelectOptions) {
WidgetSelectOptions selectOptions = (WidgetSelectOptions) selectOption;
Boolean caseSensitive = selectOptions.getCaseSensitive();
if (caseSensitive != null) {
map.put("caseSensitive", caseSensitive);
}
String ordering = selectOptions.getOrdering();
if (ordering != null) {
map.put("ordering", ordering);
}
}
}
return map;
}
/**
* @deprecated since 5.4.2, use
* {@link FaceletHandlerHelper#getHtmlComponentHandler(String, TagAttributes, FaceletHandler, String, String)}
* instead.
*/
@Deprecated
public ComponentHandler getHtmlComponentHandler(TagAttributes attributes,
FaceletHandler nextHandler, String componentType,
String rendererType) {
return getHtmlComponentHandler(null, attributes, nextHandler,
componentType, rendererType);
}
/**
* Returns an html component handler for this configuration.
* <p>
* Next handler cannot be null, use {@link LeafFaceletHandler} if no next
* handler is needed.
*/
public ComponentHandler getHtmlComponentHandler(String tagConfigId,
TagAttributes attributes, FaceletHandler nextHandler,
String componentType, String rendererType) {
ComponentConfig config = TagConfigFactory.createComponentConfig(
tagConfig, tagConfigId, attributes, nextHandler, componentType,
rendererType);
return new GenericHtmlComponentHandler(config);
}
/**
* @deprecated since 5.4.2, use
* {@link FaceletHandlerHelper#getErrorComponentHandler(String, String)}
* instead.
*/
@Deprecated
public ComponentHandler getErrorComponentHandler(String errorMessage) {
return getErrorComponentHandler(null, errorMessage);
}
/**
* Component handler that displays an error on interface
*/
public ComponentHandler getErrorComponentHandler(String tagConfigId,
String errorMessage) {
FaceletHandler leaf = new LeafFaceletHandler();
TagAttribute valueAttr = createAttribute("value",
"<span style=\"color:red;font-weight:bold;\">ERROR: "
+ errorMessage + "</span><br />");
TagAttribute escapeAttr = createAttribute("escape", "false");
ComponentHandler output = getHtmlComponentHandler(tagConfigId,
FaceletHandlerHelper.getTagAttributes(valueAttr, escapeAttr),
leaf, HtmlOutputText.COMPONENT_TYPE, null);
return output;
}
/**
* @deprecated since 5.4.2, use
* {@link FaceletHandlerHelper#getConvertHandler(String, TagAttributes, FaceletHandler, String)}
* instead.
*/
@Deprecated
public ConverterHandler getConvertHandler(TagAttributes attributes,
FaceletHandler nextHandler, String converterId) {
return getConvertHandler(null, attributes, nextHandler, converterId);
}
/**
* Returns a convert handler for this configuration.
* <p>
* Next handler cannot be null, use {@link LeafFaceletHandler} if no next
* handler is needed.
*/
public ConverterHandler getConvertHandler(String tagConfigId,
TagAttributes attributes, FaceletHandler nextHandler,
String converterId) {
ConverterConfig config = TagConfigFactory.createConverterConfig(
tagConfig, tagConfigId, attributes, nextHandler, converterId);
return new ConverterHandler(config);
}
/**
* @deprecated since 5.4.2, use
* {@link FaceletHandlerHelper#getValidateHandler(String, TagAttributes, FaceletHandler, String)}
* instead.
*/
@Deprecated
public ValidatorHandler getValidateHandler(TagAttributes attributes,
FaceletHandler nextHandler, String validatorId) {
return getValidateHandler(null, attributes, nextHandler, validatorId);
}
/**
* Returns a validate handler for this configuration.
* <p>
* Next handler cannot be null, use {@link LeafFaceletHandler} if no next
* handler is needed.
*/
public ValidatorHandler getValidateHandler(String tagConfigId,
TagAttributes attributes, FaceletHandler nextHandler,
String validatorId) {
ValidatorConfig config = TagConfigFactory.createValidatorConfig(
tagConfig, tagConfigId, attributes, nextHandler, validatorId);
return new ValidatorHandler(config);
}
/**
* @deprecated since 5.4.2, use
* {@link FaceletHandlerHelper#getMessageComponentHandler(String, String, String, String)}
* instead.
*/
@Deprecated
public ComponentHandler getMessageComponentHandler(String id, String forId,
String styleClass) {
return getMessageComponentHandler(null, id, forId, styleClass);
}
/**
* Returns a message component handler with given attributes.
* <p>
* Uses component type "javax.faces.HtmlMessage" and renderer type
* "javax.faces.Message".
*/
public ComponentHandler getMessageComponentHandler(String tagConfigId,
String id, String forId, String styleClass) {
TagAttribute forAttr = createAttribute("for", forId);
TagAttribute idAttr = createAttribute("id", id);
if (styleClass == null) {
// default style class
styleClass = "errorMessage";
}
TagAttribute styleAttr = createAttribute("styleClass", styleClass);
TagAttributes attributes = getTagAttributes(forAttr, idAttr, styleAttr);
ComponentConfig config = TagConfigFactory.createComponentConfig(
tagConfig, tagConfigId, attributes, new LeafFaceletHandler(),
HtmlMessage.COMPONENT_TYPE, null);
return new ComponentHandler(config);
}
/**
* @since 5.6
*/
public FaceletHandler getAliasTagHandler(String tagConfigId,
Map<String, ValueExpression> variables,
List<String> blockedPatterns, FaceletHandler nextHandler) {
FaceletHandler currentHandler = nextHandler;
if (variables != null) {
// XXX also set id? cache? anchor?
ComponentConfig config = TagConfigFactory.createAliasTagConfig(
tagConfig, tagConfigId, getTagAttributes(), nextHandler);
currentHandler = new AliasTagHandler(config, variables,
blockedPatterns);
}
return currentHandler;
}
/**
* @since 6.0
*/
public static boolean isDevModeEnabled(FaceletContext ctx) {
// avoid stack overflow when using layout tags within the dev
// handler
if (Framework.isDevModeSet()) {
NuxeoLayoutManagerBean bean = lookupBean(ctx.getFacesContext());
if (bean.isDevModeSet()) {
ExpressionFactory eFactory = ctx.getExpressionFactory();
ValueExpression disableDevAttr = eFactory.createValueExpression(
ctx,
String.format("#{%s}", DEV_MODE_DISABLED_VARIABLE),
Boolean.class);
if (!Boolean.TRUE.equals(disableDevAttr.getValue(ctx))) {
return true;
}
}
}
return false;
}
protected static NuxeoLayoutManagerBean lookupBean(FacesContext ctx) {
String expr = "#{" + NuxeoLayoutManagerBean.NAME + "}";
NuxeoLayoutManagerBean bean = (NuxeoLayoutManagerBean) ctx.getApplication().evaluateExpressionGet(
ctx, expr, Object.class);
if (bean == null) {
log.error("Managed bean not found: " + expr);
return null;
}
return bean;
}
/**
* @since 6.0
*/
public FaceletHandler getDisableDevModeTagHandler(String tagConfigId,
FaceletHandler nextHandler) {
ComponentConfig config = TagConfigFactory.createAliasTagConfig(
tagConfig, tagConfigId, DEV_MODE_DISABLED_VARIABLE, "true",
"true", "false", nextHandler);
return new SetTagHandler(config);
}
}