com.jgoodies.binding.value.BufferedValueModel Maven / Gradle / Ivy
Show all versions of jgoodies-binding Show documentation
/*
* 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();
}
}
}
}