Home | History | Annotate | Download | only in beans
      1 /*
      2  *  Licensed to the Apache Software Foundation (ASF) under one or more
      3  *  contributor license agreements.  See the NOTICE file distributed with
      4  *  this work for additional information regarding copyright ownership.
      5  *  The ASF licenses this file to You under the Apache License, Version 2.0
      6  *  (the "License"); you may not use this file except in compliance with
      7  *  the License.  You may obtain a copy of the License at
      8  *
      9  *     http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  *  Unless required by applicable law or agreed to in writing, software
     12  *  distributed under the License is distributed on an "AS IS" BASIS,
     13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  *  See the License for the specific language governing permissions and
     15  *  limitations under the License.
     16  */
     17 
     18 package java.beans;
     19 
     20 import java.io.IOException;
     21 import java.io.ObjectInputStream;
     22 import java.io.ObjectOutputStream;
     23 import java.io.ObjectStreamField;
     24 import java.io.Serializable;
     25 import java.util.ArrayList;
     26 import java.util.EventListener;
     27 import java.util.Hashtable;
     28 import java.util.List;
     29 import java.util.Map;
     30 import java.util.concurrent.CopyOnWriteArrayList;
     31 import libcore.util.Objects;
     32 
     33 /**
     34  * Manages a list of listeners to be notified when a property changes. Listeners
     35  * subscribe to be notified of all property changes, or of changes to a single
     36  * named property.
     37  *
     38  * <p>This class is thread safe. No locking is necessary when subscribing or
     39  * unsubscribing listeners, or when publishing events. Callers should be careful
     40  * when publishing events because listeners may not be thread safe.
     41  */
     42 public class PropertyChangeSupport implements Serializable {
     43 
     44     private static final long serialVersionUID = 6401253773779951803l;
     45     private static final ObjectStreamField[] serialPersistentFields = {
     46         new ObjectStreamField("source", Object.class),
     47         new ObjectStreamField("children", Object.class),
     48         new ObjectStreamField("propertyChangeSupportSerializedDataVersion", int.class),
     49     };
     50 
     51     private transient Object sourceBean;
     52 
     53     /**
     54      * All listeners, including PropertyChangeListenerProxy listeners that are
     55      * only be notified when the assigned property is changed. This list may be
     56      * modified concurrently!
     57      */
     58     private transient List<PropertyChangeListener> listeners
     59             = new CopyOnWriteArrayList<PropertyChangeListener>();
     60 
     61     /**
     62      * Creates a new instance that uses the source bean as source for any event.
     63      *
     64      * @param sourceBean
     65      *            the bean used as source for all events.
     66      */
     67     public PropertyChangeSupport(Object sourceBean) {
     68         if (sourceBean == null) {
     69             throw new NullPointerException();
     70         }
     71         this.sourceBean = sourceBean;
     72     }
     73 
     74     /**
     75      * Fires a {@link PropertyChangeEvent} with the given name, old value and
     76      * new value. As source the bean used to initialize this instance is used.
     77      * If the old value and the new value are not null and equal the event will
     78      * not be fired.
     79      *
     80      * @param propertyName
     81      *            the name of the property
     82      * @param oldValue
     83      *            the old value of the property
     84      * @param newValue
     85      *            the new value of the property
     86      */
     87     public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
     88         firePropertyChange(new PropertyChangeEvent(sourceBean, propertyName, oldValue, newValue));
     89     }
     90 
     91     /**
     92      * Fires an {@link IndexedPropertyChangeEvent} with the given name, old
     93      * value, new value and index. As source the bean used to initialize this
     94      * instance is used. If the old value and the new value are not null and
     95      * equal the event will not be fired.
     96      *
     97      * @param propertyName
     98      *            the name of the property
     99      * @param index
    100      *            the index
    101      * @param oldValue
    102      *            the old value of the property
    103      * @param newValue
    104      *            the new value of the property
    105      */
    106     public void fireIndexedPropertyChange(String propertyName, int index,
    107             Object oldValue, Object newValue) {
    108         firePropertyChange(new IndexedPropertyChangeEvent(sourceBean,
    109                 propertyName, oldValue, newValue, index));
    110     }
    111 
    112     /**
    113      * Unsubscribes {@code listener} from change notifications for the property
    114      * named {@code propertyName}. If multiple subscriptions exist for {@code
    115      * listener}, it will receive one fewer notifications when the property
    116      * changes. If the property name or listener is null or not subscribed, this
    117      * method silently does nothing.
    118      */
    119     public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
    120         for (PropertyChangeListener p : listeners) {
    121             if (equals(propertyName, listener, p)) {
    122                 listeners.remove(p);
    123                 return;
    124             }
    125         }
    126     }
    127 
    128     /**
    129      * Returns true if two chains of PropertyChangeListenerProxies have the same
    130      * names in the same order and bottom out in the same event listener. This
    131      * method's signature is asymmetric to avoid allocating a proxy: if
    132      * non-null, {@code aName} represents the first property name and {@code a}
    133      * is its listener.
    134      */
    135     private boolean equals(String aName, EventListener a, EventListener b) {
    136         /*
    137          * Each iteration of the loop attempts to match a pair of property names
    138          * from a and b. If they don't match, the chains must not be equal!
    139          */
    140         while (b instanceof PropertyChangeListenerProxy) {
    141             PropertyChangeListenerProxy bProxy = (PropertyChangeListenerProxy) b; // unwrap b
    142             String bName = bProxy.getPropertyName();
    143             b = bProxy.getListener();
    144             if (aName == null) {
    145                 if (!(a instanceof PropertyChangeListenerProxy)) {
    146                     return false;
    147                 }
    148                 PropertyChangeListenerProxy aProxy = (PropertyChangeListenerProxy) a; // unwrap a
    149                 aName = aProxy.getPropertyName();
    150                 a = aProxy.getListener();
    151             }
    152             if (!Objects.equal(aName, bName)) {
    153                 return false; // not equal; a and b subscribe to different properties
    154             }
    155             aName = null;
    156         }
    157         return aName == null && Objects.equal(a, b);
    158     }
    159 
    160     /**
    161      * Subscribes {@code listener} to change notifications for the property
    162      * named {@code propertyName}. If the listener is already subscribed, it
    163      * will receive an additional notification when the property changes. If the
    164      * property name or listener is null, this method silently does nothing.
    165      */
    166     public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
    167         if (listener != null && propertyName != null) {
    168             listeners.add(new PropertyChangeListenerProxy(propertyName, listener));
    169         }
    170     }
    171 
    172     /**
    173      * Returns the subscribers to be notified when {@code propertyName} changes.
    174      * This includes both listeners subscribed to all property changes and
    175      * listeners subscribed to the named property only.
    176      */
    177     public PropertyChangeListener[] getPropertyChangeListeners(String propertyName) {
    178         List<PropertyChangeListener> result = new ArrayList<PropertyChangeListener>();
    179         for (PropertyChangeListener p : listeners) {
    180             if (p instanceof PropertyChangeListenerProxy && Objects.equal(
    181                     propertyName, ((PropertyChangeListenerProxy) p).getPropertyName())) {
    182                 result.add(p);
    183             }
    184         }
    185         return result.toArray(new PropertyChangeListener[result.size()]);
    186     }
    187 
    188     /**
    189      * Fires a property change of a boolean property with the given name. If the
    190      * old value and the new value are not null and equal the event will not be
    191      * fired.
    192      *
    193      * @param propertyName
    194      *            the property name
    195      * @param oldValue
    196      *            the old value
    197      * @param newValue
    198      *            the new value
    199      */
    200     public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) {
    201         firePropertyChange(propertyName, Boolean.valueOf(oldValue), Boolean.valueOf(newValue));
    202     }
    203 
    204     /**
    205      * Fires a property change of a boolean property with the given name. If the
    206      * old value and the new value are not null and equal the event will not be
    207      * fired.
    208      *
    209      * @param propertyName
    210      *            the property name
    211      * @param index
    212      *            the index of the changed property
    213      * @param oldValue
    214      *            the old value
    215      * @param newValue
    216      *            the new value
    217      */
    218     public void fireIndexedPropertyChange(String propertyName, int index,
    219             boolean oldValue, boolean newValue) {
    220         if (oldValue != newValue) {
    221             fireIndexedPropertyChange(propertyName, index,
    222                     Boolean.valueOf(oldValue), Boolean.valueOf(newValue));
    223         }
    224     }
    225 
    226     /**
    227      * Fires a property change of an integer property with the given name. If
    228      * the old value and the new value are not null and equal the event will not
    229      * be fired.
    230      *
    231      * @param propertyName
    232      *            the property name
    233      * @param oldValue
    234      *            the old value
    235      * @param newValue
    236      *            the new value
    237      */
    238     public void firePropertyChange(String propertyName, int oldValue, int newValue) {
    239         firePropertyChange(propertyName, Integer.valueOf(oldValue), Integer.valueOf(newValue));
    240     }
    241 
    242     /**
    243      * Fires a property change of an integer property with the given name. If
    244      * the old value and the new value are not null and equal the event will not
    245      * be fired.
    246      *
    247      * @param propertyName
    248      *            the property name
    249      * @param index
    250      *            the index of the changed property
    251      * @param oldValue
    252      *            the old value
    253      * @param newValue
    254      *            the new value
    255      */
    256     public void fireIndexedPropertyChange(String propertyName, int index,
    257             int oldValue, int newValue) {
    258         if (oldValue != newValue) {
    259             fireIndexedPropertyChange(propertyName, index,
    260                     Integer.valueOf(oldValue), Integer.valueOf(newValue));
    261         }
    262     }
    263 
    264     /**
    265      * Returns true if there are listeners registered to the property with the
    266      * given name.
    267      *
    268      * @param propertyName
    269      *            the name of the property
    270      * @return true if there are listeners registered to that property, false
    271      *         otherwise.
    272      */
    273     public boolean hasListeners(String propertyName) {
    274         for (PropertyChangeListener p : listeners) {
    275             if (!(p instanceof PropertyChangeListenerProxy) || Objects.equal(
    276                     propertyName, ((PropertyChangeListenerProxy) p).getPropertyName())) {
    277                 return true;
    278             }
    279         }
    280         return false;
    281     }
    282 
    283     /**
    284      * Unsubscribes {@code listener} from change notifications for all
    285      * properties. If the listener has multiple subscriptions, it will receive
    286      * one fewer notification when properties change. If the property name or
    287      * listener is null or not subscribed, this method silently does nothing.
    288      */
    289     public void removePropertyChangeListener(PropertyChangeListener listener) {
    290         for (PropertyChangeListener p : listeners) {
    291             if (equals(null, listener, p)) {
    292                 listeners.remove(p);
    293                 return;
    294             }
    295         }
    296     }
    297 
    298     /**
    299      * Subscribes {@code listener} to change notifications for all properties.
    300      * If the listener is already subscribed, it will receive an additional
    301      * notification. If the listener is null, this method silently does nothing.
    302      */
    303     public void addPropertyChangeListener(PropertyChangeListener listener) {
    304         if (listener != null) {
    305             listeners.add(listener);
    306         }
    307     }
    308 
    309     /**
    310      * Returns all subscribers. This includes both listeners subscribed to all
    311      * property changes and listeners subscribed to a single property.
    312      */
    313     public PropertyChangeListener[] getPropertyChangeListeners() {
    314         return listeners.toArray(new PropertyChangeListener[0]); // 0 to avoid synchronization
    315     }
    316 
    317     private void writeObject(ObjectOutputStream out) throws IOException {
    318         /*
    319          * The serialized form of this class uses PropertyChangeSupport to group
    320          * PropertyChangeListeners subscribed to the same property name.
    321          */
    322         Map<String, PropertyChangeSupport> map = new Hashtable<String, PropertyChangeSupport>();
    323         for (PropertyChangeListener p : listeners) {
    324             if (p instanceof PropertyChangeListenerProxy && !(p instanceof Serializable)) {
    325                 PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) p;
    326                 PropertyChangeListener listener = (PropertyChangeListener) proxy.getListener();
    327                 if (listener instanceof Serializable) {
    328                     PropertyChangeSupport list = map.get(proxy.getPropertyName());
    329                     if (list == null) {
    330                         list = new PropertyChangeSupport(sourceBean);
    331                         map.put(proxy.getPropertyName(), list);
    332                     }
    333                     list.listeners.add(listener);
    334                 }
    335             }
    336         }
    337 
    338         ObjectOutputStream.PutField putFields = out.putFields();
    339         putFields.put("source", sourceBean);
    340         putFields.put("children", map);
    341         out.writeFields();
    342 
    343         for (PropertyChangeListener p : listeners) {
    344             if (p instanceof Serializable) {
    345                 out.writeObject(p);
    346             }
    347         }
    348         out.writeObject(null);
    349     }
    350 
    351     @SuppressWarnings("unchecked")
    352     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    353         ObjectInputStream.GetField readFields = in.readFields();
    354         sourceBean = readFields.get("source", null);
    355         listeners = new CopyOnWriteArrayList<PropertyChangeListener>();
    356 
    357         Map<String, PropertyChangeSupport> children
    358                 = (Map<String, PropertyChangeSupport>) readFields.get("children", null);
    359         if (children != null) {
    360             for (Map.Entry<String, PropertyChangeSupport> entry : children.entrySet()) {
    361                 for (PropertyChangeListener p : entry.getValue().listeners) {
    362                     listeners.add(new PropertyChangeListenerProxy(entry.getKey(), p));
    363                 }
    364             }
    365         }
    366 
    367         PropertyChangeListener listener;
    368         while ((listener = (PropertyChangeListener) in.readObject()) != null) {
    369             listeners.add(listener);
    370         }
    371     }
    372 
    373     /**
    374      * Publishes a property change event to all listeners of that property. If
    375      * the event's old and new values are equal (but non-null), no event will be
    376      * published.
    377      */
    378     public void firePropertyChange(PropertyChangeEvent event) {
    379         String propertyName = event.getPropertyName();
    380         Object oldValue = event.getOldValue();
    381         Object newValue = event.getNewValue();
    382         if (newValue != null && oldValue != null && newValue.equals(oldValue)) {
    383             return;
    384         }
    385 
    386         notifyEachListener:
    387         for (PropertyChangeListener p : listeners) {
    388             // unwrap listener proxies until we get a mismatched name or the real listener
    389             while (p instanceof PropertyChangeListenerProxy) {
    390                 PropertyChangeListenerProxy proxy = (PropertyChangeListenerProxy) p;
    391                 if (!Objects.equal(proxy.getPropertyName(), propertyName)) {
    392                     continue notifyEachListener;
    393                 }
    394                 p = (PropertyChangeListener) proxy.getListener();
    395             }
    396             p.propertyChange(event);
    397         }
    398     }
    399 }
    400