Author: tchemit Date: 2010-08-26 13:08:53 +0200 (Thu, 26 Aug 2010) New Revision: 1906 Url: http://nuiton.org/repositories/revision/nuiton-utils/1906 Log: Evolution #820: Introduce BeanMonitor api Added: trunk/src/main/java/org/nuiton/util/beans/BeanMonitor.java trunk/src/main/java/org/nuiton/util/beans/BeanUtil.java trunk/src/test/java/org/nuiton/util/beans/BeanMonitorTest.java Modified: trunk/src/test/java/org/nuiton/util/beans/BeanA.java trunk/src/test/java/org/nuiton/util/beans/BeanB.java Added: trunk/src/main/java/org/nuiton/util/beans/BeanMonitor.java =================================================================== --- trunk/src/main/java/org/nuiton/util/beans/BeanMonitor.java (rev 0) +++ trunk/src/main/java/org/nuiton/util/beans/BeanMonitor.java 2010-08-26 11:08:53 UTC (rev 1906) @@ -0,0 +1,201 @@ +package org.nuiton.util.beans; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import static org.nuiton.i18n.I18n._; + +/** + * A monitor of beans. + * <p/> + * You indicates which properties to monitor (via the constructor). + * <p/> + * Then attach a bean to monitor via the method {@link #setBean(Object)}. + * <p/> + * The method {@link #clearModified()} reset the states about modified + * properties. + * <p/> + * The method {@link #wasModified()} tells if there was any modification on + * monitored properties for the attached bean since the last call to + * {@link #clearModified()} (or {@link #setBean(Object)}. + * <p/> + * the method {@link #getModifiedProperties()} gives you the names of monitored + * properties for the attached bean since the last call to + * {@link #clearModified()} (or {@link #setBean(Object)}. + * + * @author chemit <chemit@codelutin.com> + * @since 1.4.1 + */ +public class BeanMonitor { + + /** Logger */ + private static final Log log = LogFactory.getLog(BeanMonitor.class); + + /** Names of properties to watch on attached bean. */ + protected final List<String> propertyNames; + + /** Names of monitored and modified properties for the attached bean. */ + protected final Set<String> modifiedProperties; + + /** To store the original values when watched properties are changing. */ + protected final Map<String, Object> originalValues; + + /** + * The {@link PropertyChangeListener} which listen modification on bean + * only on monitored properties. + */ + protected final PropertyChangeListener listener; + + /** The bean to monitor. */ + protected Object bean; + + /** + * Constructor of monitor with property names to monitor. + * + * @param propertyNames the names of properties to monitor + */ + public BeanMonitor(String... propertyNames) { + this.propertyNames = Arrays.asList(propertyNames); + modifiedProperties = new LinkedHashSet<String>(); + originalValues = new TreeMap<String, Object>(); + listener = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + String propertyName = evt.getPropertyName(); + if (!BeanMonitor.this.propertyNames.contains(propertyName)) { + return; + } + + // the property is monitored + + Object newValue = evt.getNewValue(); + Object oldValue = evt.getOldValue(); + + if (modifiedProperties.contains(propertyName)) { + + // property already modified + // check if value did not come back to original + Object originalValue = originalValues.get(propertyName); + + if (originalValue == null && newValue == null || + originalValue != null && originalValue.equals(newValue)) { + + // coming back to original value + // the property is no more modified + modifiedProperties.remove(propertyName); + originalValues.remove(propertyName); + return; + } + + // can safely return since original is already known + // and property is already known to be modified + return; + } + + // the property was not modified before + // just mark it and keep the original value + modifiedProperties.add(propertyName); + originalValues.put(propertyName, oldValue); + } + }; + } + + /** + * Obtains the monitored {@link #bean}. + * + * @return the attached bean, or {@code null} if none attached. + */ + public Object getBean() { + return bean; + } + + /** + * Tells if any of the monitored properties were modified on the attached + * bean since last call to {@link #clearModified()} or + * {@link #setBean(Object)}. + * + * @return {@code true} if there were a modified monitored property on + * the attached bean, {@code false} otherwise. + */ + public boolean wasModified() { + return !modifiedProperties.isEmpty(); + } + + /** + * Obtains the names of monitored properties which has been touched for the + * attached bean since last call to {@link #clearModified()} or + * {@link #setBean(Object)}. + * + * @return the array of property names to monitor. + */ + public String[] getModifiedProperties() { + return modifiedProperties.toArray(new String[modifiedProperties.size()]); + } + + /** + * Obtains the original values for all modified properties. + * + * @return the dictionnary of original values for modified properties + */ + public Map<String, Object> getOriginalValues() { + + // always expose a copy of map + Map<String, Object> map = new TreeMap<String, Object>(originalValues); + return map; + } + + /** + * Sets the {@code bean} to monitor. + * <p/> + * As a side effect, it will attach the {@link #listener} to new bean + * (if not null) and remove it from the previous bean attached (if not null). + * <p/> + * As a second side effect, it will always clean the modified states, + * using the method {@link #clearModified()}. + * + * @param bean the new bean to monitor + */ + public void setBean(Object bean) { + Object oldBean = this.bean; + this.bean = bean; + + // while removing a bean must clean modified states + clearModified(); + + if (oldBean != null) { + + // dettach listener from old bean + try { + BeanUtil.removePropertyChangeListener(listener, oldBean); + } catch (Exception eee) { + log.error(_("nuitonutil.error.could.not.removePCL", + listener, oldBean, eee.getMessage())); + } + } + if (bean != null) { + + // attach listener to new bean + try { + BeanUtil.addPropertyChangeListener(listener, bean); + } catch (Exception eee) { + log.error(_("nuitonutil.error.could.not.addPCL", + listener, bean, eee.getMessage())); + } + } + } + + /** To clear the modified states. */ + public void clearModified() { + modifiedProperties.clear(); + originalValues.clear(); + } +} Added: trunk/src/main/java/org/nuiton/util/beans/BeanUtil.java =================================================================== --- trunk/src/main/java/org/nuiton/util/beans/BeanUtil.java (rev 0) +++ trunk/src/main/java/org/nuiton/util/beans/BeanUtil.java 2010-08-26 11:08:53 UTC (rev 1906) @@ -0,0 +1,77 @@ +package org.nuiton.util.beans; + +import org.apache.commons.beanutils.MethodUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.nuiton.i18n.I18n._; + +/** + * Usefull methods around the {@link PropertyChangeListener}. + * + * @author tchemit <chemit@codelutin.com> + * @since 1.4.1 + */ +public class BeanUtil { + + protected BeanUtil() { + // no instance + } + + /** + * Add the given {@code listener} to the given {@code bean} using the + * normalized method named {@code addPropertyChangeListener}. + * + * @param listener the listener to add + * @param bean the bean on which the listener is added + * @throws InvocationTargetException if could not invoke the method + * {@code addPropertyChangeListener} + * @throws NoSuchMethodException if method + * {@code addPropertyChangeListener} + * does not exist on given bean + * @throws IllegalAccessException if an illegal access occurs when + * invoking the method + * {@code addPropertyChangeListener} + */ + public static void addPropertyChangeListener(PropertyChangeListener listener, + Object bean) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + MethodUtils.invokeExactMethod(bean, + "addPropertyChangeListener", + new Object[]{listener}, + new Class[]{PropertyChangeListener.class} + ); + } + + /** + * Remove the given {@code listener} from the given {@code bean} using the + * normalized method named {@code removePropertyChangeListener}. + * + * @param listener the listener to remove + * @param bean the bean on which the listener is removed + * @throws InvocationTargetException if could not invoke the method + * {@code removePropertyChangeListener} + * @throws NoSuchMethodException if method + * {@code removePropertyChangeListener} + * does not exist on given bean + * @throws IllegalAccessException if an illegal access occurs when + * invoking the method + * {@code removePropertyChangeListener} + */ + public static void removePropertyChangeListener(PropertyChangeListener listener, + Object bean) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException { + MethodUtils.invokeExactMethod(bean, + "removePropertyChangeListener", + new Object[]{listener}, + new Class[]{PropertyChangeListener.class} + ); + } +} Modified: trunk/src/test/java/org/nuiton/util/beans/BeanA.java =================================================================== --- trunk/src/test/java/org/nuiton/util/beans/BeanA.java 2010-08-11 14:46:50 UTC (rev 1905) +++ trunk/src/test/java/org/nuiton/util/beans/BeanA.java 2010-08-26 11:08:53 UTC (rev 1906) @@ -25,26 +25,37 @@ package org.nuiton.util.beans; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + public class BeanA { public static final String PROPERTY_A = "a"; + public static final String PROPERTY_B = "b"; + public static final String PROPERTY_C = "c"; + public static final String PROPERTY_D = "d"; + public static final String PROPERTY_E = "e"; + public static final String PROPERTY_F = "f"; - String a, b, c, d; + protected String a, b, c, d; + protected int e, f; - int e, f; + protected PropertyChangeSupport pcs = new PropertyChangeSupport(this); public String getA() { return a; } public void setA(String a) { + Object oldValue = this.a; this.a = a; + firePropertyChange(PROPERTY_A, oldValue, a); } public String getB() { @@ -52,7 +63,9 @@ } public void setB(String b) { + Object oldValue = this.b; this.b = b; + firePropertyChange(PROPERTY_B, oldValue, b); } public String getC() { @@ -60,7 +73,9 @@ } public void setC(String c) { + Object oldValue = this.c; this.c = c; + firePropertyChange(PROPERTY_C, oldValue, c); } public String getD() { @@ -68,7 +83,9 @@ } public void setD(String d) { + Object oldValue = this.d; this.d = d; + firePropertyChange(PROPERTY_D, oldValue, d); } public int getE() { @@ -76,7 +93,9 @@ } public void setE(int e) { + Object oldValue = this.e; this.e = e; + firePropertyChange(PROPERTY_E, oldValue, e); } public int getF() { @@ -84,6 +103,31 @@ } public void setF(int f) { + Object oldValue = this.f; this.f = f; + firePropertyChange(PROPERTY_F, oldValue, f); } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public void addPropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + pcs.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + public void removePropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + pcs.removePropertyChangeListener(propertyName, listener); + } + + protected void firePropertyChange(String propertyName, Object oldValue, + Object newValue) { + pcs.firePropertyChange(propertyName, oldValue, newValue); + } } Modified: trunk/src/test/java/org/nuiton/util/beans/BeanB.java =================================================================== --- trunk/src/test/java/org/nuiton/util/beans/BeanB.java 2010-08-11 14:46:50 UTC (rev 1905) +++ trunk/src/test/java/org/nuiton/util/beans/BeanB.java 2010-08-26 11:08:53 UTC (rev 1906) @@ -28,21 +28,29 @@ public class BeanB extends BeanA { public static final String PROPERTY_A2 = "a2"; + public static final String PROPERTY_B2 = "b2"; + public static final String PROPERTY_C2 = "c2"; + public static final String PROPERTY_D2 = "d2"; - public static final String PROPERTY_E2 = "e2"; + + public static final String PROPERTY_E2 = "e2"; + public static final String PROPERTY_F2 = "f2"; + String a2, b2, c2, d2; - int e2,f2; + int e2, f2; public String getA2() { return a2; } public void setA2(String a2) { + Object oldValue = this.a2; this.a2 = a2; + firePropertyChange(PROPERTY_A2, oldValue, a2); } public String getB2() { @@ -50,7 +58,9 @@ } public void setB2(String b2) { + Object oldValue = this.b2; this.b2 = b2; + firePropertyChange(PROPERTY_B2, oldValue, b2); } public String getC2() { @@ -58,7 +68,9 @@ } public void setC2(String c2) { + Object oldValue = this.c2; this.c2 = c2; + firePropertyChange(PROPERTY_C2, oldValue, c2); } public String getD2() { @@ -66,7 +78,9 @@ } public void setD2(String d2) { + Object oldValue = this.d2; this.d2 = d2; + firePropertyChange(PROPERTY_D2, oldValue, d2); } public int getE2() { @@ -74,7 +88,9 @@ } public void setE2(int e2) { + Object oldValue = this.e2; this.e2 = e2; + firePropertyChange(PROPERTY_E2, oldValue, e2); } public int getF2() { @@ -82,6 +98,8 @@ } public void setF2(int f2) { + Object oldValue = this.f2; this.f2 = f2; + firePropertyChange(PROPERTY_F2, oldValue, f2); } } Added: trunk/src/test/java/org/nuiton/util/beans/BeanMonitorTest.java =================================================================== --- trunk/src/test/java/org/nuiton/util/beans/BeanMonitorTest.java (rev 0) +++ trunk/src/test/java/org/nuiton/util/beans/BeanMonitorTest.java 2010-08-26 11:08:53 UTC (rev 1906) @@ -0,0 +1,106 @@ +package org.nuiton.util.beans; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.Map; + +/** + * Tests {@link BeanMonitor}. + * + * @author tchemit <chemit@codelutin.com> + * @serial $Id:$ + * @since 1.4.1 + */ +public class BeanMonitorTest { + + @Test + public void testMonitor() throws Exception { + + BeanMonitor monitor = new BeanMonitor(BeanA.PROPERTY_A, + BeanA.PROPERTY_B, + BeanA.PROPERTY_E + ); + + BeanA bean = new BeanA(); + monitor.setBean(bean); + + // nothing is monitored + assertMonitor(monitor); + + // property A was modified + bean.setA("A"); + assertMonitor(monitor, BeanA.PROPERTY_A, null); + + // clean monitor + monitor.clearModified(); + assertMonitor(monitor); + + // property A was not modified + bean.setA("A"); + assertMonitor(monitor); + + // property C is not monitored + bean.setC("C"); + assertMonitor(monitor); + + // property A was modified + bean.setA("AA"); + assertMonitor(monitor, BeanA.PROPERTY_A, "A"); + + // property A and B were modified + bean.setB("B"); + assertMonitor(monitor, + BeanA.PROPERTY_A, "A", + BeanA.PROPERTY_B, null); + + // property B is no more modified (came back to original value) + bean.setB(null); + assertMonitor(monitor, BeanA.PROPERTY_A, "A"); + + // property A and E were modified + bean.setE(1); + assertMonitor(monitor, BeanA.PROPERTY_A, "A", BeanA.PROPERTY_E, 0); + + // property A is no more modified (came back to original value) + bean.setA("A"); + assertMonitor(monitor, BeanA.PROPERTY_E, 0); + + // property E is no more modified (came back to original value) + bean.setE(0); + assertMonitor(monitor); + + // property B was modified + bean.setB("B"); + assertMonitor(monitor, BeanA.PROPERTY_B, null); + + // nothing is monitored (bean changed) + monitor.setBean(bean); + assertMonitor(monitor); + + } + + protected void assertMonitor(BeanMonitor monitor, + Object... propertyNamesAndOriginalValues) { + + if (propertyNamesAndOriginalValues.length % 2 != 0) { + throw new IllegalArgumentException( + "propertiesAndValues should be couple of (propertyName, originalValue)"); + } + String[] modifiedFields = monitor.getModifiedProperties(); + Map<String, Object> originalValues = monitor.getOriginalValues(); + + if (propertyNamesAndOriginalValues.length == 0) { + Assert.assertFalse(monitor.wasModified()); + Assert.assertEquals(0, modifiedFields.length); + } else { + Assert.assertTrue(monitor.wasModified()); + for (int i = 0; i < propertyNamesAndOriginalValues.length; i += 2) { + String propertyName = (String) propertyNamesAndOriginalValues[i]; + Object value = propertyNamesAndOriginalValues[i + 1]; + Assert.assertEquals(propertyName, modifiedFields[i / 2]); + Assert.assertEquals(value, originalValues.get(propertyName)); + } + } + } +}