org.jcurl.core.helpers.PropertyChangeSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.jcurl.core.helpers.PropertyChangeSupport.java

Source

/*
 * jcurl curling simulation framework http://www.jcurl.org
 * Copyright (C) 2005 M. Rohrmoser
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package org.jcurl.core.helpers;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.jcurl.core.log.JCLoggerFactory;

/**
 * Implements a beans like property change support utility that stores the
 * references to the listeners in a weak set and thus are memory-safe.
 * <p>
 * 
 * By storing the references to the listeners in weak references instead of
 * strong references, we can be sure that if the listener is no longer used by
 * other classes, it will be garbage collected with this class. Since the
 * producer of messages should not have the responsability of maintaining the
 * garbage collection status of its listeners, this is needed.
 * </p>
 * 
 * <p>
 * Note that this class implements the identical interface to
 * <tt>java.lang.PropertyChangeSupport</tt>. This means that it is completely
 * plug in compatible, enabling users to switch out current support in favor of
 * the safe version.
 * </p>
 * <p>
 * This object is not serializable because it uses a <tt>WeakHashSet</tt> to
 * hold the listeners. Refer to that class for reasons for this decision. Users
 * must declare this attribute <tt>transient</tt>.
 * </p>
 * 
 * <p>
 * This class is thread safe.
 * </p>
 * 
 * @author <a href="mailto:jcurl@gmx.net">M. Rohrmoser </a>
 * @version $Id$
 * @see java.lang.ref.WeakReference
 * @see java.beans.PropertyChangeSupport
 */

public class PropertyChangeSupport {

    /**
     * Holds the value used as the key for general listeners in the listener
     * map. This value is chosen because it would be illegal to name a property
     * the same as this value which guarantees that there will be no name
     * clashes.
     */
    private static final String ALL_PROPERTIES = "**GENERAL**";

    private static final Log log = JCLoggerFactory.getLogger(PropertyChangeSupport.class);

    /**
     * Holds the Listener map. The map is held as an index of WeakHashSet
     * objects. Each property will appear as a key once in the set and when a
     * property change event is fired on a property, the event will be fired to
     * the union of the specific listeners set and the general listeners set.
     * 
     * The general listeners are also held in the set with the key set by
     * <tt>ALL_PROPERTIES</tt>
     */

    private final Map<String, Set<PropertyChangeListener>> listenerMap = new HashMap<String, Set<PropertyChangeListener>>();

    /** Stores the producer of the events. */
    private final Object producer;

