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 }