Path: blob/master/src/java.desktop/share/classes/java/beans/VetoableChangeSupport.java
41152 views
/*1* Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package java.beans;2627import java.io.IOException;28import java.io.ObjectInputStream;29import java.io.ObjectOutputStream;30import java.io.ObjectStreamField;31import java.io.Serial;32import java.io.Serializable;33import java.util.Hashtable;34import java.util.Map.Entry;3536/**37* This is a utility class that can be used by beans that support constrained38* properties. It manages a list of listeners and dispatches39* {@link PropertyChangeEvent}s to them. You can use an instance of this class40* as a member field of your bean and delegate these types of work to it.41* The {@link VetoableChangeListener} can be registered for all properties42* or for a property specified by name.43* <p>44* Here is an example of {@code VetoableChangeSupport} usage that follows45* the rules and recommendations laid out in the JavaBeans specification:46* <pre>{@code47* public class MyBean {48* private final VetoableChangeSupport vcs = new VetoableChangeSupport(this);49*50* public void addVetoableChangeListener(VetoableChangeListener listener) {51* this.vcs.addVetoableChangeListener(listener);52* }53*54* public void removeVetoableChangeListener(VetoableChangeListener listener) {55* this.vcs.removeVetoableChangeListener(listener);56* }57*58* private String value;59*60* public String getValue() {61* return this.value;62* }63*64* public void setValue(String newValue) throws PropertyVetoException {65* String oldValue = this.value;66* this.vcs.fireVetoableChange("value", oldValue, newValue);67* this.value = newValue;68* }69*70* [...]71* }72* }</pre>73* <p>74* A {@code VetoableChangeSupport} instance is thread-safe.75* <p>76* This class is serializable. When it is serialized it will save77* (and restore) any listeners that are themselves serializable. Any78* non-serializable listeners will be skipped during serialization.79*80* @see PropertyChangeSupport81* @since 1.182*/83public class VetoableChangeSupport implements Serializable {84private VetoableChangeListenerMap map = new VetoableChangeListenerMap();8586/**87* Constructs a {@code VetoableChangeSupport} object.88*89* @param sourceBean The bean to be given as the source for any events.90*/91public VetoableChangeSupport(Object sourceBean) {92if (sourceBean == null) {93throw new NullPointerException();94}95source = sourceBean;96}9798/**99* Add a VetoableChangeListener to the listener list.100* The listener is registered for all properties.101* The same listener object may be added more than once, and will be called102* as many times as it is added.103* If {@code listener} is null, no exception is thrown and no action104* is taken.105*106* @param listener The VetoableChangeListener to be added107*/108public void addVetoableChangeListener(VetoableChangeListener listener) {109if (listener == null) {110return;111}112if (listener instanceof VetoableChangeListenerProxy) {113VetoableChangeListenerProxy proxy =114(VetoableChangeListenerProxy)listener;115// Call two argument add method.116addVetoableChangeListener(proxy.getPropertyName(),117proxy.getListener());118} else {119this.map.add(null, listener);120}121}122123/**124* Remove a VetoableChangeListener from the listener list.125* This removes a VetoableChangeListener that was registered126* for all properties.127* If {@code listener} was added more than once to the same event128* source, it will be notified one less time after being removed.129* If {@code listener} is null, or was never added, no exception is130* thrown and no action is taken.131*132* @param listener The VetoableChangeListener to be removed133*/134public void removeVetoableChangeListener(VetoableChangeListener listener) {135if (listener == null) {136return;137}138if (listener instanceof VetoableChangeListenerProxy) {139VetoableChangeListenerProxy proxy =140(VetoableChangeListenerProxy)listener;141// Call two argument remove method.142removeVetoableChangeListener(proxy.getPropertyName(),143proxy.getListener());144} else {145this.map.remove(null, listener);146}147}148149/**150* Returns an array of all the listeners that were added to the151* VetoableChangeSupport object with addVetoableChangeListener().152* <p>153* If some listeners have been added with a named property, then154* the returned array will be a mixture of VetoableChangeListeners155* and {@code VetoableChangeListenerProxy}s. If the calling156* method is interested in distinguishing the listeners then it must157* test each element to see if it's a158* {@code VetoableChangeListenerProxy}, perform the cast, and examine159* the parameter.160*161* <pre>{@code162* VetoableChangeListener[] listeners = bean.getVetoableChangeListeners();163* for (int i = 0; i < listeners.length; i++) {164* if (listeners[i] instanceof VetoableChangeListenerProxy) {165* VetoableChangeListenerProxy proxy =166* (VetoableChangeListenerProxy)listeners[i];167* if (proxy.getPropertyName().equals("foo")) {168* // proxy is a VetoableChangeListener which was associated169* // with the property named "foo"170* }171* }172* }173* }</pre>174*175* @see VetoableChangeListenerProxy176* @return all of the {@code VetoableChangeListeners} added or an177* empty array if no listeners have been added178* @since 1.4179*/180public VetoableChangeListener[] getVetoableChangeListeners(){181return this.map.getListeners();182}183184/**185* Add a VetoableChangeListener for a specific property. The listener186* will be invoked only when a call on fireVetoableChange names that187* specific property.188* The same listener object may be added more than once. For each189* property, the listener will be invoked the number of times it was added190* for that property.191* If {@code propertyName} or {@code listener} is null, no192* exception is thrown and no action is taken.193*194* @param propertyName The name of the property to listen on.195* @param listener The VetoableChangeListener to be added196* @since 1.2197*/198public void addVetoableChangeListener(199String propertyName,200VetoableChangeListener listener) {201if (listener == null || propertyName == null) {202return;203}204listener = this.map.extract(listener);205if (listener != null) {206this.map.add(propertyName, listener);207}208}209210/**211* Remove a VetoableChangeListener for a specific property.212* If {@code listener} was added more than once to the same event213* source for the specified property, it will be notified one less time214* after being removed.215* If {@code propertyName} is null, no exception is thrown and no216* action is taken.217* If {@code listener} is null, or was never added for the specified218* property, no exception is thrown and no action is taken.219*220* @param propertyName The name of the property that was listened on.221* @param listener The VetoableChangeListener to be removed222* @since 1.2223*/224public void removeVetoableChangeListener(225String propertyName,226VetoableChangeListener listener) {227if (listener == null || propertyName == null) {228return;229}230listener = this.map.extract(listener);231if (listener != null) {232this.map.remove(propertyName, listener);233}234}235236/**237* Returns an array of all the listeners which have been associated238* with the named property.239*240* @param propertyName The name of the property being listened to241* @return all the {@code VetoableChangeListeners} associated with242* the named property. If no such listeners have been added,243* or if {@code propertyName} is null, an empty array is244* returned.245* @since 1.4246*/247public VetoableChangeListener[] getVetoableChangeListeners(String propertyName) {248return this.map.getListeners(propertyName);249}250251/**252* Reports a constrained property update to listeners253* that have been registered to track updates of254* all properties or a property with the specified name.255* <p>256* Any listener can throw a {@code PropertyVetoException} to veto the update.257* If one of the listeners vetoes the update, this method passes258* a new "undo" {@code PropertyChangeEvent} that reverts to the old value259* to all listeners that already confirmed this update260* and throws the {@code PropertyVetoException} again.261* <p>262* No event is fired if old and new values are equal and non-null.263* <p>264* This is merely a convenience wrapper around the more general265* {@link #fireVetoableChange(PropertyChangeEvent)} method.266*267* @param propertyName the programmatic name of the property that is about to change268* @param oldValue the old value of the property269* @param newValue the new value of the property270* @throws PropertyVetoException if one of listeners vetoes the property update271*/272public void fireVetoableChange(String propertyName, Object oldValue, Object newValue)273throws PropertyVetoException {274if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {275fireVetoableChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue));276}277}278279/**280* Reports an integer constrained property update to listeners281* that have been registered to track updates of282* all properties or a property with the specified name.283* <p>284* Any listener can throw a {@code PropertyVetoException} to veto the update.285* If one of the listeners vetoes the update, this method passes286* a new "undo" {@code PropertyChangeEvent} that reverts to the old value287* to all listeners that already confirmed this update288* and throws the {@code PropertyVetoException} again.289* <p>290* No event is fired if old and new values are equal.291* <p>292* This is merely a convenience wrapper around the more general293* {@link #fireVetoableChange(String, Object, Object)} method.294*295* @param propertyName the programmatic name of the property that is about to change296* @param oldValue the old value of the property297* @param newValue the new value of the property298* @throws PropertyVetoException if one of listeners vetoes the property update299* @since 1.2300*/301public void fireVetoableChange(String propertyName, int oldValue, int newValue)302throws PropertyVetoException {303if (oldValue != newValue) {304fireVetoableChange(propertyName, Integer.valueOf(oldValue), Integer.valueOf(newValue));305}306}307308/**309* Reports a boolean constrained property update to listeners310* that have been registered to track updates of311* all properties or a property with the specified name.312* <p>313* Any listener can throw a {@code PropertyVetoException} to veto the update.314* If one of the listeners vetoes the update, this method passes315* a new "undo" {@code PropertyChangeEvent} that reverts to the old value316* to all listeners that already confirmed this update317* and throws the {@code PropertyVetoException} again.318* <p>319* No event is fired if old and new values are equal.320* <p>321* This is merely a convenience wrapper around the more general322* {@link #fireVetoableChange(String, Object, Object)} method.323*324* @param propertyName the programmatic name of the property that is about to change325* @param oldValue the old value of the property326* @param newValue the new value of the property327* @throws PropertyVetoException if one of listeners vetoes the property update328* @since 1.2329*/330public void fireVetoableChange(String propertyName, boolean oldValue, boolean newValue)331throws PropertyVetoException {332if (oldValue != newValue) {333fireVetoableChange(propertyName, Boolean.valueOf(oldValue), Boolean.valueOf(newValue));334}335}336337/**338* Fires a property change event to listeners339* that have been registered to track updates of340* all properties or a property with the specified name.341* <p>342* Any listener can throw a {@code PropertyVetoException} to veto the update.343* If one of the listeners vetoes the update, this method passes344* a new "undo" {@code PropertyChangeEvent} that reverts to the old value345* to all listeners that already confirmed this update346* and throws the {@code PropertyVetoException} again.347* <p>348* No event is fired if the given event's old and new values are equal and non-null.349*350* @param event the {@code PropertyChangeEvent} to be fired351* @throws PropertyVetoException if one of listeners vetoes the property update352* @since 1.2353*/354public void fireVetoableChange(PropertyChangeEvent event)355throws PropertyVetoException {356Object oldValue = event.getOldValue();357Object newValue = event.getNewValue();358if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {359String name = event.getPropertyName();360361VetoableChangeListener[] common = this.map.get(null);362VetoableChangeListener[] named = (name != null)363? this.map.get(name)364: null;365366VetoableChangeListener[] listeners;367if (common == null) {368listeners = named;369}370else if (named == null) {371listeners = common;372}373else {374listeners = new VetoableChangeListener[common.length + named.length];375System.arraycopy(common, 0, listeners, 0, common.length);376System.arraycopy(named, 0, listeners, common.length, named.length);377}378if (listeners != null) {379int current = 0;380try {381while (current < listeners.length) {382listeners[current].vetoableChange(event);383current++;384}385}386catch (PropertyVetoException veto) {387event = new PropertyChangeEvent(this.source, name, newValue, oldValue);388for (int i = 0; i < current; i++) {389try {390listeners[i].vetoableChange(event);391}392catch (PropertyVetoException exception) {393// ignore exceptions that occur during rolling back394}395}396throw veto; // rethrow the veto exception397}398}399}400}401402/**403* Check if there are any listeners for a specific property, including404* those registered on all properties. If {@code propertyName}405* is null, only check for listeners registered on all properties.406*407* @param propertyName the property name.408* @return true if there are one or more listeners for the given property409* @since 1.2410*/411public boolean hasListeners(String propertyName) {412return this.map.hasListeners(propertyName);413}414415/**416* Writes serializable fields to stream.417*418* @param s the {@code ObjectOutputStream} to write419* @throws IOException if an I/O error occurs420* @serialData Null terminated list of {@code VetoableChangeListeners}.421* <p>422* At serialization time we skip non-serializable listeners and423* only serialize the serializable listeners.424*/425@Serial426private void writeObject(ObjectOutputStream s) throws IOException {427Hashtable<String, VetoableChangeSupport> children = null;428VetoableChangeListener[] listeners = null;429synchronized (this.map) {430for (Entry<String, VetoableChangeListener[]> entry : this.map.getEntries()) {431String property = entry.getKey();432if (property == null) {433listeners = entry.getValue();434} else {435if (children == null) {436children = new Hashtable<>();437}438VetoableChangeSupport vcs = new VetoableChangeSupport(this.source);439vcs.map.set(null, entry.getValue());440children.put(property, vcs);441}442}443}444ObjectOutputStream.PutField fields = s.putFields();445fields.put("children", children);446fields.put("source", this.source);447fields.put("vetoableChangeSupportSerializedDataVersion", 2);448s.writeFields();449450if (listeners != null) {451for (VetoableChangeListener l : listeners) {452if (l instanceof Serializable) {453s.writeObject(l);454}455}456}457s.writeObject(null);458}459460/**461* Reads the {@code ObjectInputStream}.462*463* @param s the {@code ObjectInputStream} to read464* @throws ClassNotFoundException if the class of a serialized object could465* not be found466* @throws IOException if an I/O error occurs467*/468@Serial469private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {470this.map = new VetoableChangeListenerMap();471472ObjectInputStream.GetField fields = s.readFields();473474@SuppressWarnings("unchecked")475Hashtable<String, VetoableChangeSupport> children = (Hashtable<String, VetoableChangeSupport>)fields.get("children", null);476this.source = fields.get("source", null);477fields.get("vetoableChangeSupportSerializedDataVersion", 2);478479Object listenerOrNull;480while (null != (listenerOrNull = s.readObject())) {481this.map.add(null, (VetoableChangeListener)listenerOrNull);482}483if (children != null) {484for (Entry<String, VetoableChangeSupport> entry : children.entrySet()) {485for (VetoableChangeListener listener : entry.getValue().getVetoableChangeListeners()) {486this.map.add(entry.getKey(), listener);487}488}489}490}491492/**493* The object to be provided as the "source" for any generated events.494*/495private Object source;496497/**498* @serialField children Hashtable499* The list of {@code PropertyChangeListeners}500* @serialField source Object501* The object to be provided as the "source" for any generated502* events503* @serialField vetoableChangeSupportSerializedDataVersion int504* The version505*/506@Serial507private static final ObjectStreamField[] serialPersistentFields = {508new ObjectStreamField("children", Hashtable.class),509new ObjectStreamField("source", Object.class),510new ObjectStreamField("vetoableChangeSupportSerializedDataVersion", Integer.TYPE)511};512513/**514* Use serialVersionUID from JDK 1.1 for interoperability.515*/516@Serial517private static final long serialVersionUID = -5090210921595982017L;518519/**520* This is a {@link ChangeListenerMap ChangeListenerMap} implementation521* that works with {@link VetoableChangeListener VetoableChangeListener} objects.522*/523private static final class VetoableChangeListenerMap extends ChangeListenerMap<VetoableChangeListener> {524private static final VetoableChangeListener[] EMPTY = {};525526/**527* Creates an array of {@link VetoableChangeListener VetoableChangeListener} objects.528* This method uses the same instance of the empty array529* when {@code length} equals {@code 0}.530*531* @param length the array length532* @return an array with specified length533*/534@Override535protected VetoableChangeListener[] newArray(int length) {536return (0 < length)537? new VetoableChangeListener[length]538: EMPTY;539}540541/**542* Creates a {@link VetoableChangeListenerProxy VetoableChangeListenerProxy}543* object for the specified property.544*545* @param name the name of the property to listen on546* @param listener the listener to process events547* @return a {@code VetoableChangeListenerProxy} object548*/549@Override550protected VetoableChangeListener newProxy(String name, VetoableChangeListener listener) {551return new VetoableChangeListenerProxy(name, listener);552}553554/**555* {@inheritDoc}556*/557public VetoableChangeListener extract(VetoableChangeListener listener) {558while (listener instanceof VetoableChangeListenerProxy) {559listener = ((VetoableChangeListenerProxy) listener).getListener();560}561return listener;562}563}564}565566567