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.Serializable;
24import java.util.ArrayList;
25import java.util.HashMap;
26import java.util.Hashtable;
27import java.util.Iterator;
28import java.util.List;
29import java.util.Map;
30
31/**
32 * This utility class
33 *
34 */
35public class PropertyChangeSupport implements Serializable {
36
37    private static final long serialVersionUID = 6401253773779951803l;
38
39    private transient Object sourceBean;
40
41    private transient List<PropertyChangeListener> allPropertiesChangeListeners =
42            new ArrayList<PropertyChangeListener>();
43
44    private transient Map<String, List<PropertyChangeListener>>
45            selectedPropertiesChangeListeners =
46            new HashMap<String, List<PropertyChangeListener>>();
47
48    // fields for serialization compatibility
49    private Hashtable<String, List<PropertyChangeListener>> children;
50
51    private Object source;
52
53    private int propertyChangeSupportSerializedDataVersion = 1;
54
55    /**
56     * Creates a new instance that uses the source bean as source for any event.
57     *
58     * @param sourceBean
59     *            the bean used as source for all events.
60     */
61    public PropertyChangeSupport(Object sourceBean) {
62        if (sourceBean == null) {
63            throw new NullPointerException();
64        }
65        this.sourceBean = sourceBean;
66    }
67
68    /**
69     * Fires a {@link PropertyChangeEvent} with the given name, old value and
70     * new value. As source the bean used to initialize this instance is used.
71     * If the old value and the new value are not null and equal the event will
72     * not be fired.
73     *
74     * @param propertyName
75     *            the name of the property
76     * @param oldValue
77     *            the old value of the property
78     * @param newValue
79     *            the new value of the property
80     */
81    public void firePropertyChange(String propertyName, Object oldValue,
82            Object newValue) {
83        PropertyChangeEvent event = createPropertyChangeEvent(propertyName,
84                oldValue, newValue);
85        doFirePropertyChange(event);
86    }
87
88    /**
89     * Fires an {@link IndexedPropertyChangeEvent} with the given name, old
90     * value, new value and index. As source the bean used to initialize this
91     * instance is used. If the old value and the new value are not null and
92     * equal the event will not be fired.
93     *
94     * @param propertyName
95     *            the name of the property
96     * @param index
97     *            the index
98     * @param oldValue
99     *            the old value of the property
100     * @param newValue
101     *            the new value of the property
102     */
103    public void fireIndexedPropertyChange(String propertyName, int index,
104            Object oldValue, Object newValue) {
105
106        // nulls and equals check done in doFire...
107        doFirePropertyChange(new IndexedPropertyChangeEvent(sourceBean,
108                propertyName, oldValue, newValue, index));
109    }
110
111    /**
112     * Removes the listener from the specific property. This only happens if it
113     * was registered to this property. Nothing happens if it was not
114     * registered with this property or if the property name or the listener is
115     * null.
116     *
117     * @param propertyName
118     *            the property name the listener is listening to
119     * @param listener
120     *            the listener to remove
121     */
122    public synchronized void removePropertyChangeListener(String propertyName,
123            PropertyChangeListener listener) {
124        if ((propertyName != null) && (listener != null)) {
125            List<PropertyChangeListener> listeners =
126                    selectedPropertiesChangeListeners.get(propertyName);
127
128            if (listeners != null) {
129                listeners.remove(listener);
130            }
131        }
132    }
133
134    /**
135     * Adds a listener to a specific property. Nothing happens if the property
136     * name or the listener is null.
137     *
138     * @param propertyName
139     *            the name of the property
140     * @param listener
141     *            the listener to register for the property with the given name
142     */
143    public synchronized void addPropertyChangeListener(String propertyName,
144            PropertyChangeListener listener) {
145        if ((listener != null) && (propertyName != null)) {
146            List<PropertyChangeListener> listeners =
147                    selectedPropertiesChangeListeners.get(propertyName);
148
149            if (listeners == null) {
150                listeners = new ArrayList<PropertyChangeListener>();
151                selectedPropertiesChangeListeners.put(propertyName, listeners);
152            }
153
154            // RI compatibility
155            if (listener instanceof PropertyChangeListenerProxy) {
156                PropertyChangeListenerProxy proxy =
157                        (PropertyChangeListenerProxy) listener;
158
159                listeners.add(new PropertyChangeListenerProxy(
160                        proxy.getPropertyName(),
161                        (PropertyChangeListener) proxy.getListener()));
162            } else {
163                listeners.add(listener);
164            }
165        }
166    }
167
168    /**
169     * Returns an array of listeners that registered to the property with the
170     * given name. If the property name is null an empty array is returned.
171     *
172     * @param propertyName
173     *            the name of the property whose listeners should be returned
174     * @return the array of listeners to the property with the given name.
175     */
176    public synchronized PropertyChangeListener[] getPropertyChangeListeners(
177            String propertyName) {
178        List<PropertyChangeListener> listeners = null;
179
180        if (propertyName != null) {
181            listeners = selectedPropertiesChangeListeners.get(propertyName);
182        }
183
184        return (listeners == null) ? new PropertyChangeListener[] {}
185                : listeners.toArray(
186                        new PropertyChangeListener[listeners.size()]);
187    }
188
189    /**
190     * Fires a property change of a boolean property with the given name. If the
191     * old value and the new value are not null and equal the event will not be
192     * fired.
193     *
194     * @param propertyName
195     *            the property name
196     * @param oldValue
197     *            the old value
198     * @param newValue
199     *            the new value
200     */
201    public void firePropertyChange(String propertyName, boolean oldValue,
202            boolean newValue) {
203        PropertyChangeEvent event = createPropertyChangeEvent(propertyName,
204                oldValue, newValue);
205        doFirePropertyChange(event);
206    }
207
208    /**
209     * Fires a property change of a boolean property with the given name. If the
210     * old value and the new value are not null and equal the event will not be
211     * fired.
212     *
213     * @param propertyName
214     *            the property name
215     * @param index
216     *            the index of the changed property
217     * @param oldValue
218     *            the old value
219     * @param newValue
220     *            the new value
221     */
222    public void fireIndexedPropertyChange(String propertyName, int index,
223            boolean oldValue, boolean newValue) {
224
225        if (oldValue != newValue) {
226            fireIndexedPropertyChange(propertyName, index, Boolean
227                    .valueOf(oldValue), Boolean.valueOf(newValue));
228        }
229    }
230
231    /**
232     * Fires a property change of an integer property with the given name. If
233     * the old value and the new value are not null and equal the event will not
234     * be fired.
235     *
236     * @param propertyName
237     *            the property name
238     * @param oldValue
239     *            the old value
240     * @param newValue
241     *            the new value
242     */
243    public void firePropertyChange(String propertyName, int oldValue,
244            int newValue) {
245        PropertyChangeEvent event = createPropertyChangeEvent(propertyName,
246                oldValue, newValue);
247        doFirePropertyChange(event);
248    }
249
250    /**
251     * Fires a property change of an integer property with the given name. If
252     * the old value and the new value are not null and equal the event will not
253     * be fired.
254     *
255     * @param propertyName
256     *            the property name
257     * @param index
258     *            the index of the changed property
259     * @param oldValue
260     *            the old value
261     * @param newValue
262     *            the new value
263     */
264    public void fireIndexedPropertyChange(String propertyName, int index,
265            int oldValue, int newValue) {
266
267        if (oldValue != newValue) {
268            fireIndexedPropertyChange(propertyName, index,
269                    new Integer(oldValue), new Integer(newValue));
270        }
271    }
272
273    /**
274     * Returns true if there are listeners registered to the property with the
275     * given name.
276     *
277     * @param propertyName
278     *            the name of the property
279     * @return true if there are listeners registered to that property, false
280     *         otherwise.
281     */
282    public synchronized boolean hasListeners(String propertyName) {
283        boolean result = allPropertiesChangeListeners.size() > 0;
284        if (!result && (propertyName != null)) {
285            List<PropertyChangeListener> listeners =
286                    selectedPropertiesChangeListeners.get(propertyName);
287            if (listeners != null) {
288                result = listeners.size() > 0;
289            }
290        }
291        return result;
292    }
293
294    /**
295     * removes a property change listener that was registered to all properties.
296     *
297     * @param listener
298     *            the listener to remove
299     */
300    public synchronized void removePropertyChangeListener(
301            PropertyChangeListener listener) {
302        if (listener != null) {
303            if (listener instanceof PropertyChangeListenerProxy) {
304                String name = ((PropertyChangeListenerProxy) listener)
305                        .getPropertyName();
306                PropertyChangeListener lst = (PropertyChangeListener)
307                        ((PropertyChangeListenerProxy) listener).getListener();
308
309                removePropertyChangeListener(name, lst);
310            } else {
311                allPropertiesChangeListeners.remove(listener);
312            }
313        }
314    }
315
316    /**
317     * Registers a listener with all properties.
318     *
319     * @param listener
320     *            the listener to register
321     */
322    public synchronized void addPropertyChangeListener(
323            PropertyChangeListener listener) {
324        if (listener != null) {
325            if (listener instanceof PropertyChangeListenerProxy) {
326                String name = ((PropertyChangeListenerProxy) listener)
327                        .getPropertyName();
328                PropertyChangeListener lst = (PropertyChangeListener)
329                        ((PropertyChangeListenerProxy) listener).getListener();
330                addPropertyChangeListener(name, lst);
331            } else {
332                allPropertiesChangeListeners.add(listener);
333            }
334        }
335    }
336
337    /**
338     * Returns an array with the listeners that registered to all properties.
339     *
340     * @return the array of listeners
341     */
342    public synchronized PropertyChangeListener[] getPropertyChangeListeners() {
343        ArrayList<PropertyChangeListener> result =
344                new ArrayList<PropertyChangeListener>(
345                        allPropertiesChangeListeners);
346
347        for (String propertyName : selectedPropertiesChangeListeners.keySet()) {
348            List<PropertyChangeListener> selectedListeners =
349                    selectedPropertiesChangeListeners.get(propertyName);
350
351            if (selectedListeners != null) {
352
353                for (PropertyChangeListener listener : selectedListeners) {
354                    result.add(new PropertyChangeListenerProxy(propertyName,
355                            listener));
356                }
357            }
358        }
359
360        return result.toArray(new PropertyChangeListener[result.size()]);
361    }
362
363    private void writeObject(ObjectOutputStream oos) throws IOException {
364        List<PropertyChangeListener> allSerializedPropertiesChangeListeners =
365                new ArrayList<PropertyChangeListener>();
366
367        for (PropertyChangeListener pcl : allPropertiesChangeListeners) {
368            if (pcl instanceof Serializable) {
369                allSerializedPropertiesChangeListeners.add(pcl);
370            }
371        }
372
373        Map<String, List<PropertyChangeListener>>
374                selectedSerializedPropertiesChangeListeners =
375                        new HashMap<String, List<PropertyChangeListener>>();
376
377        for (String propertyName : selectedPropertiesChangeListeners.keySet()) {
378            List<PropertyChangeListener> keyValues =
379                    selectedPropertiesChangeListeners.get(propertyName);
380
381            if (keyValues != null) {
382                List<PropertyChangeListener> serializedPropertiesChangeListeners
383                        = new ArrayList<PropertyChangeListener>();
384
385                for (PropertyChangeListener pcl : keyValues) {
386                    if (pcl instanceof Serializable) {
387                        serializedPropertiesChangeListeners.add(pcl);
388                    }
389                }
390
391                if (!serializedPropertiesChangeListeners.isEmpty()) {
392                    selectedSerializedPropertiesChangeListeners.put(
393                            propertyName, serializedPropertiesChangeListeners);
394                }
395            }
396        }
397
398        children = new Hashtable<String, List<PropertyChangeListener>>(
399                selectedSerializedPropertiesChangeListeners);
400        children.put("", allSerializedPropertiesChangeListeners); //$NON-NLS-1$
401        oos.writeObject(children);
402
403        Object source = null;
404        if (sourceBean instanceof Serializable) {
405            source = sourceBean;
406        }
407        oos.writeObject(source);
408
409        oos.writeInt(propertyChangeSupportSerializedDataVersion);
410    }
411
412    @SuppressWarnings("unchecked")
413    private void readObject(ObjectInputStream ois) throws IOException,
414            ClassNotFoundException {
415        children = (Hashtable<String, List<PropertyChangeListener>>) ois
416                .readObject();
417
418        selectedPropertiesChangeListeners = new HashMap<String, List<PropertyChangeListener>>(
419                children);
420        allPropertiesChangeListeners = selectedPropertiesChangeListeners
421                .remove(""); //$NON-NLS-1$
422        if (allPropertiesChangeListeners == null) {
423            allPropertiesChangeListeners = new ArrayList<PropertyChangeListener>();
424        }
425
426        sourceBean = ois.readObject();
427        propertyChangeSupportSerializedDataVersion = ois.readInt();
428    }
429
430    /**
431     * Fires a property change event to all listeners of that property.
432     *
433     * @param event
434     *            the event to fire
435     */
436    public void firePropertyChange(PropertyChangeEvent event) {
437        doFirePropertyChange(event);
438    }
439
440    private PropertyChangeEvent createPropertyChangeEvent(String propertyName,
441            Object oldValue, Object newValue) {
442        return new PropertyChangeEvent(sourceBean, propertyName, oldValue,
443                newValue);
444    }
445
446    private PropertyChangeEvent createPropertyChangeEvent(String propertyName,
447            boolean oldValue, boolean newValue) {
448        return new PropertyChangeEvent(sourceBean, propertyName, oldValue,
449                newValue);
450    }
451
452    private PropertyChangeEvent createPropertyChangeEvent(String propertyName,
453            int oldValue, int newValue) {
454        return new PropertyChangeEvent(sourceBean, propertyName, oldValue,
455                newValue);
456    }
457
458    private void doFirePropertyChange(PropertyChangeEvent event) {
459        String propertyName = event.getPropertyName();
460        Object oldValue = event.getOldValue();
461        Object newValue = event.getNewValue();
462
463        if ((newValue != null) && (oldValue != null)
464                && newValue.equals(oldValue)) {
465            return;
466        }
467
468        /*
469         * Copy the listeners collections so they can be modified while we fire
470         * events.
471         */
472
473        // Listeners to all property change events
474        PropertyChangeListener[] listensToAll;
475        // Listens to a given property change
476        PropertyChangeListener[] listensToOne = null;
477        synchronized (this) {
478            listensToAll = allPropertiesChangeListeners
479                    .toArray(new PropertyChangeListener[allPropertiesChangeListeners
480                            .size()]);
481
482            List<PropertyChangeListener> listeners = selectedPropertiesChangeListeners
483                    .get(propertyName);
484            if (listeners != null) {
485                listensToOne = listeners
486                        .toArray(new PropertyChangeListener[listeners.size()]);
487            }
488        }
489
490        // Fire the listeners
491        for (PropertyChangeListener listener : listensToAll) {
492            listener.propertyChange(event);
493        }
494        if (listensToOne != null) {
495            for (PropertyChangeListener listener : listensToOne) {
496                listener.propertyChange(event);
497            }
498        }
499    }
500
501}
502