Coverage Report - org.openpermis.editor.policy.beans.PropertyChangeDispatcher
 
Classes in this File Line Coverage Branch Coverage Complexity
PropertyChangeDispatcher
72%
45/62
62%
15/24
4.111
 
 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 static org.openpermis.editor.policy.beans.BeanUtilities.addPropertyChangeListener;
 13  
 import static org.openpermis.editor.policy.beans.BeanUtilities.getPropertyChangeMethods;
 14  
 import static org.openpermis.editor.policy.beans.BeanUtilities.removePropertyChangeListener;
 15  
 
 16  
 import java.beans.PropertyChangeEvent;
 17  
 import java.beans.PropertyChangeListener;
 18  
 import java.lang.reflect.InvocationTargetException;
 19  
 import java.lang.reflect.Method;
 20  
 import java.util.Map;
 21  
 
 22  
 import org.slf4j.Logger;
 23  
 import org.slf4j.LoggerFactory;
 24  
 
 25  
 /**
 26  
  * Dispatcher for property change events.
 27  
  * <p>Create a dispatcher for a bean and a target class which contains {@link PropertyChange}
 28  
  * annotations and the dispatcher will automatically dispatch property change events properly.</p>
 29  
  * <p>The dispatcher expects the following signuare:</p>
 30  
  * <pre> &#64;PropertyChange(bean=MyBean.class, property="someProperty", parameter=MyValue.class)
 31  
  * public someChangeHandler (MyBean source, String property, MyValue oldValue, MyValue newValue) {
 32  
  *   ...
 33  
  * }</pre>
 34  
  * <p>The first parameter has to be the source of the property change event and must match the
 35  
  * bean class of the {@link PropertyChange} annotation. Parameters two and three correspond to
 36  
  * the old and new value received in the property change event and must match the parameter
 37  
  * class of the {@link PropertyChange} annotation.</p>
 38  
  * <p>The dispatcher will perform the casts necessary automatically and throw
 39  
  * {@link ClassCastException}s if the cast is not possible. This should not occur unless the
 40  
  * bean sends invalid property change notifications since the parameter types are checked when
 41  
  * the dispatcher is created.</p>
 42  
  * @since 0.1.0
 43  
  */
 44  
 public class PropertyChangeDispatcher
 45  
         implements PropertyChangeListener
 46  
 {
 47  
 
 48  
         //---- Static
 49  
 
 50  
         /**
 51  
          * The logger object of this class.
 52  
          * @since 0.1.0
 53  
          */
 54  1
         private static final Logger LOGGER =
 55  
                 LoggerFactory.getLogger(PropertyChangeDispatcher.class);
 56  
 
 57  
         /**
 58  
          * Verbose debug output flag.
 59  
          * @since 0.1.0
 60  
          */
 61  
         private static final boolean TRACE = false;
 62  
 
 63  
         //---- State
 64  
 
 65  
         /**
 66  
          * The bean this support class operates on.
 67  
          * @since 0.1.0
 68  
          */
 69  
         private final Object bean;
 70  
 
 71  
         /**
 72  
          * The target to dispatch property change events to.
 73  
          * @since 0.1.0
 74  
          */
 75  
         private final Object target;
 76  
 
 77  
         /**
 78  
          * The target to dispatch property change events to.
 79  
          * @since 0.1.0
 80  
          */
 81  
         private final Map<String, Method> methods;
 82  
 
 83  
         /**
 84  
          * Indicates if the support class is registered at its bean.
 85  
          * @since 0.1.0
 86  
          */
 87  
         private boolean registered;
 88  
 
 89  
         /**
 90  
          * Cache for the last propagation ID encountered.
 91  
          * @since 0.1.0
 92  
          */
 93  
         private Object lastPropagationId;
 94  
 
 95  
         //---- Constructors
 96  
 
 97  
         /**
 98  
          * Creates a bean support object for the specified Java Bean.
 99  
          * @param bean the Java Bean to operate on, must not be {@code null}.
 100  
          * @param target the target to dispatch property change events to, must not be {@code null}.
 101  
          * @since 0.1.0
 102  
          */
 103  19
         public PropertyChangeDispatcher (Object bean, Object target) {
 104  19
                 if (bean == null) {
 105  1
                         throw new IllegalArgumentException("Illegal bean [null].");
 106  
                 }
 107  18
                 if (target == null) {
 108  1
                         throw new IllegalArgumentException("Illegal target [null].");
 109  
                 }
 110  17
                 this.target = target;
 111  17
                 this.methods = getPropertyChangeMethods(bean.getClass(), this.target.getClass());
 112  17
                 if (this.methods.isEmpty()) {
 113  2
                         throw new IllegalArgumentException(
 114  
                                 "Target [" + this.target.getClass().getName() +
 115  
                                 "] has no property change annotations that match the bean [" +
 116  
                                 bean.getClass().getName() + "]."
 117  
                         );
 118  
                 }
 119  15
                 this.bean = addPropertyChangeListener(bean, this);
 120  15
                 this.registered = true;
 121  15
         }
 122  
 
 123  
         //---- Methods
 124  
 
 125  
         /**
 126  
          * Returns the Java Bean this bean support operates on.
 127  
          * @return the Java Bean this bean support operates on, never {@code null}.
 128  
          * @since 0.1.0
 129  
          */
 130  
         protected final Object getBean () {
 131  8
                 return this.bean;
 132  
         }
 133  
 
 134  
         /**
 135  
          * Returns the target to dispatch property change events to.
 136  
          * @return the target to dispatch property change events to, may be {@code null}.
 137  
          * @since 0.1.0
 138  
          */
 139  
         protected final Object getTarget () {
 140  8
                 return this.target;
 141  
         }
 142  
 
 143  
         /**
 144  
          * Returns the method to execute for property changes on the specified property.
 145  
          * @param property the property for which to find the method.
 146  
          * @return the method, {@code null} if there is none.
 147  
          * @since 0.1.0
 148  
          */
 149  
         protected final Method getPropertyChangeMethod (String property) {
 150  8
                 final Method method = this.methods.get(property);
 151  8
                 if (method != null) {
 152  7
                         return method;
 153  
                 }
 154  1
                 return this.methods.get(PropertyChange.WILDCARD_PROPERTY);
 155  
         }
 156  
 
 157  
         /**
 158  
          * Sets the internal flag which indicates if this bean support is active.
 159  
          * @param active the new registered flag value.
 160  
          * @see #dispose()
 161  
          * @see #isActive()
 162  
          * @since 0.1.0
 163  
          */
 164  
         protected final void setActive (boolean active) {
 165  1
                 this.registered = active;
 166  1
         }
 167  
 
 168  
         /**
 169  
          * Check if the dispatcher is active or has been disposed.
 170  
          * @return {@code true} if the dispatcher is active and dispatches events,
 171  
          * {@code false} if it has been {@link #dispose() disposed}.
 172  
          * @see #dispose()
 173  
          * @since 0.1.0
 174  
          */
 175  
         public synchronized boolean isActive () {
 176  7
                 return this.registered;
 177  
         }
 178  
 
 179  
         /**
 180  
          * Disposes this bean support instance.
 181  
          * <p>Once disposed you may no longer execute property methods on it.</p>
 182  
          * @see #isActive()
 183  
          * @since 0.1.0
 184  
          */
 185  
         public synchronized void dispose () {
 186  2
                 if (isActive()) {
 187  1
                         removePropertyChangeListener(this.bean, this);
 188  1
                         setActive(false);
 189  
                 }
 190  2
         }
 191  
 
 192  
         /**
 193  
          * Logs a warning with the specified message and cause.
 194  
          * @param message the message to log.
 195  
          * @param cause the cause.
 196  
          * @since 0.1.0
 197  
          */
 198  
         protected void warn (String message, Throwable cause) {
 199  0
                 LOGGER.warn(message, cause);
 200  0
         }
 201  
 
 202  
         //---- PropertyChangeListener
 203  
 
 204  
         /**
 205  
          * Listener for property changes.
 206  
          * <p>Dispatches to methods annotated with {@link PropertyChange} on the target of this
 207  
          * bean support instance.</p>
 208  
          * @param event the property change event to dispatch.
 209  
          * @since 0.1.0
 210  
          */
 211  
         public void propertyChange (PropertyChangeEvent event) {
 212  8
                 final Object propagationId = event.getPropagationId();
 213  8
                 if (propagationId != null && this.lastPropagationId == propagationId) {
 214  
                         if (TRACE) {
 215  
                                 LOGGER.debug(
 216  
                                         "Ignoring property change of property [{}], already handled.",
 217  
                                         event.getPropertyName()
 218  
                                 );
 219  
                         }
 220  0
                         return;
 221  
                 } else if (TRACE) {
 222  
                         LOGGER.debug(
 223  
                                 "Dispatching property change of property [{}].",
 224  
                                 event.getPropertyName()
 225  
                         );
 226  
                 }
 227  8
                 this.lastPropagationId = propagationId;
 228  8
                 if (event.getSource() != getBean()) {
 229  
                         // Somebody registered an additional bean, or the bean didn't fill in
 230  
                         // the property change event properly.
 231  0
                         warn(
 232  
                                 "Property change event with invalid source [" + event.getSource() +
 233  
                                 "] received, expecting [" + getBean() + "].",
 234  
                                 null
 235  
                         );
 236  0
                         return;
 237  
                 }
 238  8
                 if (event.getPropertyName() == null) {
 239  0
                         warn(
 240  
                                 "Multi-property change event received from [" + event.getSource() +
 241  
                                 "], this is not supported by bean support.",
 242  
                                 null
 243  
                         );
 244  0
                         return;
 245  
                 }
 246  8
                 final String property = event.getPropertyName();
 247  8
                 final Method method = getPropertyChangeMethod(property);
 248  8
                 if (method == null) {
 249  1
                         warn(
 250  
                                 "No property change method for property [" + property +
 251  
                                 "] found at [" + getTarget().getClass().getName() + "].",
 252  
                                 null
 253  
                         );
 254  1
                         return;
 255  
                 }
 256  
                 try {
 257  7
                         final Object[] allParameters = new Object[] {
 258  
                                 event.getSource(),
 259  
                                 property,
 260  
                                 event.getOldValue(),
 261  
                                 event.getNewValue()
 262  
                         };
 263  7
                         final Object[] parameters = new Object[method.getParameterTypes().length];
 264  7
                         System.arraycopy(allParameters, 0, parameters, 0, parameters.length);
 265  7
                         method.invoke(getTarget(), parameters);
 266  0
                 } catch (IllegalArgumentException e) {
 267  0
                         final Object oV = event.getOldValue();
 268  0
                         final Object nV = event.getNewValue();
 269  0
                         final RuntimeException rethrow = new ClassCastException(
 270  
                                 "Failed to call property change event method [" + method +
 271  
                                 "] for event [property=" + property +
 272  
                                 ", oldValueClass=" + (oV != null ? oV.getClass().getName() : "null") +
 273  
                                 ", newValueClass=" + (nV != null ? nV.getClass().getName() : "null") + "]."
 274  
                         );
 275  0
                         rethrow.initCause(e);
 276  0
                         warn("Invalid property change event method.", rethrow);
 277  0
                         throw rethrow;
 278  0
                 } catch (IllegalAccessException e) {
 279  0
                         warn("Cannot access property change event method [" + method + "].", e);
 280  0
                         throw new IllegalStateException(
 281  
                                 "Cannot access property change event method [" + method + "].", e
 282  
                         );
 283  1
                 } catch (InvocationTargetException e) {
 284  1
                         warn("Error while executing property change method [" + method + "].", e.getCause());
 285  1
                         throw new IllegalStateException(
 286  
                                 "Error while executing property change method [" + method + "].",
 287  
                                 e.getCause()
 288  
                         );
 289  6
                 }
 290  6
         }
 291  
 
 292  
 }