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 }