1 /* 2 * Copyright (c) 1996, 2012, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 package java.beans; 26 27 import java.io.Serializable; 28 import java.io.ObjectStreamField; 29 import java.io.ObjectOutputStream; 30 import java.io.ObjectInputStream; 31 import java.io.IOException; 32 import java.util.Hashtable; 33 import java.util.Map.Entry; 34 35 /** 36 * This is a utility class that can be used by beans that support constrained 37 * properties. It manages a list of listeners and dispatches 38 * {@link PropertyChangeEvent}s to them. You can use an instance of this class 39 * as a member field of your bean and delegate these types of work to it. 40 * The {@link VetoableChangeListener} can be registered for all properties 41 * or for a property specified by name. 42 * <p> 43 * Here is an example of {@code VetoableChangeSupport} usage that follows 44 * the rules and recommendations laid out in the JavaBeans™ specification: 45 * <pre> 46 * public class MyBean { 47 * private final VetoableChangeSupport vcs = new VetoableChangeSupport(this); 48 * 49 * public void addVetoableChangeListener(VetoableChangeListener listener) { 50 * this.vcs.addVetoableChangeListener(listener); 51 * } 52 * 53 * public void removeVetoableChangeListener(VetoableChangeListener listener) { 54 * this.vcs.removeVetoableChangeListener(listener); 55 * } 56 * 57 * private String value; 58 * 59 * public String getValue() { 60 * return this.value; 61 * } 62 * 63 * public void setValue(String newValue) throws PropertyVetoException { 64 * String oldValue = this.value; 65 * this.vcs.fireVetoableChange("value", oldValue, newValue); 66 * this.value = newValue; 67 * } 68 * 69 * [...] 70 * } 71 * </pre> 72 * <p> 73 * A {@code VetoableChangeSupport} instance is thread-safe. 74 * <p> 75 * This class is serializable. When it is serialized it will save 76 * (and restore) any listeners that are themselves serializable. Any 77 * non-serializable listeners will be skipped during serialization. 78 * 79 * @see PropertyChangeSupport 80 */ 81 public class VetoableChangeSupport implements Serializable { 82 private VetoableChangeListenerMap map = new VetoableChangeListenerMap(); 83 84 /** 85 * Constructs a <code>VetoableChangeSupport</code> object. 86 * 87 * @param sourceBean The bean to be given as the source for any events. 88 */ 89 public VetoableChangeSupport(Object sourceBean) { 90 if (sourceBean == null) { 91 throw new NullPointerException(); 92 } 93 source = sourceBean; 94 } 95 96 /** 97 * Add a VetoableChangeListener to the listener list. 98 * The listener is registered for all properties. 99 * The same listener object may be added more than once, and will be called 100 * as many times as it is added. 101 * If <code>listener</code> is null, no exception is thrown and no action 102 * is taken. 103 * 104 * @param listener The VetoableChangeListener to be added 105 */ 106 public void addVetoableChangeListener(VetoableChangeListener listener) { 107 if (listener == null) { 108 return; 109 } 110 if (listener instanceof VetoableChangeListenerProxy) { 111 VetoableChangeListenerProxy proxy = 112 (VetoableChangeListenerProxy)listener; 113 // Call two argument add method. 114 addVetoableChangeListener(proxy.getPropertyName(), 115 proxy.getListener()); 116 } else { 117 this.map.add(null, listener); 118 } 119 } 120 121 /** 122 * Remove a VetoableChangeListener from the listener list. 123 * This removes a VetoableChangeListener that was registered 124 * for all properties. 125 * If <code>listener</code> was added more than once to the same event 126 * source, it will be notified one less time after being removed. 127 * If <code>listener</code> is null, or was never added, no exception is 128 * thrown and no action is taken. 129 * 130 * @param listener The VetoableChangeListener to be removed 131 */ 132 public void removeVetoableChangeListener(VetoableChangeListener listener) { 133 if (listener == null) { 134 return; 135 } 136 if (listener instanceof VetoableChangeListenerProxy) { 137 VetoableChangeListenerProxy proxy = 138 (VetoableChangeListenerProxy)listener; 139 // Call two argument remove method. 140 removeVetoableChangeListener(proxy.getPropertyName(), 141 proxy.getListener()); 142 } else { 143 this.map.remove(null, listener); 144 } 145 } 146 147 /** 148 * Returns an array of all the listeners that were added to the 149 * VetoableChangeSupport object with addVetoableChangeListener(). 150 * <p> 151 * If some listeners have been added with a named property, then 152 * the returned array will be a mixture of VetoableChangeListeners 153 * and <code>VetoableChangeListenerProxy</code>s. If the calling 154 * method is interested in distinguishing the listeners then it must 155 * test each element to see if it's a 156 * <code>VetoableChangeListenerProxy</code>, perform the cast, and examine 157 * the parameter. 158 * 159 * <pre> 160 * VetoableChangeListener[] listeners = bean.getVetoableChangeListeners(); 161 * for (int i = 0; i < listeners.length; i++) { 162 * if (listeners[i] instanceof VetoableChangeListenerProxy) { 163 * VetoableChangeListenerProxy proxy = 164 * (VetoableChangeListenerProxy)listeners[i]; 165 * if (proxy.getPropertyName().equals("foo")) { 166 * // proxy is a VetoableChangeListener which was associated 167 * // with the property named "foo" 168 * } 169 * } 170 * } 171 *</pre> 172 * 173 * @see VetoableChangeListenerProxy 174 * @return all of the <code>VetoableChangeListeners</code> added or an 175 * empty array if no listeners have been added 176 * @since 1.4 177 */ 178 public VetoableChangeListener[] getVetoableChangeListeners(){ 179 return this.map.getListeners(); 180 } 181 182 /** 183 * Add a VetoableChangeListener for a specific property. The listener 184 * will be invoked only when a call on fireVetoableChange names that 185 * specific property. 186 * The same listener object may be added more than once. For each 187 * property, the listener will be invoked the number of times it was added 188 * for that property. 189 * If <code>propertyName</code> or <code>listener</code> is null, no 190 * exception is thrown and no action is taken. 191 * 192 * @param propertyName The name of the property to listen on. 193 * @param listener The VetoableChangeListener to be added 194 */ 195 public void addVetoableChangeListener( 196 String propertyName, 197 VetoableChangeListener listener) { 198 if (listener == null || propertyName == null) { 199 return; 200 } 201 listener = this.map.extract(listener); 202 if (listener != null) { 203 this.map.add(propertyName, listener); 204 } 205 } 206 207 /** 208 * Remove a VetoableChangeListener for a specific property. 209 * If <code>listener</code> was added more than once to the same event 210 * source for the specified property, it will be notified one less time 211 * after being removed. 212 * If <code>propertyName</code> is null, no exception is thrown and no 213 * action is taken. 214 * If <code>listener</code> is null, or was never added for the specified 215 * property, no exception is thrown and no action is taken. 216 * 217 * @param propertyName The name of the property that was listened on. 218 * @param listener The VetoableChangeListener to be removed 219 */ 220 public void removeVetoableChangeListener( 221 String propertyName, 222 VetoableChangeListener listener) { 223 if (listener == null || propertyName == null) { 224 return; 225 } 226 listener = this.map.extract(listener); 227 if (listener != null) { 228 this.map.remove(propertyName, listener); 229 } 230 } 231 232 /** 233 * Returns an array of all the listeners which have been associated 234 * with the named property. 235 * 236 * @param propertyName The name of the property being listened to 237 * @return all the <code>VetoableChangeListeners</code> associated with 238 * the named property. If no such listeners have been added, 239 * or if <code>propertyName</code> is null, an empty array is 240 * returned. 241 * @since 1.4 242 */ 243 public VetoableChangeListener[] getVetoableChangeListeners(String propertyName) { 244 return this.map.getListeners(propertyName); 245 } 246 247 /** 248 * Reports a constrained property update to listeners 249 * that have been registered to track updates of 250 * all properties or a property with the specified name. 251 * <p> 252 * Any listener can throw a {@code PropertyVetoException} to veto the update. 253 * If one of the listeners vetoes the update, this method passes 254 * a new "undo" {@code PropertyChangeEvent} that reverts to the old value 255 * to all listeners that already confirmed this update 256 * and throws the {@code PropertyVetoException} again. 257 * <p> 258 * No event is fired if old and new values are equal and non-null. 259 * <p> 260 * This is merely a convenience wrapper around the more general 261 * {@link #fireVetoableChange(PropertyChangeEvent)} method. 262 * 263 * @param propertyName the programmatic name of the property that is about to change 264 * @param oldValue the old value of the property 265 * @param newValue the new value of the property 266 * @throws PropertyVetoException if one of listeners vetoes the property update 267 */ 268 public void fireVetoableChange(String propertyName, Object oldValue, Object newValue) 269 throws PropertyVetoException { 270 if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { 271 fireVetoableChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue)); 272 } 273 } 274 275 /** 276 * Reports an integer constrained property update to listeners 277 * that have been registered to track updates of 278 * all properties or a property with the specified name. 279 * <p> 280 * Any listener can throw a {@code PropertyVetoException} to veto the update. 281 * If one of the listeners vetoes the update, this method passes 282 * a new "undo" {@code PropertyChangeEvent} that reverts to the old value 283 * to all listeners that already confirmed this update 284 * and throws the {@code PropertyVetoException} again. 285 * <p> 286 * No event is fired if old and new values are equal. 287 * <p> 288 * This is merely a convenience wrapper around the more general 289 * {@link #fireVetoableChange(String, Object, Object)} method. 290 * 291 * @param propertyName the programmatic name of the property that is about to change 292 * @param oldValue the old value of the property 293 * @param newValue the new value of the property 294 * @throws PropertyVetoException if one of listeners vetoes the property update 295 */ 296 public void fireVetoableChange(String propertyName, int oldValue, int newValue) 297 throws PropertyVetoException { 298 if (oldValue != newValue) { 299 fireVetoableChange(propertyName, Integer.valueOf(oldValue), Integer.valueOf(newValue)); 300 } 301 } 302 303 /** 304 * Reports a boolean constrained property update to listeners 305 * that have been registered to track updates of 306 * all properties or a property with the specified name. 307 * <p> 308 * Any listener can throw a {@code PropertyVetoException} to veto the update. 309 * If one of the listeners vetoes the update, this method passes 310 * a new "undo" {@code PropertyChangeEvent} that reverts to the old value 311 * to all listeners that already confirmed this update 312 * and throws the {@code PropertyVetoException} again. 313 * <p> 314 * No event is fired if old and new values are equal. 315 * <p> 316 * This is merely a convenience wrapper around the more general 317 * {@link #fireVetoableChange(String, Object, Object)} method. 318 * 319 * @param propertyName the programmatic name of the property that is about to change 320 * @param oldValue the old value of the property 321 * @param newValue the new value of the property 322 * @throws PropertyVetoException if one of listeners vetoes the property update 323 */ 324 public void fireVetoableChange(String propertyName, boolean oldValue, boolean newValue) 325 throws PropertyVetoException { 326 if (oldValue != newValue) { 327 fireVetoableChange(propertyName, Boolean.valueOf(oldValue), Boolean.valueOf(newValue)); 328 } 329 } 330 331 /** 332 * Fires a property change event to listeners 333 * that have been registered to track updates of 334 * all properties or a property with the specified name. 335 * <p> 336 * Any listener can throw a {@code PropertyVetoException} to veto the update. 337 * If one of the listeners vetoes the update, this method passes 338 * a new "undo" {@code PropertyChangeEvent} that reverts to the old value 339 * to all listeners that already confirmed this update 340 * and throws the {@code PropertyVetoException} again. 341 * <p> 342 * No event is fired if the given event's old and new values are equal and non-null. 343 * 344 * @param event the {@code PropertyChangeEvent} to be fired 345 * @throws PropertyVetoException if one of listeners vetoes the property update 346 */ 347 public void fireVetoableChange(PropertyChangeEvent event) 348 throws PropertyVetoException { 349 Object oldValue = event.getOldValue(); 350 Object newValue = event.getNewValue(); 351 if (oldValue == null || newValue == null || !oldValue.equals(newValue)) { 352 String name = event.getPropertyName(); 353 354 VetoableChangeListener[] common = this.map.get(null); 355 VetoableChangeListener[] named = (name != null) 356 ? this.map.get(name) 357 : null; 358 359 VetoableChangeListener[] listeners; 360 if (common == null) { 361 listeners = named; 362 } 363 else if (named == null) { 364 listeners = common; 365 } 366 else { 367 listeners = new VetoableChangeListener[common.length + named.length]; 368 System.arraycopy(common, 0, listeners, 0, common.length); 369 System.arraycopy(named, 0, listeners, common.length, named.length); 370 } 371 if (listeners != null) { 372 int current = 0; 373 try { 374 while (current < listeners.length) { 375 listeners[current].vetoableChange(event); 376 current++; 377 } 378 } 379 catch (PropertyVetoException veto) { 380 event = new PropertyChangeEvent(this.source, name, newValue, oldValue); 381 for (int i = 0; i < current; i++) { 382 try { 383 listeners[i].vetoableChange(event); 384 } 385 catch (PropertyVetoException exception) { 386 // ignore exceptions that occur during rolling back 387 } 388 } 389 throw veto; // rethrow the veto exception 390 } 391 } 392 } 393 } 394 395 /** 396 * Check if there are any listeners for a specific property, including 397 * those registered on all properties. If <code>propertyName</code> 398 * is null, only check for listeners registered on all properties. 399 * 400 * @param propertyName the property name. 401 * @return true if there are one or more listeners for the given property 402 */ 403 public boolean hasListeners(String propertyName) { 404 return this.map.hasListeners(propertyName); 405 } 406 407 /** 408 * @serialData Null terminated list of <code>VetoableChangeListeners</code>. 409 * <p> 410 * At serialization time we skip non-serializable listeners and 411 * only serialize the serializable listeners. 412 */ 413 private void writeObject(ObjectOutputStream s) throws IOException { 414 Hashtable<String, VetoableChangeSupport> children = null; 415 VetoableChangeListener[] listeners = null; 416 synchronized (this.map) { 417 for (Entry<String, VetoableChangeListener[]> entry : this.map.getEntries()) { 418 String property = entry.getKey(); 419 if (property == null) { 420 listeners = entry.getValue(); 421 } else { 422 if (children == null) { 423 children = new Hashtable<>(); 424 } 425 VetoableChangeSupport vcs = new VetoableChangeSupport(this.source); 426 vcs.map.set(null, entry.getValue()); 427 children.put(property, vcs); 428 } 429 } 430 } 431 ObjectOutputStream.PutField fields = s.putFields(); 432 fields.put("children", children); 433 fields.put("source", this.source); 434 fields.put("vetoableChangeSupportSerializedDataVersion", 2); 435 s.writeFields(); 436 437 if (listeners != null) { 438 for (VetoableChangeListener l : listeners) { 439 if (l instanceof Serializable) { 440 s.writeObject(l); 441 } 442 } 443 } 444 s.writeObject(null); 445 } 446 447 private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException { 448 this.map = new VetoableChangeListenerMap(); 449 450 ObjectInputStream.GetField fields = s.readFields(); 451 452 @SuppressWarnings("unchecked") 453 Hashtable<String, VetoableChangeSupport> children = (Hashtable<String, VetoableChangeSupport>)fields.get("children", null); 454 this.source = fields.get("source", null); 455 fields.get("vetoableChangeSupportSerializedDataVersion", 2); 456 457 Object listenerOrNull; 458 while (null != (listenerOrNull = s.readObject())) { 459 this.map.add(null, (VetoableChangeListener)listenerOrNull); 460 } 461 if (children != null) { 462 for (Entry<String, VetoableChangeSupport> entry : children.entrySet()) { 463 for (VetoableChangeListener listener : entry.getValue().getVetoableChangeListeners()) { 464 this.map.add(entry.getKey(), listener); 465 } 466 } 467 } 468 } 469 470 /** 471 * The object to be provided as the "source" for any generated events. 472 */ 473 private Object source; 474 475 /** 476 * @serialField children Hashtable 477 * @serialField source Object 478 * @serialField vetoableChangeSupportSerializedDataVersion int 479 */ 480 private static final ObjectStreamField[] serialPersistentFields = { 481 new ObjectStreamField("children", Hashtable.class), 482 new ObjectStreamField("source", Object.class), 483 new ObjectStreamField("vetoableChangeSupportSerializedDataVersion", Integer.TYPE) 484 }; 485 486 /** 487 * Serialization version ID, so we're compatible with JDK 1.1 488 */ 489 static final long serialVersionUID = -5090210921595982017L; 490 491 /** 492 * This is a {@link ChangeListenerMap ChangeListenerMap} implementation 493 * that works with {@link VetoableChangeListener VetoableChangeListener} objects. 494 */ 495 private static final class VetoableChangeListenerMap extends ChangeListenerMap<VetoableChangeListener> { 496 private static final VetoableChangeListener[] EMPTY = {}; 497 498 /** 499 * Creates an array of {@link VetoableChangeListener VetoableChangeListener} objects. 500 * This method uses the same instance of the empty array 501 * when {@code length} equals {@code 0}. 502 * 503 * @param length the array length 504 * @return an array with specified length 505 */ 506 @Override 507 protected VetoableChangeListener[] newArray(int length) { 508 return (0 < length) 509 ? new VetoableChangeListener[length] 510 : EMPTY; 511 } 512 513 /** 514 * Creates a {@link VetoableChangeListenerProxy VetoableChangeListenerProxy} 515 * object for the specified property. 516 * 517 * @param name the name of the property to listen on 518 * @param listener the listener to process events 519 * @return a {@code VetoableChangeListenerProxy} object 520 */ 521 @Override 522 protected VetoableChangeListener newProxy(String name, VetoableChangeListener listener) { 523 return new VetoableChangeListenerProxy(name, listener); 524 } 525 526 /** 527 * {@inheritDoc} 528 */ 529 public final VetoableChangeListener extract(VetoableChangeListener listener) { 530 while (listener instanceof VetoableChangeListenerProxy) { 531 listener = ((VetoableChangeListenerProxy) listener).getListener(); 532 } 533 return listener; 534 } 535 } 536 }