Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
PropertyChangeDispatcher |
|
| 4.111111111111111;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> @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 | } |