All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax.faces.component._ComponentAttributesMap Maven / Gradle / Ivy

There is a newer version: 4.1.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
package javax.faces.component;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.Resource;
import javax.faces.context.FacesContext;

/**
 * A custom implementation of the Map interface, where get and put calls
 * try to access getter/setter methods of an associated UIComponent before
 * falling back to accessing a real Map object.
 * 

* Some of the behaviours of this class don't really comply with the * definitions of the Map class; for example the key parameter to all * methods is required to be of type String only, and after clear(), * calls to get can return non-null values. However the JSF spec * requires that this class behave in the way implemented below. See * UIComponent.getAttributes for more details. *

* The term "property" is used here to refer to real javabean properties * on the underlying UIComponent, while "attribute" refers to an entry * in the associated Map. * * @author Manfred Geiler (latest modification by $Author: lu4242 $) * @version $Revision: 1157591 $ $Date: 2011-08-14 13:10:45 -0500 (Sun, 14 Aug 2011) $ */ class _ComponentAttributesMap implements Map, Serializable { private static final long serialVersionUID = -9106832179394257866L; private static final Object[] EMPTY_ARGS = new Object[0]; // The component that is read/written via this map. private UIComponent _component; // We delegate instead of derive from HashMap, so that we can later // optimize Serialization // JSF 2.0 Changed getUnderlyingMap to point to StateHelper attributesMap //private Map _attributes = null; // A cached hashmap of propertyName => PropertyDescriptor object for all // the javabean properties of the associated component. This is built by // introspection on the associated UIComponent. Don't serialize this as // it can always be recreated when needed. private transient Map _propertyDescriptorMap = null; // Cache for component property descriptors private static Map, Map> _propertyDescriptorCache = new WeakHashMap, Map>(); private boolean _isCompositeComponent; private boolean _isCompositeComponentSet; /** * Create a map backed by the specified component. *

* This method is expected to be called when a component is first created. */ _ComponentAttributesMap(UIComponent component) { _component = component; } /** * Create a map backed by the specified component. Attributes already * associated with the component are provided in the specified Map * class. A reference to the provided map is kept; this object's contents * are updated during put calls on this instance. *

* This method is expected to be called during the "restore view" phase. */ //JSF 2.0 removed because _attributes has been replaced with StateHelper attributesMap //_ComponentAttributesMap(UIComponent component, Map attributes) //{ // _component = component; //_attributes = new HashMap(attributes); //} /** * Return the number of attributes in this map. Properties of the * underlying UIComponent are not counted. *

* Note that because the get method can read properties of the * UIComponent and evaluate value-bindings, it is possible to have * size return zero while calls to the get method return non-null * values. */ public int size() { return getUnderlyingMap().size(); } /** * Clear all the attributes in this map. Properties of the * underlying UIComponent are not modified. *

* Note that because the get method can read properties of the * UIComponent and evaluate value-bindings, it is possible to have * calls to the get method return non-null values immediately after * a call to clear. */ public void clear() { getUnderlyingMap().clear(); } /** * Return true if there are no attributes in this map. Properties * of the underlying UIComponent are not counted. *

* Note that because the get method can read properties of the * UIComponent and evaluate value-bindings, it is possible to have * isEmpty return true, while calls to the get method return non-null * values. */ public boolean isEmpty() { return getUnderlyingMap().isEmpty(); } /** * Return true if there is an attribute with the specified name, * but false if there is a javabean property of that name on the * associated UIComponent. *

* Note that it should be impossible for the attributes map to contain * an entry with the same name as a javabean property on the associated * UIComponent. * * @param key must be a String. Anything else will cause a * ClassCastException to be thrown. */ public boolean containsKey(Object key) { checkKey(key); // The most common call to this method comes from UIComponent.isCompositeComponent() // to reduce the impact. This is better than two lookups, once over property descriptor map // and the other one from the underlying map. if (Resource.COMPONENT_RESOURCE_KEY.length() == ((String)key).length() && Resource.COMPONENT_RESOURCE_KEY.equals(key)) { if (!_isCompositeComponentSet) { // Note we are not setting _isCompositeComponentSet, because when the component tree is built // using JSF 1.2 state saving, PostAddToViewEvent is propagated and the component is check // if is a composite component, but the state is not restored, so the check return always // false. A check for processing events was added to prevent that scenario, but anyway that // makes invalid set _isCompositeComponentSet to true on this location. _isCompositeComponent = getUnderlyingMap().containsKey(Resource.COMPONENT_RESOURCE_KEY); } return _isCompositeComponent; } return getPropertyDescriptor((String) key) == null ? getUnderlyingMap().containsKey(key) : false; } /** * Returns true if there is an attribute with the specified * value. Properties of the underlying UIComponent aren't examined, * nor value-bindings. * * @param value null is allowed */ public boolean containsValue(Object value) { return getUnderlyingMap().containsValue(value); } /** * Return a collection of the values of all attributes. Property * values are not included, nor value-bindings. */ public Collection values() { return getUnderlyingMap().values(); } /** * Call put(key, value) for each entry in the provided map. */ public void putAll(Map t) { for (Map.Entry entry : t.entrySet()) { put(entry.getKey(), entry.getValue()); } } /** * Return a set of all attributes. Properties of the underlying * UIComponent are not included, nor value-bindings. */ public Set> entrySet() { return getUnderlyingMap().entrySet(); } /** * Return a set of the keys for all attributes. Properties of the * underlying UIComponent are not included, nor value-bindings. */ public Set keySet() { return getUnderlyingMap().keySet(); } /** * In order: get the value of a property of the underlying * UIComponent, read an attribute from this map, or evaluate * the component's value-binding of the specified name. * * @param key must be a String. Any other type will cause ClassCastException. */ public Object get(Object key) { checkKey(key); Object value; // is there a javabean property to read? PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String) key); if (propertyDescriptor != null) { value = getComponentProperty(propertyDescriptor); } else { // is there a literal value to read? value = getUnderlyingMap().get(key); if (value == null) { // is there a value-binding to read? ValueExpression ve = _component.getValueExpression((String) key); if (ve != null) { value = ve.getValue(_component.getFacesContext().getELContext()); } else { if (!_isCompositeComponentSet) { _isCompositeComponent = getUnderlyingMap().containsKey(Resource.COMPONENT_RESOURCE_KEY); _isCompositeComponentSet = true; } if (_isCompositeComponent) { BeanInfo ccBeanInfo = (BeanInfo) getUnderlyingMap().get(UIComponent.BEANINFO_KEY); if (ccBeanInfo != null) { for (PropertyDescriptor attribute : ccBeanInfo.getPropertyDescriptors()) { if (attribute.getName().equals(key)) { String attributeName = attribute.getName(); boolean isKnownMethod = "action".equals(attributeName) || "actionListener".equals(attributeName) || "validator".equals(attributeName) || "valueChangeListener".equals(attributeName); // method-signature attribute is // ValueExpression that must evaluate to String ValueExpression methodSignatureExpression = (ValueExpression) attribute.getValue("method-signature"); String methodSignature = null; if (methodSignatureExpression != null) { // Check if the value expression holds a method signature // Note that it could be null, so in that case we don't have to do anything methodSignature = (String) methodSignatureExpression.getValue(_component.getFacesContext().getELContext()); } // either the attributeName has to be a knownMethod or there has to be a method-signature if (isKnownMethod || methodSignature != null) { //In this case it is expecting a ValueExpression return attribute.getValue("default"); } else { value = attribute.getValue("default"); break; } } } // We have to check for a ValueExpression and also evaluate it // here, because in the PropertyDescriptor the default values are // always stored as (Tag-)ValueExpressions. if (value != null && value instanceof ValueExpression) { return ((ValueExpression) value).getValue(_component.getFacesContext().getELContext()); } } } // no value found //return null; } } } // Otherwise, return the actual value from the get() method. return value; } /** * Remove the attribute with the specified name. An attempt to * remove an entry whose name is that of a property on * the underlying UIComponent will cause an IllegalArgumentException. * Value-bindings for the underlying component are ignored. * * @param key must be a String. Any other type will cause ClassCastException. */ public Object remove(Object key) { checkKey(key); PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String) key); if (propertyDescriptor != null) { throw new IllegalArgumentException("Cannot remove component property attribute"); } return _component.getStateHelper().remove( UIComponentBase.PropertyKeys.attributesMap, key); } /** * Store the provided value as a property on the underlying * UIComponent, or as an attribute in a Map if no such property * exists. Value-bindings associated with the component are ignored; to * write to a value-binding, the value-binding must be explicitly * retrieved from the component and evaluated. *

* Note that this method is different from the get method, which * does read from a value-binding if one exists. When a value-binding * exists for a non-property, putting a value here essentially "masks" * the value-binding until that attribute is removed. *

* The put method is expected to return the previous value of the * property/attribute (if any). Because UIComponent property getter * methods typically try to evaluate any value-binding expression of * the same name this can cause an EL expression to be evaluated, * thus invoking a getter method on the user's model. This is fine * when the returned value will be used; Unfortunately this is quite * pointless when initialising a freshly created component with whatever * attributes were specified in the view definition (eg JSP tag * attributes). Because the UIComponent.getAttributes method * only returns a Map class and this class must be package-private, * there is no way of exposing a "putNoReturn" type method. * * @param key String, null is not allowed * @param value null is allowed */ public Object put(String key, Object value) { if (key == null) { throw new NullPointerException("key"); } PropertyDescriptor propertyDescriptor = getPropertyDescriptor(key); if (propertyDescriptor == null) { if (value == null) { throw new NullPointerException("value is null for a not available property: " + key); } } else { if (propertyDescriptor.getReadMethod() != null) { Object oldValue = getComponentProperty(propertyDescriptor); setComponentProperty(propertyDescriptor, value); return oldValue; } setComponentProperty(propertyDescriptor, value); return null; } // To keep this code in good shape, The fastest way to compare is look if the length first here // because we avoid an unnecessary cast later on equals(). if ( Resource.COMPONENT_RESOURCE_KEY.length() == key.length() && Resource.COMPONENT_RESOURCE_KEY.equals(key)) { _isCompositeComponent = true; _isCompositeComponentSet = true; } return _component.getStateHelper().put(UIComponentBase.PropertyKeys.attributesMap, key, value); } /** * Retrieve info about getter/setter methods for the javabean property * of the specified name on the underlying UIComponent object. *

* This method optimises access to javabean properties of the underlying * UIComponent by maintaining a cache of ProperyDescriptor objects for * that class. *

* TODO: Consider making the cache shared between component instances; * currently 100 UIInputText components means performing introspection * on the UIInputText component 100 times. */ private PropertyDescriptor getPropertyDescriptor(String key) { if (_propertyDescriptorMap == null) { // Try to get descriptor map from cache _propertyDescriptorMap = _propertyDescriptorCache.get(_component.getClass()); // Cache miss: create descriptor map and put it in cache if (_propertyDescriptorMap == null) { // Create descriptor map... BeanInfo beanInfo; try { beanInfo = Introspector.getBeanInfo(_component.getClass()); } catch (IntrospectionException e) { throw new FacesException(e); } PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); _propertyDescriptorMap = new HashMap(); for (int i = 0; i < propertyDescriptors.length; i++) { PropertyDescriptor propertyDescriptor = propertyDescriptors[i]; if (propertyDescriptor.getReadMethod() != null) { _propertyDescriptorMap.put(propertyDescriptor.getName(), propertyDescriptor); } } // ... and put it in cache synchronized(_propertyDescriptorCache) { // Use a synchronized block to ensure proper operation on concurrent use cases. // This is a racy single check, because initialization over the same class could happen // multiple times, but the same result is always calculated. The synchronized block // just ensure thread-safety, because only one thread will modify the cache map // at the same time. _propertyDescriptorCache.put(_component.getClass(), _propertyDescriptorMap); } } } return _propertyDescriptorMap.get(key); } /** * Execute the getter method of the specified property on the underlying * component. * * @param propertyDescriptor specifies which property to read. * @return the value returned by the getter method. * @throws IllegalArgumentException if the property is not readable. * @throws FacesException if any other problem occurs while invoking * the getter method. */ private Object getComponentProperty(PropertyDescriptor propertyDescriptor) { Method readMethod = propertyDescriptor.getReadMethod(); if (readMethod == null) { throw new IllegalArgumentException("Component property " + propertyDescriptor.getName() + " is not readable"); } try { return readMethod.invoke(_component, EMPTY_ARGS); } catch (Exception e) { FacesContext facesContext = _component.getFacesContext(); throw new FacesException("Could not get property " + propertyDescriptor.getName() + " of component " + _component.getClientId(facesContext), e); } } /** * Execute the setter method of the specified property on the underlying * component. * * @param propertyDescriptor specifies which property to write. * @throws IllegalArgumentException if the property is not writable. * @throws FacesException if any other problem occurs while invoking * the getter method. */ private void setComponentProperty(PropertyDescriptor propertyDescriptor, Object value) { Method writeMethod = propertyDescriptor.getWriteMethod(); if (writeMethod == null) { throw new IllegalArgumentException("Component property " + propertyDescriptor.getName() + " is not writable"); } try { writeMethod.invoke(_component, new Object[]{value}); } catch (Exception e) { FacesContext facesContext = _component.getFacesContext(); throw new FacesException("Could not set property " + propertyDescriptor.getName() + " of component " + _component.getClientId(facesContext) + " to value : " + value + " with type : " + (value == null ? "null" : value.getClass().getName()), e); } } private void checkKey(Object key) { if (key == null) { throw new NullPointerException("key"); } if (!(key instanceof String)) { throw new ClassCastException("key is not a String"); } } /** * Return the map containing the attributes. *

* This method is package-scope so that the UIComponentBase class can access it * directly when serializing the component. */ Map getUnderlyingMap() { Map _attributes = (Map) _component.getStateHelper().get(UIComponentBase.PropertyKeys.attributesMap); return _attributes == null ? Collections.EMPTY_MAP : _attributes; } /** * TODO: Document why this method is necessary, and why it doesn't try to * compare the _component field. */ @Override public boolean equals(Object obj) { return getUnderlyingMap().equals(obj); } @Override public int hashCode() { return getUnderlyingMap().hashCode(); } }