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
18package java.beans;
19
20import java.io.IOException;
21import java.io.ObjectInputStream;
22import java.io.ObjectOutputStream;
23import java.io.ObjectStreamField;
24import java.io.Serializable;
25import java.util.ArrayList;
26import java.util.EventListener;
27import java.util.Hashtable;
28import java.util.List;
29import java.util.Map;
30import java.util.concurrent.CopyOnWriteArrayList;
31import 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 */
42public 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("sourceBean == null");
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