001: /*
002: * Copyright 2004 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package javax.faces.component;
017:
018: import java.beans.BeanInfo;
019: import java.beans.IntrospectionException;
020: import java.beans.Introspector;
021: import java.beans.PropertyDescriptor;
022: import java.io.Serializable;
023: import java.lang.reflect.Method;
024: import java.util.Collection;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.Map;
028: import java.util.Set;
029:
030: import javax.el.ValueExpression;
031: import javax.faces.FacesException;
032: import javax.faces.context.FacesContext;
033:
034: /**
035: * A custom implementation of the Map interface, where get and put calls
036: * try to access getter/setter methods of an associated UIComponent before
037: * falling back to accessing a real Map object.
038: * <p>
039: * Some of the behaviours of this class don't really comply with the
040: * definitions of the Map class; for example the key parameter to all
041: * methods is required to be of type String only, and after clear(),
042: * calls to get can return non-null values. However the JSF spec
043: * requires that this class behave in the way implemented below. See
044: * UIComponent.getAttributes for more details.
045: * <p>
046: * The term "property" is used here to refer to real javabean properties
047: * on the underlying UIComponent, while "attribute" refers to an entry
048: * in the associated Map.
049: *
050: * @author Manfred Geiler (latest modification by $Author: mbr $)
051: * @version $Revision: 518783 $ $Date: 2007-03-15 23:23:53 +0100 (Do, 15 Mrz 2007) $
052: */
053: class _ComponentAttributesMap implements Map, Serializable {
054: private static final long serialVersionUID = -9106832179394257866L;
055:
056: private static final Object[] EMPTY_ARGS = new Object[0];
057:
058: // The component that is read/written via this map.
059: private UIComponent _component;
060:
061: // We delegate instead of derive from HashMap, so that we can later
062: // optimize Serialization
063: private Map<Object, Object> _attributes = null;
064:
065: // A cached hashmap of propertyName => PropertyDescriptor object for all
066: // the javabean properties of the associated component. This is built by
067: // introspection on the associated UIComponent. Don't serialize this as
068: // it can always be recreated when needed.
069: private transient Map<String, PropertyDescriptor> _propertyDescriptorMap = null;
070:
071: /**
072: * Create a map backed by the specified component.
073: * <p>
074: * This method is expected to be called when a component is first created.
075: */
076: _ComponentAttributesMap(UIComponent component) {
077: _component = component;
078: _attributes = new HashMap<Object, Object>();
079: }
080:
081: /**
082: * Create a map backed by the specified component. Attributes already
083: * associated with the component are provided in the specified Map
084: * class. A reference to the provided map is kept; this object's contents
085: * are updated during put calls on this instance.
086: * <p>
087: * This method is expected to be called during the "restore view" phase.
088: */
089: _ComponentAttributesMap(UIComponent component,
090: Map<Object, Object> attributes) {
091: _component = component;
092: _attributes = attributes;
093: }
094:
095: /**
096: * Return the number of <i>attributes</i> in this map. Properties of the
097: * underlying UIComponent are not counted.
098: * <p>
099: * Note that because the get method can read properties of the
100: * UIComponent and evaluate value-bindings, it is possible to have
101: * size return zero while calls to the get method return non-null
102: * values.
103: */
104: public int size() {
105: return _attributes.size();
106: }
107:
108: /**
109: * Clear all the <i>attributes</i> in this map. Properties of the
110: * underlying UIComponent are not modified.
111: * <p>
112: * Note that because the get method can read properties of the
113: * UIComponent and evaluate value-bindings, it is possible to have
114: * calls to the get method return non-null values immediately after
115: * a call to clear.
116: */
117: public void clear() {
118: _attributes.clear();
119: }
120:
121: /**
122: * Return true if there are no <i>attributes</i> in this map. Properties
123: * of the underlying UIComponent are not counted.
124: * <p>
125: * Note that because the get method can read properties of the
126: * UIComponent and evaluate value-bindings, it is possible to have
127: * isEmpty return true, while calls to the get method return non-null
128: * values.
129: */
130: public boolean isEmpty() {
131: return _attributes.isEmpty();
132: }
133:
134: /**
135: * Return true if there is an <i>attribute</i> with the specified name,
136: * but false if there is a javabean <i>property</i> of that name on the
137: * associated UIComponent.
138: * <p>
139: * Note that it should be impossible for the attributes map to contain
140: * an entry with the same name as a javabean property on the associated
141: * UIComponent.
142: *
143: * @param key <i>must</i> be a String. Anything else will cause a
144: * ClassCastException to be thrown.
145: */
146: public boolean containsKey(Object key) {
147: checkKey(key);
148:
149: return getPropertyDescriptor((String) key) == null ? _attributes
150: .containsKey(key)
151: : false;
152: }
153:
154: /**
155: * Returns true if there is an <i>attribute</i> with the specified
156: * value. Properties of the underlying UIComponent aren't examined,
157: * nor value-bindings.
158: *
159: * @param value null is allowed
160: */
161: public boolean containsValue(Object value) {
162: return _attributes.containsValue(value);
163: }
164:
165: /**
166: * Return a collection of the values of all <i>attributes</i>. Property
167: * values are not included, nor value-bindings.
168: */
169: public Collection<Object> values() {
170: return _attributes.values();
171: }
172:
173: /**
174: * Call put(key, value) for each entry in the provided map.
175: */
176: public void putAll(Map t) {
177: for (Iterator it = t.entrySet().iterator(); it.hasNext();) {
178: Map.Entry entry = (Entry) it.next();
179: put(entry.getKey(), entry.getValue());
180: }
181: }
182:
183: /**
184: * Return a set of all <i>attributes</i>. Properties of the underlying
185: * UIComponent are not included, nor value-bindings.
186: */
187: public Set entrySet() {
188: return _attributes.entrySet();
189: }
190:
191: /**
192: * Return a set of the keys for all <i>attributes</i>. Properties of the
193: * underlying UIComponent are not included, nor value-bindings.
194: */
195: public Set<Object> keySet() {
196: return _attributes.keySet();
197: }
198:
199: /**
200: * In order: get the value of a <i>property</i> of the underlying
201: * UIComponent, read an <i>attribute</i> from this map, or evaluate
202: * the component's value-binding of the specified name.
203: *
204: * @param key must be a String. Any other type will cause ClassCastException.
205: */
206: public Object get(Object key) {
207: checkKey(key);
208:
209: // is there a javabean property to read?
210: PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String) key);
211: if (propertyDescriptor != null) {
212: return getComponentProperty(propertyDescriptor);
213: }
214:
215: // is there a literal value to read?
216: Object mapValue = _attributes.get(key);
217: if (mapValue != null) {
218: return mapValue;
219: }
220:
221: // is there a value-binding to read?
222: ValueExpression ve = _component
223: .getValueExpression((String) key);
224: if (ve != null) {
225: return ve.getValue(FacesContext.getCurrentInstance()
226: .getELContext());
227: }
228:
229: // no value found
230: return null;
231: }
232:
233: /**
234: * Remove the attribute with the specified name. An attempt to
235: * remove an entry whose name is that of a <i>property</i> on
236: * the underlying UIComponent will cause an IllegalArgumentException.
237: * Value-bindings for the underlying component are ignored.
238: *
239: * @param key must be a String. Any other type will cause ClassCastException.
240: */
241: public Object remove(Object key) {
242: checkKey(key);
243: PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String) key);
244: if (propertyDescriptor != null) {
245: throw new IllegalArgumentException(
246: "Cannot remove component property attribute");
247: }
248: return _attributes.remove(key);
249: }
250:
251: /**
252: * Store the provided value as a <i>property</i> on the underlying
253: * UIComponent, or as an <i>attribute</i> in a Map if no such property
254: * exists. Value-bindings associated with the component are ignored; to
255: * write to a value-binding, the value-binding must be explicitly
256: * retrieved from the component and evaluated.
257: * <p>
258: * Note that this method is different from the get method, which
259: * does read from a value-binding if one exists. When a value-binding
260: * exists for a non-property, putting a value here essentially "masks"
261: * the value-binding until that attribute is removed.
262: * <p>
263: * The put method is expected to return the previous value of the
264: * property/attribute (if any). Because UIComponent property getter
265: * methods typically try to evaluate any value-binding expression of
266: * the same name this can cause an EL expression to be evaluated,
267: * thus invoking a getter method on the user's model. This is fine
268: * when the returned value will be used; Unfortunately this is quite
269: * pointless when initialising a freshly created component with whatever
270: * attributes were specified in the view definition (eg JSP tag
271: * attributes). Because the UIComponent.getAttributes method
272: * only returns a Map class and this class must be package-private,
273: * there is no way of exposing a "putNoReturn" type method.
274: *
275: * @param key String, null is not allowed
276: * @param value null is allowed
277: */
278: public Object put(Object key, Object value) {
279: checkKeyAndValue(key, value);
280:
281: PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String) key);
282: if (propertyDescriptor != null) {
283: if (propertyDescriptor.getReadMethod() != null) {
284: Object oldValue = getComponentProperty(propertyDescriptor);
285: setComponentProperty(propertyDescriptor, value);
286: return oldValue;
287: }
288: setComponentProperty(propertyDescriptor, value);
289: return null;
290: }
291: return _attributes.put(key, value);
292: }
293:
294: /**
295: * Retrieve info about getter/setter methods for the javabean property
296: * of the specified name on the underlying UIComponent object.
297: * <p>
298: * This method optimises access to javabean properties of the underlying
299: * UIComponent by maintaining a cache of ProperyDescriptor objects for
300: * that class.
301: * <p>
302: * TODO: Consider making the cache shared between component instances;
303: * currently 100 UIInputText components means performing introspection
304: * on the UIInputText component 100 times.
305: */
306: private PropertyDescriptor getPropertyDescriptor(String key) {
307: if (_propertyDescriptorMap == null) {
308: BeanInfo beanInfo;
309: try {
310: beanInfo = Introspector.getBeanInfo(_component
311: .getClass());
312: } catch (IntrospectionException e) {
313: throw new FacesException(e);
314: }
315: PropertyDescriptor[] propertyDescriptors = beanInfo
316: .getPropertyDescriptors();
317: _propertyDescriptorMap = new HashMap<String, PropertyDescriptor>();
318: for (int i = 0; i < propertyDescriptors.length; i++) {
319: PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
320: if (propertyDescriptor.getReadMethod() != null) {
321: _propertyDescriptorMap.put(propertyDescriptor
322: .getName(), propertyDescriptor);
323: }
324: }
325: }
326: return _propertyDescriptorMap.get(key);
327: }
328:
329: /**
330: * Execute the getter method of the specified property on the underlying
331: * component.
332: *
333: * @param propertyDescriptor specifies which property to read.
334: * @return the value returned by the getter method.
335: * @throws IllegalArgumentException if the property is not readable.
336: * @throws FacesException if any other problem occurs while invoking
337: * the getter method.
338: */
339: private Object getComponentProperty(
340: PropertyDescriptor propertyDescriptor) {
341: Method readMethod = propertyDescriptor.getReadMethod();
342: if (readMethod == null) {
343: throw new IllegalArgumentException("Component property "
344: + propertyDescriptor.getName() + " is not readable");
345: }
346: try {
347: return readMethod.invoke(_component, EMPTY_ARGS);
348: } catch (Exception e) {
349: FacesContext facesContext = FacesContext
350: .getCurrentInstance();
351: throw new FacesException("Could not get property "
352: + propertyDescriptor.getName() + " of component "
353: + _component.getClientId(facesContext), e);
354: }
355: }
356:
357: /**
358: * Execute the setter method of the specified property on the underlying
359: * component.
360: *
361: * @param propertyDescriptor specifies which property to write.
362: * @throws IllegalArgumentException if the property is not writable.
363: * @throws FacesException if any other problem occurs while invoking
364: * the getter method.
365: */
366: private void setComponentProperty(
367: PropertyDescriptor propertyDescriptor, Object value) {
368: Method writeMethod = propertyDescriptor.getWriteMethod();
369: if (writeMethod == null) {
370: throw new IllegalArgumentException("Component property "
371: + propertyDescriptor.getName() + " is not writable");
372: }
373: try {
374: writeMethod.invoke(_component, new Object[] { value });
375: } catch (Exception e) {
376: FacesContext facesContext = FacesContext
377: .getCurrentInstance();
378: throw new FacesException("Could not set property "
379: + propertyDescriptor.getName()
380: + " of component "
381: + _component.getClientId(facesContext)
382: + " to value : "
383: + value
384: + " with type : "
385: + (value == null ? "null" : value.getClass()
386: .getName()), e);
387: }
388: }
389:
390: private void checkKeyAndValue(Object key, Object value) {
391: //http://issues.apache.org/jira/browse/MYFACES-458: obviously, the spec is a little unclear here,
392: // but value == null should be allowed - if there is a TCK-test failing due to this, we should
393: // apply for getting the TCK-test dropped
394: if (value == null)
395: throw new NullPointerException("value");
396: checkKey(key);
397: }
398:
399: private void checkKey(Object key) {
400: if (key == null)
401: throw new NullPointerException("key");
402: if (!(key instanceof String))
403: throw new ClassCastException("key is not a String");
404: }
405:
406: /**
407: * Return the map containing the attributes.
408: * <p>
409: * This method is package-scope so that the UIComponentBase class can access it
410: * directly when serializing the component.
411: */
412: Map<Object, Object> getUnderlyingMap() {
413: return _attributes;
414: }
415:
416: /**
417: * TODO: Document why this method is necessary, and why it doesn't try to
418: * compare the _component field.
419: */
420: public boolean equals(Object obj) {
421: return _attributes.equals(obj);
422: }
423:
424: public int hashCode() {
425: return _attributes.hashCode();
426: }
427: }
|