All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jgoodies.binding.value.BufferedValueModel Maven / Gradle / Ivy

Go to download

The JGoodies Binding library connects object properties to Swing user interface components. And it helps you represent the state and behavior of a presentation independently of the GUI components used in the interface.

The newest version!
/*
 * Copyright (c) 2002-2015 JGoodies Software GmbH. 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 Software GmbH 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.
 */

package com.jgoodies.binding.value;

import static com.jgoodies.common.base.Preconditions.checkNotNull;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * A ValueModel that wraps another ValueModel, the subject,
 * and delays changes of the subject's value. Returns the subject's value
 * until a value has been set. The buffered value is not written to the
 * subject until the trigger channel changes to {@code Boolean.TRUE}.
 * The buffered value can be flushed by changing the trigger channel value
 * to {@code Boolean.FALSE}. Note that the commit and flush events
 * are performed only if the trigger channel fires a change event. Since a
 * plain ValueHolder fires no property change event if a value is set that has
 * been set before, it is recommended to use a {@link Trigger} instead
 * and invoke its {@code #triggerCommit} and {@code triggerFlush}
 * methods.

* * The BufferedValueModel has been designed to behave much like its subject * when accessing the value. Therefore it throws all exceptions that would * arise when accessing the subject directly. Hence, attempts to read or * write a value while the subject is {@code null} are always rejected * with a {@code NullPointerException}.

* * This class provides the bound read-write properties subject and * triggerChannel for the subject and trigger channel and a bound * read-only property buffering for the buffering state.

* * The BufferedValueModel registers listeners with the subject and * trigger channel. It is recommended to remove these listeners by invoking * {@code #release} if the subject and trigger channel live much longer * than this buffer. After {@code #release} has been called * you must not use the BufferedValueModel instance any longer. * As an alternative you may use event listener lists in subjects and * trigger channels that are based on {@code WeakReference}s.

* * If the subject value changes while this model is in buffering state * this change won't show through as this model's new value. If you want * to update the value whenever the subject value changes, register a * listener with the subject value and flush this model's trigger.

* * Constraints: The subject is of type {@code Object}, * the trigger channel value of type {@code Boolean}. * * @author Karsten Lentzsch * @version $Revision: 1.16 $ * * @see ValueModel * @see ValueModel#getValue() * @see ValueModel#setValue(Object) */ public final class BufferedValueModel extends AbstractValueModel { // Property Names ********************************************************* /** * The name of the bound read-only bean property that indicates * whether this models is buffering or in write-through state. * * @see #isBuffering() */ public static final String PROPERTY_BUFFERING = "buffering"; /** * The name of the bound read-write bean property for the subject. * * @see #getSubject() * @see #setSubject(ValueModel) */ public static final String PROPERTY_SUBJECT = "subject"; /** * The name of the bound read-write bean property for the trigger channel. * * @see #getTriggerChannel() * @see #setTriggerChannel(ValueModel) */ public static final String PROPERTY_TRIGGER_CHANNEL = "triggerChannel"; // ************************************************************************ /** * Holds the subject that provides the underlying value * of type {@code Object}. */ private ValueModel subject; /** * Holds the three-state trigger of type {@code Boolean}. */ private ValueModel triggerChannel; /** * Holds the buffered value. This value is ignored if we are not buffering. */ private Object bufferedValue; /** * Indicates whether a value has been assigned since the last trigger change. */ private boolean valueAssigned; /** * Holds a PropertyChangeListener that observes subject value changes. */ private final ValueChangeHandler valueChangeHandler; /** * Holds a PropertyChangeListener that observes trigger changes. */ private final TriggerChangeHandler triggerChangeHandler; // Instance Creation **************************************************** /** * Constructs a BufferedValueModel on the given subject * using the given trigger channel. * * @param subject the value model to be buffered * @param triggerChannel the value model that triggers the commit or flush event * @throws NullPointerException if the triggerChannel is {@code null} */ public BufferedValueModel( ValueModel subject, ValueModel triggerChannel) { valueChangeHandler = new ValueChangeHandler(); triggerChangeHandler = new TriggerChangeHandler(); setSubject(subject); setTriggerChannel(triggerChannel); setBuffering(false); } // Accessing the Subject and Trigger Channel ****************************** /** * Returns the subject, i.e. the underlying ValueModel that provides * the unbuffered value. * * @return the ValueModel that provides the unbuffered value */ public ValueModel getSubject() { return subject; } /** * Sets a new subject ValueModel, i.e. the model that provides * the unbuffered value. Notifies all listeners that the subject * property has changed. * * @param newSubject the subject ValueModel to be set */ public void setSubject(ValueModel newSubject) { ValueModel oldSubject = getSubject(); Object oldValue = null; if (oldSubject != null) { ReadAccessResult oldReadValue = readBufferedOrSubjectValue(); oldValue = oldReadValue.value; oldSubject.removeValueChangeListener(valueChangeHandler); } subject = newSubject; if (newSubject != null) { newSubject.addValueChangeListener(valueChangeHandler); } firePropertyChange(PROPERTY_SUBJECT, oldSubject, newSubject); if (isBuffering()) { return; } ReadAccessResult newReadValue = readBufferedOrSubjectValue(); Object newValue = newReadValue.value; // TODO: Check if the following conditional is valid. // Note that the old and/or new value may be null // just because the property is read-only. if (oldValue != null || newValue != null) { fireValueChange(oldValue, newValue, true); } } /** * Returns the ValueModel that is used to trigger commit and flush events. * * @return the ValueModel that is used to trigger commit and flush events */ public ValueModel getTriggerChannel() { return triggerChannel; } /** * Sets the ValueModel that triggers the commit and flush events. * * @param newTriggerChannel the ValueModel to be set as trigger channel * @throws NullPointerException if the newTriggerChannel is {@code null} */ public void setTriggerChannel(ValueModel newTriggerChannel) { checkNotNull(newTriggerChannel, "The trigger channel must not be null."); ValueModel oldTriggerChannel = getTriggerChannel(); if (oldTriggerChannel != null) { oldTriggerChannel.removeValueChangeListener(triggerChangeHandler); } triggerChannel = newTriggerChannel; //if (newTriggerChannel != null) { newTriggerChannel.addValueChangeListener(triggerChangeHandler); //} firePropertyChange(PROPERTY_TRIGGER_CHANNEL, oldTriggerChannel, newTriggerChannel); } // Implementing the ValueModel Interface ******************************** /** * Returns the subject's value if no value has been set since the last * commit or flush, and returns the buffered value otherwise. * Attempts to read a value when no subject is set are rejected * with a NullPointerException. * * @return the buffered value * @throws NullPointerException if no subject is set */ @Override public Object getValue() { checkNotNull(subject, "The subject must not be null " + "when reading a value from a BufferedValueModel."); return isBuffering() ? bufferedValue : subject.getValue(); } /** * Sets a new buffered value and turns this BufferedValueModel into * the buffering state. The buffered value is not provided to the * underlying model until the trigger channel indicates a commit. * Attempts to set a value when no subject is set are rejected * with a NullPointerException.

* * The above semantics is easy to understand, however it is tempting * to check the new value against the current subject value to avoid * that the buffer unnecessary turns into the buffering state. But here's * a problem. Let's say the subject value is "first" at buffer * creation time, and let's say the subject value has changed in the * meantime to "second". Now someone sets the value "second" to this buffer. * The subject value and the value to be set are equal. Shall we buffer? * Also, this decision would depend on the ability to read the subject. * The semantics would depend on the subject' state and capabilities.

* * It is often sufficient to observe the buffering state when enabling * or disabling a commit command button like "OK" or "Apply". * And later check the changed state in a PresentationModel. * You may want to do better and may want to observe a property like * "defersTrueChange" that indicates whether flushing a buffer will * actually change the subject. But note that such a state may change * with subject value changes, which may be hard to understand for a user.

* * TODO: Consider adding an optimized execution path for the case * that this model is already in buffering state. In this case * the old buffered value can be used instead of invoking * {@code #readBufferedOrSubjectValue()}. * * @param newBufferedValue the value to be buffered * @throws NullPointerException if no subject is set */ @Override public void setValue(Object newBufferedValue) { checkNotNull(subject, "The subject must not be null " + "when setting a value to a BufferedValueModel."); ReadAccessResult oldReadValue = readBufferedOrSubjectValue(); Object oldValue = oldReadValue.value; bufferedValue = newBufferedValue; setBuffering(true); if (oldReadValue.readable && oldValue == newBufferedValue) { return; } fireValueChange(oldValue, newBufferedValue, true); } /** * Tries to lookup the current buffered or subject value * and returns this value plus a marker that indicates * whether the read-access succeeded or failed. * The latter situation arises in an attempt to read a value from * a write-only subject if this BufferedValueModel is not buffering * and if this model changes its subject. * * @return the current value plus a boolean that indicates the success or failure */ private ReadAccessResult readBufferedOrSubjectValue() { try { Object value = getValue(); // May fail with write-only models return new ReadAccessResult(value, true); } catch (Exception e) { return new ReadAccessResult(null, false); } } // Releasing PropertyChangeListeners ************************************** /** * Removes the PropertyChangeListeners from the subject and * trigger channel.

* * To avoid memory leaks it is recommended to invoke this method * if the subject and trigger channel live much longer than this buffer. * Once #release has been invoked the BufferedValueModel instance * must not be used any longer.

* * As an alternative you may use event listener lists in subjects and * trigger channels that are based on {@code WeakReference}s. * * @see java.lang.ref.WeakReference */ public void release() { ValueModel aSubject = getSubject(); if (aSubject != null) { aSubject.removeValueChangeListener(valueChangeHandler); } ValueModel aTriggerChannel = getTriggerChannel(); if (aTriggerChannel != null) { aTriggerChannel.removeValueChangeListener(triggerChangeHandler); } } // Misc ***************************************************************** /** * Returns whether this model buffers a value or not, that is, whether * a value has been assigned since the last commit or flush. * * @return true if a value has been assigned since the last commit or flush */ public boolean isBuffering() { return valueAssigned; } private void setBuffering(boolean newValue) { boolean oldValue = isBuffering(); valueAssigned = newValue; firePropertyChange(PROPERTY_BUFFERING, oldValue, newValue); } /** * Sets the buffered value as new subject value - if any value has been set. * After this commit this BufferedValueModel behaves as if no value * has been set before. This method is invoked if the trigger has changed * to {@code Boolean.TRUE}.

* * Since the subject's value is assigned after the buffer marker * is reset, subject change notifications will be handled. In this case * the subject's old value is not this BufferedValueModel's old value; * instead the old value reported to listeners of this model * is the formerly buffered value. * * @throws NullPointerException if no subject is set */ private void commit() { if (isBuffering()) { setBuffering(false); valueChangeHandler.oldValue = bufferedValue; subject.setValue(bufferedValue); valueChangeHandler.oldValue = null; } else { checkNotNull(subject, "The subject must not be null " + "while committing a value in a BufferedValueModel."); } } /** * Flushes the buffered value. This method is invoked if the trigger * has changed to {@code Boolean.FALSE}. After this flush * this BufferedValueModel behaves as if no value has been set before.

* * TODO: Check whether we need to use #getValueSafe instead of #getValue. * * @throws NullPointerException if no subject is set */ private void flush() { Object oldValue = getValue(); setBuffering(false); Object newValue = getValue(); fireValueChange(oldValue, newValue, true); } // Overriding Superclass Behavior ***************************************** @Override protected String paramString() { return "value=" + valueString() + "; buffering" + isBuffering(); } // Helper Class *********************************************************** /** * Describes the result of a subject value read-access plus a marker * that indicates if the value could be read or not. The latter is * used in {@code #setValue} to suppress some unnecessary * change notifications in case the value could be read successfully. * * @see BufferedValueModel#setValue(Object) */ private static final class ReadAccessResult { final Object value; final boolean readable; private ReadAccessResult(Object value, boolean readable) { this.value = value; this.readable = readable; } } // Event Handling ********************************************************* /** * Listens to changes of the subject. */ private final class ValueChangeHandler implements PropertyChangeListener { Object oldValue; /** * The subject's value has changed. Notifies this BufferedValueModel's * listeners iff we are not buffering, does nothing otherwise.

* * @param evt the property change event to be handled */ @Override public void propertyChange(PropertyChangeEvent evt) { if (!isBuffering()) { fireValueChange( oldValue != null ? oldValue : evt.getOldValue(), evt.getNewValue(), true); } } } /** * Listens to changes of the trigger channel. */ private final class TriggerChangeHandler implements PropertyChangeListener { /** * The trigger has been changed. Commits or flushes the buffered value. * * @param evt the property change event to be handled */ @Override public void propertyChange(PropertyChangeEvent evt) { if (Boolean.TRUE.equals(evt.getNewValue())) { commit(); } else if (Boolean.FALSE.equals(evt.getNewValue())) { flush(); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy