blob: 2f292b8c537ac707a22e1f27a82d50063dfafb50 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2012 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.common.utility.internal.model.value;
import org.eclipse.jpt.common.utility.model.event.PropertyChangeEvent;
import org.eclipse.jpt.common.utility.model.listener.PropertyChangeAdapter;
import org.eclipse.jpt.common.utility.model.listener.PropertyChangeListener;
import org.eclipse.jpt.common.utility.model.value.PropertyValueModel;
import org.eclipse.jpt.common.utility.model.value.ModifiablePropertyValueModel;
/**
* A <code>BufferedWritablePropertyValueModel</code> is used to hold a temporary
* copy of the value
* in another property value model (the "wrapped" value model). The application
* can modify this temporary copy, ad nauseam; but the temporary copy is only
* passed through to the "wrapped" value model when the trigger "accepts" the
* buffered value. Alternatively, the application can "reset" the buffered value
* to the original, "wrapped" value.
* <p>
* The trigger is another {@link PropertyValueModel} that holds a
* {@link Boolean} and the application changes the trigger's value to
* <code>true</code> on "accept", <code>false</code> on "reset".
* <p>
* Typically, in a dialog:<ul>
* <li>pressing the "OK" button will trigger an "accept" and close the dialog
* <li>pressing the "Cancel" button will simply close the dialog,
* dropping the "buffered" values into the bit bucket
* <li>pressing the "Apply" button will trigger an "accept" and leave the
* dialog open
* <li>pressing the "Restore" button will trigger a "reset" and leave the
* dialog open
* </ul>
* A number of buffered property value models can wrap another set of
* property aspect adapters that adapt the various aspects of a single
* domain model. All the buffered property value models can be hooked to the
* same trigger, and that trigger is controlled by the application, typically
* via the "OK" button in a dialog.
*
* @param <V> the type of the model's value
* @see PropertyAspectAdapter
*/
public class BufferedWritablePropertyValueModel<V>
extends PropertyValueModelWrapper<V>
implements ModifiablePropertyValueModel<V>
{
/**
* We cache the value here until it is accepted and passed
* through to the wrapped value model.
*/
protected volatile V bufferedValue;
/**
* This is set to true when we are "accepting" the buffered value
* and passing it through to the wrapped value model. This allows
* us to ignore the property change event fired by the wrapped
* value model.
* (We can't stop listening to the wrapped value model, because
* if we are the only listener that could "deactivate" the wrapped
* value model.)
*/
protected volatile boolean accepting;
/**
* This is the trigger that indicates whether the buffered value
* should be accepted or reset.
*/
protected final PropertyValueModel<Boolean> triggerModel;
/** This listens to the trigger model. */
protected final PropertyChangeListener triggerListener;
/**
* This flag indicates whether our buffered value has been assigned
* a value and is possibly out of sync with the wrapped value.
*/
protected volatile boolean buffering;
// ********** constructor/initialization **********
/**
* Construct a buffered property value model with the specified wrapped
* property value model and trigger model.
*/
// TODO wrap the value model in a CachingPVMWrapper and get rid of accepting flag
public BufferedWritablePropertyValueModel(ModifiablePropertyValueModel<V> valueModel, PropertyValueModel<Boolean> triggerModel) {
super(valueModel);
if (triggerModel == null) {
throw new NullPointerException();
}
this.triggerModel = triggerModel;
this.bufferedValue = null;
this.buffering = false;
this.accepting = false;
this.triggerListener = this.buildTriggerListener();
}
protected PropertyChangeListener buildTriggerListener() {
return new TriggerListener();
}
protected class TriggerListener
extends PropertyChangeAdapter
{
@Override
public void propertyChanged(PropertyChangeEvent event) {
BufferedWritablePropertyValueModel.this.triggerChanged(event);
}
}
// ********** value **********
/**
* If we are currently "buffering" a value, return that;
* otherwise, return the wrapped value.
*/
public V getValue() {
return this.buffering ? this.bufferedValue : this.valueModel.getValue();
}
/**
* Assign the new value to our "buffered" value.
* It will be pushed to the wrapped value model
* when the trigger is "accepted".
*/
public void setValue(V value) {
if (this.buffering) {
if (this.valuesAreEqual(value, this.valueModel.getValue())) {
// the buffered value is being set back to the original value
this.reset();
} else {
// the buffered value is being changed
Object old = this.bufferedValue;
this.bufferedValue = value;
this.firePropertyChanged(VALUE, old, value);
}
} else {
if (this.valuesAreEqual(value, this.valueModel.getValue())) {
// the buffered value is being set to the same value as the original value - ignore
} else {
// the buffered value is being set for the first time
Object old = this.valueModel.getValue();
this.bufferedValue = value;
this.buffering = true;
this.firePropertyChanged(VALUE, old, value);
}
}
}
// ********** PropertyValueModelWrapper extensions **********
/**
* Extend to engage the trigger model also.
*/
@Override
protected void engageModel() {
super.engageModel();
this.triggerModel.addPropertyChangeListener(VALUE, this.triggerListener);
}
/**
* Extend to disengage the trigger model also.
*/
@Override
protected void disengageModel() {
this.triggerModel.removePropertyChangeListener(VALUE, this.triggerListener);
super.disengageModel();
}
// ********** behavior **********
/**
* If we are currently "accepting" the value (i.e passing it on to the
* "wrapped" model), ignore change notifications, since we caused
* them and our own listeners are already aware of the change.
*/
@Override
protected void wrappedValueChanged(PropertyChangeEvent event) {
if ( ! this.accepting) {
this.wrappedValueChanged_(event);
}
}
/**
* If we have a "buffered" value, check whether the "wrapped" value has
* changed to be the same as the "buffered" value. If it has, stop "buffering";
* if not, do nothing.
* If we do not yet have a "buffered" value, simply propagate the
* change notification with the buffered model as the source.
*/
protected void wrappedValueChanged_(PropertyChangeEvent event) {
if (this.buffering) {
if (this.valuesAreEqual(event.getNewValue(), this.bufferedValue)) {
// the buffered value is being set back to the original value
this.reset();
} else {
this.handleChangeConflict(event);
}
} else {
this.firePropertyChanged(event.clone(this));
}
}
/**
* By default, if we have a "buffered" value and the "wrapped" value changes,
* we simply ignore the new "wrapped" value and simply overlay it with the
* "buffered" value if it is "accepted". ("Last One In Wins" concurrency model)
* Subclasses can override this method to change that behavior with a
* different concurrency model. For example, you could drop the "buffered" value
* and replace it with the new "wrapped" value, or you could throw an
* exception.
*/
protected void handleChangeConflict(@SuppressWarnings("unused") PropertyChangeEvent event) {
// the default is to do nothing
}
protected void triggerChanged(PropertyChangeEvent event) {
this.triggerChanged(((Boolean) event.getNewValue()).booleanValue());
}
/**
* The trigger changed:<ul>
* <li>If it is now <code>true</code>, "accept" the buffered value and push
* it to the wrapped value model.
* <li>If it is now <code>false</code>, "reset" the buffered value to its
* original value.
* </ul>
*/
protected void triggerChanged(boolean triggerValue) {
// if nothing has been "buffered", we don't need to do anything:
// nothing needs to be passed through; nothing needs to be reset;
if (this.buffering) {
if (triggerValue) {
this.accept();
} else {
this.reset();
}
}
}
protected void accept() {
// set the accepting flag so we ignore any events
// fired by the wrapped value model
this.accepting = true;
try {
this.getValueModel().setValue(this.bufferedValue);
} finally {
this.bufferedValue = null;
this.buffering = false;
// clear the flag once the "accept" is complete
this.accepting = false;
}
}
protected void reset() {
// notify our listeners that our value has been reset
Object old = this.bufferedValue;
this.bufferedValue = null;
this.buffering = false;
this.firePropertyChanged(VALUE, old, this.valueModel.getValue());
}
@Override
public void toString(StringBuilder sb) {
sb.append(this.getValue());
}
// ********** misc **********
/**
* Return whether the buffered model is currently "buffering"
* a value.
*/
public boolean isBuffering() {
return this.buffering;
}
/**
* Our constructor accepts only a {@link ModifiablePropertyValueModel}{@code<T>}.
*/
@SuppressWarnings("unchecked")
protected ModifiablePropertyValueModel<V> getValueModel() {
return (ModifiablePropertyValueModel<V>) this.valueModel;
}
// ********** trigger **********
/**
* <code>Trigger</code> is a special property value model that only maintains its
* value (of <code>true</code> or <code>false</code>) during the change notification caused by
* {@link #setValue(Object)}. In other words, a <code>Trigger</code>
* only has a valid value when it is being set.
*/
public static class Trigger
extends SimplePropertyValueModel<Boolean>
{
/**
* Construct a trigger with a null value.
*/
public Trigger() {
super();
}
// ********** ValueModel implementation **********
/**
* Extend so that this method can only be invoked during
* change notification triggered by {@link #setValue(Object)}.
*/
@Override
public Boolean getValue() {
if (this.value == null) {
throw new IllegalStateException("The method Trigger.getValue() may only be called during change notification."); //$NON-NLS-1$
}
return this.value;
}
/**
* Extend to reset the value to <code>null</code> once all the
* listeners have been notified.
*/
@Override
public void setValue(Boolean value) {
super.setValue(value);
this.value = null;
}
// ********** convenience methods **********
/**
* Set the trigger's value:<ul>
* <li><code>true</code> indicates "accept"
* <li><code>false</code> indicates "reset"
* </ul>
*/
public void setValue(boolean value) {
this.setValue(Boolean.valueOf(value));
}
/**
* Return the trigger's value:<ul>
* <li><code>true</code> indicates "accept"
* <li><code>false</code> indicates "reset"
* </ul>
* This method can only be invoked during change notification.
*/
public boolean booleanValue() {
return this.getValue().booleanValue();
}
/**
* Accept the trigger (i.e. set its value to <code>true</code>).
*/
public void accept() {
this.setValue(true);
}
/**
* Return whether the trigger has been accepted
* (i.e. its value was changed to <code>true</code>).
* This method can only be invoked during change notification.
*/
public boolean isAccepted() {
return this.booleanValue();
}
/**
* Reset the trigger (i.e. set its value to <code>false</code>).
*/
public void reset() {
this.setValue(false);
}
/**
* Return whether the trigger has been reset
* (i.e. its value was changed to <code>false</code>).
* This method can only be invoked during change notification.
*/
public boolean isReset() {
return ! this.booleanValue();
}
}
}