View Javadoc
1   /*
2    * #%L
3    * Nuiton Utils
4    * %%
5    * Copyright (C) 2004 - 2010 CodeLutin
6    * %%
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as 
9    * published by the Free Software Foundation, either version 3 of the 
10   * License, or (at your option) any later version.
11   * 
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   * 
17   * You should have received a copy of the GNU General Lesser Public 
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-3.0.html>.
20   * #L%
21   */
22  package org.nuiton.util.beans;
23  
24  import org.apache.commons.lang3.ObjectUtils;
25  import org.apache.commons.logging.Log;
26  import org.apache.commons.logging.LogFactory;
27  
28  import java.beans.PropertyChangeEvent;
29  import java.beans.PropertyChangeListener;
30  import java.util.*;
31  
32  /**
33   * A monitor of beans.
34   *
35   * You indicates which properties to monitor (via the constructor).
36   *
37   * Then attach a bean to monitor via the method {@link #setBean(Object)}.
38   *
39   * The method {@link #clearModified()} reset the states about modified
40   * properties.
41   *
42   * The method {@link #wasModified()} tells if there was any modification on
43   * monitored properties for the attached bean since the last call to
44   * {@link #clearModified()} (or {@link #setBean(Object)}.
45   *
46   * the method {@link #getModifiedProperties()} gives you the names of monitored
47   * properties for the attached bean since the last call to
48   * {@link #clearModified()} (or {@link #setBean(Object)}.
49   *
50   * @author Tony Chemit - chemit@codelutin.com
51   * @since 1.4.1
52   */
53  public class BeanMonitor {
54  
55      /** Logger */
56      private static final Log log = LogFactory.getLog(BeanMonitor.class);
57  
58      /** Names of properties to watch or not on attached bean. */
59      protected final List<String> propertyNames;
60  
61      /** If true, then listen to the properties which are not in the propertyNames */
62      protected boolean excludeMode;
63  
64      /** Names of monitored and modified properties for the attached bean. */
65      protected final Map<String, PropertyDiff> propertyDiffs;
66  
67      /**
68       * The {@link PropertyChangeListener} which listen modification on bean
69       * only on monitored properties.
70       */
71      protected final PropertyChangeListener listener;
72  
73      /** The bean to monitor. */
74      protected Object bean;
75  
76      /**
77       * Constructor of monitor with property names to monitor.
78       *
79       * @param propertyNames the names of properties to monitor
80       */
81      public BeanMonitor(String... propertyNames) {
82          this(false, propertyNames);
83      }
84  
85      public BeanMonitor(boolean exclude, String... propertyNames) {
86          this.excludeMode = exclude;
87          this.propertyNames = new ArrayList<String>(Arrays.asList(propertyNames));
88          propertyDiffs = new LinkedHashMap<String, PropertyDiff>();
89  
90          listener = new PropertyChangeListener() {
91              @Override
92              public void propertyChange(PropertyChangeEvent evt) {
93                  String propertyName = evt.getPropertyName();
94                  if (excludeMode && BeanMonitor.this.propertyNames.contains(propertyName)
95                          || !excludeMode && !BeanMonitor.this.propertyNames.contains(propertyName)) {
96                      return;
97                  }
98  
99                  // the property is monitored
100 
101                 Object newValue = evt.getNewValue();
102                 Object oldValue = evt.getOldValue();
103 
104                 PropertyDiff propertyDiff;
105 
106                 if (propertyDiffs.containsKey(propertyName)) {
107 
108                     // property was already modified
109                     propertyDiff = propertyDiffs.get(propertyName);
110 
111                     // change the target value
112                     propertyDiff.setTargetValue(newValue);
113 
114                     // check if value did not come back to original
115                     Object originalValue = propertyDiff.getSourceValue();
116 
117                     if (Objects.equals(originalValue, newValue)) {
118 
119                         // coming back to original value
120                         // the property is no more modified
121                         propertyDiffs.remove(propertyName);
122                     }
123                 } else {
124 
125                     // check old value and newvalue are real not equals
126                     if (ObjectUtils.notEqual(newValue, oldValue)) {
127                         propertyDiff = new PropertyDiff(
128                                 propertyName,
129                                 oldValue,
130                                 propertyName,
131                                 newValue,
132                                 null
133                         );
134 
135                         // add it to modified properties
136                         propertyDiffs.put(propertyName, propertyDiff);
137                     }
138 
139                 }
140             }
141         };
142     }
143 
144     /**
145      * Obtains the monitored {@link #bean}.
146      *
147      * @return the attached bean, or {@code null} if none attached.
148      */
149     public Object getBean() {
150         return bean;
151     }
152 
153     /**
154      * Tells if any of the monitored properties were modified on the attached
155      * bean since last call to {@link #clearModified()} or
156      * {@link #setBean(Object)}.
157      *
158      * @return {@code true} if there were a modified monitored property on
159      *         the attached bean, {@code false} otherwise.
160      */
161     public boolean wasModified() {
162         return !propertyDiffs.isEmpty();
163     }
164 
165     /**
166      * Obtains the names of monitored properties which has been touched for the
167      * attached bean since last call to {@link #clearModified()} or
168      * {@link #setBean(Object)}.
169      *
170      * @return the array of property names to monitor.
171      */
172     public String[] getModifiedProperties() {
173         Set<String> propertyNames = propertyDiffs.keySet();
174         return propertyNames.toArray(new String[propertyNames.size()]);
175     }
176 
177     /**
178      * Obtains the original values for all modified properties.
179      *
180      * @return the dictionnary of original values for modified properties
181      */
182     public Map<String, Object> getOriginalValues() {
183 
184         // always expose a copy of map
185         Map<String, Object> map = new TreeMap<String, Object>();
186         for (PropertyDiff propertyDiff : propertyDiffs.values()) {
187             map.put(propertyDiff.getSourceProperty(),
188                     propertyDiff.getSourceValue());
189         }
190         return map;
191     }
192 
193     /**
194      * Obtains the property diffs since the bean is monitoried.
195      *
196      * @return the property diffs since the bean is monitoried.
197      * @since 2.4
198      */
199     public PropertyDiff[] getPropertyDiffs() {
200         Collection<PropertyDiff> values = propertyDiffs.values();
201         return values.toArray(new PropertyDiff[values.size()]);
202     }
203 
204     /**
205      * Sets the {@code bean} to monitor.
206      *
207      * As a side effect, it will attach the {@link #listener} to new bean
208      * (if not null) and remove it from the previous bean attached (if not null).
209      *
210      * As a second side effect, it will always clean the modified states,
211      * using the method {@link #clearModified()}.
212      *
213      * @param bean the new bean to monitor
214      */
215     public void setBean(Object bean) {
216         Object oldBean = this.bean;
217         this.bean = bean;
218 
219         // while removing a bean must clean modified states
220         clearModified();
221 
222         if (oldBean != null) {
223 
224             // dettach listener from old bean
225             try {
226                 BeanUtil.removePropertyChangeListener(listener, oldBean);
227             } catch (Exception eee) {
228                 log.error(String.format("Could remove the PropertychangeListener %1$s from object %2$s for following reason \\: %3$s",
229                                         listener, oldBean, eee.getMessage()));
230             }
231         }
232         if (bean != null) {
233 
234             // attach listener to new bean
235             try {
236                 BeanUtil.addPropertyChangeListener(listener, bean);
237             } catch (Exception eee) {
238                 log.error(String.format("Could not add the PropertychangeListener %1$s on object %2$s for following reason \\: %3$s",
239                                         listener, bean, eee.getMessage()));
240             }
241         }
242     }
243 
244     /** To clear the modified states. */
245     public void clearModified() {
246         propertyDiffs.clear();
247     }
248 
249     /**
250      * To change the list of properties to watch.
251      *
252      * <strong>Note:</strong> As a side-effect, we call a
253      * {@link #clearModified()} method after having changed the list of
254      * properties to watch.
255      *
256      * @param properties the list of properties to watch on the bean.
257      * @since 3.0
258      */
259     public void setProperties(String... properties) {
260         propertyNames.clear();
261         propertyNames.addAll(Arrays.asList(properties));
262         clearModified();
263     }
264 
265     public boolean isExcludeMode() {
266         return excludeMode;
267     }
268 
269     public void setExcludeMode(boolean excludeMode) {
270         this.excludeMode = excludeMode;
271         clearModified();
272     }
273 }