/*
* JBoss, Home of Professional Open Source
* Copyright 2010-2016, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.richfaces.tests.metamer;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.el.ELContext;
import javax.el.ExpressionFactory;
import javax.el.MethodExpression;
import javax.faces.FacesException;
import javax.faces.bean.ManagedBean;
import javax.faces.component.UIComponent;
import javax.faces.component.behavior.BehaviorBase;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.faces.event.AjaxBehaviorEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.SelectItem;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.richfaces.component.UIStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Lists;
/**
* Representation of all attributes of a JSF component or behavior.
*
* @author <a href="mailto:ppitonak@redhat.com">Pavol Pitonak</a>
*/
public final class Attributes implements Map<String, Attribute>, Serializable {
private static final long serialVersionUID = -1L;
private static Logger logger = LoggerFactory.getLogger(Attributes.class);
private static Map<Class<?>, List<Attribute>> richfacesAttributes;
private static final String RICH_BEAN_NAME = "richBean";
private static final String ACTION_LISTENER = "actionListener";
private static final String LISTENER = "listener";
private static final String VALUE_CHANGE_LISTENER = "valueChangeListener";
private final Class[] actionEventClassArray = new Class[] { ActionEvent.class };
private final Class[] ajaxBehaviorEventClassArray = new Class[] { AjaxBehaviorEvent.class };
private final Class[] valueChangeEventClassArray = new Class[] { ValueChangeEvent.class };
// K - name of a component attribute, V - value of the component attribute
private Map<String, Attribute> attributes;
// class object of managed bean
private Class<?> beanClass;
/**
* Constructor for empty Attributes.
*
* @param beanClass class object of a managed bean
*/
private Attributes(Class<?> beanClass) {
this.beanClass = beanClass;
attributes = new TreeMap<String, Attribute>();
}
/**
* Constructor for class Attributes.
*
* @param componentClass class object of a JSF component whose attributes will be stored
* @param beanClass class object of a managed bean
*/
private Attributes(Class<?> componentClass, Class<?> beanClass, boolean loadFromClass) {
this.beanClass = beanClass;
logger.debug("creating attributes map for " + componentClass);
if (!loadFromClass && richfacesAttributes == null) {
// load all components (using regex)
loadRichFacesComponents();
}
if (!loadFromClass && richfacesAttributes.containsKey(componentClass)) {
logger.debug("retrieving attributes of " + componentClass.getName() + " from faces-config.xml");
if (attributes == null) {
attributes = new TreeMap<String, Attribute>();
}
for (Attribute a : richfacesAttributes.get(componentClass)) {
Attribute newAttr = new Attribute(a);
attributes.put(newAttr.getName(), newAttr);
}
} else if (!loadFromClass && !richfacesAttributes.containsKey(componentClass)) {
throw new FacesException("Componnent " + componentClass.getName() + " is not included in faces-config.xml.");
} else {
logger.debug("retrieving attributes of " + componentClass.getName() + " from class descriptor");
loadAttributesFromClass(componentClass);
}
logger.debug(attributes.keySet().toString());
loadHelp();
loadSelectOptions();
}
/**
* Constructor for empty class Attributes.
*
* @param componentClass class object of a JSF component whose attributes will be stored
* @param beanClass class object of a managed bean
*/
private Attributes(Class<?> componentClass, Class<?> beanClass) {
logger.debug("creating attributes map for " + componentClass);
this.beanClass = beanClass;
attributes = new TreeMap<String, Attribute>();
}
/**
* Factory method for creating instances of class Attributes. Attributes are loaded from faces-config.xml.
*
* @param clazz class object of a JSF component whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getComponentAttributesFromFacesConfig(Class<? extends UIComponent> clazz, Class<?> beanClass) {
return new Attributes(clazz, beanClass, false);
}
/**
* Factory method for creating instances of class Attributes. Attributes are loaded from class.
*
* @param interfaze general class object whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getAttributesFromClass(Class<?> interfaze, Class<?> beanClass) {
return new Attributes(interfaze, beanClass, true);
}
/**
* Factory method for creating instances of class Attributes. Attributes are loaded from class.
*
* @param clazz class object of a JSF component whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getComponentAttributesFromClass(Class<? extends UIComponent> clazz, Class<?> beanClass) {
return new Attributes(clazz, beanClass, true);
}
/**
* Factory method for creating instances of class Attributes. Attributes are loaded from faces-config.xml.
*
* @param clazz class object of a JSF behavior whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getBehaviorAttributesFromFacesConfig(Class<? extends BehaviorBase> clazz, Class<?> beanClass) {
return new Attributes(clazz, beanClass, false);
}
/**
* Factory method for creating instances of class Attributes. Attributes are loaded from class.
*
* @param clazz class object of a JSF behavior whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getBehaviorAttributesFromClass(Class<? extends BehaviorBase> clazz, Class<?> beanClass) {
return new Attributes(clazz, beanClass, true);
}
/**
* Factory method for creating empty instance of class Attributes. Needs to be filled with attributes explicitly.
*
* @param beanClass class object of a managed bean
*/
public static Attributes getEmptyAttributes(Class<?> beanClass) {
return new Attributes(beanClass);
}
/**
* Factory method for creating instances of class Attributes.
*
* @param componentClass class object of a JSF behavior whose attributes will be stored
* @param beanClass class object of a managed bean
*/
public static Attributes getFaceletsComponentAttributes(String componentClass, Class<?> beanClass) {
Class<?> faceletsClass = null;
try {
faceletsClass = Class.forName(componentClass);
} catch (ClassNotFoundException cnfe1) {
try {
if (componentClass.startsWith("com.sun.faces.facelets")) {
faceletsClass = Class.forName(componentClass.replace("com.sun.faces.facelets",
"org.apache.myfaces.view.facelets"));
} else {
faceletsClass = Class.forName(componentClass.replace("org.apache.myfaces.view.facelets",
"com.sun.faces.facelets"));
}
} catch (ClassNotFoundException cnfe2) {
logger.error(cnfe2.getMessage());
}
}
return new Attributes(faceletsClass, beanClass, true);
}
/**
* Loads help strings from property file.
*/
private void loadHelp() {
ResourceBundle rb;
try {
rb = ResourceBundle.getBundle(beanClass.getName());
} catch (MissingResourceException mre) {
return;
}
Enumeration<String> keys = rb.getKeys();
String key;
Attribute attribute;
while (keys.hasMoreElements()) {
key = keys.nextElement();
if (key.startsWith("testapp.help.")) {
attribute = attributes.get(key.replaceFirst("testapp.help.", ""));
if (attribute != null) {
attribute.setHelp(rb.getString(key));
}
}
}
}
/**
* Loads select options used on the page for selecting attribute value.
*
* @return map where key is attribute's name and value is list of select items usable to select attribute value
*/
private void loadSelectOptions() {
ResourceBundle rb;
try {
rb = ResourceBundle.getBundle(beanClass.getName());
} catch (MissingResourceException mre) {
return;
}
Enumeration<String> keys = rb.getKeys();
String key;
// e.g. attr.action.toUpperCaseAction
Pattern pattern = Pattern.compile("(.*)\\.(.*)\\.(.*)");
Matcher matcher;
SelectItem item;
Attribute attribute;
while (keys.hasMoreElements()) {
key = keys.nextElement();
if (key.startsWith("attr.")) {
matcher = pattern.matcher(key);
matcher.find();
attribute = attributes.get(matcher.group(2));
if (attribute == null) {
continue;
}
if (attribute.getSelectOptions() == null) {
attribute.setSelectOptions(new ArrayList<SelectItem>());
}
item = new SelectItem(rb.getString(key), matcher.group(3));
attribute.getSelectOptions().add(item);
}
}
// sort all select options
for (String aKey : attributes.keySet()) {
List<SelectItem> selectOptions = attributes.get(aKey).getSelectOptions();
if (selectOptions != null) {
Collections.sort(selectOptions, new SelectItemComparator());
}
}
}
/**
* {@inheritDoc}
*/
public void clear() {
attributes.clear();
}
public boolean containsKey(String key) {
return attributes.containsKey(key);
}
public boolean containsValue(String value) {
return attributes.containsKey(value);
}
/**
* {@inheritDoc}
*/
public Set<Map.Entry<String, Attribute>> entrySet() {
return attributes.entrySet();
}
public Class<?> getBeanClass() {
return beanClass;
}
public Attribute get(String key) {
return attributes.get(key);
}
/**
* {@inheritDoc}
*/
public boolean isEmpty() {
return attributes.isEmpty();
}
/**
* {@inheritDoc}
*/
public Set<String> keySet() {
return attributes.keySet();
}
/**
* {@inheritDoc}
*/
public Attribute put(String key, Attribute value) {
return attributes.put(key, value);
}
/**
* {@inheritDoc}
*/
public void putAll(Map<? extends String, ? extends Attribute> m) {
attributes.putAll(m);
}
/**
* {@inheritDoc}
*/
public Attribute remove(Object key) {
return attributes.remove((String) key);
}
/**
* {@inheritDoc}
*/
public int size() {
return attributes.size();
}
/**
* {@inheritDoc}
*/
public Collection<Attribute> values() {
return attributes.values();
}
/**
* Getter for exclude set.
*
* @return set containing all attributes of a JSF component that cannot/shouldn't be set on page.
*/
private Set<String> getExcludeSet() {
Set<String> set = new HashSet<String>();
set.add("actionExpression");
set.add("actionListeners");
set.add("attributes");
set.add("children");
set.add("childCount");
set.add("class");
set.add("clientBehaviors");
set.add("clientId");
set.add("defaultEventName");
set.add("eventNames");
set.add("facetCount");
set.add("facets");
set.add("facetsAndChildren");
set.add("family");
set.add("hints");
set.add("id");
set.add("inView");
set.add("itemChangeListeners");
set.add("localValue");
set.add("localValueSet");
set.add("namingContainer");
set.add("parent");
set.add("rendererType");
set.add("rendersChildren");
set.add("resourceBundleMap");
set.add("stateHelper");
set.add("transient");
set.add("transientStateHelper");
set.add("valueChangeListeners");
return set;
}
/**
* Determines whether given object represents an EL expression, e.g. #{bean.property}.
*
* @param value value of a property of tested JSF component
* @return true if object is a string representing an expression, e.g. #{bean.property}, false otherwise
*/
private boolean isStringEL(Object value) {
if (!(value instanceof String)) {
return false;
}
return ((String) value).matches("#\\{.*\\}");
}
/**
* An action for tested JSF component. Can be modified dynamically.
*
* @return outcome of an action or null if no navigation should be performed
*/
public String action() {
ELContext elContext = FacesContext.getCurrentInstance().getELContext();
MethodExpression method;
if (attributes.get("action") == null) {
return null;
}
String outcome = (String) attributes.get("action").getValue();
if (outcome == null) {
return null;
}
// if no select options for "action" are defined in property file and it is an EL expression
if (!hasSelectOptions("action") && isStringEL(outcome)) {
method = getExpressionFactory().createMethodExpression(elContext, outcome, String.class, new Class[0]);
return (String) method.invoke(elContext, null);
}
// if select options for "action" are defined in property file
if (hasSelectOptions("action")) {
method = getExpressionFactory().createMethodExpression(elContext, getMethodEL(outcome), String.class, new Class[0]);
return (String) method.invoke(elContext, null);
}
return outcome;
}
/**
* An action listener for tested JSF component. Can be modified dynamically.
*
* @param event event representing the activation of a user interface component
*/
public void actionListener(ActionEvent event) {
ELContext elContext = FacesContext.getCurrentInstance().getELContext();
MethodExpression method;
if (attributes.get(ACTION_LISTENER) == null) {
return;
}
String listener = (String) attributes.get(ACTION_LISTENER).getValue();
if (listener == null) {
return;
}
// if no select options for "actionListener" are defined in property file and it is an EL expression
if (!hasSelectOptions(ACTION_LISTENER) && isStringEL(listener)) {
method = getExpressionFactory().createMethodExpression(elContext, listener, void.class, actionEventClassArray);
method.invoke(elContext, new Object[] { event });
}
// if select options for "actionListener" are defined in property file
if (hasSelectOptions(ACTION_LISTENER)) {
method = getExpressionFactory().createMethodExpression(elContext, getMethodEL(listener), void.class, actionEventClassArray);
method.invoke(elContext, new Object[] { event });
}
}
/**
* An action listener for tested JSF component. Can be modified dynamically.
*
* @param event event representing the activation of a user interface component
*/
public void listener(AjaxBehaviorEvent event) {
ELContext elContext = FacesContext.getCurrentInstance().getELContext();
MethodExpression method;
if (attributes.get(LISTENER) == null) {
return;
}
String listener = (String) attributes.get(LISTENER).getValue();
if (listener == null) {
return;
}
// if no select options for "listener" are defined in property file and it is an EL expression
if (!hasSelectOptions(LISTENER) && isStringEL(listener)) {
method = getExpressionFactory().createMethodExpression(elContext, listener, void.class, ajaxBehaviorEventClassArray);
method.invoke(elContext, new Object[] { event });
}
// if select options for "listener" are defined in property file
if (hasSelectOptions(LISTENER)) {
method = getExpressionFactory().createMethodExpression(elContext, getMethodEL(listener), void.class, ajaxBehaviorEventClassArray);
method.invoke(elContext, new Object[] { event });
}
}
/**
* A value change listener for tested JSF component. Can be modified dynamically.
*
* @param event event representing the activation of a user interface component
*/
public void valueChangeListener(ValueChangeEvent event) {
ELContext elContext = FacesContext.getCurrentInstance().getELContext();
MethodExpression method;
if (attributes.get(VALUE_CHANGE_LISTENER) == null) {
return;
}
String listener = (String) attributes.get(VALUE_CHANGE_LISTENER).getValue();
if (listener == null) {
return;
}
// if no select options for "valueChangeListener" are defined in property file and it is an EL expression
if (!hasSelectOptions(VALUE_CHANGE_LISTENER) && isStringEL(listener)) {
method = getExpressionFactory().createMethodExpression(elContext, listener, void.class, valueChangeEventClassArray);
method.invoke(elContext, new Object[] { event });
}
// if select options for "valueChangeListener" are defined in property file
if (hasSelectOptions(VALUE_CHANGE_LISTENER)) {
method = getExpressionFactory().createMethodExpression(elContext, getMethodEL(listener), void.class, valueChangeEventClassArray);
method.invoke(elContext, new Object[] { event });
}
}
/**
* Method used for creating EL expressions for methods.
*
* @param methodName name of the action or action listener, e.g. toUpperCaseAction
* @return string containing an expression for an action or action listener, e.g. #{bean.toUpperCaseAction}
*/
private String getMethodEL(String methodName) {
String beanName = RICH_BEAN_NAME;
// check if bean has such method, if not, invoke method over richBean
for (Method declaredMethod : beanClass.getDeclaredMethods()) {
if (declaredMethod.getName().equals(methodName)) {
// get name of the managed bean
beanName = beanClass.getAnnotation(ManagedBean.class).name();
if ("".equals(beanName)) {
// create name of a managed bean according to standard, i.e. MyBean -> myBean
beanName = beanClass.getSimpleName().substring(0, 1).toLowerCase() + beanClass.getSimpleName().substring(1);
}
break;
}
}
return new StringBuilder("#{")
.append(beanName)
.append(".")
.append(methodName)
.append("}").toString();
}
/**
* Decides if there are any select options for given attribute. If true, radio buttons should be rendered on a page.
*
* @param attributeName name of a component attribute
* @return true if select options were defined, false otherwise
*/
public boolean hasSelectOptions(String attributeName) {
List<SelectItem> options = attributes.get(attributeName).getSelectOptions();
return options != null && !options.isEmpty();
}
public boolean containsKey(Object key) {
return attributes.containsKey((String) key);
}
public boolean containsValue(Object value) {
return attributes.containsValue((Attribute) value);
}
public Attribute get(Object key) {
return attributes.get((String) key);
}
public void setAttribute(String name, Object value) {
Attribute attr = attributes.get(name);
if (attr == null) {
attr = new Attribute(name);
}
attr.setValue(value);
attributes.put(name, attr);
}
/**
* Obtains the ExpressionFactory instance from current context.
*
* @return the ExpressionFactory instance from current context
*/
private ExpressionFactory getExpressionFactory() {
return FacesContext.getCurrentInstance().getApplication().getExpressionFactory();
}
private void loadAttributesFromClass(Class<?> componentClass) {
PropertyDescriptor[] descriptors = null;
try {
descriptors = Introspector.getBeanInfo(componentClass).getPropertyDescriptors();
} catch (IntrospectionException e) {
logger.error("Could not get a list with attributes of class" + componentClass);
attributes = Collections.emptyMap();
}
attributes = new TreeMap<String, Attribute>();
// not all attributes of given class are needed
Set<String> excludeSet = getExcludeSet();
Attribute attribute;
// create list of all attributes and their types
for (PropertyDescriptor descriptor : descriptors) {
if (!excludeSet.contains(descriptor.getName())) {
attribute = new Attribute(descriptor.getName());
attribute.setType(descriptor.getPropertyType());
attributes.put(descriptor.getName(), attribute);
}
}
}
/**
* Method for loading RF 4.5 components
*/
private void loadRichFacesComponents() {
if (richfacesAttributes == null) {
richfacesAttributes = new HashMap<Class<?>, List<Attribute>>();
}
try {
ClassLoader cl = UIStatus.class.getClassLoader();
Enumeration<URL> fileUrls = cl.getResources("META-INF/faces-config.xml");
List<URL> configFiles = Lists.newArrayList();
while (fileUrls.hasMoreElements()) {
URL url = fileUrls.nextElement();
if (url.getPath().matches(".*richfaces[^/]+jar.*")) {
configFiles.add(url);
}
}
JAXBContext context = JAXBContext.newInstance(FacesConfigHolder.class);
FacesConfigHolder facesConfigHolder;
for (URL configFile : configFiles) {
facesConfigHolder = (FacesConfigHolder) context.createUnmarshaller().unmarshal(configFile);
for (Component c : facesConfigHolder.getComponents()) {
if (c.getAttributes() == null) {
continue;
}
removeHiddenAttributes(c.getAttributes());
richfacesAttributes.put(c.getComponentClass(), c.getAttributes());
logger.debug("attributes for component " + c.getComponentClass().getName() + " loaded");
}
for (Behavior b : facesConfigHolder.getBehaviors()) {
if (b.getAttributes() == null) {
continue;
}
removeHiddenAttributes(b.getAttributes());
richfacesAttributes.put(b.getBehaviorClass(), b.getAttributes());
logger.debug("attributes for behavior " + b.getBehaviorClass().getName() + " loaded");
}
}
} catch (IOException ex) {
logger.error("Input/output error.", ex);
} catch (JAXBException ex) {
logger.error("XML reading error.", ex);
}
}
private void removeHiddenAttributes(List<Attribute> list) {
Iterator<Attribute> i = list.iterator();
while (i.hasNext()) {
Attribute a = i.next();
if (a.isHidden() || "id".equals(a.getName()) || "binding".equals(a.getName())) {
i.remove();
}
}
}
@XmlRootElement(name = "faces-config", namespace = "http://java.sun.com/xml/ns/javaee")
private static final class FacesConfigHolder {
private List<Component> components;
private List<Behavior> behaviors;
@XmlElement(name = "component", namespace = "http://java.sun.com/xml/ns/javaee")
public List<Component> getComponents() {
return components;
}
public void setComponents(List<Component> components) {
this.components = components;
}
@XmlElement(name = "behavior", namespace = "http://java.sun.com/xml/ns/javaee")
public List<Behavior> getBehaviors() {
return behaviors;
}
public void setBehaviors(List<Behavior> behaviors) {
this.behaviors = behaviors;
}
}
}