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