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 }