001    /*
002     * Copyright 2002-2005 the original author or authors.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.binding.value.support;
017    
018    import java.beans.PropertyChangeEvent;
019    import java.beans.PropertyChangeListener;
020    
021    import org.springframework.binding.value.CommitTrigger;
022    import org.springframework.binding.value.CommitTriggerListener;
023    import org.springframework.binding.value.ValueModel;
024    import org.springframework.core.style.ToStringCreator;
025    import org.springframework.util.Assert;
026    
027    /**
028     * A value model that wraps another value model; delaying or buffering changes
029     * until a commit is triggered.
030     * 
031     * TODO: more class docs...
032     * 
033     * @author Karsten Lentzsch
034     * @author Keith Donald
035     * @author Oliver Hutchison  
036     */
037    public class BufferedValueModel extends AbstractValueModel implements ValueModelWrapper {
038    
039        /**
040         * Name of the bound property <em>buffering</em>.
041         */
042        public static final String BUFFERING_PROPERTY = "buffering";
043    
044        private final ValueModel wrappedModel;
045    
046        private final PropertyChangeListener wrappedModelChangeHandler;
047    
048        private Object bufferedValue;
049    
050        private CommitTrigger commitTrigger;
051    
052        private CommitTriggerListener commitTriggerHandler;
053    
054        private boolean buffering;
055    
056        /**
057         * Constructs a <code>BufferedValueHolder</code> that wraps the given wrappedModel.  
058         * 
059         * @param wrappedModel the value model to be buffered
060         */
061        public BufferedValueModel(ValueModel wrappedModel) {
062            this(wrappedModel, null);
063        }
064    
065        /**
066         * Constructs a <code>BufferedValueHolder</code> that wraps the given wrappedModel
067         * and listens to the provided commitTrigger for commit and revert events.  
068         * 
069         * @param wrappedModel the value model to be buffered
070         * @param commitTrigger the commit trigger that triggers the commit or flush event
071         */
072        public BufferedValueModel(ValueModel wrappedModel, CommitTrigger commitTrigger) {
073            Assert.notNull(wrappedModel, "Wrapped value model can not be null.");
074            this.wrappedModel = wrappedModel;
075            this.wrappedModelChangeHandler = new WrappedModelValueChangeHandler();
076            this.wrappedModel.addValueChangeListener(wrappedModelChangeHandler);
077            this.bufferedValue = wrappedModel.getValue();
078            setCommitTrigger(commitTrigger);
079        }
080    
081        /**
082         * Returns the CommitTrigger that is used to trigger commit and flush events.
083         * 
084         * @return the CommitTrigger that is used to trigger commit and flush events
085         */
086        public final CommitTrigger getCommitTrigger() {
087            return commitTrigger;
088        }
089    
090        /**
091         * Sets the <code>CommitTrigger</code> that triggers the commit and flush events.
092         *  
093         * @param commitTrigger  the commit trigger; or null to deregister the 
094         * existing trigger. 
095         */
096        public final void setCommitTrigger(CommitTrigger commitTrigger) {
097            if (this.commitTrigger == commitTrigger) {
098                return;
099            }
100            if (this.commitTrigger != null) {
101                this.commitTrigger.removeCommitTriggerListener(commitTriggerHandler);
102                this.commitTrigger = null;
103            }
104            if (commitTrigger != null) {
105                if (this.commitTriggerHandler == null) {
106                    this.commitTriggerHandler = new CommitTriggerHandler();
107                }
108                this.commitTrigger = commitTrigger;
109                this.commitTrigger.addCommitTriggerListener(commitTriggerHandler);
110            }
111        }
112    
113        /**
114         * Returns whether this model buffers a value or not, that is, whether
115         * a value has been assigned since the last commit or flush. 
116         * 
117         * @return true if a value has been assigned since the last commit or revert
118         */
119        public boolean isBuffering() {
120            return buffering;
121        }
122    
123        /**
124         * Returns the wrappedModel value if no value has been set since the last
125         * commit or flush, and returns the buffered value otherwise.
126         * 
127         * @return the buffered value
128         */
129        public Object getValue() {
130            return bufferedValue;
131        }
132    
133        /**
134         * Sets a new buffered value and turns this BufferedValueModel into
135         * the buffering state. The buffered value is not provided to the
136         * underlying model until the trigger channel indicates a commit.
137         *
138         * @param value  the value to be buffered
139         */
140        public void setValue(Object value) {
141            if (logger.isDebugEnabled()) {
142                logger.debug("Setting buffered value to '" + value + "'");
143            }
144            final Object oldValue = getValue();
145            this.bufferedValue = value;
146            updateBuffering();
147            fireValueChange(oldValue, bufferedValue);
148        }
149    
150        /**
151         * Returns the wrappedModel, i.e. the underlying ValueModel that provides 
152         * the unbuffered value.
153         * 
154         * @return the ValueModel that provides the unbuffered value  
155         */
156        public final ValueModel getWrappedValueModel() {
157            return wrappedModel;
158        }
159        
160        /**
161         * Returns the inner most wrappedModel; i.e. the root ValueModel that provides 
162         * the unbuffered value. This is found by repeatedly unwrapping any ValueModelWrappers
163         * until we find the inner most value model.
164         * 
165         * @return the inner most ValueModel that provides the unbuffered value
166         * 
167         * @see ValueModelWrapper#getInnerMostWrappedValueModel()  
168         */
169        public final ValueModel getInnerMostWrappedValueModel() {
170            if (wrappedModel instanceof ValueModelWrapper)
171                return ((ValueModelWrapper)wrappedModel).getInnerMostWrappedValueModel();
172    
173            return wrappedModel;
174        }
175    
176        /**
177         * Called when the value held by the wrapped value model changes.
178         */
179        protected void onWrappedValueChanged() {
180            if (logger.isDebugEnabled()) {
181                logger.debug("Wrapped model value has changed; new value is '" + wrappedModel.getValue() + "'");
182            }
183            setValue(wrappedModel.getValue());
184        }
185    
186        /**
187         * Commits the value buffered by this value model back to the 
188         * wrapped value model. 
189         */
190        public void commit() {
191            if (isBuffering()) {
192                if (logger.isDebugEnabled()) {
193                    logger.debug("Committing buffered value '" + getValue() + "' to wrapped value model '" + wrappedModel
194                            + "'");
195                }
196                wrappedModel.setValueSilently(getValueToCommit(), wrappedModelChangeHandler);
197                setValue(wrappedModel.getValue()); // check if the wrapped model changed the committed value
198            }
199            else {
200                if (logger.isDebugEnabled()) {
201                    logger.debug("No buffered edit to commit; nothing to do...");
202                }
203            }
204        }
205    
206        /**
207         * Provides a hook that allows for modification of the value that is committed 
208         * to the underlying value model. 
209         */
210        protected Object getValueToCommit() {
211            return bufferedValue;
212        }
213    
214        /**
215         * Updates the value of the buffering property. Fires a property change event 
216         * if there's been a change.
217         */
218        private void updateBuffering() {
219            boolean wasBuffering = isBuffering();
220            buffering = hasValueChanged(wrappedModel.getValue(), bufferedValue);
221            firePropertyChange(BUFFERING_PROPERTY, wasBuffering, buffering);
222        }
223    
224        /**
225         * Reverts the value held by the value model back to the value held by the 
226         * wrapped value model.
227         */
228        public final void revert() {
229            if (isBuffering()) {
230                if (logger.isDebugEnabled()) {
231                    logger.debug("Reverting buffered value '" + getValue() + "' to value '" + wrappedModel.getValue() + "'");
232                }
233                setValue(wrappedModel.getValue());
234            }
235            else {
236                if (logger.isDebugEnabled()) {
237                    logger.debug("No buffered edit to commit; nothing to do...");
238                }
239            }
240        }
241    
242        public String toString() {
243            return new ToStringCreator(this).append("bufferedValue", bufferedValue).toString();
244        }
245    
246        /**
247         * Listens for changes to the wrapped value model.
248         */
249        private class WrappedModelValueChangeHandler implements PropertyChangeListener {
250            public void propertyChange(PropertyChangeEvent evt) {
251                onWrappedValueChanged();
252            }
253        }
254    
255        /**
256         * Listens for commit/revert on the commitTrigger.
257         */
258        private class CommitTriggerHandler implements CommitTriggerListener {
259            public void commit() {
260                if (logger.isDebugEnabled()) {
261                    logger.debug("Commit trigger fired commit event.");
262                }
263                BufferedValueModel.this.commit();
264            }
265    
266            public void revert() {
267                if (logger.isDebugEnabled()) {
268                    logger.debug("Commit trigger fired revert event.");
269                }
270                BufferedValueModel.this.revert();
271            }
272        }
273    
274    }