/** * Copyright (C) 2015 Valkyrie RCP * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.valkyriercp.binding.value.support; import org.springframework.core.style.ToStringCreator; import org.springframework.util.Assert; import org.valkyriercp.binding.value.CommitTrigger; import org.valkyriercp.binding.value.CommitTriggerListener; import org.valkyriercp.binding.value.ValueModel; import org.valkyriercp.binding.value.ValueModelWrapper; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; /** * A value model that wraps another value model; delaying or buffering changes * until a commit is triggered. * * TODO: more class docs... * * @author Karsten Lentzsch * @author Keith Donald * @author Oliver Hutchison */ public class BufferedValueModel extends AbstractValueModel implements ValueModelWrapper { /** * Name of the bound property <em>buffering</em>. */ public static final String BUFFERING_PROPERTY = "buffering"; private final ValueModel wrappedModel; private PropertyChangeListener wrappedModelChangeHandler; private Object bufferedValue; private CommitTrigger commitTrigger; private CommitTriggerListener commitTriggerHandler; private boolean buffering; /** * Constructs a <code>BufferedValueHolder</code> that wraps the given wrappedModel. * * @param wrappedModel the value model to be buffered */ public BufferedValueModel(ValueModel wrappedModel) { this(wrappedModel, null); } /** * Constructs a <code>BufferedValueHolder</code> that wraps the given wrappedModel * and listens to the provided commitTrigger for commit and revert events. * * @param wrappedModel the value model to be buffered * @param commitTrigger the commit trigger that triggers the commit or flush event */ public BufferedValueModel(ValueModel wrappedModel, CommitTrigger commitTrigger) { Assert.notNull(wrappedModel, "Wrapped value model can not be null."); this.wrappedModel = wrappedModel; this.bufferedValue = wrappedModel.getValue(); setCommitTrigger(commitTrigger); this.wrappedModelChangeHandler = new WrappedModelValueChangeHandler(); this.wrappedModel.addValueChangeListener(wrappedModelChangeHandler); } /** * Returns the CommitTrigger that is used to trigger commit and flush events. * * @return the CommitTrigger that is used to trigger commit and flush events */ public final CommitTrigger getCommitTrigger() { return commitTrigger; } /** * Sets the <code>CommitTrigger</code> that triggers the commit and flush events. * * @param commitTrigger the commit trigger; or null to deregister the * existing trigger. */ public final void setCommitTrigger(CommitTrigger commitTrigger) { if (this.commitTrigger == commitTrigger) { return; } if (this.commitTrigger != null) { this.commitTrigger.removeCommitTriggerListener(commitTriggerHandler); this.commitTrigger = null; } if (commitTrigger != null) { if (this.commitTriggerHandler == null) { this.commitTriggerHandler = new CommitTriggerHandler(); } this.commitTrigger = commitTrigger; this.commitTrigger.addCommitTriggerListener(commitTriggerHandler); } } /** * 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 revert */ public boolean isBuffering() { return buffering; } /** * Returns the wrappedModel value if no value has been set since the last * commit or flush, and returns the buffered value otherwise. * * @return the buffered value */ public Object getValue() { return bufferedValue; } /** * 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. * * @param value the value to be buffered */ public void setValue(Object value) { if (logger.isDebugEnabled()) { logger.debug("Setting buffered value to '" + value + "'"); } final Object oldValue = getValue(); this.bufferedValue = value; updateBuffering(); fireValueChange(oldValue, bufferedValue); } /** * Returns the wrappedModel, i.e. the underlying ValueModel that provides * the unbuffered value. * * @return the ValueModel that provides the unbuffered value */ public final ValueModel getWrappedValueModel() { return wrappedModel; } /** * Returns the inner most wrappedModel; i.e. the root ValueModel that provides * the unbuffered value. This is found by repeatedly unwrapping any ValueModelWrappers * until we find the inner most value model. * * @return the inner most ValueModel that provides the unbuffered value * * @see ValueModelWrapper#getInnerMostWrappedValueModel() */ public final ValueModel getInnerMostWrappedValueModel() { if (wrappedModel instanceof ValueModelWrapper) return ((ValueModelWrapper)wrappedModel).getInnerMostWrappedValueModel(); return wrappedModel; } /** * Called when the value held by the wrapped value model changes. */ protected void onWrappedValueChanged() { if (logger.isDebugEnabled()) { logger.debug("Wrapped model value has changed; new value is '" + wrappedModel.getValue() + "'"); } setValue(wrappedModel.getValue()); } /** * Commits the value buffered by this value model back to the * wrapped value model. */ public void commit() { if (isBuffering()) { if (logger.isDebugEnabled()) { logger.debug("Committing buffered value '" + getValue() + "' to wrapped value model '" + wrappedModel + "'"); } wrappedModel.setValueSilently(getValueToCommit(), wrappedModelChangeHandler); setValue(wrappedModel.getValue()); // check if the wrapped model changed the committed value } else { if (logger.isDebugEnabled()) { logger.debug("No buffered edit to commit; nothing to do..."); } } } /** * Provides a hook that allows for modification of the value that is committed * to the underlying value model. */ protected Object getValueToCommit() { return bufferedValue; } /** * Updates the value of the buffering property. Fires a property change event * if there's been a change. */ private void updateBuffering() { boolean wasBuffering = isBuffering(); buffering = hasValueChanged(wrappedModel.getValue(), bufferedValue); firePropertyChange(BUFFERING_PROPERTY, wasBuffering, buffering); } /** * Reverts the value held by the value model back to the value held by the * wrapped value model. */ public final void revert() { if (isBuffering()) { if (logger.isDebugEnabled()) { logger.debug("Reverting buffered value '" + getValue() + "' to value '" + wrappedModel.getValue() + "'"); } setValue(wrappedModel.getValue()); } else { if (logger.isDebugEnabled()) { logger.debug("No buffered edit to commit; nothing to do..."); } } } public String toString() { return new ToStringCreator(this).append("bufferedValue", bufferedValue).toString(); } /** * Listens for changes to the wrapped value model. */ private class WrappedModelValueChangeHandler implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { onWrappedValueChanged(); } } /** * Listens for commit/revert on the commitTrigger. */ private class CommitTriggerHandler implements CommitTriggerListener { public void commit() { if (logger.isDebugEnabled()) { logger.debug("Commit trigger fired commit event."); } BufferedValueModel.this.commit(); } public void revert() { if (logger.isDebugEnabled()) { logger.debug("Commit trigger fired revert event."); } BufferedValueModel.this.revert(); } } }