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