Skip to main content
summaryrefslogtreecommitdiffstats
blob: 87d191ca6a30e5c5895ce52e81b739e6a78d0c39 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
/*******************************************************************************
 * 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 <code>BufferedWritablePropertyValueModel</code> 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.
 * <p>
 * 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:<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>
 * <p>
 * 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<T>
	extends PropertyValueModelWrapper<T>
	implements WritablePropertyValueModel<T>
{

	/**
	 * 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<Boolean> 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<T> valueHolder, PropertyValueModel<Boolean> 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:<ul>
	 * <li>If it is now true, "accept" the buffered value and push
	 * it to the wrapped value holder.
	 * <li>If it is now false, "reset" the buffered value to its original value.
	 * </ul>
	 */
	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<T>}.
	 */
	@SuppressWarnings("unchecked")
	protected WritablePropertyValueModel<T> getValueHolder() {
		return (WritablePropertyValueModel<T>) this.valueHolder;
	}


	// ********** inner class **********

	/**
	 * <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(T)}. In other words, a <code>Trigger</code>
	 * only has a valid value when it is being set.
	 */
	public static class Trigger extends SimplePropertyValueModel<Boolean> {


		// ********** 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 <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();
		}

	}

}

Back to the top