View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package javax.faces.component;
20  
21  import java.beans.BeanInfo;
22  import java.beans.IntrospectionException;
23  import java.beans.Introspector;
24  import java.beans.PropertyDescriptor;
25  import java.io.Serializable;
26  import java.lang.reflect.Method;
27  import java.util.Collection;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import javax.faces.FacesException;
34  import javax.faces.context.FacesContext;
35  import javax.faces.el.ValueBinding;
36  
37  /**
38   * A custom implementation of the Map interface, where get and put calls
39   * try to access getter/setter methods of an associated UIComponent before
40   * falling back to accessing a real Map object.
41   * <p>
42   * Some of the behaviours of this class don't really comply with the
43   * definitions of the Map class; for example the key parameter to all
44   * methods is required to be of type String only, and after clear(),
45   * calls to get can return non-null values. However the JSF spec
46   * requires that this class behave in the way implemented below. See
47   * UIComponent.getAttributes for more details.
48   * <p>
49   * The term "property" is used here to refer to real javabean properties
50   * on the underlying UIComponent, while "attribute" refers to an entry
51   * in the associated Map.
52   *
53   * @author Manfred Geiler (latest modification by $Author: lu4242 $)
54   * @version $Revision: 949070 $ $Date: 2010-05-27 21:22:03 -0500 (Thu, 27 May 2010) $
55   */
56  class _ComponentAttributesMap
57          implements Map, Serializable
58  {
59      private static final long serialVersionUID = -9106832179394257866L;
60  
61      private static final Object[] EMPTY_ARGS = new Object[0];
62  
63      // The component that is read/written via this map.
64      private UIComponent _component;
65  
66      // We delegate instead of derive from HashMap, so that we can later
67      // optimize Serialization
68      private Map _attributes = null;
69  
70      // A cached hashmap of propertyName => PropertyDescriptor object for all
71      // the javabean properties of the associated component. This is built by
72      // introspection on the associated UIComponent. Don't serialize this as
73      // it can always be recreated when needed.
74      private transient Map _propertyDescriptorMap = null;
75  
76      /**
77       * Create a map backed by the specified component.
78       * <p>
79       * This method is expected to be called when a component is first created.
80       */
81      _ComponentAttributesMap(UIComponent component)
82      {
83          _component = component;
84          _attributes = new HashMap();
85      }
86  
87      /**
88       * Create a map backed by the specified component. Attributes already
89       * associated with the component are provided in the specified Map
90       * class. A reference to the provided map is kept; this object's contents
91       * are updated during put calls on this instance.
92       * <p>
93       * This method is expected to be called during the "restore view" phase. 
94       */
95      _ComponentAttributesMap(UIComponent component, Map attributes)
96      {
97          _component = component;
98          _attributes = attributes;
99      }
100 
101     /**
102      * Return the number of <i>attributes</i> in this map. Properties of the
103      * underlying UIComponent are not counted.
104      * <p>
105      * Note that because the get method can read properties of the
106      * UIComponent and evaluate value-bindings, it is possible to have
107      * size return zero while calls to the get method return non-null
108      * values.
109      */
110     public int size()
111     {
112         return _attributes.size();
113     }
114 
115     /**
116      * Clear all the <i>attributes</i> in this map. Properties of the
117      * underlying UIComponent are not modified.
118      * <p>
119      * Note that because the get method can read properties of the
120      * UIComponent and evaluate value-bindings, it is possible to have
121      * calls to the get method return non-null values immediately after
122      * a call to clear.
123      */
124     public void clear()
125     {
126         _attributes.clear();
127     }
128 
129     /**
130      * Return true if there are no <i>attributes</i> in this map. Properties
131      * of the underlying UIComponent are not counted.
132      * <p>
133      * Note that because the get method can read properties of the
134      * UIComponent and evaluate value-bindings, it is possible to have
135      * isEmpty return true, while calls to the get method return non-null
136      * values.
137      */
138     public boolean isEmpty()
139     {
140         return _attributes.isEmpty();
141     }
142 
143     /**
144      * Return true if there is an <i>attribute</i> with the specified name,
145      * but false if there is a javabean <i>property</i> of that name on the
146      * associated UIComponent.
147      * <p>
148      * Note that it should be impossible for the attributes map to contain
149      * an entry with the same name as a javabean property on the associated
150      * UIComponent.
151      * 
152      * @param key <i>must</i> be a String. Anything else will cause a
153      * ClassCastException to be thrown.
154      */
155     public boolean containsKey(Object key)
156     {
157         checkKey(key);
158         if (getPropertyDescriptor((String)key) == null)
159         {
160             return _attributes.containsKey(key);
161         }
162         else
163         {
164             return false;
165         }
166     }
167 
168     /**
169      * Returns true if there is an <i>attribute</i> with the specified
170      * value. Properties of the underlying UIComponent aren't examined,
171      * nor value-bindings.
172      * 
173      * @param value null is allowed
174      */
175     public boolean containsValue(Object value)
176     {
177         return _attributes.containsValue(value);
178     }
179 
180     /**
181      * Return a collection of the values of all <i>attributes</i>. Property
182      * values are not included, nor value-bindings.
183      */
184     public Collection values()
185     {
186         return _attributes.values();
187     }
188 
189     /**
190      * Call put(key, value) for each entry in the provided map.
191      */
192     public void putAll(Map t)
193     {
194         for (Iterator it = t.entrySet().iterator(); it.hasNext(); )
195         {
196             Map.Entry entry = (Entry)it.next();
197             put(entry.getKey(), entry.getValue());
198         }
199     }
200 
201     /**
202      * Return a set of all <i>attributes</i>. Properties of the underlying
203      * UIComponent are not included, nor value-bindings.
204      */
205     public Set entrySet()
206     {
207         return _attributes.entrySet();
208     }
209 
210     /**
211      * Return a set of the keys for all <i>attributes</i>. Properties of the
212      * underlying UIComponent are not included, nor value-bindings.
213      */
214     public Set keySet()
215     {
216         return _attributes.keySet();
217     }
218 
219     /**
220      * In order: get the value of a <i>property</i> of the underlying
221      * UIComponent, read an <i>attribute</i> from this map, or evaluate
222      * the component's value-binding of the specified name.
223      * 
224      * @param key must be a String. Any other type will cause ClassCastException.
225      */
226     public Object get(Object key)
227     {
228         checkKey(key);
229         
230         // is there a javabean property to read?
231         PropertyDescriptor propertyDescriptor
232            = getPropertyDescriptor((String)key);
233         if (propertyDescriptor != null)
234         {
235             return getComponentProperty(propertyDescriptor);
236         }
237         
238         // is there a literal value to read?
239         Object mapValue = _attributes.get(key);
240         if (mapValue != null)
241         {
242             return mapValue;
243         }
244         
245         // is there a value-binding to read?
246         ValueBinding vb = _component.getValueBinding((String) key);
247         if (vb != null)
248         {
249             return vb.getValue(_component.getFacesContext());
250         }
251 
252         // no value found
253         return null;
254     }
255 
256     /**
257      * Remove the attribute with the specified name. An attempt to
258      * remove an entry whose name is that of a <i>property</i> on
259      * the underlying UIComponent will cause an IllegalArgumentException.
260      * Value-bindings for the underlying component are ignored. 
261      * 
262      * @param key must be a String. Any other type will cause ClassCastException.
263      */
264     public Object remove(Object key)
265     {
266         checkKey(key);
267         PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String)key);
268         if (propertyDescriptor != null)
269         {
270             throw new IllegalArgumentException("Cannot remove component property attribute");
271         }
272         return _attributes.remove(key);
273     }
274 
275     /**
276      * Store the provided value as a <i>property</i> on the underlying
277      * UIComponent, or as an <i>attribute</i> in a Map if no such property
278      * exists. Value-bindings associated with the component are ignored; to
279      * write to a value-binding, the value-binding must be explicitly
280      * retrieved from the component and evaluated.
281      * <p>
282      * Note that this method is different from the get method, which
283      * does read from a value-binding if one exists. When a value-binding
284      * exists for a non-property, putting a value here essentially "masks"
285      * the value-binding until that attribute is removed.
286      * <p>
287      * The put method is expected to return the previous value of the
288      * property/attribute (if any). Because UIComponent property getter
289      * methods typically try to evaluate any value-binding expression of
290      * the same name this can cause an EL expression to be evaluated,
291      * thus invoking a getter method on the user's model. This is fine
292      * when the returned value will be used; Unfortunately this is quite
293      * pointless when initialising a freshly created component with whatever
294      * attributes were specified in the view definition (eg JSP tag
295      * attributes). Because the UIComponent.getAttributes method
296      * only returns a Map class and this class must be package-private,
297      * there is no way of exposing a "putNoReturn" type method.
298      *  
299      * @param key String, null is not allowed
300      * @param value null is allowed
301      */
302     public Object put(Object key, Object value)
303     {
304         checkKey(key);
305 
306         PropertyDescriptor propertyDescriptor = getPropertyDescriptor((String)key);
307 
308         if(propertyDescriptor == null)
309         {
310             if(value==null)
311                 throw new NullPointerException("value is null for a not available property: " + key);
312         }
313 
314         if (propertyDescriptor != null)
315         {
316             if (propertyDescriptor.getReadMethod() != null)
317             {
318                 Object oldValue = getComponentProperty(propertyDescriptor);
319                 setComponentProperty(propertyDescriptor, value);
320                 return oldValue;
321             }
322             else
323             {
324                 setComponentProperty(propertyDescriptor, value);
325                 return null;
326             }
327         }
328         else
329         {
330             return _attributes.put(key, value);
331         }
332     }
333 
334     /**
335      * Retrieve info about getter/setter methods for the javabean property
336      * of the specified name on the underlying UIComponent object.
337      * <p>
338      * This method optimises access to javabean properties of the underlying
339      * UIComponent by maintaining a cache of ProperyDescriptor objects for
340      * that class.
341      * <p>
342      * TODO: Consider making the cache shared between component instances;
343      * currently 100 UIInputText components means performing introspection
344      * on the UIInputText component 100 times.
345      */
346     private PropertyDescriptor getPropertyDescriptor(String key)
347     {
348         if (_propertyDescriptorMap == null)
349         {
350             BeanInfo beanInfo;
351             try
352             {
353                 beanInfo = Introspector.getBeanInfo(_component.getClass());
354             }
355             catch (IntrospectionException e)
356             {
357                 throw new FacesException(e);
358             }
359             PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
360             _propertyDescriptorMap = new HashMap();
361             for (int i = 0; i < propertyDescriptors.length; i++)
362             {
363                 PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
364                 if (propertyDescriptor.getReadMethod() != null)
365                 {
366                     _propertyDescriptorMap.put(propertyDescriptor.getName(),
367                                                propertyDescriptor);
368                 }
369             }
370         }
371         return (PropertyDescriptor)_propertyDescriptorMap.get(key);
372     }
373 
374 
375     /**
376      * Execute the getter method of the specified property on the underlying
377      * component.
378      * 
379      * @param propertyDescriptor specifies which property to read.
380      * @return the value returned by the getter method.
381      * @throws IllegalArgumentException if the property is not readable.
382      * @throws FacesException if any other problem occurs while invoking
383      *  the getter method. 
384      */
385     private Object getComponentProperty(PropertyDescriptor propertyDescriptor)
386     {
387         Method readMethod = propertyDescriptor.getReadMethod();
388         if (readMethod == null)
389         {
390             throw new IllegalArgumentException("Component property " + propertyDescriptor.getName() + " is not readable");
391         }
392         try
393         {
394             return readMethod.invoke(_component, EMPTY_ARGS);
395         }
396         catch (Exception e)
397         {
398             FacesContext facesContext = _component.getFacesContext();
399             throw new FacesException("Could not get property " + propertyDescriptor.getName() + " of component " + _component.getClientId(facesContext), e);
400         }
401     }
402 
403     /**
404      * Execute the setter method of the specified property on the underlying
405      * component.
406      * 
407      * @param propertyDescriptor specifies which property to write.
408      * @throws IllegalArgumentException if the property is not writable.
409      * @throws FacesException if any other problem occurs while invoking
410      *  the getter method. 
411      */
412     private void setComponentProperty(PropertyDescriptor propertyDescriptor, Object value)
413     {
414         Method writeMethod = propertyDescriptor.getWriteMethod();
415         if (writeMethod == null)
416         {
417             throw new IllegalArgumentException("Component property " + propertyDescriptor.getName() + " is not writable");
418         }
419         try
420         {
421             writeMethod.invoke(_component, new Object[] {value});
422         }
423         catch (Exception e)
424         {
425             FacesContext facesContext = _component.getFacesContext();
426             throw new FacesException("Could not set property " + propertyDescriptor.getName() +
427                     " of component " + _component.getClientId(facesContext) +" to value : "+value+" with type : "+
428                     (value==null?"null":value.getClass().getName()), e);
429         }
430     }
431 
432     private void checkKey(Object key)
433     {
434         if (key == null) throw new NullPointerException("key");
435         if (!(key instanceof String)) throw new ClassCastException("key is not a String");
436     }
437 
438     /**
439      * Return the map containing the attributes.
440      * <p>
441      * This method is package-scope so that the UIComponentBase class can access it
442      * directly when serializing the component.
443      */
444     Map getUnderlyingMap()
445     {
446         return _attributes;
447     }
448 
449     /**
450      * TODO: Document why this method is necessary, and why it doesn't try to
451      * compare the _component field.
452      */
453     public boolean equals(Object obj)
454     {
455         return _attributes.equals(obj);
456     }
457 
458     public int hashCode()
459     {
460         return _attributes.hashCode();
461     }
462 }