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}