/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
*/
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.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.TagHandler;
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 org.nuxeo.runtime.services.config.ConfigurationService;
import com.sun.faces.facelets.tag.TagAttributeImpl;
import com.sun.faces.facelets.tag.TagAttributesImpl;
import com.sun.faces.facelets.tag.ui.ComponentRef;
import com.sun.faces.facelets.tag.ui.ComponentRefHandler;
/**
* 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";
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 TagConfig tagConfig;
public FaceletHandlerHelper(TagConfig tagConfig) {
this.tagConfig = tagConfig;
}
/**
* Returns a id unique within the facelet context.
*/
public String generateUniqueId(FaceletContext context) {
String id;
TagAttribute idAttr = tagConfig.getTag().getAttributes().get("id");
if (idAttr != null) {
id = idAttr.getValue(context);
} else {
id = context.getFacesContext().getViewRoot().createUniqueId();
}
return generateUniqueId(context, id);
}
/**
* Returns a id unique within the facelet context using given id as base.
*/
public static String generateUniqueId(FaceletContext context, String base) {
return generateUniqueId(context.getFacesContext(), base);
}
/**
* Returns a id unique within the faces context using given id as base.
*
* @since 8.1
*/
public static String generateUniqueId(FacesContext faces, String base) {
NuxeoLayoutIdManagerBean bean = lookupIdBean(faces);
return bean.generateUniqueId(base);
}
protected static NuxeoLayoutIdManagerBean lookupIdBean(FacesContext ctx) {
String expr = "#{" + NuxeoLayoutIdManagerBean.NAME + "}";
NuxeoLayoutIdManagerBean bean = (NuxeoLayoutIdManagerBean) ctx.getApplication().evaluateExpressionGet(ctx, expr,
Object.class);
if (bean == null) {
throw new RuntimeException("Managed bean not found: " + expr);
}
return bean;
}
/**
* 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;
}
/**
* @throws IllegalArgumentException if the given string is null or empty.
*/
protected static String generateValidIdString(String base) {
if (base == null) {
throw new IllegalArgumentException("Parameter base is null");
}
int n = base.length();
if (n < 1) {
throw new IllegalArgumentException(base);
}
return Functions.jsfTagIdEscape(base);
}
public static String generateWidgetId(FaceletContext context, String widgetName) {
return generateUniqueId(context, WIDGET_ID_PREFIX + widgetName);
}
public static String generateLayoutId(FaceletContext context, String layoutName) {
return generateUniqueId(context, LAYOUT_ID_PREFIX + layoutName);
}
public static String generateMessageId(FaceletContext context, String widgetName) {
return generateUniqueId(context, WIDGET_ID_PREFIX + widgetName + MESSAGE_ID_SUFFIX);
}
/**
* @since 6.0
*/
public static String generateDevRegionId(FaceletContext context, String widgetName) {
return generateUniqueId(context, WIDGET_ID_PREFIX + widgetName + DEV_REGION_ID_SUFFIX);
}
/**
* @since 6.0
*/
public static String generateDevContainerId(FaceletContext context, String widgetName) {
return generateUniqueId(context, 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(FaceletContext context, String base) {
String value = generateUniqueId(context, 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) {
return new TagAttributeImpl(tagConfig.getTag().getLocation(), "", name, name, value);
}
/**
* 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<>(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(FaceletContext context, String id, String... names) {
List<TagAttribute> list = new ArrayList<>();
list.add(createIdAttribute(context, 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<>();
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 = Framework.getService(WebLayoutManager.class);
List<TagAttribute> attrs = new ArrayList<>();
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,
"#{" + RenderVariables.widgetVariables.widget.name() + ".properties." + 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<>();
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;
}
/**
* Returns an html component handler for this configuration.
* <p>
* Next handler cannot be null, use {@link org.nuxeo.ecm.platform.ui.web.tag.handler.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);
}
/**
* Component handler that displays an error on interface
*/
public ComponentHandler getErrorComponentHandler(String tagConfigId, String errorMessage) {
FaceletHandler leaf = new org.nuxeo.ecm.platform.ui.web.tag.handler.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;
}
/**
* Returns a convert handler for this configuration.
* <p>
* Next handler cannot be null, use {@link org.nuxeo.ecm.platform.ui.web.tag.handler.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);
}
/**
* Returns a validate handler for this configuration.
* <p>
* Next handler cannot be null, use {@link org.nuxeo.ecm.platform.ui.web.tag.handler.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);
}
/**
* 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 org.nuxeo.ecm.platform.ui.web.tag.handler.LeafFaceletHandler(), HtmlMessage.COMPONENT_TYPE, null);
return new ComponentHandler(config);
}
/**
* @since 5.6
*/
public FaceletHandler getAliasFaceletHandler(String tagConfigId, Map<String, ValueExpression> variables,
List<String> blockedPatterns, FaceletHandler nextHandler) {
FaceletHandler currentHandler = nextHandler;
if (variables != null) {
currentHandler = getBareAliasTagHandler(tagConfigId, variables, blockedPatterns, nextHandler);
}
return currentHandler;
}
/**
* @since 8.1
*/
public TagHandler getAliasTagHandler(String tagConfigId, Map<String, ValueExpression> variables,
List<String> blockedPatterns, TagHandler nextHandler) {
TagHandler currentHandler = nextHandler;
if (variables != null) {
currentHandler = getBareAliasTagHandler(tagConfigId, variables, blockedPatterns, nextHandler);
}
return currentHandler;
}
protected TagHandler getBareAliasTagHandler(String tagConfigId, Map<String, ValueExpression> variables,
List<String> blockedPatterns, FaceletHandler nextHandler) {
// XXX also set id? cache? anchor?
ComponentConfig config = TagConfigFactory.createAliasTagConfig(tagConfig, tagConfigId, getTagAttributes(),
nextHandler);
AliasTagHandler alias = new AliasTagHandler(config, variables, blockedPatterns);
// NXP-18639: always wrap next alias handler in a component ref for tagConfigId to be taken into account and
// anchored in the view with this id.
ComponentConfig ref = TagConfigFactory.createComponentConfig(tagConfig, tagConfigId, getTagAttributes(), alias,
ComponentRef.COMPONENT_TYPE, null);
return new ComponentRefHandler(ref);
}
/**
* @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,
"#{" + 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) {
TagAttribute[] attrs = new TagAttribute[4];
attrs[0] = createAttribute("var", DEV_MODE_DISABLED_VARIABLE);
attrs[1] = createAttribute("value", "true");
attrs[2] = createAttribute("cache", "true");
attrs[3] = createAttribute("blockMerge", "true");
TagAttributes attributes = new TagAttributesImpl(attrs);
ComponentConfig config = TagConfigFactory.createAliasTagConfig(tagConfig, tagConfigId, attributes, nextHandler);
return new SetTagHandler(config);
}
/**
* @since 8.2
*/
public static boolean isAliasOptimEnabled() {
ConfigurationService cs = Framework.getService(ConfigurationService.class);
return !cs.isBooleanPropertyTrue("nuxeo.jsf.layout.removeAliasOptims");
}
}