/*
Copyright (c) 2003-2009 ITerative Consulting Pty Ltd. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
o Redistributions of source code must retain the above copyright notice, this list of conditions and
the following disclaimer.
o Redistributions in binary form must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials provided with the distribution.
o This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package DisplayProject.binding.adapter;
import java.awt.Component;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeListenerProxy;
import java.beans.PropertyChangeSupport;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import javax.swing.JCheckBox;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JColorChooser;
import javax.swing.JComboBox;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPasswordField;
import javax.swing.JRadioButton;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JTextField;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import DisplayProject.binding.beans.PropertyConnector;
import DisplayProject.binding.list.SelectionInList;
import DisplayProject.binding.value.BufferedValueModel;
import DisplayProject.binding.value.ValueModel;
import Framework.UsageException;
/**
* Consists only of static methods that bind Swing components to ValueModels.
* This is one of two helper classes that assist you in establishing a binding:
* 1) this Bindings class binds components that have been created before;
* it wraps ValueModels with the adapters from package
* <code>DisplayProject.binding.adapter</code>.
* 2) the BasicComponentFactory creates Swing components that are then
* bound using this Bindings class.<p>
*
* If you have an existing factory that vends Swing components,
* you can use this Bindings class to bind them to ValueModels.
* If you don't have such a factory, you can use the BasicComponentFactory
* or a custom subclass to create and bind Swing components.<p>
*
* The binding process for JCheckBox, JRadioButton, and other AbstractButtons
* retains the former enablement state. Before the new (adapting) model
* is set, the enablement is requested from the model, not the button.
* This enablement is set after the new model has been set.<p>
*
*
*
* @author Karsten Lentzsch
* @version $Revision: 1.15 $
*
* @see DisplayProject.binding.value.ValueModel
* @see BasicComponentFactory
* Copyright (c) 2002-2005 JGoodies Karsten Lentzsch. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Karsten Lentzsch nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
public final class Bindings {
/**
* Triggers a commit in the shared focus lost trigger
* if focus is lost permanently. Shared among all components
* that are configured to commit on focus lost.
*
* @see #createCommitOnFocusLostModel(ValueModel, Component)
*/
private static final FocusLostHandler FOCUS_LOST_HANDLER =
new FocusLostHandler();
/**
* Holds a weak trigger that is shared by BufferedValueModels
* that commit on permanent focus change.
*
* @see #createCommitOnFocusLostModel(ValueModel, Component)
*/
private static final WeakTrigger FOCUS_LOST_TRIGGER =
new WeakTrigger();
private Bindings() {
// Suppresses default constructor, ensuring non-instantiability.
}
// Binding Methods ********************************************************
/**
* Binds a JCheckBox to the given ValueModel and retains the enablement
* state. The bound check box is selected if and only if the model's value
* equals <code>Boolean.TRUE</code>.<p>
*
* The value model is converted to the required interface
* ToggleButtonModel using a ToggleButtonAdapter.
*
* @param checkBox the check box to be bound
* @param valueModel the model that provides a Boolean value
*
* @throws NullPointerException if the checkBox or valueModel
* is <code>null</code>
*/
public static void bind(JCheckBox checkBox, ValueModel valueModel) {
boolean enabled = checkBox.getModel().isEnabled();
checkBox.setModel(new ToggleButtonAdapter(valueModel));
checkBox.setEnabled(enabled);
}
/**
* Binds a JCheckBoxMenuItem to the given ValueModel and retains
* the enablement state. The bound menu item is selected if and only if
* the model's value equals <code>Boolean.TRUE</code>.<p>
*
* <strong>Note:</strong> For users of the JGoodies UIF (user interface
* framework) the recommended way to create and bind check box menu items
* is the class <code>DisplayProject.uif.ToggleAction</code>.<p>
*
* The value model is converted to the required interface
* <code>ToggleButtonModel</code> using a <code>ToggleButtonAdapter</code>.
*
* @param checkBoxMenuItem the check box menu item to be bound
* @param valueModel the model that provides a Boolean value
*
* @throws NullPointerException if the menu item or valueModel
* is <code>null</code>
*/
public static void bind(JCheckBoxMenuItem checkBoxMenuItem, ValueModel valueModel) {
boolean enabled = checkBoxMenuItem.getModel().isEnabled();
checkBoxMenuItem.setModel(new ToggleButtonAdapter(valueModel));
checkBoxMenuItem.setEnabled(enabled);
}
/**
* Binds a JColorChooser to the given Color-typed ValueModel.
* The ValueModel must be of type Color and must
* allow read-access to its value. Also, it is strongly recommended
* (though not required) that the underlying ValueModel provides only
* non-null values. This is so because the ColorSelectionModel behavior
* is undefined for <code>null</code> values and it may have unpredictable
* results.<p>
*
* <strong>Note:</strong> There's a bug in Java 1.4.2, Java 5 and Java 6
* that affects this binding. The BasicColorChooserUI doesn't listen
* to changes in the selection model, and so the preview panel won't
* update if the selected color changes. As a workaround you can use
* {@link BasicComponentFactory#createColorChooser(ValueModel)},
* or you could use a Look&Feel that fixes the bug mentioned above.
*
* @param colorChooser the color chooser to be bound
* @param valueModel the model that provides non-<code>null</code>
* Color values.
*
* @throws NullPointerException if the color chooser or value model
* is <code>null</code>
*
* @since 1.0.3
*/
public static void bind(JColorChooser colorChooser, ValueModel valueModel) {
colorChooser.setSelectionModel(new ColorSelectionAdapter(valueModel));
}
/**
* Binds a non-editable JComboBox to the given SelectionInList using
* the SelectionInList's ListModel as list data provider and the
* SelectionInList's selection index holder for the combo box model's
* selected item.<p>
*
* There are a couple of other possibilities to bind a JComboBox.
* See the constructors and the class comment of the
* {@link ComboBoxAdapter}.
*
* @param comboBox the combo box to be bound
* @param selectionInList provides the list and selection
*
* @throws NullPointerException if the combo box or the selectionInList
* is <code>null</code>
*
* @see ComboBoxAdapter
*
* @since 1.0.1
*/
public static void bind(JComboBox comboBox, SelectionInList selectionInList) {
if (selectionInList == null)
throw new NullPointerException("The SelectionInList must not be null.");
comboBox.setModel(new ComboBoxAdapter(selectionInList));
}
/**
* Binds the given JFormattedTextField to the specified ValueModel.
* Synchronized the ValueModel's value with the text field's value
* by means of a PropertyConnector.
*
* @param textField the JFormattedTextField that is to be bound
* @param valueModel the model that provides the value
*
* @throws NullPointerException if the text field or valueModel
* is <code>null</code>
*/
public static void bind(JFormattedTextField textField, ValueModel valueModel) {
if (valueModel == null)
throw new NullPointerException("The value model must not be null.");
PropertyConnector connector =
new PropertyConnector(valueModel, "value", textField, "value");
connector.updateProperty2();
}
/**
* Binds the given JLabel to the specified ValueModel.
*
* @param label a label that shall be bound to the given value model
* @param valueModel the model that provides the value
*
* @throws NullPointerException if the label or valueModel is <code>null</code>
*/
public static void bind(JLabel label, ValueModel valueModel) {
PropertyConnector connector =
new PropertyConnector(valueModel, "value", label, "text");
connector.updateProperty2();
}
/**
* Binds a JList to the given SelectionInList using the SelectionInList's
* ListModel as list data provider and the SelectionInList's selection
* index holder for the selection model.
*
* @param list the list to be bound
* @param selectionInList provides the list and selection
*
* @throws NullPointerException if the list or the selectionInList
* is <code>null</code>
*/
public static void bind(JList list, SelectionInList selectionInList) {
if (selectionInList == null)
throw new NullPointerException("The SelectionInList must not be null.");
list.setModel(selectionInList);
list.setSelectionModel(
new SingleListSelectionAdapter(
selectionInList.getSelectionIndexHolder()));
}
/**
* Binds a JRadioButton to the given ValueModel and retains the enablement
* state. The bound radio button is selected if and only if the model's
* value equals the specified choice value.<p>
*
* The model is converted to the required interface
* ToggleButtonModel using a RadioButtonAdapter.
*
* @param radioButton the radio button to be bound to the given model
* @param model the model that provides the current choice
* @param choice this button's value
*
* @throws NullPointerException if the valueModel is <code>null</code>
*/
public static void bind(JRadioButton radioButton, ValueModel model, Object choice) {
boolean enabled = radioButton.getModel().isEnabled();
radioButton.setModel(new RadioButtonAdapter(model, choice));
radioButton.setEnabled(enabled);
}
/**
* Binds a JRadioButtonMenuItem to the given ValueModel and retains
* the enablement state. The bound menu item is selected if and only if
* the model's value equals the specified choice.<p>
*
* <strong>Note:</strong> For users of the JGoodies UIF (user interface
* framework) the recommended way to create and bind radio button menu items
* is the class <code>DisplayProject.uif.ToggleAction</code>.<p>
*
* The model is converted to the required interface
* ToggleButtonModel using a RadioButtonAdapter.
*
* @param radioButtonMenuItem the radio item to be bound to the given model
* @param model the model that provides the current choice
* @param choice this button's value
*
* @throws NullPointerException if the valueModel is <code>null</code>
*/
public static void bind(
JRadioButtonMenuItem radioButtonMenuItem,
ValueModel model,
Object choice) {
boolean enabled = radioButtonMenuItem.getModel().isEnabled();
radioButtonMenuItem.setModel(new RadioButtonAdapter(model, choice));
radioButtonMenuItem.setEnabled(enabled);
}
/**
* Binds text fields, text areas, password fields and other JTextComponents
* to the given ValueModel. The model is updated on every character typed.<p>
*
* <strong>Security Note: </strong> If you use this method to bind a
* JPasswordField, the field's password will be requested as Strings
* from the field and will be held as String by the given ValueModel.
* These password String could potentially be observed in a security fraud.
* For stronger security it is recommended to request a character array
* from the JPasswordField and clear the array after use by setting
* each character to zero. Method {@link JPasswordField#getPassword()}
* return's the field's password as a character array.<p>
*
*
* @param textComponent the component to be bound to the value model
* @param valueModel the model that provides the text value
*
* @throws NullPointerException if the text component or valueModel
* is <code>null</code>
*
* @see JPasswordField#getPassword()
*/
public static void bind(
JTextComponent textComponent,
ValueModel valueModel) {
bind(textComponent, valueModel, false);
}
/**
* Binds text fields, text areas, password fields and other JTextComponents
* to the given ValueModel. The model can be updated on focus lost or
* on every character typed.<p>
*
* The DocumentAdapter used in this binding is configured
* to filter newlines for JTextField and its subclasses.<p>
*
* <strong>Security Note: </strong> If you use this method to bind a
* JPasswordField, the field's password will be requested as Strings
* from the field and will be held as String by the given ValueModel.
* These password String could potentially be observed in a security fraud.
* For stronger security it is recommended to request a character array
* from the JPasswordField and clear the array after use by setting
* each character to zero. Method {@link JPasswordField#getPassword()}
* return's the field's password as a character array.
*
* @param textComponent the component to be bound to the value model
* @param valueModel the model that provides the text value
* @param commitOnFocusLost true to commit text changes on focus lost,
* false to commit text changes on every character typed
*
* @throws NullPointerException if the text component or valueModel
* is <code>null</code>
*
* @see JPasswordField#getPassword()
*/
public static void bind(
JTextComponent textComponent,
ValueModel valueModel,
boolean commitOnFocusLost) {
bind(textComponent, valueModel, new PlainDocument(), commitOnFocusLost);
}
/**
* Binds text fields, text areas, password fields and other JTextComponents
* to the given ValueModel. The model can be updated on focus lost or
* on every character typed.<p>
*
* The DocumentAdapter used in this binding stores the contents
* in the given Document and is configured to filter newlines
* for JTextField and its subclasses.<p>
*
* <strong>Security Note: </strong> If you use this method to bind a
* JPasswordField, the field's password will be requested as Strings
* from the field and will be held as String by the given ValueModel.
* These password String could potentially be observed in a security fraud.
* For stronger security it is recommended to request a character array
* from the JPasswordField and clear the array after use by setting
* each character to zero. Method {@link JPasswordField#getPassword()}
* return's the field's password as a character array.
*
* @param textComponent the component to be bound to the value model
* @param valueModel the model that provides the text value
* @param document the Document used to store the contents
* @param commitOnFocusLost true to commit text changes on focus lost,
* false to commit text changes on every character typed
*
* @throws NullPointerException if the component, valueModel, or document
* is <code>null</code>
*
* @see JPasswordField#getPassword()
*/
public static void bind(
JTextComponent textComponent,
ValueModel valueModel,
Document document,
boolean commitOnFocusLost) {
if (valueModel == null)
throw new NullPointerException("The value model must not be null.");
ValueModel textModel = commitOnFocusLost
? createCommitOnFocusLostModel(valueModel, textComponent)
: valueModel;
boolean filterNewlines = textComponent instanceof JTextField;
textComponent.setDocument(
new DocumentAdapter(textModel, document, filterNewlines));
}
// Helper Code ************************************************************
/**
* Creates and returns a ValueModel that commits its value
* if the given component looses the focus permanently.
* It wraps the underlying ValueModel with a BufferedValueModel
* and delays the value commit until this class' shared FOCUS_LOST_TRIGGER
* commits. This happens, because this class' shared FOCUS_LOST_HANDLER
* is registered with the specified component.
*
* @param valueModel the model that provides the value
* @param component the component that looses the focus
* @return a buffering ValueModel that commits on focus lost
*
* @throws NullPointerException if the value model is <code>null</code>
*/
private static ValueModel createCommitOnFocusLostModel(
ValueModel valueModel,
Component component) {
if (valueModel == null)
throw new NullPointerException("The value model must not be null.");
ValueModel model = new BufferedValueModel(valueModel, FOCUS_LOST_TRIGGER);
component.addFocusListener(FOCUS_LOST_HANDLER);
return model;
}
// Helper Classes *********************************************************
/**
* Triggers a commit event on permanent focus lost.
*/
private static class FocusLostHandler extends FocusAdapter {
/**
* Triggers a commit event if the focus lost is permanent.
*
* @param evt the focus lost event
*/
public void focusLost(FocusEvent evt) {
if (!evt.isTemporary())
FOCUS_LOST_TRIGGER.triggerCommit();
}
}
/**
* Unlike the Trigger class, this implementation uses WeakReferences
* to store value change listeners.
*/
private static final class WeakTrigger implements ValueModel {
private final transient WeakPropertyChangeSupport changeSupport;
private Boolean value;
// Instance Creation ******************************************************
/**
* Constructs a WeakTrigger set to neutral.
*/
WeakTrigger() {
value = null;
changeSupport = new WeakPropertyChangeSupport(this);
}
// ValueModel Implementation **********************************************
/**
* Returns a Boolean that indicates the current trigger state.
*
* @return a Boolean that indicates the current trigger state
*/
public Object getValue() {
return value;
}
/**
* Sets a new Boolean value and rejects all non-Boolean values.
* Fires no change event if the new value is equal to the
* previously set value.<p>
*
* This method is not intended to be used by API users.
* Instead you should trigger commit and flush events by invoking
* <code>#triggerCommit</code> or <code>#triggerFlush</code>.
*
* @param newValue the Boolean value to be set
* @throws UsageException if the newValue is not a Boolean
*/
public void setValue(Object newValue) {
if ((newValue != null) && !(newValue instanceof Boolean))
throw new UsageException("Trigger values must be of type Boolean.");
Object oldValue = value;
value = (Boolean) newValue;
fireValueChange(oldValue, newValue);
}
// Change Management ****************************************************
/**
* Registers the given PropertyChangeListener with this model.
* The listener will be notified if the value has changed.<p>
*
* The PropertyChangeEvents delivered to the listener have the name
* set to "value". In other words, the listeners won't get notified
* when a PropertyChangeEvent is fired that has a null object as
* the name to indicate an arbitrary set of the event source's
* properties have changed.<p>
*
* In the rare case, where you want to notify a PropertyChangeListener
* even with PropertyChangeEvents that have no property name set,
* you can register the listener with #addPropertyChangeListener,
* not #addValueChangeListener.
*
* @param listener the listener to add
*
* @see ValueModel
*/
public void addValueChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
changeSupport.addPropertyChangeListener("value", listener);
}
/**
* Removes the given PropertyChangeListener from the model.
*
* @param listener the listener to remove
*/
public void removeValueChangeListener(PropertyChangeListener listener) {
if (listener == null) {
return;
}
changeSupport.removePropertyChangeListener("value", listener);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param oldValue the value before the change
* @param newValue the value after the change
*
* @see java.beans.PropertyChangeSupport
*/
private void fireValueChange(Object oldValue, Object newValue) {
changeSupport.firePropertyChange("value", oldValue, newValue);
}
// Triggering *************************************************************
/**
* Triggers a commit event in BufferedValueModels that share this Trigger.
* Sets the value to <code>Boolean.TRUE</code> and ensures that dependents
* are notified about a value change to this new value. Only if necessary
* the value is temporarily set to <code>null</code>. This way it minimizes
* the number of PropertyChangeEvents fired by this Trigger.
*/
void triggerCommit() {
if (Boolean.TRUE.equals(getValue()))
setValue(null);
setValue(Boolean.TRUE);
}
}
/**
* Differs from its superclass {@link PropertyChangeSupport} in that it
* uses WeakReferences for registering listeners. It wraps registered
* PropertyChangeListeners with instances of WeakPropertyChangeListener
* and cleans up a list of stale references when firing an event.
*/
@SuppressWarnings("serial")
private static final class WeakPropertyChangeSupport extends PropertyChangeSupport {
// Instance Creation ******************************************************
/**
* Constructs a WeakPropertyChangeSupport object.
*
* @param sourceBean The bean to be given as the source for any events.
*/
WeakPropertyChangeSupport(
Object sourceBean) {
super(sourceBean);
}
// Managing Property Change Listeners **********************************
/** {@inheritDoc} */
public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
if (listener == null)
return;
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy) listener;
// Call two argument add method.
addPropertyChangeListener(proxy.getPropertyName(),
(PropertyChangeListener) proxy.getListener());
} else {
super.addPropertyChangeListener(new WeakPropertyChangeListener(listener));
}
}
/** {@inheritDoc} */
public synchronized void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listener == null)
return;
super.addPropertyChangeListener(propertyName,
new WeakPropertyChangeListener(propertyName, listener));
}
/** {@inheritDoc} */
public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
if (listener == null)
return;
if (listener instanceof PropertyChangeListenerProxy) {
PropertyChangeListenerProxy proxy =
(PropertyChangeListenerProxy) listener;
// Call two argument remove method.
removePropertyChangeListener(proxy.getPropertyName(),
(PropertyChangeListener) proxy.getListener());
return;
}
PropertyChangeListener[] listeners = getPropertyChangeListeners();
WeakPropertyChangeListener wpcl;
for (int i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] instanceof PropertyChangeListenerProxy)
continue;
wpcl = (WeakPropertyChangeListener) listeners[i];
if (wpcl.get() == listener) {
super.removePropertyChangeListener(wpcl);
break;
}
}
}
/** {@inheritDoc} */
public synchronized void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
if (listener == null)
return;
PropertyChangeListener[] listeners = getPropertyChangeListeners(propertyName);
WeakPropertyChangeListener wpcl;
for (int i = listeners.length - 1; i >= 0; i--) {
wpcl = (WeakPropertyChangeListener) listeners[i];
if (wpcl.get() == listener) {
super.removePropertyChangeListener(propertyName, wpcl);
break;
}
}
}
// Firing Events **********************************************************
/**
* Fires the specified PropertyChangeEvent to any registered listeners.
*
* @param evt The PropertyChangeEvent object.
*
* @see PropertyChangeSupport#firePropertyChange(PropertyChangeEvent)
*/
public void firePropertyChange(PropertyChangeEvent evt) {
cleanUp();
super.firePropertyChange(evt);
}
/**
* Reports a bound property update to any registered listeners.
*
* @param propertyName The programmatic name of the property
* that was changed.
* @param oldValue The old value of the property.
* @param newValue The new value of the property.
*
* @see PropertyChangeSupport#firePropertyChange(String, Object, Object)
*/
public void firePropertyChange(
String propertyName,
Object oldValue,
Object newValue) {
cleanUp();
super.firePropertyChange(propertyName, oldValue, newValue);
}
@SuppressWarnings("unchecked")
private static final ReferenceQueue QUEUE = new ReferenceQueue();
private static void cleanUp() {
WeakPropertyChangeListener wpcl;
while ((wpcl = (WeakPropertyChangeListener) QUEUE.poll()) != null) {
wpcl.removeListener();
}
}
void removeWeakPropertyChangeListener(WeakPropertyChangeListener l) {
if (l.propertyName == null) {
super.removePropertyChangeListener(l);
} else {
super.removePropertyChangeListener(l.propertyName, l);
}
}
/**
* Wraps a PropertyChangeListener to make it weak.
*/
@SuppressWarnings("unchecked")
private final class WeakPropertyChangeListener extends WeakReference
implements PropertyChangeListener {
final String propertyName;
private WeakPropertyChangeListener(PropertyChangeListener delegate) {
this(null, delegate);
}
private WeakPropertyChangeListener(String propertyName,
PropertyChangeListener delegate) {
super(delegate, QUEUE);
this.propertyName = propertyName;
}
/** {@inheritDoc} */
public void propertyChange(PropertyChangeEvent evt) {
PropertyChangeListener delegate = (PropertyChangeListener) get();
if (delegate != null) {
delegate.propertyChange(evt);
}
}
private void removeListener() {
removeWeakPropertyChangeListener(this);
}
}
}
}