Coverage Report - org.openpermis.editor.policy.beans.BeanUtilities
 
Classes in this File Line Coverage Branch Coverage Complexity
BeanUtilities
81%
122/150
89%
129/144
9.5
 
 1  
 /*
 2  
  * Copyright (c) 2009, Swiss Federal Department of Defence Civil Protection and Sport
 3  
  *                     (http://www.vbs.admin.ch)
 4  
  * Copyright (c) 2009, Ergon Informatik AG (http://www.ergon.ch)
 5  
  * All rights reserved.
 6  
  *
 7  
  * Licensed under the Open Permis License which accompanies this distribution,
 8  
  * and is available at http://www.openpermis.org/BSDlicenceKent.txt
 9  
  */
 10  
 package org.openpermis.editor.policy.beans;
 11  
 
 12  
 import java.beans.PropertyChangeListener;
 13  
 import java.lang.reflect.InvocationTargetException;
 14  
 import java.lang.reflect.Method;
 15  
 import java.lang.reflect.Modifier;
 16  
 import java.util.Arrays;
 17  
 import java.util.HashMap;
 18  
 import java.util.Map;
 19  
 
 20  
 /**
 21  
  * Utility functions around Java Beans.
 22  
  * @since 0.1.0
 23  
  */
 24  
 final class BeanUtilities {
 25  
 
 26  
         //---- Static
 27  
         
 28  
         /**
 29  
          * Method name of 'addPropertyChangeListener' support method.
 30  
          * @since 0.1.0
 31  
          */
 32  
         private static final String ADD = "addPropertyChangeListener";
 33  
         
 34  
         /**
 35  
          * Method name of 'removePropertyChangeListener' support method.
 36  
          * @since 0.1.0
 37  
          */
 38  
         private static final String REMOVE = "removePropertyChangeListener";
 39  
         
 40  
         /**
 41  
          * Prefix of Java Beans setter methods.
 42  
          * @since 0.1.0
 43  
          */
 44  
         private static final String SET_PREFIX = "set";
 45  
         
 46  
         /**
 47  
          * Prefix of Java Beans getter methods.
 48  
          * @since 0.1.0
 49  
          */
 50  
         private static final String GET_PREFIX = "get";
 51  
         
 52  
         /**
 53  
          * Alternative prefix of Java Beans boolean getter methods.
 54  
          * @since 0.1.0
 55  
          */
 56  
         private static final String IS_PREFIX = "is";
 57  
                 
 58  
         /**
 59  
          * An array of allowed number of arguments in a property change method.
 60  
          * @since 0.1.0
 61  
          */
 62  1
         private static final int[] ARG_COUNT = { 0, 1, 2, 4 };
 63  
 
 64  
         /**
 65  
          * The source argument position in a property change method.
 66  
          * @since 0.1.0
 67  
          */
 68  
         private static final int ARG_SOURCE = 0;
 69  
 
 70  
         /**
 71  
          * The property argument position in a property change method.
 72  
          * @since 0.1.0
 73  
          */
 74  
         private static final int ARG_PROPERTY = 1;
 75  
 
 76  
         /**
 77  
          * The old value argument position in a property change method.
 78  
          * @since 0.1.0
 79  
          */
 80  
         private static final int ARG_OLD_VALUE = 2;
 81  
 
 82  
         /**
 83  
          * The new value argument position in a property change method.
 84  
          * @since 0.1.0
 85  
          */
 86  
         private static final int ARG_NEW_VALUE = 3;
 87  
         
 88  
         /**
 89  
          * Composes a method name given the specified prefix and property name.
 90  
          * @param prefix the prefix to use.
 91  
          * @param property the name of the property, must not be {@code null} or empty.
 92  
          * @return the method name requested.
 93  
          * @since 0.1.0
 94  
          */
 95  
         private static final String methodName (String prefix, String property) {
 96  88
                 if (property == null) {
 97  4
                         throw new IllegalArgumentException("Invalid property name [null].");
 98  84
                 } else if (property.length() == 0) {
 99  4
                         throw new IllegalArgumentException("Invalid empty property name.");
 100  
                 }
 101  80
                 final StringBuilder sb = new StringBuilder(prefix);
 102  80
                 sb.append(Character.toUpperCase(property.charAt(0)));
 103  80
                 sb.append(property.substring(1));
 104  80
                 return sb.toString();
 105  
         }
 106  
         
 107  
         /**
 108  
          * Returns the method name of the getter for the specified property.
 109  
          * @param property the name of the property, must not be {@code null} or empty.
 110  
          * @param booleanValue indicates if the property is a boolean value.
 111  
          * @return the method name of the getter for the specified property.
 112  
          * @since 0.1.0
 113  
          */
 114  
         static final String getterName (String property, boolean booleanValue) {
 115  65
                 return methodName(booleanValue ? IS_PREFIX : GET_PREFIX, property);
 116  
         }
 117  
         
 118  
         /**
 119  
          * Returns the method name of the setter for the specified property.
 120  
          * @param property the name of the property, must not be {@code null} or empty.
 121  
          * @return the method name of the setter for the specified property.
 122  
          * @since 0.1.0
 123  
          */
 124  
         static final String setterName (String property) {
 125  23
                 return methodName(SET_PREFIX, property);
 126  
         }
 127  
         
 128  
         /**
 129  
          * Returns the signature of the specified method.
 130  
          * <p>Returns the name of the method including its parameter type if non-{@code null}.</p>
 131  
          * @param beanOrClass the bean or the bean class for which to create the signature. 
 132  
          * @param methodName the name of the method.
 133  
          * @param parameterClass the parameter type or {@code null} if the method has no parameters.
 134  
          * @return the signature requested.
 135  
          * @since 0.1.0
 136  
          */
 137  
         static String signature (
 138  
                 Object beanOrClass, String methodName, Class<?> parameterClass
 139  
         ) {
 140  8
                 if (methodName == null) {
 141  2
                         throw new IllegalArgumentException("Invalid method name [null].");
 142  6
                 } else if (methodName.length() == 0) {
 143  1
                         throw new IllegalArgumentException("Invalid empty method name.");
 144  
                 }
 145  5
                 final StringBuilder sb = new StringBuilder();
 146  5
                 if (beanOrClass != null) {
 147  3
                         final Class<?> cls = beanOrClass instanceof Class<?> ? 
 148  
                                 (Class<?>) beanOrClass : beanOrClass.getClass();
 149  3
                         sb.append(cls.getName()).append('.');
 150  
                 }
 151  5
                 sb.append(methodName).append('(');
 152  5
                 if (parameterClass != null) {
 153  3
                         sb.append(parameterClass.getName());
 154  
                 }
 155  5
                 return sb.append(')').toString();
 156  
         }
 157  
         
 158  
         /**
 159  
          * Returns the method with the specified name and parameter class in the given class.
 160  
          * @param cls the class for which to retrieve the method, must not be {@code null}.
 161  
          * @param methodName the name of the method, must not be {@code null} or empty.
 162  
          * @param parameterClass the type of the parameter, may be {@code null} for methods without
 163  
          * any parameters.
 164  
          * @return the method requested, {@code null} if the method does not exist.
 165  
          * @since 0.1.0
 166  
          */
 167  
         public static Method method (Class<?> cls, String methodName, Class<?> parameterClass) {
 168  263
                 if (cls == null) {
 169  2
                         throw new IllegalArgumentException("Invalid class [null].");
 170  
                 }
 171  261
                 if (methodName == null) {
 172  1
                         throw new IllegalArgumentException("Invalid method name [null].");
 173  260
                 } else if (methodName.length() == 0) {
 174  1
                         throw new IllegalArgumentException("Invalid empty method name.");
 175  
                 }
 176  
                 try {
 177  259
                         final Class<?>[] parameters = parameterClass == null ? 
 178  
                                 new Class<?>[0] : new Class<?>[] { parameterClass };
 179  259
                         return cls.getMethod(methodName, parameters);
 180  0
                 } catch (SecurityException e) {
 181  0
                         throw new IllegalArgumentException(
 182  
                                 "Cannot access method [" + signature(cls, methodName, parameterClass) + "].", e
 183  
                         );
 184  17
                 } catch (NoSuchMethodException e) {
 185  17
                         return null;
 186  
                 }
 187  
         }
 188  
         
 189  
         /**
 190  
          * Finds the method for the getter of the specified property.
 191  
          * @note The return type is not checked.
 192  
          * @param beanClass the Java bean class to search for the getter, must not be {@code null}.
 193  
          * @param property the name of the property, must not be {@code null} or empty.
 194  
          * @return the getter requested or {@code null} if there is no such getter.
 195  
          * @since 0.1.0
 196  
          */
 197  
         public static Method getter (Class<?> beanClass, String property) {
 198  47
                 Method method = method(beanClass, getterName(property, false), null);
 199  44
                 if (method == null) {
 200  
                         // Fallback, try boolean getter name.
 201  8
                         method = method(beanClass, getterName(property, true), null);
 202  
                 }
 203  44
                 if (method == null) {
 204  3
                         return null;
 205  
                 }
 206  41
                 if (Void.TYPE.equals(method.getReturnType())) {
 207  1
                         return null;
 208  
                 }
 209  40
                 return method;
 210  
         }
 211  
         
 212  
         /**
 213  
          * Finds the method for the setter of the specified property.
 214  
          * @note The parameter type is not checked.
 215  
          * @param beanClass the Java bean class to search for the setter, must not be {@code null}.
 216  
          * @param property the name of the property, must not be {@code null} or empty.
 217  
          * @return the getter requested or {@code null} if there is no such setter.
 218  
          * @since 0.1.0
 219  
          */
 220  
         public static Method setter (Class<?> beanClass, String property) {
 221  18
                 if (beanClass == null) {
 222  1
                         throw new IllegalArgumentException("Invalid class [null].");
 223  
                 }
 224  17
                 final String setterName = setterName(property);
 225  
                 try {
 226  192
                         for (Method method : beanClass.getMethods()) {
 227  189
                                 if (
 228  
                                         setterName.equals(method.getName()) && 
 229  
                                         method.getParameterTypes().length == 1
 230  
                                 ) {
 231  12
                                         return method;
 232  
                                 }
 233  
                         }
 234  0
                 } catch (SecurityException e) {
 235  0
                         throw new IllegalArgumentException(
 236  
                                 "Cannot access methods of class [" + beanClass.getName() + "].", e
 237  
                         );
 238  3
                 }
 239  3
                 return null;
 240  
         }
 241  
         
 242  
         /**
 243  
          * Tests if the specified class provides Java Beans support.
 244  
          * @param cls the class to test, may be {@code null}.
 245  
          * @return {@code true} if the class provides Java Beans support.
 246  
          * @since 0.1.0
 247  
          */
 248  
         public static boolean isJavaBean (Class<?> cls) {
 249  88
                 return 
 250  
                         cls != null && 
 251  
                         method(cls, ADD, PropertyChangeListener.class) != null &&
 252  
                         method(cls, REMOVE, PropertyChangeListener.class) != null;
 253  
         }
 254  
         
 255  
         /**
 256  
          * Tests if the specified object is a Java Bean.
 257  
          * @param object the object to test, may be {@code null}.
 258  
          * @return {@code true} if the object is a Java Bean.
 259  
          * @since 0.1.0
 260  
          */
 261  
         public static boolean isJavaBean (Object object) {
 262  22
                 return object != null && isJavaBean(object.getClass());
 263  
         }
 264  
         
 265  
         /**
 266  
          * Validates the property change object and throws illegal state exceptions if it is not valid.
 267  
          * @param method the method to validate the object for, must not be {@code null} (not checked).
 268  
          * @param pc the property change object to validate, must not be {@code null} (not checked).
 269  
          * @throws IllegalStateException if the property change object is invalid.
 270  
          * @since 0.1.0
 271  
          */
 272  
         static void validate (Method method, PropertyChange pc) {
 273  63
                 if (pc.bean() == null) {
 274  0
                         throw new IllegalStateException(
 275  
                                 "@PropertyChange bean is [null] for method [" + method + "]."
 276  
                         );
 277  
                 }
 278  63
                 if (pc.property() == null) {
 279  0
                         throw new IllegalStateException(
 280  
                                 "@PropertyChange property is [null] for method [" + method + "]."
 281  
                         );
 282  
                 }
 283  63
                 if (!isJavaBean(pc.bean())) {
 284  1
                         throw new IllegalStateException(
 285  
                                 "@PropertyChange bean [" + pc.bean().getName() + 
 286  
                                 "] is not a Java Bean for method [" + method + "]."
 287  
                         );
 288  
                 }
 289  62
                 Class<?>[] parameterTypes = method.getParameterTypes();
 290  62
                 boolean validLength = false;
 291  204
                 for (int length : ARG_COUNT) {
 292  199
                         if (length == parameterTypes.length) {
 293  57
                                 validLength = true;
 294  57
                                 break;
 295  
                         }
 296  
                 }
 297  62
                 if (!validLength) {
 298  5
                         throw new IllegalStateException(
 299  
                                 "Invalid number of parameters [" + parameterTypes.length + 
 300  
                                 "] for property change method [" + method + 
 301  
                                 "] expecting one of " + Arrays.toString(ARG_COUNT) + " parameters."
 302  
                         );
 303  
                 }
 304  57
                 final boolean hasSource = parameterTypes.length > ARG_SOURCE;
 305  57
                 final boolean hasProperty = parameterTypes.length > ARG_PROPERTY;
 306  57
                 final boolean hasOldValue = parameterTypes.length > ARG_OLD_VALUE;
 307  57
                 final boolean hasNewValue = parameterTypes.length > ARG_NEW_VALUE;
 308  57
                 final boolean hasParameters = hasOldValue && hasNewValue;
 309  57
                 if (hasSource && !pc.bean().equals(parameterTypes[ARG_SOURCE])) {
 310  1
                         throw new IllegalStateException(
 311  
                                 "Type mismatch of first method parameter of [" + method + 
 312  
                                 "], expecting same class as bean class [" + pc.bean().getName() + 
 313  
                                 "] to receive the source of the property change." 
 314  
                         );
 315  
                 }
 316  56
                 if (PropertyChange.WILDCARD_PROPERTY.equals(pc.property())) {
 317  23
                         if (hasProperty && !String.class.equals(parameterTypes[ARG_PROPERTY])) {
 318  0
                                 throw new IllegalStateException(
 319  
                                         "Type mismatch of second method parameter of [" + method + 
 320  
                                         "] has invalid type, expecting [String] to receive the property name." 
 321  
                                 );
 322  
                         }
 323  23
                         if (hasOldValue && !Object.class.equals(parameterTypes[ARG_OLD_VALUE])) {
 324  2
                                 throw new IllegalStateException(
 325  
                                         "Type mismatch of third method parameter of [" + method + 
 326  
                                         "] has invalid type, expecting [Object] to receive the old value." 
 327  
                                 );
 328  
                         }
 329  21
                         if (hasNewValue && !Object.class.equals(parameterTypes[ARG_NEW_VALUE])) {
 330  1
                                 throw new IllegalStateException(
 331  
                                         "Type mismatch of fourth method parameter of [" + method + 
 332  
                                         "] has invalid type, expecting [Object] to receive the new value." 
 333  
                                 );
 334  
                         }
 335  
                         // Nothing more to check, this property change matches anything.
 336  20
                         return;
 337  
                 }
 338  33
                 final Method getter = getter(pc.bean(), pc.property());
 339  33
                 if (getter == null) {
 340  1
                         throw new IllegalStateException(
 341  
                                 "@PropertyChange property [" + pc.property() + 
 342  
                                 "] is not a bean property of [" + pc.bean().getName() + "]."
 343  
                         );
 344  
                 }
 345  32
                 if (hasParameters && !getter.getReturnType().equals(pc.parameter())) {
 346  1
                         throw new IllegalStateException(
 347  
                                 "@PropertyChange type mismatch of 'parameter' type," + 
 348  
                                 " expecting [parameter=" + getter.getReturnType().getName() + "]."
 349  
                         );
 350  31
                 } else if (!hasParameters && !Void.class.equals(pc.parameter())) {
 351  3
                         throw new IllegalStateException(
 352  
                                 "@PropertyChange parameter specified but method [" + method + 
 353  
                                 "] does not declare old and new value parameters."
 354  
                         );
 355  
                 }
 356  28
                 if (hasProperty && !String.class.equals(parameterTypes[ARG_PROPERTY])) {
 357  0
                         throw new IllegalStateException(
 358  
                                 "Type mismatch of second method parameter of [" + method + 
 359  
                                 "] has invalid type, expecting [String] to receive the property name." 
 360  
                         );
 361  
                 }
 362  28
                 if (hasOldValue && !getter.getReturnType().equals(parameterTypes[ARG_OLD_VALUE])) {
 363  1
                         throw new IllegalStateException(
 364  
                                 "Type mismatch of third method parameter of [" + method + 
 365  
                                 "], expecting [" + getter.getReturnType().getName() + 
 366  
                                 "] to receive the old value of the property change."
 367  
                         );
 368  
                 }
 369  27
                 if (hasNewValue && !getter.getReturnType().equals(parameterTypes[ARG_NEW_VALUE])) {
 370  1
                         throw new IllegalStateException(
 371  
                                 "Type mismatch of fourth method parameter of [" + method + 
 372  
                                 "], expecting [" + getter.getReturnType().getName() + 
 373  
                                 "] to receive the new value of the property change."
 374  
                         );
 375  
                 }
 376  26
         }
 377  
         
 378  
         /**
 379  
          * Builds a map of methods with a {@link PropertyChange} annotation.
 380  
          * @param beanClass the bean class for which to build the map, must not be {@code null}.
 381  
          * @param targetClass the class for which to build the map, must not be {@code null}.
 382  
          * @return the map containing property names as keys and methods as values.
 383  
          * @since 0.1.0
 384  
          */
 385  
         public static Map<String, Method> getPropertyChangeMethods (
 386  
                 Class<?> beanClass, Class<?> targetClass
 387  
         ) {
 388  59
                 if (targetClass == null) {
 389  1
                         throw new IllegalArgumentException("Invalid class [null].");
 390  
                 }
 391  58
                 final Map<String, Method> map = new HashMap<String, Method>();
 392  
                 // Pass one, just check for bad signatures in the class (does not include superclasses).
 393  384
                 for (Method method : targetClass.getDeclaredMethods()) {
 394  329
                         if (method.getAnnotation(PropertyChange.class) == null) {
 395  257
                                 continue;
 396  
                         }
 397  72
                         if (!Modifier.isPublic(method.getModifiers())) {
 398  3
                                 throw new IllegalStateException(
 399  
                                         "Bad modifiers [" + Modifier.toString(method.getModifiers()) + 
 400  
                                         "] for property change support method [" + method + "]."
 401  
                                 );
 402  
                         }
 403  
                 }
 404  
                 // Pass two, retrieve all public methods with property change annotation.
 405  708
                 for (Method method : targetClass.getMethods()) {
 406  672
                         final PropertyChange pc = method.getAnnotation(PropertyChange.class);
 407  672
                         if (pc != null && pc.bean().isAssignableFrom(beanClass)) {
 408  63
                                 validate(method, pc);
 409  46
                                 final Method old = map.put(pc.property(), method);
 410  46
                                 if (old != null) {
 411  2
                                         if (PropertyChange.WILDCARD_PROPERTY.equals(pc.property())) {
 412  1
                                                 throw new IllegalStateException(
 413  
                                                         "Doubly defined wildcard property change annotation in method [" + 
 414  
                                                         method + "] with first occurence in method [ " + old + "]."
 415  
                                                 );
 416  
                                         }
 417  1
                                         throw new IllegalStateException(
 418  
                                                 "Doubly defined property change annotation [" + pc.property() + 
 419  
                                                 "] in method [" + method + 
 420  
                                                 "] with first occurence in method [ " + old + "]."
 421  
                                         );
 422  
                                 }
 423  
                         }
 424  
                 }
 425  36
                 return map;
 426  
         }
 427  
         
 428  
         /**
 429  
          * Registers the specified property change listener at the given bean.
 430  
          * @param bean the bean to add the property change listener to, must not be {@code null}.
 431  
          * @param listener the listener to add, must not be {@code null}.
 432  
          * @return the bean passed in for fluent use.
 433  
          * @throws IllegalArgumentException if the object passed in is not a Java Bean.
 434  
          * @throws IllegalStateException if there is an error registering.
 435  
          * @since 0.1.0
 436  
          */
 437  
         public static <T> T addPropertyChangeListener (T bean, PropertyChangeListener listener) {
 438  15
                 if (bean == null) {
 439  0
                         throw new IllegalArgumentException("Cannot add a listener to a [null] bean.");
 440  
                 }
 441  15
                 if (listener == null) {
 442  0
                         throw new IllegalArgumentException("Cannot add [null] listener to a bean.");
 443  
                 }
 444  15
                 final Method add = method(bean.getClass(), ADD, PropertyChangeListener.class);
 445  15
                 if (add == null || method(bean.getClass(), REMOVE, PropertyChangeListener.class) == null) {
 446  0
                         throw new IllegalArgumentException(
 447  
                                 "Object [" + bean.toString() + 
 448  
                                 "] of class [" + bean.getClass().getName() + 
 449  
                                 "] is not a Java Bean."
 450  
                         );
 451  
                 }
 452  
                 try {
 453  15
                         add.invoke(bean, listener);
 454  0
                 } catch (IllegalArgumentException e) {
 455  0
                         throw new IllegalStateException(
 456  
                                 "Internal error while invoking [" + ADD + "()] of [" + bean.toString() +
 457  
                                 "] of class [" + bean.getClass().getName() + "].", e
 458  
                         );
 459  0
                 } catch (IllegalAccessException e) {
 460  0
                         throw new IllegalStateException(
 461  
                                 "Cannot access method [" + ADD + "()] of [" + bean.toString() +
 462  
                                 "] of class [" + bean.getClass().getName() + "].", e
 463  
                         );
 464  0
                 } catch (InvocationTargetException e) {
 465  0
                         throw new IllegalStateException(
 466  
                                 "Invocation of [" + ADD + "()] of [" + bean.toString() +
 467  
                                 "] of class [" + bean.getClass().getName() + "] failed.", e.getCause()
 468  
                         );
 469  15
                 }
 470  15
                 return bean;
 471  
         }
 472  
         
 473  
         /**
 474  
          * Deregisters the specified property change listener from the given bean.
 475  
          * @param bean the bean to remove the listener from, must not be {@code null}.
 476  
          * @param listener the listener to remove, must not be {@code null}.
 477  
          * @return the bean passed in for fluent use.
 478  
          * @throws IllegalArgumentException if the object passed in is not a Java Bean.
 479  
          * @throws IllegalStateException if there is an error deregistering.
 480  
          * @since 0.1.0
 481  
          */
 482  
         public static <T> T removePropertyChangeListener (T bean, PropertyChangeListener listener) {
 483  1
                 if (bean == null) {
 484  0
                         throw new IllegalArgumentException("Cannot remove listener from a [null] bean.");
 485  
                 }
 486  1
                 if (listener == null) {
 487  0
                         throw new IllegalArgumentException("Cannot remove [null] listener from a bean.");
 488  
                 }
 489  1
                 final Method remove = method(bean.getClass(), REMOVE, PropertyChangeListener.class);
 490  1
                 if (remove == null) {
 491  0
                         throw new IllegalStateException(
 492  
                                 "Method [" + REMOVE + "()] of [" + bean.toString() +
 493  
                                 "] of class [" + bean.getClass().getName() + "] not found."
 494  
                         );
 495  
                 }
 496  
                 try {
 497  1
                         remove.invoke(bean, listener);
 498  0
                 } catch (IllegalArgumentException e) {
 499  0
                         throw new IllegalStateException(
 500  
                                 "Internal error while invoking [" + REMOVE + "()] of [" + bean.toString() +
 501  
                                 "] of class [" + bean.getClass().getName() + "].", e
 502  
                         );
 503  0
                 } catch (IllegalAccessException e) {
 504  0
                         throw new IllegalStateException(
 505  
                                 "Cannot access method [" + REMOVE + "()] of [" + bean.toString() +
 506  
                                 "] of class [" + bean.getClass().getName() + "].", e
 507  
                         );
 508  0
                 } catch (InvocationTargetException e) {
 509  0
                         throw new IllegalStateException(
 510  
                                 "Invocation of [" + REMOVE + "()] of [" + bean.toString() +
 511  
                                 "] of class [" + bean.getClass().getName() + "] failed.", e.getCause()
 512  
                         );
 513  1
                 }
 514  1
                 return bean;
 515  
         }
 516  
         
 517  
         //---- Constructors
 518  
         
 519  
         /**
 520  
          * Objects of this class cannot be instantiated.
 521  
          * @since 0.1.0
 522  
          */
 523  
         private BeanUtilities () {
 524  0
                 super();
 525  0
         }
 526  
         
 527  
 }