/******************************************************************************* * Copyright (c) 2007, 2009 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.PropertyChangeListener; import org.eclipse.jpt.common.utility.model.value.PropertyValueModel; import org.eclipse.jpt.common.utility.model.value.WritablePropertyValueModel; /** * A BufferedWritablePropertyValueModel is used to hold a temporary copy of the value * in another property value model (the "wrapped" value holder). The application * can modify this temporary copy, ad nauseam; but the temporary copy is only * passed through to the "wrapped" value holder when the trigger "accepts" the * buffered value. Alternatively, the application can "reset" the buffered value * to the original, "wrapped" value. *

* The trigger is another {@link PropertyValueModel} that holds a {@link Boolean} * and the application changes the trigger's value to true on "accept", false on "reset". * Typically, in a dialog:

*

* 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 bufferd 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. * * @see PropertyAspectAdapter */ public class BufferedWritablePropertyValueModel extends PropertyValueModelWrapper implements WritablePropertyValueModel { /** * We cache the value here until it is accepted and passed * through to the wrapped value holder. */ protected T bufferedValue; /** * This is set to true when we are "accepting" the buffered value * and passing it through to the wrapped value holder. This allows * us to ignore the property change event fired by the wrapped * value holder. * (We can't stop listening to the wrapped value holder, because * if we are the only listener that could "deactivate" the wrapped * value holder.) */ protected boolean accepting; /** * This is the trigger that indicates whether the buffered value * should be accepted or reset. */ protected final PropertyValueModel triggerHolder; /** This listens to the trigger holder. */ protected final PropertyChangeListener triggerChangeListener; /** * This flag indicates whether our buffered value has been assigned * a value and is possibly out of synch with the wrapped value. */ protected boolean buffering; // ********** constructors ********** /** * Construct a buffered property value model with the specified wrapped * property value model and trigger holder. */ public BufferedWritablePropertyValueModel(WritablePropertyValueModel valueHolder, PropertyValueModel triggerHolder) { super(valueHolder); if (triggerHolder == null) { throw new NullPointerException(); } this.triggerHolder = triggerHolder; this.bufferedValue = null; this.buffering = false; this.accepting = false; this.triggerChangeListener = this.buildTriggerChangeListener(); } // ********** initialization ********** protected PropertyChangeListener buildTriggerChangeListener() { return new PropertyChangeListener() { public void propertyChanged(PropertyChangeEvent event) { BufferedWritablePropertyValueModel.this.triggerChanged(event); } @Override public String toString() { return "trigger change listener"; //$NON-NLS-1$ } }; } // ********** ValueModel implementation ********** /** * If we are currently "buffering" a value, return that; * otherwise, return the wrapped value. */ public T getValue() { return this.buffering ? this.bufferedValue : this.valueHolder.getValue(); } /** * Assign the new value to our "buffered" value. * It will be pushed to the wrapped value holder * when the trigger is "accepted". */ public void setValue(T value) { if (this.buffering) { if (this.valuesAreEqual(value, this.valueHolder.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.valueHolder.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.valueHolder.getValue(); this.bufferedValue = value; this.buffering = true; this.firePropertyChanged(VALUE, old, value); } } } // ********** PropertyValueModelWrapper extensions ********** /** * extend to engage the trigger holder also */ @Override protected void engageModel() { super.engageModel(); this.triggerHolder.addPropertyChangeListener(VALUE, this.triggerChangeListener); } /** * extend to disengage the trigger holder also */ @Override protected void disengageModel() { this.triggerHolder.removePropertyChangeListener(VALUE, this.triggerChangeListener); 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 valueChanged(PropertyChangeEvent event) { if ( ! this.accepting) { this.valueChanged_(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 valueChanged_(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 } /** * The trigger changed:

*/ protected void triggerChanged(PropertyChangeEvent event) { // 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 (((Boolean) event.getNewValue()).booleanValue()) { this.accept(); } else { this.reset(); } } } protected void accept() { // set the accepting flag so we ignore any events // fired by the wrapped value holder this.accepting = true; try { this.getValueHolder().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.valueHolder.getValue()); } @Override public void toString(StringBuilder sb) { sb.append(this.getValue()); } // ********** convenience methods ********** /** * Return whether the buffered model is currently "buffering" * a value. */ public boolean isBuffering() { return this.buffering; } /** * Our constructor accepts only a {@link WritablePropertyValueModel}{@code}. */ @SuppressWarnings("unchecked") protected WritablePropertyValueModel getValueHolder() { return (WritablePropertyValueModel) this.valueHolder; } // ********** inner class ********** /** * Trigger is a special property value model that only maintains its * value (of true or false) during the change notification caused by * {@link #setValue(T)}. In other words, a Trigger * only has a valid value when it is being set. */ public static class Trigger extends SimplePropertyValueModel { // ********** constructor ********** /** * 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 null 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:
    *
  • true indicates "accept" *
  • false indicates "reset" *
*/ public void setValue(boolean value) { this.setValue(Boolean.valueOf(value)); } /** * Return the trigger's value:
    *
  • true indicates "accept" *
  • false indicates "reset" *
* 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 true). */ public void accept() { this.setValue(true); } /** * Return whether the trigger has been accepted * (i.e. its value was changed to true). * This method can only be invoked during change notification. */ public boolean isAccepted() { return this.booleanValue(); } /** * Reset the trigger (i.e. set its value to false). */ public void reset() { this.setValue(false); } /** * Return whether the trigger has been reset * (i.e. its value was changed to false). * This method can only be invoked during change notification. */ public boolean isReset() { return ! this.booleanValue(); } } }