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