Path: blob/master/src/java.desktop/share/classes/java/beans/PropertyChangeSupport.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 bound38* 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 PropertyChangeListener} can be registered for all properties42* or for a property specified by name.43* <p>44* Here is an example of {@code PropertyChangeSupport} usage that follows45* the rules and recommendations laid out in the JavaBeans specification:46* <pre>47* public class MyBean {48* private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);49*50* public void addPropertyChangeListener(PropertyChangeListener listener) {51* this.pcs.addPropertyChangeListener(listener);52* }53*54* public void removePropertyChangeListener(PropertyChangeListener listener) {55* this.pcs.removePropertyChangeListener(listener);56* }57*58* private String value;59*60* public String getValue() {61* return this.value;62* }63*64* public void setValue(String newValue) {65* String oldValue = this.value;66* this.value = newValue;67* this.pcs.firePropertyChange("value", oldValue, newValue);68* }69*70* [...]71* }72* </pre>73* <p>74* A {@code PropertyChangeSupport} 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 VetoableChangeSupport81* @since 1.182*/83public class PropertyChangeSupport implements Serializable {84private PropertyChangeListenerMap map = new PropertyChangeListenerMap();8586/**87* Constructs a {@code PropertyChangeSupport} object.88*89* @param sourceBean The bean to be given as the source for any events.90*/91public PropertyChangeSupport(Object sourceBean) {92if (sourceBean == null) {93throw new NullPointerException();94}95source = sourceBean;96}9798/**99* Add a PropertyChangeListener 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 PropertyChangeListener to be added107*/108public void addPropertyChangeListener(PropertyChangeListener listener) {109if (listener == null) {110return;111}112if (listener instanceof PropertyChangeListenerProxy) {113PropertyChangeListenerProxy proxy =114(PropertyChangeListenerProxy)listener;115// Call two argument add method.116addPropertyChangeListener(proxy.getPropertyName(),117proxy.getListener());118} else {119this.map.add(null, listener);120}121}122123/**124* Remove a PropertyChangeListener from the listener list.125* This removes a PropertyChangeListener 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 PropertyChangeListener to be removed133*/134public void removePropertyChangeListener(PropertyChangeListener listener) {135if (listener == null) {136return;137}138if (listener instanceof PropertyChangeListenerProxy) {139PropertyChangeListenerProxy proxy =140(PropertyChangeListenerProxy)listener;141// Call two argument remove method.142removePropertyChangeListener(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* PropertyChangeSupport object with addPropertyChangeListener().152* <p>153* If some listeners have been added with a named property, then154* the returned array will be a mixture of PropertyChangeListeners155* and {@code PropertyChangeListenerProxy}s. If the calling156* method is interested in distinguishing the listeners then it must157* test each element to see if it's a158* {@code PropertyChangeListenerProxy}, perform the cast, and examine159* the parameter.160*161* <pre>{@code162* PropertyChangeListener[] listeners = bean.getPropertyChangeListeners();163* for (int i = 0; i < listeners.length; i++) {164* if (listeners[i] instanceof PropertyChangeListenerProxy) {165* PropertyChangeListenerProxy proxy =166* (PropertyChangeListenerProxy)listeners[i];167* if (proxy.getPropertyName().equals("foo")) {168* // proxy is a PropertyChangeListener which was associated169* // with the property named "foo"170* }171* }172* }173* }</pre>174*175* @see PropertyChangeListenerProxy176* @return all of the {@code PropertyChangeListeners} added or an177* empty array if no listeners have been added178* @since 1.4179*/180public PropertyChangeListener[] getPropertyChangeListeners() {181return this.map.getListeners();182}183184/**185* Add a PropertyChangeListener for a specific property. The listener186* will be invoked only when a call on firePropertyChange 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 PropertyChangeListener to be added196* @since 1.2197*/198public void addPropertyChangeListener(199String propertyName,200PropertyChangeListener 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 PropertyChangeListener 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 PropertyChangeListener to be removed222* @since 1.2223*/224public void removePropertyChangeListener(225String propertyName,226PropertyChangeListener 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 of the {@code PropertyChangeListeners} 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 PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {248return this.map.getListeners(propertyName);249}250251/**252* Reports a bound property update to listeners253* that have been registered to track updates of254* all properties or a property with the specified name.255* <p>256* No event is fired if old and new values are equal and non-null.257* <p>258* This is merely a convenience wrapper around the more general259* {@link #firePropertyChange(PropertyChangeEvent)} method.260*261* @param propertyName the programmatic name of the property that was changed262* @param oldValue the old value of the property263* @param newValue the new value of the property264*/265public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {266if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {267firePropertyChange(new PropertyChangeEvent(this.source, propertyName, oldValue, newValue));268}269}270271/**272* Reports an integer bound property update to listeners273* that have been registered to track updates of274* all properties or a property with the specified name.275* <p>276* No event is fired if old and new values are equal.277* <p>278* This is merely a convenience wrapper around the more general279* {@link #firePropertyChange(String, Object, Object)} method.280*281* @param propertyName the programmatic name of the property that was changed282* @param oldValue the old value of the property283* @param newValue the new value of the property284* @since 1.2285*/286public void firePropertyChange(String propertyName, int oldValue, int newValue) {287if (oldValue != newValue) {288firePropertyChange(propertyName, Integer.valueOf(oldValue), Integer.valueOf(newValue));289}290}291292/**293* Reports a boolean bound property update to listeners294* that have been registered to track updates of295* all properties or a property with the specified name.296* <p>297* No event is fired if old and new values are equal.298* <p>299* This is merely a convenience wrapper around the more general300* {@link #firePropertyChange(String, Object, Object)} method.301*302* @param propertyName the programmatic name of the property that was changed303* @param oldValue the old value of the property304* @param newValue the new value of the property305* @since 1.2306*/307public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {308if (oldValue != newValue) {309firePropertyChange(propertyName, Boolean.valueOf(oldValue), Boolean.valueOf(newValue));310}311}312313/**314* Fires a property change event to listeners315* that have been registered to track updates of316* all properties or a property with the specified name.317* <p>318* No event is fired if the given event's old and new values are equal and non-null.319*320* @param event the {@code PropertyChangeEvent} to be fired321* @since 1.2322*/323public void firePropertyChange(PropertyChangeEvent event) {324Object oldValue = event.getOldValue();325Object newValue = event.getNewValue();326if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {327String name = event.getPropertyName();328329PropertyChangeListener[] common = this.map.get(null);330PropertyChangeListener[] named = (name != null)331? this.map.get(name)332: null;333334fire(common, event);335fire(named, event);336}337}338339private static void fire(PropertyChangeListener[] listeners, PropertyChangeEvent event) {340if (listeners != null) {341for (PropertyChangeListener listener : listeners) {342listener.propertyChange(event);343}344}345}346347/**348* Reports a bound indexed property update to listeners349* that have been registered to track updates of350* all properties or a property with the specified name.351* <p>352* No event is fired if old and new values are equal and non-null.353* <p>354* This is merely a convenience wrapper around the more general355* {@link #firePropertyChange(PropertyChangeEvent)} method.356*357* @param propertyName the programmatic name of the property that was changed358* @param index the index of the property element that was changed359* @param oldValue the old value of the property360* @param newValue the new value of the property361* @since 1.5362*/363public void fireIndexedPropertyChange(String propertyName, int index, Object oldValue, Object newValue) {364if (oldValue == null || newValue == null || !oldValue.equals(newValue)) {365firePropertyChange(new IndexedPropertyChangeEvent(source, propertyName, oldValue, newValue, index));366}367}368369/**370* Reports an integer bound indexed property update to listeners371* that have been registered to track updates of372* all properties or a property with the specified name.373* <p>374* No event is fired if old and new values are equal.375* <p>376* This is merely a convenience wrapper around the more general377* {@link #fireIndexedPropertyChange(String, int, Object, Object)} method.378*379* @param propertyName the programmatic name of the property that was changed380* @param index the index of the property element that was changed381* @param oldValue the old value of the property382* @param newValue the new value of the property383* @since 1.5384*/385public void fireIndexedPropertyChange(String propertyName, int index, int oldValue, int newValue) {386if (oldValue != newValue) {387fireIndexedPropertyChange(propertyName, index, Integer.valueOf(oldValue), Integer.valueOf(newValue));388}389}390391/**392* Reports a boolean bound indexed property update to listeners393* that have been registered to track updates of394* all properties or a property with the specified name.395* <p>396* No event is fired if old and new values are equal.397* <p>398* This is merely a convenience wrapper around the more general399* {@link #fireIndexedPropertyChange(String, int, Object, Object)} method.400*401* @param propertyName the programmatic name of the property that was changed402* @param index the index of the property element that was changed403* @param oldValue the old value of the property404* @param newValue the new value of the property405* @since 1.5406*/407public void fireIndexedPropertyChange(String propertyName, int index, boolean oldValue, boolean newValue) {408if (oldValue != newValue) {409fireIndexedPropertyChange(propertyName, index, Boolean.valueOf(oldValue), Boolean.valueOf(newValue));410}411}412413/**414* Check if there are any listeners for a specific property, including415* those registered on all properties. If {@code propertyName}416* is null, only check for listeners registered on all properties.417*418* @param propertyName the property name.419* @return true if there are one or more listeners for the given property420* @since 1.2421*/422public boolean hasListeners(String propertyName) {423return this.map.hasListeners(propertyName);424}425426/**427* Writes serializable fields to stream.428*429* @param s the {@code ObjectOutputStream} to write430* @throws IOException if an I/O error occurs431* @serialData Null terminated list of {@code PropertyChangeListeners}.432* <p>433* At serialization time we skip non-serializable listeners and434* only serialize the serializable listeners.435*/436@Serial437private void writeObject(ObjectOutputStream s) throws IOException {438Hashtable<String, PropertyChangeSupport> children = null;439PropertyChangeListener[] listeners = null;440synchronized (this.map) {441for (Entry<String, PropertyChangeListener[]> entry : this.map.getEntries()) {442String property = entry.getKey();443if (property == null) {444listeners = entry.getValue();445} else {446if (children == null) {447children = new Hashtable<>();448}449PropertyChangeSupport pcs = new PropertyChangeSupport(this.source);450pcs.map.set(null, entry.getValue());451children.put(property, pcs);452}453}454}455ObjectOutputStream.PutField fields = s.putFields();456fields.put("children", children);457fields.put("source", this.source);458fields.put("propertyChangeSupportSerializedDataVersion", 2);459s.writeFields();460461if (listeners != null) {462for (PropertyChangeListener l : listeners) {463if (l instanceof Serializable) {464s.writeObject(l);465}466}467}468s.writeObject(null);469}470471/**472* Reads the {@code ObjectInputStream}.473*474* @param s the {@code ObjectInputStream} to read475* @throws ClassNotFoundException if the class of a serialized object could476* not be found477* @throws IOException if an I/O error occurs478*/479@Serial480private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {481this.map = new PropertyChangeListenerMap();482483ObjectInputStream.GetField fields = s.readFields();484485@SuppressWarnings("unchecked")486Hashtable<String, PropertyChangeSupport> children = (Hashtable<String, PropertyChangeSupport>) fields.get("children", null);487this.source = fields.get("source", null);488fields.get("propertyChangeSupportSerializedDataVersion", 2);489490Object listenerOrNull;491while (null != (listenerOrNull = s.readObject())) {492this.map.add(null, (PropertyChangeListener)listenerOrNull);493}494if (children != null) {495for (Entry<String, PropertyChangeSupport> entry : children.entrySet()) {496for (PropertyChangeListener listener : entry.getValue().getPropertyChangeListeners()) {497this.map.add(entry.getKey(), listener);498}499}500}501}502503/**504* The object to be provided as the "source" for any generated events.505*/506private Object source;507508/**509* @serialField children Hashtable510* The list of {@code PropertyChangeListeners}511* @serialField source Object512* The object to be provided as the "source" for any generated513* events514* @serialField propertyChangeSupportSerializedDataVersion int515* The version516*/517@Serial518private static final ObjectStreamField[] serialPersistentFields = {519new ObjectStreamField("children", Hashtable.class),520new ObjectStreamField("source", Object.class),521new ObjectStreamField("propertyChangeSupportSerializedDataVersion", Integer.TYPE)522};523524/**525* Use serialVersionUID from JDK 1.1 for interoperability.526*/527@Serial528private static final long serialVersionUID = 6401253773779951803L;529530/**531* This is a {@link ChangeListenerMap ChangeListenerMap} implementation532* that works with {@link PropertyChangeListener PropertyChangeListener} objects.533*/534private static final class PropertyChangeListenerMap extends ChangeListenerMap<PropertyChangeListener> {535private static final PropertyChangeListener[] EMPTY = {};536537/**538* Creates an array of {@link PropertyChangeListener PropertyChangeListener} objects.539* This method uses the same instance of the empty array540* when {@code length} equals {@code 0}.541*542* @param length the array length543* @return an array with specified length544*/545@Override546protected PropertyChangeListener[] newArray(int length) {547return (0 < length)548? new PropertyChangeListener[length]549: EMPTY;550}551552/**553* Creates a {@link PropertyChangeListenerProxy PropertyChangeListenerProxy}554* object for the specified property.555*556* @param name the name of the property to listen on557* @param listener the listener to process events558* @return a {@code PropertyChangeListenerProxy} object559*/560@Override561protected PropertyChangeListener newProxy(String name, PropertyChangeListener listener) {562return new PropertyChangeListenerProxy(name, listener);563}564565/**566* {@inheritDoc}567*/568public PropertyChangeListener extract(PropertyChangeListener listener) {569while (listener instanceof PropertyChangeListenerProxy) {570listener = ((PropertyChangeListenerProxy) listener).getListener();571}572return listener;573}574}575}576577578