001package jmri.beans; 002 003import java.beans.IndexedPropertyDescriptor; 004import java.beans.IntrospectionException; 005import java.beans.Introspector; 006import java.beans.PropertyChangeListener; 007import java.beans.PropertyChangeListenerProxy; 008import java.beans.PropertyDescriptor; 009import java.lang.reflect.InvocationTargetException; 010import java.util.HashSet; 011import java.util.Set; 012 013import javax.annotation.Nonnull; 014 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017 018/** 019 * JMRI-specific tools for the introspection of JavaBean properties. 020 * 021 * @author Randall Wood 022 */ 023public class BeanUtil { 024 025 private static final Logger log = LoggerFactory.getLogger(BeanUtil.class); 026 027 private BeanUtil() { 028 // prevent construction since all methods are static 029 } 030 031 /** 032 * Set element <i>index</i> of property <i>key</i> of <i>bean</i> to 033 * <i>value</i>. 034 * <p> 035 * If <i>bean</i> implements {@link BeanInterface}, this method calls 036 * {@link jmri.beans.BeanInterface#setIndexedProperty(java.lang.String, int, java.lang.Object)} 037 * otherwise it calls 038 * {@link jmri.beans.BeanUtil#setIntrospectedIndexedProperty(java.lang.Object, java.lang.String, int, java.lang.Object)} 039 * 040 * @param bean The bean to update. 041 * @param key The indexed property to set. 042 * @param index The element to use. 043 * @param value The value to set. 044 * @see jmri.beans.BeanInterface#setIndexedProperty(java.lang.String, int, 045 * java.lang.Object) 046 */ 047 public static void setIndexedProperty(Object bean, String key, int index, Object value) { 048 if (implementsBeanInterface(bean)) { 049 ((BeanInterface) bean).setIndexedProperty(key, index, value); 050 } else { 051 setIntrospectedIndexedProperty(bean, key, index, value); 052 } 053 } 054 055 /** 056 * Set element <i>index</i> of property <i>key</i> of <i>bean</i> to 057 * <i>value</i>. 058 * <p> 059 * This method relies on the standard JavaBeans coding patterns to get and 060 * invoke the setter for the property. Note that if <i>key</i> is not a 061 * {@link String}, this method will not attempt to set the property 062 * (JavaBeans introspection rules require that <i>key</i> be a String, while 063 * other JMRI coding patterns accept that <i>key</i> can be an Object). Note 064 * also that the setter must be public. This should only be called from 065 * outside this class in an implementation of 066 * {@link jmri.beans.BeanInterface#setIndexedProperty(java.lang.String, int, java.lang.Object)}, 067 * but is public so it can be accessed by any potential implementation of 068 * that method. 069 * 070 * @param bean The bean to update. 071 * @param key The indexed property to set. 072 * @param index The element to use. 073 * @param value The value to set. 074 */ 075 public static void setIntrospectedIndexedProperty(Object bean, String key, int index, Object value) { 076 if (bean != null && key != null) { 077 try { 078 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 079 for (PropertyDescriptor pd : pds) { 080 if (pd instanceof IndexedPropertyDescriptor && pd.getName().equalsIgnoreCase(key)) { 081 ((IndexedPropertyDescriptor) pd).getIndexedWriteMethod().invoke(bean, index, value); 082 // short circuit, since there is nothing left to do at 083 // this point 084 return; 085 } 086 } 087 // catch only introspection-related exceptions, and allow all 088 // other to pass through 089 } catch ( 090 IllegalAccessException | 091 IllegalArgumentException | 092 InvocationTargetException | 093 IntrospectionException ex) { 094 log.warn("Exception: ", ex); 095 } 096 } 097 } 098 099 /** 100 * Get the item at index <i>index</i> of property <i>key</i> of <i>bean</i>. 101 * If the index <i>index</i> of property <i>key</i> does not exist, this 102 * method returns null instead of throwing 103 * {@link java.lang.ArrayIndexOutOfBoundsException} do to the inability to 104 * get the size of the indexed property using introspection. 105 * 106 * @param bean The bean to inspect. 107 * @param key The indexed property to get. 108 * @param index The element to return. 109 * @return the value at <i>index</i> or null 110 */ 111 public static Object getIndexedProperty(Object bean, String key, int index) { 112 if (implementsBeanInterface(bean)) { 113 return ((BeanInterface) bean).getIndexedProperty(key, index); 114 } else { 115 return getIntrospectedIndexedProperty(bean, key, index); 116 } 117 } 118 119 /** 120 * Get the item at index <i>index</i> of property <i>key</i> of <i>bean</i>. 121 * This should only be called from outside this class in an implementation 122 * of 123 * {@link jmri.beans.BeanInterface#setProperty(java.lang.String, java.lang.Object)}, 124 * but is public so it can be accessed by any potential implementation of 125 * that method. 126 * 127 * @param bean The bean to inspect. 128 * @param key The indexed property to get. 129 * @param index The element to return. 130 * @return the value at <i>index</i> or null 131 */ 132 public static Object getIntrospectedIndexedProperty(Object bean, String key, int index) { 133 if (bean != null && key != null) { 134 try { 135 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 136 for (PropertyDescriptor pd : pds) { 137 if (pd instanceof IndexedPropertyDescriptor && pd.getName().equalsIgnoreCase(key)) { 138 return ((IndexedPropertyDescriptor) pd).getIndexedReadMethod().invoke(bean, index); 139 } 140 } 141 // catch only introspection-related exceptions, and allow all 142 // other to pass through 143 } catch (InvocationTargetException ex) { 144 Throwable tex = ex.getCause(); 145 if (tex instanceof RuntimeException) { 146 throw (RuntimeException) tex; 147 } else { 148 log.error("RuntimeException: ", ex); 149 } 150 } catch ( 151 IllegalAccessException | 152 IllegalArgumentException | 153 IntrospectionException ex) { 154 log.warn("Exception: ", ex); 155 } 156 } 157 return null; 158 } 159 160 /** 161 * Set property <i>key</i> of <i>bean</i> to <i>value</i>. 162 * <p> 163 * If <i>bean</i> implements {@link BeanInterface}, this method calls 164 * {@link jmri.beans.BeanInterface#setProperty(java.lang.String, java.lang.Object)}, 165 * otherwise it calls 166 * {@link jmri.beans.BeanUtil#setIntrospectedProperty(java.lang.Object, java.lang.String, java.lang.Object)}. 167 * 168 * @param bean The bean to update. 169 * @param key The property to set. 170 * @param value The value to set. 171 * @see jmri.beans.BeanInterface#setProperty(java.lang.String, 172 * java.lang.Object) 173 */ 174 public static void setProperty(Object bean, String key, Object value) { 175 if (implementsBeanInterface(bean)) { 176 ((BeanInterface) bean).setProperty(key, value); 177 } else { 178 setIntrospectedProperty(bean, key, value); 179 } 180 } 181 182 /** 183 * Set property <i>key</i> of <i>bean</i> to <i>value</i>. 184 * <p> 185 * This method relies on the standard JavaBeans coding patterns to get and 186 * invoke the property's write method. Note that if <i>key</i> is not a 187 * {@link String}, this method will not attempt to set the property 188 * (JavaBeans introspection rules require that <i>key</i> be a String, while 189 * other JMRI coding patterns accept that <i>key</i> can be an Object). This 190 * should only be called from outside this class in an implementation of 191 * {@link jmri.beans.BeanInterface#setProperty(java.lang.String, java.lang.Object)}, 192 * but is public so it can be accessed by any potential implementation of 193 * that method. 194 * 195 * @param bean The bean to update. 196 * @param key The property to set. 197 * @param value The value to set. 198 */ 199 public static void setIntrospectedProperty(Object bean, String key, Object value) { 200 if (bean != null && key != null) { 201 try { 202 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 203 for (PropertyDescriptor pd : pds) { 204 if (pd.getName().equalsIgnoreCase(key)) { 205 pd.getWriteMethod().invoke(bean, value); 206 return; // short circut, since there is nothing left to 207 // do at this point. 208 } 209 } 210 // catch only introspection-related exceptions, and allow all 211 // other to pass through 212 } catch ( 213 IllegalAccessException | 214 IllegalArgumentException | 215 InvocationTargetException | 216 IntrospectionException ex) { 217 log.warn("Exception: ", ex); 218 } 219 } 220 } 221 222 /** 223 * Get the property <i>key</i> of <i>bean</i>. 224 * <p> 225 * If the property <i>key</i> cannot be found, this method returns null. 226 * <p> 227 * If <i>bean</i> implements {@link BeanInterface}, this method calls 228 * {@link jmri.beans.BeanInterface#getProperty(java.lang.String)}, otherwise 229 * it calls 230 * {@link jmri.beans.BeanUtil#getIntrospectedProperty(java.lang.Object, java.lang.String)}. 231 * 232 * @param bean The bean to inspect. 233 * @param key The property to get. 234 * @return value of property <i>key</i> 235 * @see jmri.beans.BeanInterface#getProperty(java.lang.String) 236 */ 237 public static Object getProperty(Object bean, String key) { 238 if (implementsBeanInterface(bean)) { 239 return ((BeanInterface) bean).getProperty(key); 240 } else { 241 return getIntrospectedProperty(bean, key); 242 } 243 } 244 245 /** 246 * Get the property <i>key</i> of <i>bean</i>. 247 * <p> 248 * If the property <i>key</i> cannot be found, this method returns null. 249 * <p> 250 * This method relies on the standard JavaBeans coding patterns to get and 251 * invoke the property's read method. Note that if <i>key</i> is not a 252 * {@link String}, this method will not attempt to get the property 253 * (JavaBeans introspection rules require that <i>key</i> be a String, while 254 * other JMRI coding patterns accept that <i>key</i> can be an Object). This 255 * should only be called from outside this class in an implementation of 256 * {@link jmri.beans.BeanInterface#getProperty(java.lang.String)}, but is 257 * public so it can be accessed by any potential implementation of that 258 * method. 259 * 260 * @param bean The bean to inspect. 261 * @param key The property to get. 262 * @return value of property <i>key</i> or null 263 */ 264 public static Object getIntrospectedProperty(Object bean, String key) { 265 if (bean != null && key != null) { 266 try { 267 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 268 for (PropertyDescriptor pd : pds) { 269 if (pd.getName().equalsIgnoreCase(key)) { 270 return pd.getReadMethod().invoke(bean, (Object[]) null); 271 } 272 } 273 // catch only introspection-related exceptions, and allow all 274 // other to pass through 275 } catch ( 276 IllegalAccessException | 277 IllegalArgumentException | 278 InvocationTargetException | 279 IntrospectionException ex) { 280 log.warn("Exception: ", ex); 281 } 282 } 283 return null; 284 } 285 286 /** 287 * Test if <i>bean</i> has the property <i>key</i>. 288 * <p> 289 * If <i>bean</i> implements {@link BeanInterface}, this method calls 290 * {@link jmri.beans.BeanInterface#hasProperty(java.lang.String)}, otherwise 291 * it calls 292 * {@link jmri.beans.BeanUtil#hasIntrospectedProperty(java.lang.Object, java.lang.String)}. 293 * 294 * @param bean The bean to inspect. 295 * @param key The property key to check for. 296 * @return true if <i>bean</i> has property <i>key</i> 297 */ 298 public static boolean hasProperty(Object bean, String key) { 299 if (implementsBeanInterface(bean)) { 300 return ((BeanInterface) bean).hasProperty(key); 301 } else { 302 return hasIntrospectedProperty(bean, key); 303 } 304 } 305 306 /** 307 * Test if <i>bean</i> has the indexed property <i>key</i>. 308 * <p> 309 * If <i>bean</i> implements {@link BeanInterface}, this method calls 310 * {@link jmri.beans.BeanInterface#hasIndexedProperty(java.lang.String)}, 311 * otherwise it calls 312 * {@link jmri.beans.BeanUtil#hasIntrospectedIndexedProperty(java.lang.Object, java.lang.String)}. 313 * 314 * @param bean The bean to inspect. 315 * @param key The indexed property to check for. 316 * @return true if <i>bean</i> has indexed property <i>key</i> 317 */ 318 public static boolean hasIndexedProperty(Object bean, String key) { 319 if (BeanUtil.implementsBeanInterface(bean)) { 320 return ((BeanInterface) bean).hasIndexedProperty(key); 321 } else { 322 return BeanUtil.hasIntrospectedIndexedProperty(bean, key); 323 } 324 } 325 326 /** 327 * Test that <i>bean</i> has the property <i>key</i>. 328 * <p> 329 * This method relies on the standard JavaBeans coding patterns to find the 330 * property. Note that if <i>key</i> is not a {@link String}, this method 331 * will not attempt to find the property (JavaBeans introspection rules 332 * require that <i>key</i> be a String, while other JMRI coding patterns 333 * accept that <i>key</i> can be an Object). This should only be called from 334 * outside this class in an implementation of 335 * {@link jmri.beans.BeanInterface#hasProperty(java.lang.String)}, but is 336 * public so it can be accessed by any potential implementation of that 337 * method. 338 * 339 * @param bean The bean to inspect. 340 * @param key The property to check for. 341 * @return true if <i>bean</i> has property <i>key</i> 342 */ 343 public static boolean hasIntrospectedProperty(Object bean, String key) { 344 if (bean != null && key != null) { 345 try { 346 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 347 for (PropertyDescriptor pd : pds) { 348 if (pd.getName().equalsIgnoreCase(key)) { 349 return true; 350 } 351 } 352 // catch only introspection-related exceptions, and allow all 353 // other to pass through 354 } catch (IntrospectionException ex) { 355 log.warn("Exception: ", ex); 356 } 357 } 358 return false; 359 } 360 361 /** 362 * Test that <i>bean</i> has the indexed property <i>key</i>. 363 * <p> 364 * This method relies on the standard JavaBeans coding patterns to find the 365 * property. Note that if <i>key</i> is not a {@link String}, this method 366 * will not attempt to find the property (JavaBeans introspection rules 367 * require that <i>key</i> be a String, while other JMRI coding patterns 368 * accept that <i>key</i> can be an Object). This should only be called from 369 * outside this class in an implementation of 370 * {@link jmri.beans.BeanInterface#hasIndexedProperty(java.lang.String)}, 371 * but is public so it can be accessed by any potential implementation of 372 * that method. 373 * 374 * @param bean The bean to inspect. 375 * @param key The indexed property to check for. 376 * @return true if <i>bean</i> has indexed property <i>key</i> 377 */ 378 public static boolean hasIntrospectedIndexedProperty(Object bean, String key) { 379 if (bean != null && key != null) { 380 try { 381 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 382 for (PropertyDescriptor pd : pds) { 383 if (pd instanceof IndexedPropertyDescriptor && pd.getName().equalsIgnoreCase(key)) { 384 return true; 385 } 386 } 387 // catch only introspection-related exceptions, and allow all 388 // other to pass through 389 } catch (IntrospectionException ex) { 390 log.warn("Introspection Exception: ", ex); 391 } 392 } 393 return false; 394 } 395 396 public static Set<String> getPropertyNames(Object bean) { 397 if (bean != null) { 398 if (implementsBeanInterface(bean)) { 399 return ((BeanInterface) bean).getPropertyNames(); 400 } else { 401 return getIntrospectedPropertyNames(bean); 402 } 403 } 404 return new HashSet<>(); // return an empty set instead of null 405 } 406 407 /** 408 * Use an {@link java.beans.Introspector} to get a set of the named 409 * properties of the bean. Note that properties discovered through this 410 * mechanism must have public accessors per the JavaBeans specification. 411 * This should only be called from outside this class in an implementation 412 * of {@link jmri.beans.BeanInterface#getPropertyNames()}, but is public so 413 * it can be accessed by any potential implementation of that method. 414 * 415 * @param bean The bean to inspect. 416 * @return {@link Set} of property names 417 */ 418 public static Set<String> getIntrospectedPropertyNames(Object bean) { 419 HashSet<String> names = new HashSet<>(); 420 if (bean != null) { 421 try { 422 PropertyDescriptor[] pds = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors(); 423 for (PropertyDescriptor pd : pds) { 424 names.add(pd.getName()); 425 } 426 // catch only introspection-related exceptions, and allow all 427 // other to pass through 428 } catch (IntrospectionException ex) { 429 log.warn("Introspection Exception: ", ex); 430 } 431 } 432 return names; 433 } 434 435 /** 436 * Test that <i>bean</i> implements {@link jmri.beans.BeanInterface}. 437 * 438 * @param bean The bean to inspect. 439 * @return true if <i>bean</i> implements BeanInterface. 440 */ 441 public static boolean implementsBeanInterface(Object bean) { 442 return (null != bean && BeanInterface.class.isAssignableFrom(bean.getClass())); 443 } 444 445 /** 446 * Test that <i>listeners</i> contains <i>needle</i> even if listener is 447 * contained within a {@link PropertyChangeListenerProxy}. 448 * <p> 449 * This is intended to be used where action needs to be taken (or not taken) 450 * if <i>needle</i> is (or is not) listening for property changes. Note that 451 * if a listener was registered to listen for changes in a single property, 452 * it is wrapped by a PropertyChangeListenerProxy such that using 453 * {@code Arrays.toList(getPropertyChangeListeners()).contains(needle) } may 454 * return false when <i>needle</i> is listening to a specific property. 455 * 456 * @param listeners the array of listeners to search through 457 * @param needle the listener to search for 458 * @return true if <i>needle</i> is in <i>listeners</i>; false otherwise 459 */ 460 public static boolean contains(PropertyChangeListener[] listeners, @Nonnull PropertyChangeListener needle) { 461 for (PropertyChangeListener listener : listeners) { 462 if (listener.equals(needle) || 463 (listener instanceof PropertyChangeListenerProxy && 464 ((PropertyChangeListenerProxy) listener).getListener().equals(needle))) { 465 return true; 466 } 467 } 468 return false; 469 } 470}