    /**
     * Creates a new instance of SafePropertyChangeSupport.
     * 
     * @param producer
     *            This is the object that is producing the property change
     *            events.
     * @throws RuntimeException
     *             If there is an introspection problem.
     */
    public PropertyChangeSupport(final Object producer) {
        try {
            final BeanInfo info = Introspector.getBeanInfo(producer.getClass());
            for (final PropertyDescriptor element : info.getPropertyDescriptors())
                listenerMap.put(element.getName(), new WeakHashSet());
            listenerMap.put(ALL_PROPERTIES, new WeakHashSet());
            this.producer = producer;
        } catch (final IntrospectionException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Add a non-specific PropertyChangeListener. Adds a listener that will
     * recieve events on all properties.
     * 
     * @param pcl
     *            The PropertyChangeListener add.
     */
    public void addPropertyChangeListener(final PropertyChangeListener pcl) {
        synchronized (listenerMap) {
            listenerMap.get(ALL_PROPERTIES).add(pcl);
        }
    }

    /**
     * Add a PropertyChangeListener for a specific property.
     * 
     * @param property
     *            The name of the relevant property.
     * @param pcl
     *            The listener to add.
     */
    public void addPropertyChangeListener(final String property, final PropertyChangeListener pcl) {
        validateNamedProperty(property);
        synchronized (listenerMap) {
            listenerMap.get(property).add(pcl);
        }
    }

    /**
     * Fire a property change event to all of the listeners.
     * <p>
     * This method is called by all the fire methods to perform the firing of
     * the events.
     * </p>
     * <p>
     * The firing will go to the listeners that are registered for the specific
     * property as well as general purpose listeners.
     * </p>
     * <p>
     * If the old and new values for the event are the same, by the
     * <tt>equals()</tt> method, the event will not be fired.
     * </p>
     * 
     * @param event
     *            The event to fire to the listeners.
     */
    public void firePropertyChange(final PropertyChangeEvent event) {
        validateNamedProperty(event.getPropertyName());
        {
            final Object a = event.getOldValue();
            final Object b = event.getNewValue();
            if (a != null && a.equals(b) || a == null && b == null)
                return;
        }
        // if (event.getOldValue() == null) {
        // if (event.getOldValue() == null) {
        // return;
        // }
        // } else if (event.getOldValue().equals(event.getNewValue())) {
        // return;
        // }
        // validated that an event must be thrown; now throw it.
        synchronized (listenerMap) {
            // First gets the list of listeners and stores them in strong
            // references by copying them into a new set.
            final Set<PropertyChangeListener> targets = new HashSet<PropertyChangeListener>(
                    listenerMap.get(ALL_PROPERTIES));
            targets.addAll(listenerMap.get(event.getPropertyName()));
            for (final PropertyChangeListener element : targets)
                element.propertyChange(event);
        }
    }

    /**
     * Shortcut for firing an event on boolean properties.
     * 
     * @param property
     *            the name of the property which changed.
     * @param old
     *            The old value.
     * @param neo
     *            The new value.
     */
    public void firePropertyChange(final String property, final boolean old, final boolean neo) {
        final PropertyChangeEvent event = new PropertyChangeEvent(producer, property, Boolean.valueOf(old),
                Boolean.valueOf(neo));
        this.firePropertyChange(event);
    }

    /**
     * Shortcut for firing an event on double properties.
     * 
     * @param property
     *            the name of the property which changed.
     * @param old
     *            The old value.
     * @param neo
     *            The new value.
     */
    public void firePropertyChange(final String property, final double old, final double neo) {
        final PropertyChangeEvent event = new PropertyChangeEvent(producer, property, new Double(old),
                new Double(neo));
        this.firePropertyChange(event);
    }

    /**
     * Shortcut for firing an event on integer properties.
     * 
     * @param property
     *            the name of the property which changed.
     * @param old
     *            The old value.
     * @param neo
     *            The new value.
     */
    public void firePropertyChange(final String property, final int old, final int neo) {
        final PropertyChangeEvent event = new PropertyChangeEvent(producer, property, new Integer(old),
                new Integer(neo));
        this.firePropertyChange(event);
    }

    /**
     * Notify listeners that an object type property has changed
     * 
     * @param property
     *            the name of the property which changed.
     * @param old
     *            The old value.
     * @param neo
     *            The new value.
     */
    public void firePropertyChange(final String property, final Object old, final Object neo) {
        final PropertyChangeEvent event = new PropertyChangeEvent(producer, property, old, neo);
        this.firePropertyChange(event);
    }

    /**
     * Returns an array of all the listeners that were added to the
     * PropertyChangeSupport object with addPropertyChangeListener(). The array
     * is computed as late as possible to give as much cleanout time as neded.
     * 
     * @return An array of all listeners.
     */
    public PropertyChangeListener[] getPropertyChangeListeners() {
        final Set<PropertyChangeListener> all = new WeakHashSet();
        synchronized (listenerMap) {
            for (final Set<PropertyChangeListener> element : listenerMap.values())
                all.addAll(element);
        }
        final PropertyChangeListener[] pcls = new PropertyChangeListener[all.size()];
        return all.toArray(pcls);
    }

    /**
     * Returns an array of all the listeners which have been associated with the
     * named property.
     * 
     * @param property
     *            The name of the relevant property.
     * @return An array of listeners listening to the specified property.
     */
    public PropertyChangeListener[] getPropertyChangeListeners(final String property) {
        validateNamedProperty(property);
        final Set<PropertyChangeListener> namedListeners;
        synchronized (listenerMap) {
            namedListeners = new HashSet<PropertyChangeListener>(listenerMap.get(property));
        }
        final PropertyChangeListener[] pcls = new PropertyChangeListener[namedListeners.size()];
        return namedListeners.toArray(pcls);
    }

    /**
     * Check if there are any listeners interested in a specific property.
     * 
     * @param property
     *            The relvant property name.
     * @return true If there are listeners for the given property
     */
    public boolean hasListeners(final String property) {
        validateNamedProperty(property);
        synchronized (listenerMap) {
            return !listenerMap.get(property).isEmpty();
        }
    }

    /**
     * Remove a PropertyChangeListener from the listener list. This removes a
     * PropertyChangeListener that was registered for all properties.
     * 
     * @param pcl
     *            The PropertyChangeListener to be removed
     */
    public void removePropertyChangeListener(final PropertyChangeListener pcl) {
        synchronized (listenerMap) {
            listenerMap.get(ALL_PROPERTIES).remove(pcl);
        }
    }

    /**
     * Remove a PropertyChangeListener for a specific property.
     * 
     * @param property
     *            The name of the relevant property.
     * @param pcl
     *            The listener to remove.
     */
    public void removePropertyChangeListener(final String property, final PropertyChangeListener pcl) {
        validateNamedProperty(property);
        synchronized (listenerMap) {
            listenerMap.get(property).remove(pcl);
        }
    }

    /**
     * Validate that a property name is a member of the producer object.
     * <p>
     * This is a helper method so that all methods that must validate this need
     * not replicate the code.
     * </p>
     * 
     * @param property
     *            The name of the property to validate.
     * @throws IllegalArgumentException
     */
    private void validateNamedProperty(final String property) {
        if (!listenerMap.containsKey(property)) {
            if (log.isDebugEnabled())
                log.debug("Key Set: " + listenerMap.keySet());
            throw new IllegalArgumentException("The property '" + property + "' is not a valid property of "
                    + producer.getClass() + ". Valid values = " + listenerMap.keySet().toString());
        }
    }
}