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 }