1 /*
2  * Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 package java.beans;
26 
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.EventListener;
30 import java.util.EventListenerProxy;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Map.Entry;
35 import java.util.Set;
36 
37 /**
38  * This is an abstract class that provides base functionality
39  * for the {@link PropertyChangeSupport PropertyChangeSupport} class
40  * and the {@link VetoableChangeSupport VetoableChangeSupport} class.
41  *
42  * @see PropertyChangeListenerMap
43  * @see VetoableChangeListenerMap
44  *
45  * @author Sergey A. Malenkov
46  */
47 abstract class ChangeListenerMap<L extends EventListener> {
48     private Map<String, L[]> map;
49 
50     /**
51      * Creates an array of listeners.
52      * This method can be optimized by using
53      * the same instance of the empty array
54      * when {@code length} is equal to {@code 0}.
55      *
56      * @param length  the array length
57      * @return        an array with specified length
58      */
newArray(int length)59     protected abstract L[] newArray(int length);
60 
61     /**
62      * Creates a proxy listener for the specified property.
63      *
64      * @param name      the name of the property to listen on
65      * @param listener  the listener to process events
66      * @return          a proxy listener
67      */
newProxy(String name, L listener)68     protected abstract L newProxy(String name, L listener);
69 
70     /**
71      * Adds a listener to the list of listeners for the specified property.
72      * This listener is called as many times as it was added.
73      *
74      * @param name      the name of the property to listen on
75      * @param listener  the listener to process events
76      */
add(String name, L listener)77     public final synchronized void add(String name, L listener) {
78         if (this.map == null) {
79             this.map = new HashMap<>();
80         }
81         L[] array = this.map.get(name);
82         int size = (array != null)
83                 ? array.length
84                 : 0;
85 
86         L[] clone = newArray(size + 1);
87         clone[size] = listener;
88         if (array != null) {
89             System.arraycopy(array, 0, clone, 0, size);
90         }
91         this.map.put(name, clone);
92     }
93 
94     /**
95      * Removes a listener from the list of listeners for the specified property.
96      * If the listener was added more than once to the same event source,
97      * this listener will be notified one less time after being removed.
98      *
99      * @param name      the name of the property to listen on
100      * @param listener  the listener to process events
101      */
remove(String name, L listener)102     public final synchronized void remove(String name, L listener) {
103         if (this.map != null) {
104             L[] array = this.map.get(name);
105             if (array != null) {
106                 for (int i = 0; i < array.length; i++) {
107                     if (listener.equals(array[i])) {
108                         int size = array.length - 1;
109                         if (size > 0) {
110                             L[] clone = newArray(size);
111                             System.arraycopy(array, 0, clone, 0, i);
112                             System.arraycopy(array, i + 1, clone, i, size - i);
113                             this.map.put(name, clone);
114                         }
115                         else {
116                             this.map.remove(name);
117                             if (this.map.isEmpty()) {
118                                 this.map = null;
119                             }
120                         }
121                         break;
122                     }
123                 }
124             }
125         }
126     }
127 
128     /**
129      * Returns the list of listeners for the specified property.
130      *
131      * @param name  the name of the property
132      * @return      the corresponding list of listeners
133      */
get(String name)134     public final synchronized L[] get(String name) {
135         return (this.map != null)
136                 ? this.map.get(name)
137                 : null;
138     }
139 
140     /**
141      * Sets new list of listeners for the specified property.
142      *
143      * @param name       the name of the property
144      * @param listeners  new list of listeners
145      */
set(String name, L[] listeners)146     public final void set(String name, L[] listeners) {
147         if (listeners != null) {
148             if (this.map == null) {
149                 this.map = new HashMap<>();
150             }
151             this.map.put(name, listeners);
152         }
153         else if (this.map != null) {
154             this.map.remove(name);
155             if (this.map.isEmpty()) {
156                 this.map = null;
157             }
158         }
159     }
160 
161     /**
162      * Returns all listeners in the map.
163      *
164      * @return an array of all listeners
165      */
getListeners()166     public final synchronized L[] getListeners() {
167         if (this.map == null) {
168             return newArray(0);
169         }
170         List<L> list = new ArrayList<>();
171 
172         L[] listeners = this.map.get(null);
173         if (listeners != null) {
174             for (L listener : listeners) {
175                 list.add(listener);
176             }
177         }
178         for (Entry<String, L[]> entry : this.map.entrySet()) {
179             String name = entry.getKey();
180             if (name != null) {
181                 for (L listener : entry.getValue()) {
182                     list.add(newProxy(name, listener));
183                 }
184             }
185         }
186         return list.toArray(newArray(list.size()));
187     }
188 
189     /**
190      * Returns listeners that have been associated with the named property.
191      *
192      * @param name  the name of the property
193      * @return an array of listeners for the named property
194      */
getListeners(String name)195     public final L[] getListeners(String name) {
196         if (name != null) {
197             L[] listeners = get(name);
198             if (listeners != null) {
199                 return listeners.clone();
200             }
201         }
202         return newArray(0);
203     }
204 
205     /**
206      * Indicates whether the map contains
207      * at least one listener to be notified.
208      *
209      * @param name  the name of the property
210      * @return      {@code true} if at least one listener exists or
211      *              {@code false} otherwise
212      */
hasListeners(String name)213     public final synchronized boolean hasListeners(String name) {
214         if (this.map == null) {
215             return false;
216         }
217         L[] array = this.map.get(null);
218         return (array != null) || ((name != null) && (null != this.map.get(name)));
219     }
220 
221     /**
222      * Returns a set of entries from the map.
223      * Each entry is a pair consisted of the property name
224      * and the corresponding list of listeners.
225      *
226      * @return a set of entries from the map
227      */
getEntries()228     public final Set<Entry<String, L[]>> getEntries() {
229         return (this.map != null)
230                 ? this.map.entrySet()
231                 : Collections.<Entry<String, L[]>>emptySet();
232     }
233 
234     /**
235      * Extracts a real listener from the proxy listener.
236      * It is necessary because default proxy class is not serializable.
237      *
238      * @return a real listener
239      */
extract(L listener)240     public abstract L extract(L listener);
241 }
242