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.Collections;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.WeakHashMap;
32  import java.util.concurrent.ConcurrentHashMap;
33  
34  import javax.el.ValueExpression;
35  import javax.faces.FacesException;
36  import javax.faces.application.Resource;
37  import javax.faces.context.FacesContext;
38  
39  /**
40   * <p>
41   * A custom implementation of the Map interface, where get and put calls
42   * try to access getter/setter methods of an associated UIComponent before
43   * falling back to accessing a real Map object.
44   * </p>
45   * <p>
46   * Some of the behaviours of this class don't really comply with the
47   * definitions of the Map class; for example the key parameter to all
48   * methods is required to be of type String only, and after clear(),
49   * calls to get can return non-null values. However the JSF spec
50   * requires that this class behave in the way implemented below. See
51   * UIComponent.getAttributes for more details.
52   * </p>
53   * <p>
54   * The term "property" is used here to refer to real javabean properties
55   * on the underlying UIComponent, while "attribute" refers to an entry
56   * in the associated Map.
57   * </p>
58   */
59  class _ComponentAttributesMap implements Map<String, Object>, Serializable
60  {
61      private static final long serialVersionUID = -9106832179394257866L;
62  
63      private static final Object[] EMPTY_ARGS = new Object[0];
64      
65      private final static String MARK_CREATED = "oam.vf.MARK_ID";
66      
67      private final static String FACET_NAME_KEY = "facelets.FACET_NAME";
68      
69      public final static String FACET_CREATED_UIPANEL_MARKER = "oam.vf.createdUIPanel";
70      
71      private final static String COMPONENT_ADDED_BY_HANDLER_MARKER = "oam.vf.addedByHandler";
72      
73      public static final String PROPERTY_DESCRIPTOR_MAP_KEY = "oam.cc.beanInfo.PDM";
74      
75      /**
76       * This variable works as a check to indicate the minimun lenght we need to check
77       * for the special attributes, and save some time in get(), containsKey() and 
78       * put() operations.
79       */
80      private final static int MIN_LENGHT_CHECK = MARK_CREATED.length();
81  
82      // The component that is read/written via this map.
83      private UIComponentBase _component;
84  
85      // We delegate instead of derive from HashMap, so that we can later
86      // optimize Serialization
87      // JSF 2.0 Changed getUnderlyingMap to point to StateHelper attributesMap
88      //private Map<String, Object> _attributes = null;
89  
90      // A cached hashmap of propertyName => PropertyDescriptor object for all
91      // the javabean properties of the associated component. This is built by
92      // introspection on the associated UIComponent. Don't serialize this as
93      // it can always be recreated when needed.
94      private transient Map<String, _PropertyDescriptorHolder> _propertyDescriptorMap = null;
95  
96      // Cache for component property descriptors
97      private static Map<Class<?>, Map<String, _PropertyDescriptorHolder>> propertyDescriptorCache =
98          new WeakHashMap<Class<?>, Map<String, _PropertyDescriptorHolder>>();
99      
100     private boolean _isCompositeComponent;
101     private boolean _isCompositeComponentSet;
102     
103     private BeanInfo _ccBeanInfo;
104 
105     /**
106      * Create a map backed by the specified component.
107      * <p/>
108      * This method is expected to be called when a component is first created.
109      */
110     _ComponentAttributesMap(UIComponentBase component)
111     {
112         _component = component;
113     }
114     
115     /**
116      * Create a map backed by the specified component. Attributes already
117      * associated with the component are provided in the specified Map
118      * class. A reference to the provided map is kept; this object's contents
119      * are updated during put calls on this instance.
120      * <p/>
121      * This method is expected to be called during the "restore view" phase.
122      */
123     //JSF 2.0 removed because _attributes has been replaced with StateHelper attributesMap
124     //_ComponentAttributesMap(UIComponent component, Map<String, Object> attributes)
125     //{
126     //    _component = component;
127         //_attributes = new HashMap<String, Object>(attributes);
128     //}
129     
130     /**
131      * Return the number of <i>attributes</i> in this map. Properties of the
132      * underlying UIComponent are not counted.
133      * <p/>
134      * Note that because the get method can read properties of the
135      * UIComponent and evaluate value-bindings, it is possible to have
136      * size return zero while calls to the get method return non-null
137      * values.
138      */
139     @Override
140     public int size()
141     {
142         return getUnderlyingMap().size();
143     }
144 
145     /**
146      * Clear all the <i>attributes</i> in this map. Properties of the
147      * underlying UIComponent are not modified.
148      * <p/>
149      * Note that because the get method can read properties of the
150      * UIComponent and evaluate value-bindings, it is possible to have
151      * calls to the get method return non-null values immediately after
152      * a call to clear.
153      */
154     @Override
155     public void clear()
156     {
157         getUnderlyingMap().clear();
158     }
159 
160     /**
161      * Return true if there are no <i>attributes</i> in this map. Properties
162      * of the underlying UIComponent are not counted.
163      * <p/>
164      * Note that because the get method can read properties of the
165      * UIComponent and evaluate value-bindings, it is possible to have
166      * isEmpty return true, while calls to the get method return non-null
167      * values.
168      */
169     @Override
170     public boolean isEmpty()
171     {
172         return getUnderlyingMap().isEmpty();
173     }
174 
175     /**
176      * Return true if there is an <i>attribute</i> with the specified name,
177      * but false if there is a javabean <i>property</i> of that name on the
178      * associated UIComponent.
179      * <p/>
180      * Note that it should be impossible for the attributes map to contain
181      * an entry with the same name as a javabean property on the associated
182      * UIComponent.
183      *
184      * @param key <i>must</i> be a String. Anything else will cause a
185      *            ClassCastException to be thrown.
186      */
187     @Override
188     public boolean containsKey(Object key)
189     {
190         checkKey(key);
191 
192         int keyLength = ((String)key).length();
193         if (keyLength >= MIN_LENGHT_CHECK)
194         {
195             if (MARK_CREATED.length() == keyLength &&
196                 MARK_CREATED.equals(key))
197             {
198                 return ((UIComponentBase)_component).getOamVfMarkCreated() != null;
199             }
200             else if (FACET_NAME_KEY.length() == keyLength &&
201                 FACET_NAME_KEY.equals(key))
202             {
203                 return _component.getOamVfFacetName() != null;
204             }
205             else if (COMPONENT_ADDED_BY_HANDLER_MARKER.length() == keyLength &&
206                 COMPONENT_ADDED_BY_HANDLER_MARKER.equals(key))
207             {
208                 return _component.isOamVfAddedByHandler();
209             }
210             else if (FACET_CREATED_UIPANEL_MARKER.length() == keyLength &&
211                 FACET_CREATED_UIPANEL_MARKER.equals(key))
212             {
213                 return _component.isOamVfFacetCreatedUIPanel();
214             }
215             // The most common call to this method comes from UIComponent.isCompositeComponent()
216             // to reduce the impact. This is better than two lookups, once over property descriptor map
217             // and the other one from the underlying map.
218             if (Resource.COMPONENT_RESOURCE_KEY.length() == keyLength &&
219                 Resource.COMPONENT_RESOURCE_KEY.equals(key))
220             {
221                 if (!_isCompositeComponentSet)
222                 {
223                     // Note we are not setting _isCompositeComponentSet, because when the component tree is built
224                     // using JSF 1.2 state saving, PostAddToViewEvent is propagated and the component is check 
225                     // if is a composite component, but the state is not restored, so the check return always
226                     // false. A check for processing events was added to prevent that scenario, but anyway that 
227                     // makes invalid set _isCompositeComponentSet to true on this location.
228                     _isCompositeComponent = getUnderlyingMap().containsKey(Resource.COMPONENT_RESOURCE_KEY);
229                 }
230                 return _isCompositeComponent;
231             }
232         }
233         return getPropertyDescriptor((String) key) == null ? getUnderlyingMap().containsKey(key) : false;
234     }
235 
236     /**
237      * Returns true if there is an <i>attribute</i> with the specified
238      * value. Properties of the underlying UIComponent aren't examined,
239      * nor value-bindings.
240      *
241      * @param value null is allowed
242      */
243     @Override
244     public boolean containsValue(Object value)
245     {
246         return getUnderlyingMap().containsValue(value);
247     }
248 
249     /**
250      * Return a collection of the values of all <i>attributes</i>. Property
251      * values are not included, nor value-bindings.
252      */
253     @Override
254     public Collection<Object> values()
255     {
256         return getUnderlyingMap().values();
257     }
258 
259     /**
260      * Call put(key, value) for each entry in the provided map.
261      */
262     @Override
263     public void putAll(Map<? extends String, ?> t)
264     {
265         for (Map.Entry<? extends String, ?> entry : t.entrySet())
266         {
267             put(entry.getKey(), entry.getValue());
268         }
269     }
270 
271     /**
272      * Return a set of all <i>attributes</i>. Properties of the underlying
273      * UIComponent are not included, nor value-bindings.
274      */
275     @Override
276     public Set<Map.Entry<String, Object>> entrySet()
277     {
278         return getUnderlyingMap().entrySet();
279     }
280 
281     /**
282      * Return a set of the keys for all <i>attributes</i>. Properties of the
283      * underlying UIComponent are not included, nor value-bindings.
284      */
285     @Override
286     public Set<String> keySet()
287     {
288         return getUnderlyingMap().keySet();
289     }
290 
291     /**
292      * In order: get the value of a <i>property</i> of the underlying
293      * UIComponent, read an <i>attribute</i> from this map, or evaluate
294      * the component's value-binding of the specified name.
295      *
296      * @param key must be a String. Any other type will cause ClassCastException.
297      */
298     @Override
299     public Object get(Object key)
300     {
301         checkKey(key);
302         
303         Object value;
304 
305         int keyLength = ((String)key).length();
306         if (keyLength >= MIN_LENGHT_CHECK)
307         {
308             if (MARK_CREATED.length() == keyLength &&
309                 MARK_CREATED.equals(key))
310             {
311                 return _component.getOamVfMarkCreated();
312             }
313             else if (FACET_NAME_KEY.length() == keyLength &&
314                 FACET_NAME_KEY.equals(key))
315             {
316                 return _component.getOamVfFacetName();
317             }
318             else if (COMPONENT_ADDED_BY_HANDLER_MARKER.length() == keyLength &&
319                 COMPONENT_ADDED_BY_HANDLER_MARKER.equals(key))
320             {
321                 return _component.isOamVfAddedByHandler();
322             }
323             else if (FACET_CREATED_UIPANEL_MARKER.length() == keyLength &&
324                 FACET_CREATED_UIPANEL_MARKER.equals(key))
325             {
326                 return _component.isOamVfFacetCreatedUIPanel();
327             }
328         }
329         // is there a javabean property to read?
330         _PropertyDescriptorHolder propertyDescriptor = getPropertyDescriptor((String) key);
331         if (propertyDescriptor != null)
332         {
333             value = getComponentProperty(propertyDescriptor);
334         }
335         else
336         {
337             // is there a literal value to read?
338             value = getUnderlyingMap().get(key);
339             if (value == null)
340             {
341                 // is there a value-binding to read?
342                 ValueExpression ve = _component.getValueExpression((String) key);
343                 if (ve != null)
344                 {
345                     value = ve.getValue(_component.getFacesContext().getELContext());
346                 }
347                 else
348                 {
349                     if (!_isCompositeComponentSet)
350                     {
351                         _isCompositeComponent = getUnderlyingMap().containsKey(Resource.COMPONENT_RESOURCE_KEY);
352                         _isCompositeComponentSet = true;
353                     }
354                     if (_isCompositeComponent)
355                     {
356                         BeanInfo ccBeanInfo = _ccBeanInfo != null ? _ccBeanInfo :
357                             (BeanInfo) getUnderlyingMap().get(UIComponent.BEANINFO_KEY);
358                         if (ccBeanInfo != null)
359                         {
360                             //Fast shortcut to allow fast lookup.
361                             Map<String, PropertyDescriptor> attributeMap = (Map<String, PropertyDescriptor>) 
362                                 ccBeanInfo.getBeanDescriptor().getValue(
363                                     PROPERTY_DESCRIPTOR_MAP_KEY);
364                             if (attributeMap != null)
365                             {
366                                 PropertyDescriptor attribute = attributeMap.get(key);
367                                 if (attribute != null)
368                                 {
369                                     String attributeName = attribute.getName();
370                                     boolean isKnownMethod = "action".equals(attributeName)
371                                             || "actionListener".equals(attributeName)
372                                             || "validator".equals(attributeName)
373                                             || "valueChangeListener".equals(attributeName);
374 
375                                     // <composite:attribute> method-signature attribute is 
376                                     // ValueExpression that must evaluate to String
377                                     ValueExpression methodSignatureExpression
378                                             = (ValueExpression) attribute.getValue("method-signature");
379                                     String methodSignature = null;
380                                     if (methodSignatureExpression != null)
381                                     {
382                                         // Check if the value expression holds a method signature
383                                         // Note that it could be null, so in that case we don't have to 
384                                         // do anything
385                                         methodSignature = (String) methodSignatureExpression.getValue(
386                                                                     _component.getFacesContext().getELContext());
387                                     }
388 
389                                     // either the attributeName has to be a knownMethod
390                                     // or there has to be a method-signature
391                                     if (isKnownMethod || methodSignature != null)
392                                     {
393                                         //In this case it is expecting a ValueExpression
394                                         return attribute.getValue("default");
395                                     }
396                                     else
397                                     {
398                                         value = attribute.getValue("default");
399                                     }
400                                 }
401                             }
402                             else
403                             {
404                                 // Failsafe if another implementation for composite components is set
405                                 for (PropertyDescriptor attribute : ccBeanInfo.getPropertyDescriptors())
406                                 {
407                                     if (attribute.getName().equals(key))
408                                     {
409                                         String attributeName = attribute.getName();
410                                         boolean isKnownMethod = "action".equals(attributeName)
411                                                 || "actionListener".equals(attributeName)
412                                                 || "validator".equals(attributeName)
413                                                 || "valueChangeListener".equals(attributeName);
414 
415                                         // <composite:attribute> method-signature attribute is 
416                                         // ValueExpression that must evaluate to String
417                                         ValueExpression methodSignatureExpression
418                                                 = (ValueExpression) attribute.getValue("method-signature");
419                                         String methodSignature = null;
420                                         if (methodSignatureExpression != null)
421                                         {
422                                             // Check if the value expression holds a method signature
423                                             // Note that it could be null, so in that case we don't have to 
424                                             // do anything
425                                             methodSignature = (String) methodSignatureExpression.getValue(
426                                                                         _component.getFacesContext().getELContext());
427                                         }
428 
429                                         // either the attributeName has to be a knownMethod
430                                         // or there has to be a method-signature
431                                         if (isKnownMethod || methodSignature != null)
432                                         {
433                                             //In this case it is expecting a ValueExpression
434                                             return attribute.getValue("default");
435                                         }
436                                         else
437                                         {
438                                             value = attribute.getValue("default");
439                                             break;
440                                         }
441                                     }
442                                 }
443                             }
444                             // We have to check for a ValueExpression and also evaluate it
445                             // here, because in the PropertyDescriptor the default values are
446                             // always stored as (Tag-)ValueExpressions.
447                             if (value != null && value instanceof ValueExpression)
448                             {
449                                 return ((ValueExpression) value).getValue(_component.getFacesContext().getELContext());
450                             }
451                         }
452                     }
453                     // no value found
454                     //return null;
455                 }
456             }
457         }
458         
459         // Otherwise, return the actual value from the get() method. 
460         return value;
461     }
462 
463     /**
464      * Remove the attribute with the specified name. An attempt to
465      * remove an entry whose name is that of a <i>property</i> on
466      * the underlying UIComponent will cause an IllegalArgumentException.
467      * Value-bindings for the underlying component are ignored.
468      *
469      * @param key must be a String. Any other type will cause ClassCastException.
470      */
471     @Override
472     public Object remove(Object key)
473     {
474         checkKey(key);
475         int keyLength = ((String)key).length();
476         if (keyLength >= MIN_LENGHT_CHECK)
477         {
478             if (MARK_CREATED.length() == keyLength &&
479                 MARK_CREATED.equals(key))
480             {
481                 Object oldValue = _component.getOamVfMarkCreated();
482                 _component.setOamVfMarkCreated(null);
483                 return oldValue;
484             }
485             else if (FACET_NAME_KEY.length() == keyLength &&
486                 FACET_NAME_KEY.equals(key))
487             {
488                 Object oldValue = _component.getOamVfFacetName();
489                 _component.setOamVfFacetName(null);
490                 return oldValue;
491             }
492             else if (COMPONENT_ADDED_BY_HANDLER_MARKER.length() == keyLength &&
493                 COMPONENT_ADDED_BY_HANDLER_MARKER.equals(key))
494             {
495                 Object oldValue = _component.isOamVfAddedByHandler();
496                 _component.setOamVfAddedByHandler(false);
497                 return oldValue;
498             }
499             else if (FACET_CREATED_UIPANEL_MARKER.length() == keyLength &&
500                 FACET_CREATED_UIPANEL_MARKER.equals(key))
501             {
502                 Object oldValue = _component.isOamVfFacetCreatedUIPanel();
503                 _component.setOamVfFacetCreatedUIPanel(false);
504                 return oldValue;
505             }
506             else if (UIComponent.BEANINFO_KEY.length() == keyLength 
507                 && UIComponent.BEANINFO_KEY.equals(key))
508             {
509                 _ccBeanInfo = null;
510             }
511         }
512         _PropertyDescriptorHolder propertyDescriptor = getPropertyDescriptor((String) key);
513         if (propertyDescriptor != null)
514         {
515             throw new IllegalArgumentException("Cannot remove component property attribute");
516         }
517         return _component.getStateHelper().remove(
518                 UIComponentBase.PropertyKeys.attributesMap, key);
519     }
520 
521     /**
522      * Store the provided value as a <i>property</i> on the underlying
523      * UIComponent, or as an <i>attribute</i> in a Map if no such property
524      * exists. Value-bindings associated with the component are ignored; to
525      * write to a value-binding, the value-binding must be explicitly
526      * retrieved from the component and evaluated.
527      * <p/>
528      * Note that this method is different from the get method, which
529      * does read from a value-binding if one exists. When a value-binding
530      * exists for a non-property, putting a value here essentially "masks"
531      * the value-binding until that attribute is removed.
532      * <p/>
533      * The put method is expected to return the previous value of the
534      * property/attribute (if any). Because UIComponent property getter
535      * methods typically try to evaluate any value-binding expression of
536      * the same name this can cause an EL expression to be evaluated,
537      * thus invoking a getter method on the user's model. This is fine
538      * when the returned value will be used; Unfortunately this is quite
539      * pointless when initialising a freshly created component with whatever
540      * attributes were specified in the view definition (eg JSP tag
541      * attributes). Because the UIComponent.getAttributes method
542      * only returns a Map class and this class must be package-private,
543      * there is no way of exposing a "putNoReturn" type method.
544      *
545      * @param key   String, null is not allowed
546      * @param value null is allowed
547      */
548     @Override
549     public Object put(String key, Object value)
550     {
551         if (key == null)
552         {
553             throw new NullPointerException("key");
554         }
555         int keyLength = ((String)key).length();
556         if (keyLength >= MIN_LENGHT_CHECK)
557         {
558             if (MARK_CREATED.length() == keyLength &&
559                 MARK_CREATED.equals(key))
560             {
561                 String oldValue = _component.getOamVfMarkCreated();
562                 _component.setOamVfMarkCreated((String)value);
563                 return oldValue;
564             }
565             else if (FACET_NAME_KEY.length() == keyLength &&
566                 FACET_NAME_KEY.equals(key))
567             {
568                 Object oldValue = _component.getOamVfFacetName();
569                 _component.setOamVfFacetName((String)value);
570                 return oldValue;
571             }
572             else if (COMPONENT_ADDED_BY_HANDLER_MARKER.length() == keyLength &&
573                 COMPONENT_ADDED_BY_HANDLER_MARKER.equals(key))
574             {
575                 Object oldValue = _component.isOamVfAddedByHandler();
576                 _component.setOamVfAddedByHandler((Boolean)value);
577                 return oldValue;
578             }
579             else if (FACET_CREATED_UIPANEL_MARKER.length() == keyLength &&
580                 FACET_CREATED_UIPANEL_MARKER.equals(key))
581             {
582                 Object oldValue = _component.isOamVfFacetCreatedUIPanel();
583                 _component.setOamVfFacetCreatedUIPanel((Boolean)value);
584                 return oldValue;
585             }
586         }
587         _PropertyDescriptorHolder propertyDescriptor = getPropertyDescriptor(key);
588         if (propertyDescriptor == null)
589         {
590             if (value == null)
591             {
592                 throw new NullPointerException("value is null for a not available property: " + key);
593             }
594         }
595         else
596         {
597             if (propertyDescriptor.getReadMethod() != null)
598             {
599                 Object oldValue = getComponentProperty(propertyDescriptor);
600                 setComponentProperty(propertyDescriptor, value);
601                 return oldValue;
602             }
603             setComponentProperty(propertyDescriptor, value);
604             return null;
605         }
606         // To keep this code in good shape, The fastest way to compare is look if the length first here
607         // because we avoid an unnecessary cast later on equals().
608         if ( Resource.COMPONENT_RESOURCE_KEY.length() == keyLength 
609              && Resource.COMPONENT_RESOURCE_KEY.equals(key))
610         {
611             _isCompositeComponent = true;
612             _isCompositeComponentSet = true;
613         }
614         if (UIComponent.BEANINFO_KEY.length() == keyLength 
615             && UIComponent.BEANINFO_KEY.equals(key))
616         {
617             _ccBeanInfo = (BeanInfo) value;
618         }
619         return _component.getStateHelper().put(UIComponentBase.PropertyKeys.attributesMap, key, value);
620     }
621 
622     /**
623      * Retrieve info about getter/setter methods for the javabean property
624      * of the specified name on the underlying UIComponent object.
625      * <p/>
626      * This method optimises access to javabean properties of the underlying
627      * UIComponent by maintaining a cache of ProperyDescriptor objects for
628      * that class.
629      * <p/>
630      * TODO: Consider making the cache shared between component instances;
631      * currently 100 UIInputText components means performing introspection
632      * on the UIInputText component 100 times.
633      */
634     private _PropertyDescriptorHolder getPropertyDescriptor(String key)
635     {
636         if (_propertyDescriptorMap == null)
637         {
638             // Try to get descriptor map from cache
639             _propertyDescriptorMap = propertyDescriptorCache.get(_component.getClass());
640             // Cache miss: create descriptor map and put it in cache
641             if (_propertyDescriptorMap == null)
642             {
643                 // Create descriptor map...
644                 BeanInfo beanInfo;
645                 try
646                 {
647                     beanInfo = Introspector.getBeanInfo(_component.getClass());
648                 }
649                 catch (IntrospectionException e)
650                 {
651                     throw new FacesException(e);
652                 }
653                 PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
654                 _propertyDescriptorMap = new ConcurrentHashMap<String, _PropertyDescriptorHolder>();
655                 for (int i = 0; i < propertyDescriptors.length; i++)
656                 {
657                     PropertyDescriptor propertyDescriptor = propertyDescriptors[i];
658                     Method readMethod = propertyDescriptor.getReadMethod();
659                     if (readMethod != null)
660                     {
661                         _propertyDescriptorMap.put(propertyDescriptor.getName(),
662                                 new _PropertyDescriptorHolder(propertyDescriptor, readMethod));
663                     }
664                 }
665                 // ... and put it in cache
666                 synchronized(propertyDescriptorCache)
667                 {
668                     // Use a synchronized block to ensure proper operation on concurrent use cases.
669                     // This is a racy single check, because initialization over the same class could happen
670                     // multiple times, but the same result is always calculated. The synchronized block 
671                     // just ensure thread-safety, because only one thread will modify the cache map
672                     // at the same time.
673                     propertyDescriptorCache.put(_component.getClass(), _propertyDescriptorMap);
674                 }
675             }
676         }
677         return _propertyDescriptorMap.get(key);
678     }
679 
680 
681     /**
682      * Execute the getter method of the specified property on the underlying
683      * component.
684      *
685      * @param propertyDescriptor specifies which property to read.
686      * @return the value returned by the getter method.
687      * @throws IllegalArgumentException if the property is not readable.
688      * @throws FacesException           if any other problem occurs while invoking
689      *                                  the getter method.
690      */
691     private Object getComponentProperty(_PropertyDescriptorHolder propertyDescriptor)
692     {
693         Method readMethod = propertyDescriptor.getReadMethod();
694         if (readMethod == null)
695         {
696             throw new IllegalArgumentException("Component property " + propertyDescriptor.getName()
697                                                + " is not readable");
698         }
699         try
700         {
701             return readMethod.invoke(_component, EMPTY_ARGS);
702         }
703         catch (Exception e)
704         {
705             FacesContext facesContext = _component.getFacesContext();
706             throw new FacesException("Could not get property " + propertyDescriptor.getName() + " of component "
707                                      + _component.getClientId(facesContext), e);
708         }
709     }
710 
711     /**
712      * Execute the setter method of the specified property on the underlying
713      * component.
714      *
715      * @param propertyDescriptor specifies which property to write.
716      * @throws IllegalArgumentException if the property is not writable.
717      * @throws FacesException           if any other problem occurs while invoking
718      *                                  the getter method.
719      */
720     private void setComponentProperty(_PropertyDescriptorHolder propertyDescriptor, Object value)
721     {
722         Method writeMethod = propertyDescriptor.getWriteMethod();
723         if (writeMethod == null)
724         {
725             throw new IllegalArgumentException("Component property " + propertyDescriptor.getName()
726                                                + " is not writable");
727         }
728         try
729         {
730             writeMethod.invoke(_component, new Object[]{value});
731         }
732         catch (Exception e)
733         {
734             FacesContext facesContext = _component.getFacesContext();
735             throw new FacesException("Could not set property " + propertyDescriptor.getName() +
736                     " of component " + _component.getClientId(facesContext) + " to value : " + value + " with type : " +
737                     (value == null ? "null" : value.getClass().getName()), e);
738         }
739     }
740 
741     private void checkKey(Object key)
742     {
743         if (key == null)
744         {
745             throw new NullPointerException("key");
746         }
747         if (!(key instanceof String))
748         {
749             throw new ClassCastException("key is not a String");
750         }
751     }
752 
753     /**
754      * Return the map containing the attributes.
755      * <p/>
756      * This method is package-scope so that the UIComponentBase class can access it
757      * directly when serializing the component.
758      */
759     Map<String, Object> getUnderlyingMap()
760     {
761         StateHelper stateHelper = _component.getStateHelper(false);
762         Map<String, Object> attributes = null;
763         if (stateHelper != null)
764         {
765             attributes = (Map<String, Object>) stateHelper.get(UIComponentBase.PropertyKeys.attributesMap);
766         }
767         return attributes == null ? Collections.EMPTY_MAP : attributes;
768     }
769     
770     /**
771      * TODO: Document why this method is necessary, and why it doesn't try to
772      * compare the _component field.
773      */
774     @Override
775     public boolean equals(Object obj)
776     {
777         return getUnderlyingMap().equals(obj);
778     }
779 
780     @Override
781     public int hashCode()
782     {
783         return getUnderlyingMap().hashCode();
784     }
785 }