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 }