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