/*
* Beanfabrics Framework Copyright (C) by Michael Karneim, beanfabrics.org
* Use is subject to license terms. See license.txt.
*/
/*
* This class is slightly based on Sun's PropertyChangeSupport source which has
* the following copyright notice: Copyright 2002 Sun Microsystems, Inc. All
* rights reserved. SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license
* terms.
*/
package org.beanfabrics.event;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
/**
* The {@link BnPropertyChangeSupport} is a utility class for handling listeners
* of bound properties.
*
* @author Michael Karneim
*/
public class BnPropertyChangeSupport {
private final WeakHashMap<PropertyChangeListener, WeakWrapper> weakWrapper = new WeakHashMap<PropertyChangeListener, WeakWrapper>();
private List<PropertyChangeListener> listeners;
/**
* {@link Map} for managing listeners on named properties. Maps each
* property name to a {@link BnPropertyChangeSupport} object.
*/
private Map<String, BnPropertyChangeSupport> children;
/**
* The object to be provided as the "source" for any generated events.
*/
private Object source;
/**
* Constructs a {@link BnPropertyChangeSupport}.
*
* @param sourceBean The bean to be given as the source for any events
*/
public BnPropertyChangeSupport(Object sourceBean) {
if (sourceBean == null) {
throw new NullPointerException();
}
source = sourceBean;
}
/**
* Adds the given {@link PropertyChangeListener} for any property of the
* source bean.
*
* @param listener the listener add
*/
public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener instanceof WeakListener) {
listener = this.getWeakWrapper(listener);
}
if (listeners == null) {
listeners = new ArrayList<PropertyChangeListener>();
} else {
this.removeUnusedWeakWrappers(listeners);
}
listeners.add(listener);
}
/**
* Removes the given {@link PropertyChangeListener}.
*
* @param listener the listener to remove
*/
public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
if (listeners == null) {
return;
}
if (listener instanceof WeakListener) {
listener = this.getWeakWrapper(listener);
this.removeWeakWrapper((WeakWrapper)listener);
}
listeners.remove(listener);
}
/**
* Adds the given {@link PropertyChangeListener} for the specified property
* of the source bean. The listener will only receive events that are
* triggered by that property.
*
* @param propertyName the name of the property
* @param listener the listener to add
*/
public synchronized void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
if (listener instanceof WeakListener) {
listener = this.getWeakWrapper(listener);
}
if (children == null) {
children = new HashMap<String, BnPropertyChangeSupport>();
}
BnPropertyChangeSupport child = children.get(propertyName);
if (child == null) {
child = new BnPropertyChangeSupport(source);
children.put(propertyName, child);
}
child.addPropertyChangeListener(listener);
}
/**
* Removes the given {@link PropertyChangeListener} for a specific property.
*
* @param propertyName the name of the property
* @param listener the listener to remove
*/
public synchronized void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
if (children == null) {
return;
}
if (listener instanceof WeakListener) {
listener = this.getWeakWrapper(listener);
this.removeWeakWrapper((WeakWrapper)listener);
}
BnPropertyChangeSupport child = children.get(propertyName);
if (child == null) {
return;
}
child.removePropertyChangeListener(listener);
}
/**
* Report a bound property update to each registered listener. No event is
* fired if old and new are equal and non-null.
*
* @param propertyName the property name of the property that was changed
* @param oldValue the old value of the property
* @param newValue the new value of the property
*/
public void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
this.firePropertyChange(propertyName, oldValue, newValue, null);
}
/**
* Report a bound property update to each registered listener. No event is
* fired if old and new are equal and non-null.
*
* @param propertyName the property name of the property that was changed
* @param oldValue the old value of the property
* @param newValue the new value of the property
* @param cause the event that triggered the new event
*/
public void firePropertyChange(String propertyName, Object oldValue, Object newValue, EventObject cause) {
if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
return;
}
List<PropertyChangeListener> targets = null;
BnPropertyChangeSupport child = null;
synchronized (this) {
if (listeners != null) {
targets = new ArrayList<PropertyChangeListener>(listeners);
}
if (children != null && propertyName != null) {
child = children.get(propertyName);
}
}
BnPropertyChangeEvent evt = new BnPropertyChangeEvent(source, propertyName, oldValue, newValue, cause);
if (targets != null) {
for (int i = 0; i < targets.size(); i++) {
PropertyChangeListener target = (PropertyChangeListener)targets.get(i);
target.propertyChange(evt);
}
}
if (child != null) {
child.firePropertyChange(evt);
}
}
/**
* Fire thw given {@link PropertyChangeEvent} to each registered listener.
* No event is fired if the given event's old and new values are equal and
* non-<code>null</code>.
*
* @param evt the <code>PropertyChangeEvent</code> object
*/
public void firePropertyChange(BnPropertyChangeEvent evt) {
Object oldValue = evt.getOldValue();
Object newValue = evt.getNewValue();
String propertyName = evt.getPropertyName();
if (oldValue != null && newValue != null && oldValue.equals(newValue)) {
return;
}
List<PropertyChangeListener> targets = null;
BnPropertyChangeSupport child = null;
synchronized (this) {
if (listeners != null) {
targets = new ArrayList<PropertyChangeListener>(listeners);
}
if (children != null && propertyName != null) {
child = children.get(propertyName);
}
}
if (targets != null) {
for (int i = 0; i < targets.size(); i++) {
PropertyChangeListener target = targets.get(i);
target.propertyChange(evt);
}
}
if (child != null) {
child.firePropertyChange(evt);
}
}
/**
* Returns <code>true</code> if there are any listeners for the property
* with the specified name.
*
* @param propertyName the property name
* @return <code>true</code> if there are any listeners
*/
public synchronized boolean hasListeners(String propertyName) {
if (listeners != null && !listeners.isEmpty()) {
// there is a generic listener
return true;
}
if (children != null) {
BnPropertyChangeSupport child = children.get(propertyName);
if (child != null && child.listeners != null) {
return !child.listeners.isEmpty();
}
}
return false;
}
/**
* Returns <code>true</code> if there are any listeners registered at all.
*
* @return <code>true</code> if there are any listeners for any property
*/
public boolean hasListeners() {
return !((this.listeners == null || this.listeners.isEmpty()) && (this.children == null || this.children.isEmpty()));
}
/**
* Returns the {@link WeakWrapper} for the given listener.
*
* @param l
* @return the WeakWrapper for the given listener
*/
private WeakWrapper getWeakWrapper(PropertyChangeListener listener) {
WeakWrapper result = this.weakWrapper.get(listener);
if (result == null) {
result = new WeakWrapper(listener);
this.weakWrapper.put(listener, result);
}
return result;
}
/**
* Removes the given {@link WeakWrapper} from the weakWrapper cache.
*
* @param wrapper
*/
private void removeWeakWrapper(WeakWrapper wrapper) {
this.weakWrapper.remove(wrapper);
}
/**
* Removes all unused WeakWrapper instances from the given list of
* listeners.
*
* @param listeners
*/
private void removeUnusedWeakWrappers(Collection<PropertyChangeListener> listeners) {
List<PropertyChangeListener> toRemove = new ArrayList<PropertyChangeListener>();
for (PropertyChangeListener l : listeners) {
if (l instanceof WeakWrapper && ((WeakWrapper)l).isCleared()) {
toRemove.add(l);
}
}
listeners.removeAll(toRemove);
}
/**
* The {@link WeakWrapper} is a delegator that forwards
* {@link PropertyChangeEvent}s to a weakly referenced delegate object as
* long as that reference has not been cleared.
*/
private static class WeakWrapper implements PropertyChangeListener {
private WeakReference<PropertyChangeListener> ref;
/**
* Constructs a {@link WeakWrapper} for the given delegate.
*
* @param aDelegate
*/
WeakWrapper(PropertyChangeListener aDelegate) {
this.setDelegate(aDelegate);
}
/**
* Returns <code>true</code>, if the delegate reference has been
* cleared.
*
* @return <code>true</code>, if the delegate reference has been cleared
*/
boolean isCleared() {
return this.ref.get() == null;
}
/** {@inheritDoc} */
public void propertyChange(java.beans.PropertyChangeEvent evt) {
PropertyChangeListener listener = this.getDelegate();
if (listener == null) {
return;
}
listener.propertyChange(evt);
}
/**
* Returns the delegate.
*
* @return the delegate
*/
PropertyChangeListener getDelegate() {
if (this.ref == null) {
return null;
}
return this.ref.get();
}
/**
* Sets the delegate.
*
* @param aDelegate
*/
void setDelegate(PropertyChangeListener aDelegate) {
if (aDelegate == null) {
this.ref = null;
} else {
this.ref = new WeakReference<PropertyChangeListener>(aDelegate);
}
}
}
}