### Eclipse Workspace Patch 1.0 #P org.eclipse.jface.tests.databinding Index: src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java,v retrieving revision 1.1 diff -u -r1.1 MultiValidatorTest.java --- src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java 24 Mar 2008 22:55:55 -0000 1.1 +++ src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java 16 Jun 2008 20:08:52 -0000 @@ -7,12 +7,15 @@ * * Contributors: * Matthew Hall - initial API and implementation (bug 218269) + * Ovidio Mallo - bug 233191 ******************************************************************************/ package org.eclipse.core.tests.databinding.validation; import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.observable.IStaleListener; import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.StaleEvent; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.databinding.validation.MultiValidator; @@ -25,13 +28,13 @@ public class MultiValidatorTest extends AbstractDefaultRealmTestCase { private WritableValue dependency; - private MultiValidator validator; + private TestMultiValidator validator; private IObservableValue validationStatus; protected void setUp() throws Exception { super.setUp(); dependency = new WritableValue(null, IStatus.class); - validator = new MultiValidator() { + validator = new TestMultiValidator() { protected IStatus validate() { return (IStatus) dependency.getValue(); } @@ -58,7 +61,7 @@ public void testGetValidationStatus_ExceptionThrownYieldsErrorStatus() { final RuntimeException e = new RuntimeException("message"); - validator = new MultiValidator() { + validator = new TestMultiValidator() { protected IStatus validate() { throw e; } @@ -115,4 +118,49 @@ assertEquals(target.getValue(), validated.getValue()); assertFalse(validated.isStale()); } + + public void testEnterExitStale_ValidationStatusStaleness() { + StaleCounter staleCounter = new StaleCounter(); + validationStatus.addStaleListener(staleCounter); + + assertEquals(0, staleCounter.count); + + validator.enterStaleDelegate(); + assertEquals(1, staleCounter.count); + assertTrue(validationStatus.isStale()); + + validator.exitStaleDelegate((IStatus) validationStatus.getValue()); + assertFalse(validationStatus.isStale()); + } + + public void testEnterExitStale_ValidationStatusValue() { + validator.enterStaleDelegate(); + IStatus status = ValidationStatus.error("done"); + validator.exitStaleDelegate(status); + assertSame(status, validationStatus.getValue()); + } + + /** + * Simple extension of the MultiValidator class which is functionally + * equivalent while making some methods accessible to the unit tests. + */ + private static abstract class TestMultiValidator extends MultiValidator { + + void enterStaleDelegate() { + enterStale(); + } + + void exitStaleDelegate(IStatus status) { + exitStale(status); + } + } + + private static class StaleCounter implements IStaleListener { + + int count; + + public void handleStale(StaleEvent event) { + count++; + } + } } Index: src/org/eclipse/core/tests/internal/databinding/QueueTest.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/internal/databinding/QueueTest.java,v retrieving revision 1.1 diff -u -r1.1 QueueTest.java --- src/org/eclipse/core/tests/internal/databinding/QueueTest.java 2 Oct 2007 19:33:52 -0000 1.1 +++ src/org/eclipse/core/tests/internal/databinding/QueueTest.java 16 Jun 2008 20:08:52 -0000 @@ -64,5 +64,14 @@ assertEquals("moo", queue.dequeue()); assertTrue(queue.isEmpty()); } - + + public void testClear() { + assertTrue(queue.isEmpty()); + queue.enqueue("foo"); + assertFalse(queue.isEmpty()); + queue.clear(); + assertTrue(queue.isEmpty()); + queue.clear(); + assertTrue(queue.isEmpty()); + } } Index: src/org/eclipse/jface/tests/databinding/scenarios/PropertyScenarios.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/scenarios/PropertyScenarios.java,v retrieving revision 1.34 diff -u -r1.34 PropertyScenarios.java --- src/org/eclipse/jface/tests/databinding/scenarios/PropertyScenarios.java 21 Mar 2007 20:46:31 -0000 1.34 +++ src/org/eclipse/jface/tests/databinding/scenarios/PropertyScenarios.java 16 Jun 2008 20:08:52 -0000 @@ -203,6 +203,10 @@ String remainingChars = modelValue.substring(1); return firstChar.toUpperCase() + remainingChars.toLowerCase(); } + + public boolean isAsync() { + return false; + } }; IConverter converter2 = new IConverter() { public Object getFromType() { @@ -216,6 +220,10 @@ public Object convert(Object fromObject) { return ((String) fromObject).toUpperCase(); } + + public boolean isAsync() { + return false; + } }; getDbc().bindValue(SWTObservables.observeText(text, SWT.Modify), Index: src/org/eclipse/jface/tests/databinding/BindingTestSuite.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/jface/tests/databinding/BindingTestSuite.java,v retrieving revision 1.84 diff -u -r1.84 BindingTestSuite.java --- src/org/eclipse/jface/tests/databinding/BindingTestSuite.java 25 Apr 2008 23:16:36 -0000 1.84 +++ src/org/eclipse/jface/tests/databinding/BindingTestSuite.java 16 Jun 2008 20:08:52 -0000 @@ -61,6 +61,8 @@ import org.eclipse.core.tests.internal.databinding.BindingStatusTest; import org.eclipse.core.tests.internal.databinding.QueueTest; import org.eclipse.core.tests.internal.databinding.RandomAccessListIteratorTest; +import org.eclipse.core.tests.internal.databinding.UpdateExecutorTest; +import org.eclipse.core.tests.internal.databinding.UpdateValidationObservableValueTest; import org.eclipse.core.tests.internal.databinding.beans.BeanObservableListDecoratorTest; import org.eclipse.core.tests.internal.databinding.beans.BeanObservableSetDecoratorTest; import org.eclipse.core.tests.internal.databinding.beans.BeanObservableValueDecoratorTest; @@ -132,6 +134,7 @@ import org.eclipse.jface.tests.databinding.viewers.ObservableSetContentProviderTest; import org.eclipse.jface.tests.databinding.viewers.ObservableSetTreeContentProviderTest; import org.eclipse.jface.tests.databinding.viewers.ViewersObservablesTest; +import org.eclipse.jface.tests.databinding.wizard.WizardPageSupportTest; import org.eclipse.jface.tests.examples.databinding.mask.internal.EditMaskLexerAndTokenTest; import org.eclipse.jface.tests.examples.databinding.mask.internal.EditMaskParserTest; import org.eclipse.jface.tests.internal.databinding.swt.ButtonObservableValueTest; @@ -242,6 +245,8 @@ addTestSuite(BindingStatusTest.class); addTestSuite(RandomAccessListIteratorTest.class); addTestSuite(QueueTest.class); + addTestSuite(UpdateExecutorTest.class); + addTest(UpdateValidationObservableValueTest.suite()); // org.eclipse.core.tests.internal.databinding.conversion addTestSuite(DateConversionSupportTest.class); @@ -332,7 +337,10 @@ addTestSuite(ObservableSetContentProviderTest.class); addTestSuite(ObservableSetTreeContentProviderTest.class); addTestSuite(ViewersObservablesTest.class); - + + // org.eclipse.jface.tests.databinding.wizard + addTestSuite(WizardPageSupportTest.class); + //org.eclipse.jface.tests.example.databinding.mask.internal addTestSuite(EditMaskLexerAndTokenTest.class); addTestSuite(EditMaskParserTest.class); Index: src/org/eclipse/core/tests/databinding/UpdateValueStrategyTest.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/UpdateValueStrategyTest.java,v retrieving revision 1.2 diff -u -r1.2 UpdateValueStrategyTest.java --- src/org/eclipse/core/tests/databinding/UpdateValueStrategyTest.java 29 Apr 2007 04:12:54 -0000 1.2 +++ src/org/eclipse/core/tests/databinding/UpdateValueStrategyTest.java 16 Jun 2008 20:08:52 -0000 @@ -16,9 +16,13 @@ import java.util.Date; import org.eclipse.core.databinding.UpdateValueStrategy; +import org.eclipse.core.databinding.conversion.Converter; +import org.eclipse.core.databinding.conversion.IConverter; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.databinding.validation.IValidator; +import org.eclipse.core.databinding.validation.IValidator2; +import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.internal.databinding.validation.NumberToByteValidator; import org.eclipse.core.internal.databinding.validation.NumberToDoubleValidator; import org.eclipse.core.internal.databinding.validation.NumberToFloatValidator; @@ -33,6 +37,7 @@ import org.eclipse.core.internal.databinding.validation.StringToIntegerValidator; import org.eclipse.core.internal.databinding.validation.StringToLongValidator; import org.eclipse.core.internal.databinding.validation.StringToShortValidator; +import org.eclipse.core.runtime.IStatus; import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase; /** @@ -138,7 +143,46 @@ assertSame(validator,strategy.validator); } - + + public void testIsAsync_DefaultDelegatesToValidatorsAndConverter() throws Exception { + IValidator2 asyncValidator = new IValidator2() { + public IStatus validate(Object value) { + return ValidationStatus.ok(); + } + + public boolean isAsync() { + return true; + } + }; + + IConverter asyncConverter = new Converter(null, null) { + public Object convert(Object fromObject) { + return null; + } + + public boolean isAsync() { + return true; + } + }; + + UpdateValueStrategy strategy; + + strategy = new UpdateValueStrategy(); + assertFalse(strategy.isAsync()); + + strategy = new UpdateValueStrategy().setAfterGetValidator(asyncValidator); + assertTrue(strategy.isAsync()); + + strategy = new UpdateValueStrategy().setConverter(asyncConverter); + assertTrue(strategy.isAsync()); + + strategy = new UpdateValueStrategy().setAfterConvertValidator(asyncValidator); + assertTrue(strategy.isAsync()); + + strategy = new UpdateValueStrategy().setBeforeSetValidator(asyncValidator); + assertTrue(strategy.isAsync()); + } + private void assertDefaultValidator(Class fromType, Class toType, Class validatorType) { WritableValue source = WritableValue.withValueType(fromType); WritableValue destination = WritableValue.withValueType(toType); Index: src/org/eclipse/core/tests/databinding/ValueBindingTest.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/ValueBindingTest.java,v retrieving revision 1.2 diff -u -r1.2 ValueBindingTest.java --- src/org/eclipse/core/tests/databinding/ValueBindingTest.java 21 Apr 2007 22:35:33 -0000 1.2 +++ src/org/eclipse/core/tests/databinding/ValueBindingTest.java 16 Jun 2008 20:08:52 -0000 @@ -9,16 +9,24 @@ * Brad Reynolds - initial API and implementation * Brad Reynolds - bug 116920 * Brad Reynolds - bug 164653, 159768 + * Ovidio Mallo - bug 233191 ******************************************************************************/ package org.eclipse.core.tests.databinding; +import java.util.Random; + +import org.eclipse.core.databinding.AggregateValidationStatus; import org.eclipse.core.databinding.Binding; import org.eclipse.core.databinding.DataBindingContext; import org.eclipse.core.databinding.UpdateValueStrategy; +import org.eclipse.core.databinding.conversion.Converter; +import org.eclipse.core.databinding.conversion.IConverter; import org.eclipse.core.databinding.observable.Diffs; import org.eclipse.core.databinding.observable.value.AbstractObservableValue; import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.core.databinding.observable.value.ValueChangeEvent; import org.eclipse.core.databinding.observable.value.ValueDiff; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.databinding.validation.IValidator; @@ -27,6 +35,7 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase; +import org.eclipse.swt.widgets.Display; /** * @since 1.1 @@ -228,7 +237,127 @@ target.fireValueChange(Diffs.createValueDiff("", "")); assertEquals("update does not occur", count, strategy.afterGetCount); } - + + public void testIsAsync() throws Exception { + UpdateValueStrategy asyncStrategy = new UpdateValueStrategy() { + public boolean isAsync() { + return true; + } + }; + + assertFalse(dbc.bindValue(target, model, null, null).isAsync()); + assertTrue(dbc.bindValue(target, model, asyncStrategy, null).isAsync()); + assertTrue(dbc.bindValue(target, model, null, asyncStrategy).isAsync()); + assertTrue(dbc.bindValue(target, model, asyncStrategy, asyncStrategy) + .isAsync()); + } + + public void testAsyncExecutesInSeparateThread() throws Exception { + final Thread realmThread = Thread.currentThread(); + + IConverter asyncConverter = new Converter(null, null) { + public Object convert(Object fromObject) { + assertNotSame(Thread.currentThread(), realmThread); + return null; + } + + public boolean isAsync() { + return true; + } + }; + + UpdateValueStrategy targetToModel = new UpdateValueStrategy(); + targetToModel.setConverter(asyncConverter); + UpdateValueStrategy modelToTarget = new UpdateValueStrategy(); + modelToTarget.setConverter(asyncConverter); + + Binding binding = dbc.bindValue(target, model, targetToModel, modelToTarget); + + assertTrue(binding.isAsync()); + + binding.updateTargetToModel(); + binding.updateModelToTarget(); + } + + public void testAsyncUpdateSerialization() throws Exception { + UpdateValueStrategy targetToModel = new UpdateValueStrategy(); + targetToModel.setConverter(asyncConverter()); + + UpdateValueStrategy modelToTarget = new UpdateValueStrategy(); + modelToTarget.setConverter(asyncConverter()); + + dbc.bindValue(target, model, targetToModel, modelToTarget); + + // Write to the target. + for (int i = 0; i <= 10; i++) { + target.setValue(String.valueOf(i)); + } + awaitPendingValidations(dbc); + // Check that the updates to the model have been correctly serialized. + assertEquals("10", model.getValue()); + + // Write to the model. + for (int i = 0; i <= 10; i++) { + model.setValue(String.valueOf(i)); + } + awaitPendingValidations(dbc); + // Check that the updates to the target have been correctly serialized. + assertEquals("10", target.getValue()); + + // Alternatively write to the target and model. + for (int i = 0; i <= 10; i++) { + if (i % 2 == 0) { + target.setValue(String.valueOf(i)); + } else { + model.setValue(String.valueOf(i)); + } + } + awaitPendingValidations(dbc); + // Check that target and model end up having the correct value. + assertEquals("10", target.getValue()); + assertEquals("10", model.getValue()); + } + + public void testDisposeCancelsPendingUpdates() throws Exception { + UpdateValueStrategy targetToModel = new UpdateValueStrategy(); + targetToModel.setConverter(asyncConverter()); + + final Binding binding = dbc.bindValue(target, model, targetToModel, + null); + + model.addValueChangeListener(new IValueChangeListener() { + public void handleValueChange(ValueChangeEvent event) { + fail("No update should get to the model if we dispose the Binding."); + } + }); + + // Note that neither v1 nor v2 should make their way to the model since + // we only dispatch the events on the UI thread by calling the method + // awaitPendingValidations(...) below after having disposed the binding. + target.setValue("v1"); + binding.dispose(); + target.setValue("v2"); + + awaitPendingValidations(dbc); + } + + private void awaitPendingValidations(DataBindingContext dbc) { + AggregateValidationStatus validation = new AggregateValidationStatus( + dbc, AggregateValidationStatus.MERGED); + + while (validation.isStale()) { + // By dispatching on the Display, we get the pending runnables + // executed on the Realm belonging to the Display. + Display display = Display.getCurrent(); + while (display.readAndDispatch()) { + // just dispatch + } + if (validation.isStale()) { + display.sleep(); + } + } + } + private IValidator warningValidator() { return new IValidator() { public IStatus validate(Object value) { @@ -261,6 +390,26 @@ }; } + private static IConverter asyncConverter() { + return new Converter(String.class, String.class) { + + private final Random random = new Random(System.currentTimeMillis()); + + public Object convert(Object fromObject) { + try { + Thread.sleep(random.nextInt(100)); + } catch (InterruptedException e) { + // do nothing + } + return fromObject; + } + + public boolean isAsync() { + return true; + } + }; + } + private static class ObservableValueStub extends AbstractObservableValue { protected Object doGetValue() { // do nothing Index: src/org/eclipse/core/tests/internal/databinding/UpdateValidationObservableValueTest.java =================================================================== RCS file: src/org/eclipse/core/tests/internal/databinding/UpdateValidationObservableValueTest.java diff -N src/org/eclipse/core/tests/internal/databinding/UpdateValidationObservableValueTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/tests/internal/databinding/UpdateValidationObservableValueTest.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2007 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.tests.internal.databinding; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.eclipse.core.databinding.observable.IObservable; +import org.eclipse.core.databinding.observable.IStaleListener; +import org.eclipse.core.databinding.observable.ObservableTracker; +import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.StaleEvent; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.core.databinding.observable.value.ValueChangeEvent; +import org.eclipse.core.databinding.validation.ValidationStatus; +import org.eclipse.core.internal.databinding.UpdateValidationObservableValue; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.databinding.conformance.MutableObservableValueContractTest; +import org.eclipse.jface.databinding.conformance.delegate.AbstractObservableValueContractDelegate; +import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase; + +/** + * @since 1.2 + */ +public class UpdateValidationObservableValueTest extends + AbstractDefaultRealmTestCase { + + public static Test suite() { + TestSuite suite = new TestSuite( + UpdateValidationObservableValueTest.class.getName()); + suite.addTestSuite(UpdateValidationObservableValueTest.class); + suite.addTest(MutableObservableValueContractTest.suite(new Delegate())); + return suite; + } + + public void testStaleness() { + UpdateValidationObservableValueStub observable = new UpdateValidationObservableValueStub( + Realm.getDefault()); + + ValueChangeCounter valueChangeCounter = new ValueChangeCounter(); + observable.addValueChangeListener(valueChangeCounter); + + StaleCounter staleCounter = new StaleCounter(); + observable.addStaleListener(staleCounter); + + assertEquals(0, valueChangeCounter.count); + assertEquals(0, staleCounter.count); + assertFalse(observable.isStale()); + + observable.setStale(true); + assertEquals(0, valueChangeCounter.count); + assertEquals(1, staleCounter.count); + assertTrue(observable.isStale()); + + observable.setStale(false); + assertEquals(1, valueChangeCounter.count); + assertEquals(1, staleCounter.count); + assertFalse(observable.isStale()); + } + + /* package */static class Delegate extends + AbstractObservableValueContractDelegate { + + public IObservableValue createObservableValue(Realm realm) { + return new UpdateValidationObservableValueStub(realm); + } + + public void change(IObservable observable) { + IObservableValue observableValue = (IObservableValue) observable; + observableValue.setValue(createValue(observableValue)); + } + + public void setStale(IObservable observable, boolean stale) { + ((UpdateValidationObservableValueStub) observable).setStale(stale); + } + + public Object getValueType(IObservableValue observable) { + return IStatus.class; + } + + public Object createValue(IObservableValue observable) { + IStatus status = (IStatus) observable.getValue(); + return ValidationStatus.error(status.getMessage() + "a"); + } + } + + private static class UpdateValidationObservableValueStub extends + UpdateValidationObservableValue { + + private boolean stale = false; + + public UpdateValidationObservableValueStub(Realm realm) { + super(realm); + } + + public boolean isStale() { + ObservableTracker.getterCalled(this); + return stale; + } + + public void setStale(boolean stale) { + this.stale = stale; + updateStaleness(); + } + } + + private static class ValueChangeCounter implements IValueChangeListener { + + int count; + + public void handleValueChange(ValueChangeEvent event) { + count++; + } + } + + private static class StaleCounter implements IStaleListener { + + int count; + + public void handleStale(StaleEvent event) { + count++; + } + } +} Index: src/org/eclipse/core/tests/internal/databinding/UpdateExecutorTest.java =================================================================== RCS file: src/org/eclipse/core/tests/internal/databinding/UpdateExecutorTest.java diff -N src/org/eclipse/core/tests/internal/databinding/UpdateExecutorTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/tests/internal/databinding/UpdateExecutorTest.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,250 @@ +/******************************************************************************* + * Copyright (c) 2007 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.tests.internal.databinding; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +import org.eclipse.core.internal.databinding.UpdateExecutor; +import org.eclipse.core.internal.databinding.UpdateRunnable; + +/** + * @since 1.2 + */ +public class UpdateExecutorTest extends TestCase { + + public void testSyncExecutesInSameThread() throws Throwable { + UpdateExecutor executor = new UpdateExecutor(false, null); + + final Thread mainThread = Thread.currentThread(); + TestUpdateRunnable update = new TestUpdateRunnable() { + public void doRun() { + assertSame(mainThread, Thread.currentThread()); + notifyDone(); + } + }; + + executor.execute(update, false); + + awaitPendingUpdates(executor); + assertNotFailed(update); + } + + public void testAsyncExecutesInSeparateThread() throws Throwable { + UpdateExecutor executor = new UpdateExecutor(true, null); + + final Thread mainThread = Thread.currentThread(); + TestUpdateRunnable update = new TestUpdateRunnable() { + public void doRun() { + assertNotSame(mainThread, Thread.currentThread()); + notifyDone(); + } + }; + + executor.execute(update, false); + + awaitPendingUpdates(executor); + assertNotFailed(update); + } + + public void testHasPendingUpdates() throws Throwable { + // Test for both, synchronous and asynchronous executions. + testHasPendingUpdates(false); + testHasPendingUpdates(true); + } + + private static void testHasPendingUpdates(boolean isAsync) throws Throwable { + final UpdateExecutor executor = new UpdateExecutor(isAsync, null); + + TestUpdateRunnable update = new TestUpdateRunnable() { + public void doRun() { + assertTrue(executor.hasPendingUpdates()); + notifyDone(); + assertFalse(executor.hasPendingUpdates()); + } + }; + + assertFalse(executor.hasPendingUpdates()); + executor.execute(update, false); + + awaitPendingUpdates(executor); + assertNotFailed(update); + } + + public void testSchedulingEventCallbackInvoked() throws Throwable { + // Test for both, synchronous and asynchronous executions. + testSchedulingEventCallbackInvoked(false); + testSchedulingEventCallbackInvoked(true); + } + + private static void testSchedulingEventCallbackInvoked(boolean isAsync) + throws Throwable { + final SchedulingEventCallbackCounter callbackCounter = new SchedulingEventCallbackCounter(); + UpdateExecutor executor = new UpdateExecutor(isAsync, callbackCounter); + + assertEquals(0, callbackCounter.count); + + TestUpdateRunnable update = new TestUpdateRunnable() { + public void doRun() { + // At this point, we must already have received the scheduling + // event of having started this update. + assertEquals(1, callbackCounter.count); + notifyDone(); + // At this point, we must have received the scheduling event + // about the update having terminated. + assertEquals(2, callbackCounter.count); + } + }; + + executor.execute(update, false); + + awaitPendingUpdates(executor); + assertNotFailed(update); + } + + public void testAsyncExecuteInOrder() throws Throwable { + UpdateExecutor executor = new UpdateExecutor(true, null); + + final ArrayList updateList = new ArrayList(); + for (int i = 0; i < 10; i++) { + updateList.add(new TestUpdateRunnable() { + public void doRun() { + assertSame(updateList.remove(0), this); + notifyDone(); + } + }); + } + + // The original list is modified by the updates, so we make a copy. + ArrayList updateListCopy = new ArrayList(updateList); + for (int i = 0; i < updateListCopy.size(); i++) { + UpdateRunnable update = (UpdateRunnable) updateListCopy.get(i); + executor.execute(update, false); + } + + awaitPendingUpdates(executor); + for (int i = 0; i < updateListCopy.size(); i++) { + assertNotFailed((TestUpdateRunnable) updateListCopy.get(i)); + } + } + + public void testCancelPendingUpdates() throws Throwable { + // Test for both, canceling and not canceling pending updates. + testCancelPendingUpdates(false); + testCancelPendingUpdates(true); + } + + private void testCancelPendingUpdates(boolean doCancel) throws Throwable { + UpdateExecutor executor = new UpdateExecutor(true, null); + + // In order to reliably ensure that update1 has not already terminated + // when we request its canceling, we start the two updates while holding + // a lock which is also tried to be acquired by update1. + synchronized (this) { + TestUpdateRunnable update1 = new TestUpdateRunnable() { + public void doRun() { + synchronized (UpdateExecutorTest.this) { + // just try to acquire the lock + } + notifyDone(); + } + }; + + executor.execute(update1, false); + + assertFalse(update1.isCanceled()); + executor.execute(new TestUpdateRunnable(), doCancel); + assertEquals(doCancel, update1.isCanceled()); + } + } + + public void testTerminateCancelsPendingUpdates() throws Throwable { + UpdateExecutor executor = new UpdateExecutor(true, null); + + // In order to reliably ensure that the update has not already + // terminated when we request its canceling, we start the two updates + // while holding a lock which is also tried to be acquired by the + // update. + synchronized (this) { + TestUpdateRunnable update = new TestUpdateRunnable() { + public void doRun() { + synchronized (UpdateExecutorTest.this) { + // just try to acquire the lock + } + notifyDone(); + } + }; + + executor.execute(update, false); + + assertFalse(update.isCanceled()); + executor.terminate(); + assertTrue(update.isCanceled()); + } + } + + private static void awaitPendingUpdates(UpdateExecutor executor) { + while (executor.hasPendingUpdates()) { + try { + // avoid continuous polling + Thread.sleep(20); + } catch (InterruptedException e) { + // just go ahead + } + } + } + + private static void assertNotFailed(TestUpdateRunnable update) + throws Throwable { + if (update.throwable != null) { + throw update.throwable; + } + } + + /** + * Simple extension of an UpdateRunnable which catches any Throwable thrown + * during the execution of the {@link #run()} method and stores it in a + * class field for later querying. We do this to be able to use JUnit + * assertions from within the run method since when running an + * UpdateRunnable asynchronously, the UpdateExecutor class executes it from + * within a Job which would otherwise swallow all Throwables. + * + * TODO Maybe, this could be done more elegantly? + */ + private static class TestUpdateRunnable extends UpdateRunnable { + + public Throwable throwable = null; + + public void run() { + try { + doRun(); + } catch (Throwable t) { + throwable = t; + notifyDone(); + } + } + + public void doRun() { + // do nothing + } + } + + private static class SchedulingEventCallbackCounter implements Runnable { + + public int count = 0; + + public void run() { + count++; + } + } +} Index: src/org/eclipse/jface/tests/databinding/wizard/WizardPageSupportTest.java =================================================================== RCS file: src/org/eclipse/jface/tests/databinding/wizard/WizardPageSupportTest.java diff -N src/org/eclipse/jface/tests/databinding/wizard/WizardPageSupportTest.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/jface/tests/databinding/wizard/WizardPageSupportTest.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,138 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.jface.tests.databinding.wizard; + +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.ValidationStatusProvider; +import org.eclipse.core.databinding.observable.Diffs; +import org.eclipse.core.databinding.observable.Observables; +import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.list.IObservableList; +import org.eclipse.core.databinding.observable.value.AbstractObservableValue; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.validation.ValidationStatus; +import org.eclipse.core.internal.commands.util.Util; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.databinding.wizard.WizardPageSupport; +import org.eclipse.jface.tests.databinding.AbstractSWTTestCase; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.widgets.Composite; + +/** + * @since 1.2 + */ +public class WizardPageSupportTest extends AbstractSWTTestCase { + + public void testPageCompleteOnValidationStaleness() { + IWizardPage page = new WizardPage("Page") { + public void createControl(Composite parent) { + setControl(parent); + + ValidationObservable validation = new ValidationObservable(); + + DataBindingContext dbc = new DataBindingContext(); + dbc.addValidationStatusProvider(new ValidationProvider( + validation)); + + WizardPageSupport.create(this, dbc); + + assertTrue(isPageComplete()); + + validation.setStale(true); + assertFalse(isPageComplete()); + + validation.setStale(false); + assertTrue(isPageComplete()); + } + }; + + loadWizardPage(page); + } + + private void loadWizardPage(IWizardPage page) { + Wizard wizard = new Wizard() { + public boolean performFinish() { + return true; + } + }; + wizard.addPage(page); + + WizardDialog dialog = new WizardDialog(getShell(), wizard); + dialog.create(); + } + + private static class ValidationObservable extends AbstractObservableValue { + + private Object value = ValidationStatus.ok(); + + private boolean stale = false; + + public ValidationObservable() { + super(Realm.getDefault()); + } + + protected Object doGetValue() { + return value; + } + + protected void doSetValue(Object value) { + Object oldValue = this.value; + this.value = value; + if (!Util.equals(oldValue, value)) { + fireValueChange(Diffs.createValueDiff(oldValue, value)); + } + } + + public boolean isStale() { + return stale; + } + + public void setStale(boolean stale) { + if (this.stale != stale) { + this.stale = stale; + if (stale) { + fireStale(); + } else { + fireValueChange(Diffs.createValueDiff(value, value)); + } + } + } + + public Object getValueType() { + return IStatus.class; + } + } + + private static class ValidationProvider extends ValidationStatusProvider { + + private final IObservableValue validation; + + public ValidationProvider(IObservableValue validation) { + this.validation = validation; + } + + public IObservableValue getValidationStatus() { + return validation; + } + + public IObservableList getTargets() { + return Observables.emptyObservableList(); + } + + public IObservableList getModels() { + return Observables.emptyObservableList(); + } + } +} #P org.eclipse.jface.databinding Index: src/org/eclipse/jface/databinding/wizard/WizardPageSupport.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.databinding/src/org/eclipse/jface/databinding/wizard/WizardPageSupport.java,v retrieving revision 1.5 diff -u -r1.5 WizardPageSupport.java --- src/org/eclipse/jface/databinding/wizard/WizardPageSupport.java 15 May 2008 23:19:43 -0000 1.5 +++ src/org/eclipse/jface/databinding/wizard/WizardPageSupport.java 16 Jun 2008 20:08:54 -0000 @@ -9,7 +9,8 @@ * IBM Corporation - initial API and implementation * Boris Bokowski - bug 218269 * Matthew Hall - bug 218269 - * Ashley Cambrell - bug 199179 + * Ashley Cambrell - bug 199179 + * Ovidio Mallo - bug 233191 *******************************************************************************/ package org.eclipse.jface.databinding.wizard; @@ -21,6 +22,8 @@ import org.eclipse.core.databinding.observable.ChangeEvent; import org.eclipse.core.databinding.observable.IChangeListener; import org.eclipse.core.databinding.observable.IObservable; +import org.eclipse.core.databinding.observable.IStaleListener; +import org.eclipse.core.databinding.observable.StaleEvent; import org.eclipse.core.databinding.observable.list.IListChangeListener; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.ListChangeEvent; @@ -41,6 +44,17 @@ * given wizard page, updating the wizard page's completion state and its error * message accordingly. * + *

+ * The completion state of the wizard page will only be set to true + * if all of the following conditions are met: + *

+ *

+ * * @noextend This class is not intended to be subclassed by clients. * * @since 1.1 @@ -137,6 +151,11 @@ handleStatusChanged(); } }); + aggregateStatus.addStaleListener(new IStaleListener() { + public void handleStale(StaleEvent staleEvent) { + handleStatusChanged(); + } + }); currentStatus = (IStatus) aggregateStatus.getValue(); handleStatusChanged(); dbc.getValidationStatusProviders().addListChangeListener( @@ -188,7 +207,8 @@ } else if (currentStatus != null && currentStatus.getSeverity() != IStatus.OK) { int severity = currentStatus.getSeverity(); - wizardPage.setPageComplete((severity & IStatus.CANCEL) != 0); + wizardPage.setPageComplete(((severity & IStatus.CANCEL) == 0) + && !aggregateStatus.isStale()); int type; switch (severity) { case IStatus.OK: @@ -213,7 +233,7 @@ wizardPage.setErrorMessage(null); wizardPage.setMessage(currentStatus.getMessage(), type); } else { - wizardPage.setPageComplete(true); + wizardPage.setPageComplete(!aggregateStatus.isStale()); wizardPage.setMessage(null); wizardPage.setErrorMessage(null); } #P org.eclipse.core.databinding Index: src/org/eclipse/core/databinding/Binding.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/Binding.java,v retrieving revision 1.13 diff -u -r1.13 Binding.java --- src/org/eclipse/core/databinding/Binding.java 9 May 2008 14:13:00 -0000 1.13 +++ src/org/eclipse/core/databinding/Binding.java 16 Jun 2008 20:08:55 -0000 @@ -10,6 +10,7 @@ * Brad Reynolds - bug 159768 * Boris Bokowski - bug 218269 * Matthew Hall - bug 218269 + * Ovidio Mallo - bug 233191 *******************************************************************************/ package org.eclipse.core.databinding; @@ -110,7 +111,26 @@ * by the time this call returns. */ public abstract void validateModelToTarget(); - + + /** + * Returns whether the updates performed by this binding are executed + * asynchronously. + * + *

+ * By default, this method returns false. Subclasses will + * typically want to overwrite this method to return whether any of + * the binding's update strategies is intended to be run asynchronously. + * However, subclasses may always decide not to execute the updates + * asynchronously. + *

+ * + * @return whether the updates performed by this binding are executed + * asynchronously. + */ + public boolean isAsync() { + return false; + } + /** * Disposes of this Binding. Subclasses may extend, but must call super.dispose(). */ Index: src/org/eclipse/core/databinding/UpdateListStrategy.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/UpdateListStrategy.java,v retrieving revision 1.7 diff -u -r1.7 UpdateListStrategy.java --- src/org/eclipse/core/databinding/UpdateListStrategy.java 9 May 2008 14:13:00 -0000 1.7 +++ src/org/eclipse/core/databinding/UpdateListStrategy.java 16 Jun 2008 20:08:55 -0000 @@ -229,4 +229,37 @@ } return Status.OK_STATUS; } + + /** + * Returns whether the list update defined by this strategy is intended to + * be executed asynchronously. + * + *

+ * By default, this method returns true if and only if the + * converter associated to this strategy is intended to be executed + * asynchronously. Clients may extend without having to call the super + * implementation. + *

+ * + *

+ * Note that even if this method returns true, there is no + * guarantee as of whether the list update will indeed by executed + * asynchronously or not. + *

+ * + * @return whether the list update defined by this strategy is intended to + * be executed asynchronously. + * + * @see IConverter#isAsync() + */ + public boolean isAsync() { + return isAsyncConverter(converter); + } + + private boolean isAsyncConverter(IConverter converter) { + if (converter != null) { + return converter.isAsync(); + } + return false; + } } Index: src/org/eclipse/core/databinding/UpdateStrategy.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/UpdateStrategy.java,v retrieving revision 1.14 diff -u -r1.14 UpdateStrategy.java --- src/org/eclipse/core/databinding/UpdateStrategy.java 9 May 2008 14:13:00 -0000 1.14 +++ src/org/eclipse/core/databinding/UpdateStrategy.java 16 Jun 2008 20:08:55 -0000 @@ -68,6 +68,21 @@ private static Map converterMap; + /** + * Returns whether the update defined by this strategy is intended to be + * executed asynchronously. + * + *

+ * By default, this method returns false. + *

+ * + * @return whether the update defined by this strategy is intended to be + * executed asynchronously. + */ + public boolean isAsync() { + return false; + } + private static Class autoboxed(Class clazz) { if (clazz == Float.TYPE) return Float.class; @@ -705,6 +720,10 @@ public Object getToType() { return toType; } + + public boolean isAsync() { + return false; + } } } \ No newline at end of file Index: src/org/eclipse/core/databinding/UpdateValueStrategy.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/UpdateValueStrategy.java,v retrieving revision 1.15 diff -u -r1.15 UpdateValueStrategy.java --- src/org/eclipse/core/databinding/UpdateValueStrategy.java 9 May 2008 14:13:00 -0000 1.15 +++ src/org/eclipse/core/databinding/UpdateValueStrategy.java 16 Jun 2008 20:08:55 -0000 @@ -9,6 +9,7 @@ * IBM Corporation - initial API and implementation * Matt Carter - Character support completed (bug 197679) * Tom Schindl - bugfix for 217940 + * Ovidio Mallo - bug 233191 *******************************************************************************/ package org.eclipse.core.databinding; @@ -19,6 +20,7 @@ import org.eclipse.core.databinding.conversion.IConverter; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.validation.IValidator; +import org.eclipse.core.databinding.validation.IValidator2; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.internal.databinding.BindingMessages; import org.eclipse.core.internal.databinding.Pair; @@ -490,6 +492,50 @@ return Status.OK_STATUS; } + /** + * Returns whether the value update defined by this strategy is intended to + * be executed asynchronously. + * + *

+ * By default, this method returns true if and only if + * any of the validators or the converter associated to this strategy + * is intended to be executed asynchronously. Clients may extend without + * having to call the super implementation. + *

+ * + *

+ * Note that even if this method returns true, there is no + * guarantee as of whether the value update will indeed by executed + * asynchronously or not. + *

+ * + * @return whether the value update defined by this strategy is intended to + * be executed asynchronously. + * + * @see IValidator2#isAsync() + * @see IConverter#isAsync() + */ + public boolean isAsync() { + return isAsyncValidator(afterGetValidator) + || isAsyncConverter(converter) + || isAsyncValidator(afterConvertValidator) + || isAsyncValidator(beforeSetValidator); + } + + private boolean isAsyncValidator(IValidator validator) { + if (validator instanceof IValidator2) { + return ((IValidator2) validator).isAsync(); + } + return false; + } + + private boolean isAsyncConverter(IConverter converter) { + if (converter != null) { + return converter.isAsync(); + } + return false; + } + private static class ValidatorRegistry { private HashMap validators = new HashMap(); Index: src/org/eclipse/core/databinding/ListBinding.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ListBinding.java,v retrieving revision 1.4 diff -u -r1.4 ListBinding.java --- src/org/eclipse/core/databinding/ListBinding.java 5 Apr 2007 01:50:07 -0000 1.4 +++ src/org/eclipse/core/databinding/ListBinding.java 16 Jun 2008 20:08:55 -0000 @@ -14,17 +14,19 @@ import java.util.Collections; import org.eclipse.core.databinding.observable.Diffs; +import org.eclipse.core.databinding.observable.ObservableTracker; import org.eclipse.core.databinding.observable.list.IListChangeListener; import org.eclipse.core.databinding.observable.list.IObservableList; import org.eclipse.core.databinding.observable.list.ListChangeEvent; import org.eclipse.core.databinding.observable.list.ListDiff; import org.eclipse.core.databinding.observable.list.ListDiffEntry; import org.eclipse.core.databinding.observable.value.IObservableValue; -import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.internal.databinding.BindingStatus; +import org.eclipse.core.internal.databinding.UpdateExecutor; +import org.eclipse.core.internal.databinding.UpdateRunnable; +import org.eclipse.core.internal.databinding.UpdateValidationObservableValue; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; -import org.eclipse.core.runtime.Status; /** * @since 1.0 @@ -34,9 +36,10 @@ private UpdateListStrategy targetToModel; private UpdateListStrategy modelToTarget; - private IObservableValue validationStatusObservable; + private UpdateValidationObservableValue validationStatusObservable; private boolean updatingTarget; private boolean updatingModel; + private UpdateExecutor updateExecutor; private IListChangeListener targetChangeListener = new IListChangeListener() { public void handleListChange(ListChangeEvent event) { @@ -86,8 +89,30 @@ } protected void preInit() { - validationStatusObservable = new WritableValue(context - .getValidationRealm(), Status.OK_STATUS, IStatus.class); + updateExecutor = new UpdateExecutor(isAsync(), new Runnable() { + public void run() { + // If we are in asynchronous mode, we must update the staleness + // of the validation observable upon every scheduling event. + if (isAsync()) { + validationStatusObservable.getRealm().exec(new Runnable() { + public void run() { + validationStatusObservable.updateStaleness(); + } + }); + } + } + }); + + validationStatusObservable = new UpdateValidationObservableValue( + context.getValidationRealm()) { + public boolean isStale() { + ObservableTracker.getterCalled(this); + // If not in asynchronous mode, we never set the validation + // observable to be stale. + return ListBinding.this.isAsync() + && updateExecutor.hasPendingUpdates(); + } + }; } protected void postInit() { @@ -131,6 +156,10 @@ // nothing for now } + public boolean isAsync() { + return targetToModel.isAsync() || modelToTarget.isAsync(); + } + /* * This method may be moved to UpdateListStrategy in the future if clients * need more control over how the two lists are kept in sync. @@ -140,10 +169,33 @@ final UpdateListStrategy updateListStrategy, final boolean explicit, final boolean clearDestination) { final int policy = updateListStrategy.getUpdatePolicy(); - if (policy != UpdateListStrategy.POLICY_NEVER) { - if (policy != UpdateListStrategy.POLICY_ON_REQUEST || explicit) { + if (policy == UpdateListStrategy.POLICY_NEVER) + return; + if (policy == UpdateListStrategy.POLICY_ON_REQUEST && !explicit) + return; + + UpdateRunnable update = new UpdateRunnable() { + public void run() { + final ListDiffEntry[] diffEntries = diff.getDifferences(); + for (int i = 0; i < diffEntries.length; i++) { + ListDiffEntry entry = diffEntries[i]; + if (entry.isAddition()) { + diffEntries[i] = Diffs.createListDiffEntry(entry + .getPosition(), entry.isAddition(), + updateListStrategy.convert(entry.getElement())); + } + } + destination.getRealm().exec(new Runnable() { public void run() { + // If the update has been canceled before writing to the + // destination observable, we do not set the validation + // status, so we return outside the below try block. + if (isCanceled()) { + notifyDone(); + return; + } + if (destination == getTarget()) { updatingTarget = true; } else { @@ -155,29 +207,27 @@ if (clearDestination) { destination.clear(); } - ListDiffEntry[] diffEntries = diff.getDifferences(); for (int i = 0; i < diffEntries.length; i++) { ListDiffEntry listDiffEntry = diffEntries[i]; if (listDiffEntry.isAddition()) { IStatus setterStatus = updateListStrategy - .doAdd( - destination, - updateListStrategy - .convert(listDiffEntry - .getElement()), + .doAdd(destination, listDiffEntry + .getElement(), listDiffEntry.getPosition()); mergeStatus(multiStatus, setterStatus); // TODO - at this point, the two lists - // will be out of sync if an error occurred... + // will be out of sync if an error + // occurred... } else { IStatus setterStatus = updateListStrategy .doRemove(destination, listDiffEntry.getPosition()); - + mergeStatus(multiStatus, setterStatus); // TODO - at this point, the two lists - // will be out of sync if an error occurred... + // will be out of sync if an error + // occurred... } } } finally { @@ -188,11 +238,15 @@ } else { updatingModel = false; } + + notifyDone(); } } }); } - } + }; + + updateExecutor.execute(update, false); } /** @@ -209,6 +263,8 @@ } public void dispose() { + updateExecutor.terminate(); + if (targetChangeListener != null) { ((IObservableList)getTarget()).removeListChangeListener(targetChangeListener); targetChangeListener = null; Index: src/org/eclipse/core/databinding/ValueBinding.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/ValueBinding.java,v retrieving revision 1.8 diff -u -r1.8 ValueBinding.java --- src/org/eclipse/core/databinding/ValueBinding.java 24 Mar 2008 19:13:39 -0000 1.8 +++ src/org/eclipse/core/databinding/ValueBinding.java 16 Jun 2008 20:08:55 -0000 @@ -8,16 +8,20 @@ * Contributors: * IBM Corporation - initial API and implementation * Matthew Hall - bug 220700 + * Ovidio Mallo - bug 233191 *******************************************************************************/ package org.eclipse.core.databinding; +import org.eclipse.core.databinding.observable.ObservableTracker; import org.eclipse.core.databinding.observable.value.IObservableValue; import org.eclipse.core.databinding.observable.value.IValueChangeListener; import org.eclipse.core.databinding.observable.value.ValueChangeEvent; -import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.databinding.util.Policy; import org.eclipse.core.internal.databinding.BindingStatus; +import org.eclipse.core.internal.databinding.UpdateExecutor; +import org.eclipse.core.internal.databinding.UpdateRunnable; +import org.eclipse.core.internal.databinding.UpdateValidationObservableValue; import org.eclipse.core.internal.databinding.Util; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; @@ -30,9 +34,10 @@ class ValueBinding extends Binding { private final UpdateValueStrategy targetToModel; private final UpdateValueStrategy modelToTarget; - private WritableValue validationStatusObservable; + private UpdateValidationObservableValue validationStatusObservable; private IObservableValue target; private IObservableValue model; + private UpdateExecutor updateExecutor; private boolean updatingTarget; private boolean updatingModel; @@ -78,8 +83,30 @@ } protected void preInit() { - validationStatusObservable = new WritableValue(context - .getValidationRealm(), Status.OK_STATUS, IStatus.class); + updateExecutor = new UpdateExecutor(isAsync(), new Runnable() { + public void run() { + // If we are in asynchronous mode, we must update the staleness + // of the validation observable upon every scheduling event. + if (isAsync()) { + validationStatusObservable.getRealm().exec(new Runnable() { + public void run() { + validationStatusObservable.updateStaleness(); + } + }); + } + } + }); + + validationStatusObservable = new UpdateValidationObservableValue( + context.getValidationRealm()) { + public boolean isStale() { + ObservableTracker.getterCalled(this); + // If not in asynchronous mode, we never set the validation + // observable to be stale. + return ValueBinding.this.isAsync() + && updateExecutor.hasPendingUpdates(); + } + }; } protected void postInit() { @@ -104,6 +131,35 @@ } /** + * Returns whether the updates performed by this binding are executed + * asynchronously. + * + *

+ * This method returns true if and only if any of the + * binding's update strategies is intended to be run asynchronously. + * Thereby, the individual updates of this binding are always guaranteed to + * be executed in the correct order, thus ensuring that the target and model + * values will never get out of sync due to the asynchronous nature of the + * updates. + *

+ * + *

+ * In case the updates are executed asynchronously, the staleness state of + * the validation status observable can be used to determine whether there + * are any pending updates. This allows for awaiting the termination of + * those updates, if desired, by tracking the observable's staleness state. + *

+ * + * @return whether the updates performed by this binding are executed + * asynchronously. + * + * @see UpdateValueStrategy#isAsync() + */ + public boolean isAsync() { + return targetToModel.isAsync() || modelToTarget.isAsync(); + } + + /** * Incorporates the provided newStats into the * multieStatus. * @@ -128,7 +184,6 @@ final IObservableValue destination, final UpdateValueStrategy updateValueStrategy, final boolean explicit, final boolean validateOnly) { - final int policy = updateValueStrategy.getUpdatePolicy(); if (policy == UpdateValueStrategy.POLICY_NEVER) return; @@ -137,78 +192,99 @@ source.getRealm().exec(new Runnable() { public void run() { - boolean destinationRealmReached = false; - final MultiStatus multiStatus = BindingStatus.ok(); - try { - // Get value - Object value = source.getValue(); - - // Validate after get - IStatus status = updateValueStrategy - .validateAfterGet(value); - if (!mergeStatus(multiStatus, status)) - return; - - // Convert value - final Object convertedValue = updateValueStrategy - .convert(value); - - // Validate after convert - status = updateValueStrategy - .validateAfterConvert(convertedValue); - if (!mergeStatus(multiStatus, status)) - return; - if (policy == UpdateValueStrategy.POLICY_CONVERT - && !explicit) - return; - - // Validate before set - status = updateValueStrategy - .validateBeforeSet(convertedValue); - if (!mergeStatus(multiStatus, status)) - return; - if (validateOnly) - return; - - // Set value - destinationRealmReached = true; - destination.getRealm().exec(new Runnable() { - public void run() { - if (destination == target) { - updatingTarget = true; - } else { - updatingModel = true; - } - try { - IStatus setterStatus = updateValueStrategy - .doSet(destination, convertedValue); - - mergeStatus(multiStatus, setterStatus); - } finally { - if (destination == target) { - updatingTarget = false; - } else { - updatingModel = false; + // Get value + final Object value = source.getValue(); + + UpdateRunnable update = new UpdateRunnable() { + public void run() { + boolean destinationRealmReached = false; + final MultiStatus multiStatus = BindingStatus.ok(); + try { + // Validate after get + IStatus status = updateValueStrategy + .validateAfterGet(value); + if (!mergeStatus(multiStatus, status)) + return; + + // Convert value + final Object convertedValue = updateValueStrategy + .convert(value); + + // Validate after convert + status = updateValueStrategy + .validateAfterConvert(convertedValue); + if (!mergeStatus(multiStatus, status)) + return; + if (policy == UpdateValueStrategy.POLICY_CONVERT + && !explicit) + return; + + // Validate before set + status = updateValueStrategy + .validateBeforeSet(convertedValue); + if (!mergeStatus(multiStatus, status)) + return; + if (validateOnly) + return; + + // Set value + destinationRealmReached = true; + destination.getRealm().exec(new Runnable() { + public void run() { + // If the update has been canceled before + // writing to the destination observable, we + // do not set the validation status, so we + // return outside the below try block. + if (isCanceled()) { + notifyDone(); + return; + } + + if (destination == target) { + updatingTarget = true; + } else { + updatingModel = true; + } + try { + IStatus setterStatus = updateValueStrategy + .doSet(destination, + convertedValue); + + mergeStatus(multiStatus, setterStatus); + } finally { + if (destination == target) { + updatingTarget = false; + } else { + updatingModel = false; + } + setValidationStatus(multiStatus); + notifyDone(); + } + } + }); + } catch (Exception ex) { + // This check is necessary as in 3.2.2 Status + // doesn't accept a null message (bug 177264). + String message = (ex.getMessage() != null) ? ex + .getMessage() : ""; //$NON-NLS-1$ + + mergeStatus(multiStatus, new Status(IStatus.ERROR, + Policy.JFACE_DATABINDING, IStatus.ERROR, + message, ex)); + } finally { + if (!destinationRealmReached) { + // Set the validation status unless the update + // has been canceled in the meanwhile. + if (!isCanceled()) { + setValidationStatus(multiStatus); } - setValidationStatus(multiStatus); + notifyDone(); } } - }); - } catch (Exception ex) { - // This check is necessary as in 3.2.2 Status - // doesn't accept a null message (bug 177264). - String message = (ex.getMessage() != null) ? ex - .getMessage() : ""; //$NON-NLS-1$ - - mergeStatus(multiStatus, new Status(IStatus.ERROR, - Policy.JFACE_DATABINDING, IStatus.ERROR, message, - ex)); - } finally { - if (!destinationRealmReached) { - setValidationStatus(multiStatus); } + }; - } + updateExecutor.execute(update, !explicit); } }); } @@ -230,6 +306,8 @@ } public void dispose() { + updateExecutor.terminate(); + if (targetChangeListener != null) { target.removeValueChangeListener(targetChangeListener); targetChangeListener = null; Index: src/org/eclipse/core/internal/databinding/conversion/ObjectToStringConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/ObjectToStringConverter.java,v retrieving revision 1.1 diff -u -r1.1 ObjectToStringConverter.java --- src/org/eclipse/core/internal/databinding/conversion/ObjectToStringConverter.java 16 Mar 2007 21:19:39 -0000 1.1 +++ src/org/eclipse/core/internal/databinding/conversion/ObjectToStringConverter.java 16 Jun 2008 20:08:56 -0000 @@ -53,4 +53,7 @@ return String.class; } + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/internal/databinding/conversion/StringToBooleanPrimitiveConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/StringToBooleanPrimitiveConverter.java,v retrieving revision 1.3 diff -u -r1.3 StringToBooleanPrimitiveConverter.java --- src/org/eclipse/core/internal/databinding/conversion/StringToBooleanPrimitiveConverter.java 16 Apr 2008 02:51:52 -0000 1.3 +++ src/org/eclipse/core/internal/databinding/conversion/StringToBooleanPrimitiveConverter.java 16 Jun 2008 20:08:56 -0000 @@ -85,4 +85,7 @@ return Boolean.TYPE; } + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/internal/databinding/conversion/StringToDateConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/StringToDateConverter.java,v retrieving revision 1.1 diff -u -r1.1 StringToDateConverter.java --- src/org/eclipse/core/internal/databinding/conversion/StringToDateConverter.java 16 Mar 2007 21:19:38 -0000 1.1 +++ src/org/eclipse/core/internal/databinding/conversion/StringToDateConverter.java 16 Jun 2008 20:08:56 -0000 @@ -32,5 +32,9 @@ public Object getToType() { return Date.class; - } + } + + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/internal/databinding/conversion/DateToStringConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/DateToStringConverter.java,v retrieving revision 1.1 diff -u -r1.1 DateToStringConverter.java --- src/org/eclipse/core/internal/databinding/conversion/DateToStringConverter.java 16 Mar 2007 21:19:38 -0000 1.1 +++ src/org/eclipse/core/internal/databinding/conversion/DateToStringConverter.java 16 Jun 2008 20:08:56 -0000 @@ -35,5 +35,9 @@ public Object getToType() { return String.class; - } + } + + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/internal/databinding/conversion/IdentityConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/IdentityConverter.java,v retrieving revision 1.2 diff -u -r1.2 IdentityConverter.java --- src/org/eclipse/core/internal/databinding/conversion/IdentityConverter.java 7 Nov 2007 03:19:45 -0000 1.2 +++ src/org/eclipse/core/internal/databinding/conversion/IdentityConverter.java 16 Jun 2008 20:08:56 -0000 @@ -107,4 +107,7 @@ return toType; } + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/internal/databinding/conversion/StringToCharacterConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/conversion/StringToCharacterConverter.java,v retrieving revision 1.2 diff -u -r1.2 StringToCharacterConverter.java --- src/org/eclipse/core/internal/databinding/conversion/StringToCharacterConverter.java 7 Nov 2007 03:19:45 -0000 1.2 +++ src/org/eclipse/core/internal/databinding/conversion/StringToCharacterConverter.java 16 Jun 2008 20:08:56 -0000 @@ -70,6 +70,10 @@ return primitiveTarget ? Character.TYPE : Character.class; } + public boolean isAsync() { + return false; + } + /** * @param primitive * @return converter Index: src/org/eclipse/core/internal/databinding/Queue.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/Queue.java,v retrieving revision 1.3 diff -u -r1.3 Queue.java --- src/org/eclipse/core/internal/databinding/Queue.java 9 May 2008 14:13:00 -0000 1.3 +++ src/org/eclipse/core/internal/databinding/Queue.java 16 Jun 2008 20:08:56 -0000 @@ -72,4 +72,12 @@ public boolean isEmpty() { return first == null; } + + /** + * Removes all elements from this queue. + */ + public void clear() { + first = null; + last = null; + } } \ No newline at end of file Index: META-INF/MANIFEST.MF =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/META-INF/MANIFEST.MF,v retrieving revision 1.14 diff -u -r1.14 MANIFEST.MF --- META-INF/MANIFEST.MF 11 Apr 2008 22:18:08 -0000 1.14 +++ META-INF/MANIFEST.MF 16 Jun 2008 20:08:55 -0000 @@ -22,7 +22,8 @@ org.eclipse.core.internal.databinding.observable.masterdetail;x-friends:="org.eclipse.jface.tests.databinding", org.eclipse.core.internal.databinding.observable.tree;x-friends:="org.eclipse.jface.databinding,org.eclipse.jface.tests.databinding", org.eclipse.core.internal.databinding.validation;x-friends:="org.eclipse.jface.tests.databinding" -Require-Bundle: org.eclipse.equinox.common;bundle-version="[3.2.0,4.0.0)" +Require-Bundle: org.eclipse.equinox.common;bundle-version="[3.2.0,4.0.0)", + org.eclipse.core.jobs Import-Package-Comment: see http://wiki.eclipse.org/ Import-Package: com.ibm.icu.text, org.osgi.framework;version="[1.4.0,2.0.0)";resolution:=optional, Index: src/org/eclipse/core/databinding/conversion/Converter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/conversion/Converter.java,v retrieving revision 1.3 diff -u -r1.3 Converter.java --- src/org/eclipse/core/databinding/conversion/Converter.java 22 May 2007 19:22:18 -0000 1.3 +++ src/org/eclipse/core/databinding/conversion/Converter.java 16 Jun 2008 20:08:55 -0000 @@ -40,4 +40,10 @@ return toType; } + /** + * By default, this method returns false. + */ + public boolean isAsync() { + return false; + } } Index: src/org/eclipse/core/databinding/conversion/IConverter.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/conversion/IConverter.java,v retrieving revision 1.8 diff -u -r1.8 IConverter.java --- src/org/eclipse/core/databinding/conversion/IConverter.java 9 May 2008 14:13:00 -0000 1.8 +++ src/org/eclipse/core/databinding/conversion/IConverter.java 16 Jun 2008 20:08:55 -0000 @@ -50,4 +50,19 @@ * @return the converted object, of type {@link #getToType()} */ public Object convert(Object fromObject); + + /** + * Returns whether the {@link #convert(Object)} method of this converter is + * intended to be executed asynchronously. + * + *

+ * Note that even if this method returns true, there is no + * guarantee as of whether the conversion will indeed by executed + * asynchronously or not. + *

+ * + * @return whether the {@link #convert(Object)} method of this converter is + * intended to be executed asynchronously. + */ + public boolean isAsync(); } Index: src/org/eclipse/core/databinding/validation/MultiValidator.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/databinding/validation/MultiValidator.java,v retrieving revision 1.1 diff -u -r1.1 MultiValidator.java --- src/org/eclipse/core/databinding/validation/MultiValidator.java 24 Mar 2008 22:55:58 -0000 1.1 +++ src/org/eclipse/core/databinding/validation/MultiValidator.java 16 Jun 2008 20:08:56 -0000 @@ -8,6 +8,7 @@ * Contributors: * Matthew Hall - initial API and implementation (bug 218269) * Boris Bokowski - bug 218269 + * Ovidio Mallo - bug 233191 ******************************************************************************/ package org.eclipse.core.databinding.validation; @@ -17,6 +18,7 @@ import org.eclipse.core.databinding.ValidationStatusProvider; import org.eclipse.core.databinding.observable.ChangeEvent; +import org.eclipse.core.databinding.observable.Diffs; import org.eclipse.core.databinding.observable.IChangeListener; import org.eclipse.core.databinding.observable.IObservable; import org.eclipse.core.databinding.observable.ObservableTracker; @@ -30,6 +32,7 @@ import org.eclipse.core.databinding.observable.map.IObservableMap; import org.eclipse.core.databinding.observable.set.IObservableSet; import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.ValueDiff; import org.eclipse.core.databinding.observable.value.WritableValue; import org.eclipse.core.internal.databinding.observable.ValidatedObservableList; import org.eclipse.core.internal.databinding.observable.ValidatedObservableMap; @@ -115,11 +118,12 @@ */ public abstract class MultiValidator extends ValidationStatusProvider { private Realm realm; - private IObservableValue validationStatus; + private ValidationStatusObservableValue validationStatus; private IObservableValue unmodifiableValidationStatus; private WritableList targets; private IObservableList unmodifiableTargets; private IObservableList models; + private boolean stale = false; IListChangeListener targetsListener = new IListChangeListener() { public void handleListChange(ListChangeEvent event) { @@ -160,8 +164,7 @@ Assert.isNotNull(realm, "Realm cannot be null"); //$NON-NLS-1$ this.realm = realm; - validationStatus = new WritableValue(realm, ValidationStatus.ok(), - IStatus.class); + validationStatus = new ValidationStatusObservableValue(realm); targets = new WritableList(realm, new ArrayList(), IObservable.class); targets.addListChangeListener(targetsListener); @@ -182,6 +185,13 @@ * validation status of this MultiValidator. The returned observable is in * the same realm as this MultiValidator. * + *

+ * In case the validation of this MultiValidator is performed + * asynchronously, the staleness state of the validation status observable + * can be used to determine whether there are any pending validations. This + * allows for awaiting the termination of those validations, if desired, by + * tracking the observable's staleness state. + * * @return an {@link IObservableValue} whose value is always the current * validation status of this MultiValidator. */ @@ -232,6 +242,68 @@ protected abstract IStatus validate(); /** + * Notifies the MultiValidator that the current validation status is being + * computed asynchronously and is not available yet. + * + *

+ * Subclasses should invoke this method whenever the {@link #validate() + * validation} is performed asynchronously. Calling this method guarantees + * that the {@link #getValidationStatus() validation status observable} is + * correctly set to be stale. + * + *

+ * Typically, this method should be called from within the + * {@link #validate()} method when the asynchronous validation is started + * while a reasonable validation status must always be returned to be used + * while the pending validation has not terminated. Once the validation is + * available, it can be passed to the MultiValidator by calling the + * {@link #exitStale(IStatus)} method. + * + *

+ * Note: This method must be called from within the MultiValidator's realm. + * + * @see #exitStale(IStatus) + * @see IObservableValue#isStale() + */ + protected final void enterStale() { + stale = true; + validationStatus.fireStale(); + } + + /** + * Notifies the MultiValidator that an asynchronous validation has + * terminated which resulted in the given validation status. + * + *

+ * This method sets the given status as the current validation + * status and sets the {@link #getValidationStatus() validation status + * observable} to not be stale anymore. + * + *

+ * Note: This method must be called from within the MultiValidator's realm. + * + * @param status + * the resulting status of the terminated validation. + * + * @see #enterStale() + * @see IObservableValue#isStale() + */ + protected final void exitStale(IStatus status) { + stale = false; + Object oldStatus = validationStatus.getValue(); + validationStatus.setValue(status); + + // In order to signal that the validation status observable is not stale + // anymore, we must always fire a value change event, even if the + // validation status has not changed. In the latter case, we must do so + // explicitly. + if (oldStatus.equals(status)) { + validationStatus.fireValueChange(Diffs.createValueDiff(oldStatus, + status)); + } + } + + /** * Returns a wrapper {@link IObservableValue} which stays in sync with the * given target observable only when the validation status is valid. * Statuses of {@link IStatus#OK OK}, {@link IStatus#INFO INFO} or @@ -366,4 +438,25 @@ super.dispose(); } + private class ValidationStatusObservableValue extends WritableValue { + + public ValidationStatusObservableValue(Realm realm) { + super(realm, ValidationStatus.ok(), IStatus.class); + } + + public boolean isStale() { + ObservableTracker.getterCalled(this); + return stale; + } + + protected void fireStale() { + // Make the method accessible to the MultiValidator class. + super.fireStale(); + } + + protected void fireValueChange(ValueDiff diff) { + // Make the method accessible to the MultiValidator class. + super.fireValueChange(diff); + } + } } Index: src/org/eclipse/core/internal/databinding/observable/UnmodifiableObservableValue.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.core.databinding/src/org/eclipse/core/internal/databinding/observable/UnmodifiableObservableValue.java,v retrieving revision 1.2 diff -u -r1.2 UnmodifiableObservableValue.java --- src/org/eclipse/core/internal/databinding/observable/UnmodifiableObservableValue.java 22 Feb 2008 05:33:48 -0000 1.2 +++ src/org/eclipse/core/internal/databinding/observable/UnmodifiableObservableValue.java 16 Jun 2008 20:08:56 -0000 @@ -55,4 +55,8 @@ public Object getValueType() { return wrappedValue.getValueType(); } + + public boolean isStale() { + return wrappedValue.isStale(); + } } Index: src/org/eclipse/core/internal/databinding/UpdateExecutor.java =================================================================== RCS file: src/org/eclipse/core/internal/databinding/UpdateExecutor.java diff -N src/org/eclipse/core/internal/databinding/UpdateExecutor.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/internal/databinding/UpdateExecutor.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,234 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.internal.databinding; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; + +/** + * Simple class to manage the execution of {@link UpdateRunnable}s intended to + * be used for performing the updates of a binding. + * + *

+ * This class is thread safe and supports the synchronous as well as + * asynchronous execution of a set of UpdateRunnables where consecutive updates + * are guaranteed to be executed in the correct order. In addition, this class + * allows for tracking the set of pending updates which can eventually be + * canceled upon scheduling a new update which makes previous updates obsolete. + *

+ * + * @see #execute(UpdateRunnable, boolean) + * @see #hasPendingUpdates() + */ +public class UpdateExecutor { + + private final boolean isAsync; + + private final Runnable schedulingEventCallback; + + private final Set pendingUpdates = new HashSet(); + + private final UpdateJob updateJob; + + /** + * Creates a new UpdateExecutor which will run the individual updates either + * synchronously or asynchronously. + * + * @param isAsync + * whether the UpdateRunnables passed to this UpdateExecutor + * should be executed asynchronously. + * @param schedulingEventCallback + * a callback Runnable which will be executed whenever some + * scheduling event in the execution of the updates happened. The + * code in the runnable can then use this classes API to retrieve + * the relevant information. May be null. + */ + public UpdateExecutor(boolean isAsync, Runnable schedulingEventCallback) { + this.isAsync = isAsync; + this.schedulingEventCallback = schedulingEventCallback; + + // Only instantiate the UpdateJob if we are in asynchronous mode. + if (isAsync) { + this.updateJob = new UpdateJob(); + this.updateJob.setSystem(true); + } else { + this.updateJob = null; + } + } + + /** + * Executes the given {@link UpdateRunnable update} and eventually tries to + * cancel previous, still pending updates, if desired. + * + * @param update + * the new update to execute. + * @param cancelPendingUpdates + * whether previous, still pending updates, should be requested + * to be {@link UpdateRunnable#cancel() canceled} before + * executing the new update. + * + * @see UpdateRunnable#run() + * @see UpdateRunnable#cancel() + */ + public void execute(UpdateRunnable update, boolean cancelPendingUpdates) { + // If requested, we try to cancel previous updates. + if (cancelPendingUpdates) { + cancelPendingUpdates(); + } + + // The executor must be set on the UpdateRunnable in order to get + // notified of the update's termination. + update.setExecutor(this); + + // Add the new update to the set of pending updates. + synchronized (pendingUpdates) { + pendingUpdates.add(update); + } + + // Right before starting the update, we notify about the new update. + if (schedulingEventCallback != null) { + schedulingEventCallback.run(); + } + + if (isAsync) { + synchronized (updateJob.updateQueue) { + // Whenever we are adding a new update to an empty queue, we + // must re-schedule the UpdateJob. Note that it is OK to do this + // before actually adding the update to the queue since we are + // already holding the lock on the queue at this point. + if (updateJob.updateQueue.isEmpty()) { + updateJob.schedule(); + } + updateJob.updateQueue.enqueue(update); + } + } else { + // In synchronous mode, we simply execute the run method. + update.run(); + } + } + + /** + * Indicates that the given update was running and has now terminated. + * + * @param update + * the update which has terminated. + * + * @see #hasPendingUpdates() + * @see UpdateRunnable#notifyDone() + */ + /* package */void endUpdate(UpdateRunnable update) { + synchronized (pendingUpdates) { + pendingUpdates.remove(update); + } + + if (schedulingEventCallback != null) { + schedulingEventCallback.run(); + } + } + + /** + * Returns whether there are any pending updates to be executed on behalf of + * this UpdateExecutor. + * + *

+ * Note that an update is defined to be pending as soon as it has been + * passed to this UpdateExecutor for + * {@link #execute(UpdateRunnable, boolean) execution}, regardless of + * whether the update is already running or not. + *

+ * + * @return whether there are any pending updates to be executed on behalf of + * this UpdateExecutor. + */ + public boolean hasPendingUpdates() { + synchronized (pendingUpdates) { + return !pendingUpdates.isEmpty(); + } + } + + /** + * Requests all the pending updates to be canceled. + * + *

+ * This method guarantees that by the end of its execution, all the pending + * updates have been requested to be {@link UpdateRunnable#cancel() + * canceled} and that updates which have not been started yet (if in + * asynchronous mode) will never be run. + *

+ * + * @see UpdateRunnable#cancel() + */ + private void cancelPendingUpdates() { + synchronized (pendingUpdates) { + // Cancel all the pending updates. + for (Iterator iter = pendingUpdates.iterator(); iter.hasNext();) { + UpdateRunnable update = (UpdateRunnable) iter.next(); + update.cancel(); + } + pendingUpdates.clear(); + } + + if (isAsync) { + // If in asynchronous mode, we must also clear the queue of updates + // waiting to be run. + synchronized (updateJob.updateQueue) { + updateJob.updateQueue.clear(); + } + } + } + + /** + * Terminates the UpdateExecutor by canceling any eventual pending update. + * + *

+ * Note that once this method has been called, no further updates should be + * run on behalf of this UpdateExecutor. + *

+ */ + public void terminate() { + cancelPendingUpdates(); + + if (schedulingEventCallback != null) { + schedulingEventCallback.run(); + } + } + + private class UpdateJob extends Job { + + private final Queue updateQueue = new Queue(); + + public UpdateJob() { + super("Update Job"); //$NON-NLS-1$ + } + + protected IStatus run(IProgressMonitor monitor) { + while (true) { + UpdateRunnable update; + synchronized (updateQueue) { + // As soon as we get out of work, we return in order to + // release the Thread on whose behalf the job is running. + if (updateQueue.isEmpty()) { + return Status.OK_STATUS; + } + update = (UpdateRunnable) updateQueue.dequeue(); + } + update.run(); + } + } + } +} Index: src/org/eclipse/core/databinding/validation/IValidator2.java =================================================================== RCS file: src/org/eclipse/core/databinding/validation/IValidator2.java diff -N src/org/eclipse/core/databinding/validation/IValidator2.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/databinding/validation/IValidator2.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.databinding.validation; + +/** + * Extension of the {@link IValidator} interface which adds API for expressing + * the intend of a validator to be executed asynchronously. + */ +public interface IValidator2 extends IValidator { + + /** + * Returns whether the {@link #validate(Object)} method of this validator is + * intended to be executed asynchronously. + * + *

+ * Note that even if this method returns true, there is no + * guarantee as of whether the validation will indeed by executed + * asynchronously or not. + *

+ * + * @return whether the {@link #validate(Object)} method of this validator is + * intended to be executed asynchronously. + */ + public boolean isAsync(); +} Index: src/org/eclipse/core/internal/databinding/UpdateValidationObservableValue.java =================================================================== RCS file: src/org/eclipse/core/internal/databinding/UpdateValidationObservableValue.java diff -N src/org/eclipse/core/internal/databinding/UpdateValidationObservableValue.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/internal/databinding/UpdateValidationObservableValue.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.internal.databinding; + +import org.eclipse.core.databinding.observable.Diffs; +import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.value.AbstractObservableValue; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +/** + * Simple {@link IObservableValue} of type {@link IStatus} intended to be used + * for the validation resulting from the update process of a binding. + * + *

+ * This observable provides some convenience API to signal a possible change in + * its staleness state. Whenever the staleness of the validation observable + * changes (or may have changed), {@link #updateStaleness()} should be called + * which either fires a stale event in case the method {@link #isStale()} + * returns true, or else, a value change event is fired in order to + * signal that the observable is not stale anymore. Subclasses must implement + * the {@link #isStale()} method. + *

+ */ +public abstract class UpdateValidationObservableValue extends + AbstractObservableValue { + + private Object value = Status.OK_STATUS; + + /** + * Creates a new observable with the initial value + * Status.OK_STATUS in the given realm. + * + * @param realm + * the observable's realm. + */ + public UpdateValidationObservableValue(Realm realm) { + super(realm); + } + + protected Object doGetValue() { + return value; + } + + protected void doSetValue(Object value) { + Object oldValue = this.value; + this.value = value; + if (!Util.equals(oldValue, value)) { + fireValueChange(Diffs.createValueDiff(oldValue, value)); + } + } + + /** + * Updates the staleness of this validation by either firing a stale event + * in case the method {@link #isStale()} returns true, or else, + * by firing a value change event in order to signal that the observable is + * not stale anymore. + */ + public void updateStaleness() { + if (isStale()) { + fireStale(); + } else { + // This synthetic value change event is merely used to signal that + // the observable is not stale anymore. + fireValueChange(Diffs.createValueDiff(getValue(), getValue())); + } + } + + public abstract boolean isStale(); + + public Object getValueType() { + return IStatus.class; + } +} Index: src/org/eclipse/core/internal/databinding/UpdateRunnable.java =================================================================== RCS file: src/org/eclipse/core/internal/databinding/UpdateRunnable.java diff -N src/org/eclipse/core/internal/databinding/UpdateRunnable.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/core/internal/databinding/UpdateRunnable.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,92 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.core.internal.databinding; + +/** + * Simple implementation of a Runnable which can be used to have a + * binding update executed by an {@link UpdateExecutor}. + * + *

+ * This thread safe class adds methods to support a collaborative + * {@link #cancel() canceling} of a running update. In addition, in order to + * allow for an UpdateExecutor to keep track of the updates + * currently running on its behalf, every UpdateRunnable is + * responsible for {@link #notifyDone() notifying} the executor about its + * completion. + *

+ * + * @see #cancel() + * @see #notifyDone() + */ +public abstract class UpdateRunnable implements Runnable { + + private UpdateExecutor executor; + + private volatile boolean canceled = false; + + /** + * Sets the {@link UpdateExecutor} on whose behalf this update is executed. + * + * @param executor + * the UpdateExecutor on whose behalf this update is + * executed. + */ + /* package */final synchronized void setExecutor(UpdateExecutor executor) { + this.executor = executor; + } + + /** + * Signals that this update has completed. + * + *

+ * Calling this method is typically the responsibility of the running update + * itself and is used to allow for the {@link UpdateExecutor executor} of + * this update to keep track of the pending updates. + *

+ * + *

+ * Note that if an update terminates asynchronously, this method should only + * be called as soon as the update really terminates and not already when + * reaching the end of the {@link #run()} method. + *

+ * + * @see #setExecutor(UpdateExecutor) + * @see UpdateExecutor#endUpdate(UpdateRunnable) + */ + protected final synchronized void notifyDone() { + if (executor != null) { + executor.endUpdate(this); + } + } + + /** + * Returns whether this update has been requested to be canceled. + * + * @return whether this update has been requested to be canceled. + * + * @see #cancel() + */ + public final boolean isCanceled() { + return canceled; + } + + /** + * Requests this update to be canceled while it is up to the running update + * to decide if and when the update can be canceled before its natural + * completion. + * + * @see #isCanceled() + */ + public final void cancel() { + this.canceled = true; + } +} #P org.eclipse.jface.examples.databinding Index: src/org/eclipse/jface/examples/databinding/nestedselection/TestMasterDetail.java =================================================================== RCS file: /cvsroot/eclipse/org.eclipse.jface.examples.databinding/src/org/eclipse/jface/examples/databinding/nestedselection/TestMasterDetail.java,v retrieving revision 1.18 diff -u -r1.18 TestMasterDetail.java --- src/org/eclipse/jface/examples/databinding/nestedselection/TestMasterDetail.java 16 Mar 2007 21:19:35 -0000 1.18 +++ src/org/eclipse/jface/examples/databinding/nestedselection/TestMasterDetail.java 16 Jun 2008 20:09:00 -0000 @@ -252,6 +252,10 @@ public Object getToType() { return String.class; } + + public boolean isAsync() { + return false; + } }; IValidator vowelValidator = new IValidator() { public IStatus validate(Object value) { Index: src/org/eclipse/jface/examples/databinding/snippets/Snippet022AsyncUpdate.java =================================================================== RCS file: src/org/eclipse/jface/examples/databinding/snippets/Snippet022AsyncUpdate.java diff -N src/org/eclipse/jface/examples/databinding/snippets/Snippet022AsyncUpdate.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/jface/examples/databinding/snippets/Snippet022AsyncUpdate.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,320 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.jface.examples.databinding.snippets; + +import java.util.Random; + +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.UpdateListStrategy; +import org.eclipse.core.databinding.UpdateValueStrategy; +import org.eclipse.core.databinding.conversion.Converter; +import org.eclipse.core.databinding.conversion.IConverter; +import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.list.IObservableList; +import org.eclipse.core.databinding.observable.list.WritableList; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.WritableValue; +import org.eclipse.jface.databinding.swt.SWTObservables; +import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.ListViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Spinner; +import org.eclipse.swt.widgets.Text; + +/** + * Snippet that demonstrates the usage of asynchronous bindings by defining an + * asynchronous converter which introduces some artificial delay during + * conversion. The example allows to manually change the target or model of a + * ValueBinding/ListBinding and to see how the counterpart gets updated + * asynchronously. In addition, automatic updates of the target and/or model can + * be triggered. + */ +public class Snippet022AsyncUpdate { + + private int updateDelay = 1000; + + private boolean randomizeUpdateDelay = true; + + private DataBindingContext dbc; + + private WritableValue valueTarget; + + private WritableValue valueModel; + + private WritableList listTarget; + + private WritableList listModel; + + public void createControl(Composite parent) { + dbc = new DataBindingContext(); + + parent.setLayout(GridLayoutFactory.fillDefaults().margins(5, 5) + .spacing(15, 10).create()); + + createSettingsGroup(parent); + createValueBindingGroup(parent); + createListBindingGroup(parent); + createAutomaticUpdatesGroup(parent); + } + + private void createSettingsGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + group.setText("Settings"); + group.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).margins( + 5, 7).spacing(15, 10).create()); + GridDataFactory.fillDefaults().grab(true, false).applyTo(group); + + new Label(group, SWT.NONE).setText("Update delay"); + final Spinner updateDelaySpinner = new Spinner(group, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo( + updateDelaySpinner); + updateDelaySpinner.setMinimum(1); + updateDelaySpinner.setMaximum(Integer.MAX_VALUE); + updateDelaySpinner.setSelection(updateDelay); + updateDelaySpinner.addModifyListener(new ModifyListener() { + public void modifyText(ModifyEvent e) { + updateDelay = updateDelaySpinner.getSelection(); + } + }); + + final Button randomizeUpdateDelayButton = new Button(group, SWT.CHECK); + randomizeUpdateDelayButton.setText("Randomize update delay"); + GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo( + randomizeUpdateDelayButton); + randomizeUpdateDelayButton.setSelection(randomizeUpdateDelay); + randomizeUpdateDelayButton.addSelectionListener(new SelectionAdapter() { + public void widgetSelected(SelectionEvent e) { + randomizeUpdateDelay = randomizeUpdateDelayButton + .getSelection(); + } + }); + } + + private void createValueBindingGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + group.setText("Value Binding"); + group.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).margins( + 5, 7).spacing(15, 10).create()); + GridDataFactory.fillDefaults().grab(true, false).applyTo(group); + + valueTarget = WritableValue.withValueType(String.class); + createValueComponent(group, "Target", valueTarget); + + valueModel = WritableValue.withValueType(String.class); + createValueComponent(group, "Model", valueModel); + + dbc.bindValue(valueTarget, valueModel, new UpdateValueStrategy() + .setConverter(asyncConverter()), new UpdateValueStrategy() + .setConverter(asyncConverter())); + } + + private void createValueComponent(Composite parent, String title, + IObservableValue observableValue) { + new Label(parent, SWT.NONE).setText(title); + Text text = new Text(parent, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo(text); + + dbc.bindValue(SWTObservables.observeText(text, SWT.Modify), + observableValue, null, null); + } + + private void createListBindingGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + group.setText("List Binding"); + group.setLayout(GridLayoutFactory.fillDefaults().numColumns(2).margins( + 5, 7).spacing(15, 10).create()); + GridDataFactory.fillDefaults().grab(true, true).applyTo(group); + + listTarget = WritableList.withElementType(String.class); + createListComponent(group, "Target", listTarget); + + listModel = WritableList.withElementType(String.class); + createListComponent(group, "Model", listModel); + + dbc.bindList(listTarget, listModel, new UpdateListStrategy() + .setConverter(asyncConverter()), new UpdateListStrategy() + .setConverter(asyncConverter())); + } + + private void createListComponent(Composite parent, String title, + final IObservableList observableList) { + new Label(parent, SWT.NONE).setText(title); + final Text targetText = new Text(parent, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo(targetText); + targetText.addKeyListener(new KeyAdapter() { + public void keyPressed(KeyEvent e) { + if (e.keyCode == SWT.CR) { + observableList.add(targetText.getText()); + targetText.selectAll(); + } + } + }); + new Label(parent, SWT.NONE); + final ListViewer targetList = new ListViewer(parent, SWT.BORDER + | SWT.MULTI | SWT.V_SCROLL); + GridDataFactory.fillDefaults().minSize(SWT.DEFAULT, 150).grab(true, + true).applyTo(targetList.getList()); + targetList.setContentProvider(new ObservableListContentProvider()); + targetList.setLabelProvider(new LabelProvider()); + targetList.setInput(observableList); + targetList.getList().addKeyListener(new KeyAdapter() { + public void keyPressed(KeyEvent e) { + if (e.keyCode == SWT.DEL) { + IStructuredSelection selection = (IStructuredSelection) targetList + .getSelection(); + observableList.removeAll(selection.toList()); + } + } + }); + } + + private void createAutomaticUpdatesGroup(Composite parent) { + Group group = new Group(parent, SWT.NONE); + group.setText("Automatic Updates"); + group.setLayout(GridLayoutFactory.fillDefaults().numColumns(3) + .equalWidth(true).margins(5, 5).create()); + GridDataFactory.fillDefaults().grab(true, false).applyTo(group); + + Button targetButton = new Button(group, SWT.PUSH); + targetButton.setText("Target"); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, + false).applyTo(targetButton); + targetButton.addSelectionListener(new SelectionAdapter() { + public void widgetSelected(SelectionEvent e) { + valueModel.setValue(""); + listTarget.clear(); + for (int i = 0; i < 10; i++) { + String value = String.valueOf(i); + valueTarget.setValue(value); + listTarget.add(value); + + Display.getCurrent().update(); + sleep(100); + } + } + }); + + Button modelButton = new Button(group, SWT.PUSH); + modelButton.setText("Model"); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, + false).applyTo(modelButton); + modelButton.addSelectionListener(new SelectionAdapter() { + public void widgetSelected(SelectionEvent e) { + valueTarget.setValue(""); + listModel.clear(); + for (int i = 0; i < 10; i++) { + String value = String.valueOf(i); + valueModel.setValue(value); + listModel.add(value); + + Display.getCurrent().update(); + sleep(100); + } + } + }); + + Button twowayButton = new Button(group, SWT.PUSH); + twowayButton.setText("Twoway"); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).grab(true, + false).applyTo(twowayButton); + twowayButton.addSelectionListener(new SelectionAdapter() { + public void widgetSelected(SelectionEvent e) { + valueTarget.setValue(""); + valueModel.setValue(""); + listTarget.clear(); + listModel.clear(); + for (int i = 0; i < 10; i++) { + String value = String.valueOf(i); + if (i % 2 == 0) { + valueTarget.setValue(value); + listTarget.add(value); + } else { + valueModel.setValue(value); + listModel.add(value); + } + + Display.getCurrent().update(); + sleep(100); + } + } + }); + } + + private IConverter asyncConverter() { + return new Converter(null, null) { + private final Random random = new Random(System.currentTimeMillis()); + + public Object convert(Object fromObject) { + if (randomizeUpdateDelay) { + sleep(random.nextInt(updateDelay)); + } else { + sleep(updateDelay); + } + + return fromObject; + } + + public boolean isAsync() { + return true; + } + }; + } + + private static void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e1) { + // go ahead + } + } + + public static void main(String[] args) { + Display display = new Display(); + + Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() { + public void run() { + Display display = Display.getCurrent(); + final Shell shell = new Shell(display); + shell.setText("Asynchronous Bindings"); + new Snippet022AsyncUpdate().createControl(shell); + + shell.setMinimumSize(shell.computeSize(350, SWT.DEFAULT)); + shell.pack(); + shell.open(); + + // The SWT event loop + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + }); + } +} Index: src/org/eclipse/jface/examples/databinding/snippets/Snippet023AsyncUpdateWizard.java =================================================================== RCS file: src/org/eclipse/jface/examples/databinding/snippets/Snippet023AsyncUpdateWizard.java diff -N src/org/eclipse/jface/examples/databinding/snippets/Snippet023AsyncUpdateWizard.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/jface/examples/databinding/snippets/Snippet023AsyncUpdateWizard.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,225 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation (bug 233191) + ******************************************************************************/ + +package org.eclipse.jface.examples.databinding.snippets; + +import java.util.Random; + +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.UpdateValueStrategy; +import org.eclipse.core.databinding.observable.Realm; +import org.eclipse.core.databinding.observable.value.WritableValue; +import org.eclipse.core.databinding.validation.IValidator2; +import org.eclipse.core.databinding.validation.MultiValidator; +import org.eclipse.core.databinding.validation.ValidationStatus; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.databinding.swt.SWTObservables; +import org.eclipse.jface.databinding.wizard.WizardPageSupport; +import org.eclipse.jface.examples.databinding.decoration.ControlDecorator; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.util.Util; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.jface.wizard.WizardDialog; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Snippet that demonstrates the usage of asynchronous binding as well as cross + * field validations in the context of a wizard page. The example mainly tries + * to illustrate how the WizardPageSupport class tracks the staleness state of + * all the ValidationStatusProviders of the given data binding context, thus + * waiting for pending validations before allowing the user to flip to the next + * wizard page. + */ +public class Snippet023AsyncUpdateWizard { + + private static class AsyncBindingWizardPage extends WizardPage { + + public AsyncBindingWizardPage() { + super("Asynchronous Bindings", "Asynchronous Bindings", + ImageDescriptor.createFromImage(new Image(Display + .getCurrent(), 16, 16))); + } + + public void createControl(Composite parent) { + Composite container = new Composite(parent, SWT.NONE); + container.setLayout(GridLayoutFactory.fillDefaults().numColumns(2) + .margins(5, 5).spacing(15, 10).create()); + GridDataFactory.fillDefaults().grab(true, false).applyTo(container); + + final DataBindingContext dbc = new DataBindingContext(); + + // We use a control decorator to better illustrate when validations + // are pending on the individual bindings. + ControlDecorator controlDecorator = new ControlDecorator(); + controlDecorator.addDbc(dbc); + + WizardPageSupport.create(this, dbc); + + new Label(container, SWT.NONE).setText("Value 1"); + Text v1Text = new Text(container, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo(v1Text); + + final WritableValue value1 = new WritableValue(); + dbc.bindValue(SWTObservables.observeText(v1Text, SWT.Modify), + value1, asyncStrategy(), null); + + new Label(container, SWT.NONE).setText("Value 2"); + Text v2Text = new Text(container, SWT.BORDER); + GridDataFactory.fillDefaults().grab(true, false).applyTo(v2Text); + + final WritableValue value2 = new WritableValue(); + dbc.bindValue(SWTObservables.observeText(v2Text, SWT.Modify), + value2, asyncStrategy(), null); + + // Define the cross field validator. + MultiValidator multiValidator = new MultiValidator() { + private Job validationJob; + + protected IStatus validate() { + // Cancel any previous job when re-evaluating the + // validation. + if (validationJob != null) { + validationJob.cancel(); + } + + // The observables cannot be accessed from within a + // different thread so we must extract their containing + // values at this point. + final Object v1 = value1.getValue(); + final Object v2 = value2.getValue(); + validationJob = new Job("Equality validation") { + protected IStatus run(final IProgressMonitor monitor) { + // Do the actual validation. + final IStatus validation; + if (!Util.equals(v1, v2)) { + validation = ValidationStatus + .error("The two values must be equal."); + } else { + validation = ValidationStatus.ok(); + } + + // Take it easy :-). + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // go ahead + } + + // Notify the MultiValidator about the completed + // validation. This must be done from within the + // MultiValidator's realm (here the validation + // realm). + dbc.getValidationRealm().exec(new Runnable() { + public void run() { + // Check if this validation has been + // canceled in the meanwhile. + if (!monitor.isCanceled()) { + exitStale(validation); + } + } + }); + + return Status.OK_STATUS; + } + }; + validationJob.setSystem(true); + validationJob.schedule(); + + // Notify the MultiValidator about the validation being + // performed asynchronously. + enterStale(); + + // Return a temporary validation to be shown to the user + // while the actual validation has not completed. + return ValidationStatus + .info("Cross field validation is pending..."); + } + + public void dispose() { + if (validationJob != null) { + validationJob.cancel(); + } + + super.dispose(); + } + }; + dbc.addValidationStatusProvider(multiValidator); + + setControl(container); + } + } + + private static UpdateValueStrategy asyncStrategy() { + IValidator2 asyncValidator = new IValidator2() { + private final Random random = new Random(System.currentTimeMillis()); + + public IStatus validate(Object value) { + try { + Thread.sleep(random.nextInt(1000)); + } catch (InterruptedException e) { + // go ahead + } + + String input = (String) value; + if (input != null) { + if (input.length() > 15) { + return ValidationStatus.error("Input is too long."); + } + } + + return ValidationStatus.ok(); + } + + public boolean isAsync() { + return true; + } + }; + + return new UpdateValueStrategy().setBeforeSetValidator(asyncValidator); + } + + public static void main(String[] args) { + Display display = new Display(); + + Realm.runWithDefault(SWTObservables.getRealm(display), new Runnable() { + public void run() { + Display display = Display.getCurrent(); + final Shell shell = new Shell(display); + shell.setText("Asynchronous Bindings"); + + Wizard wizard = new Wizard() { + public boolean performFinish() { + return true; + } + }; + wizard.setWindowTitle("Asynchronous Bindings"); + wizard.addPage(new AsyncBindingWizardPage()); + + WizardDialog dialog = new WizardDialog(shell, wizard); + dialog.open(); + + display.dispose(); + } + }); + } +} Index: src/org/eclipse/jface/examples/databinding/decoration/ControlDecorator.java =================================================================== RCS file: src/org/eclipse/jface/examples/databinding/decoration/ControlDecorator.java diff -N src/org/eclipse/jface/examples/databinding/decoration/ControlDecorator.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/org/eclipse/jface/examples/databinding/decoration/ControlDecorator.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,153 @@ +/******************************************************************************* + * Copyright (c) 2008 Ovidio Mallo and others. + * 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: + * Ovidio Mallo - initial API and implementation + ******************************************************************************/ + +package org.eclipse.jface.examples.databinding.decoration; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.eclipse.core.databinding.Binding; +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.observable.IStaleListener; +import org.eclipse.core.databinding.observable.StaleEvent; +import org.eclipse.core.databinding.observable.list.IListChangeListener; +import org.eclipse.core.databinding.observable.list.IObservableList; +import org.eclipse.core.databinding.observable.list.ListChangeEvent; +import org.eclipse.core.databinding.observable.list.ListDiff; +import org.eclipse.core.databinding.observable.list.ListDiffEntry; +import org.eclipse.core.databinding.observable.value.IObservableValue; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.core.databinding.observable.value.ValueChangeEvent; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.databinding.swt.ISWTObservable; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Control; + +/** + * Simple control decorator which visualizes the validation status of a binding + * on a control assuming the binding's target observable is of type + * {@link ISWTObservable} and {@link ISWTObservable#getWidget()} returns a + * {@link Control} instance to be decorated. In addition, the staleness state of + * a binding's validation status observable is also visualized on the control. + */ +public class ControlDecorator { + + private static final int DECORATION_POSITION = SWT.LEFT | SWT.TOP; + + private final Map bindingToDecorationMap = new HashMap(); + + private IListChangeListener bindingsListener = new IListChangeListener() { + public void handleListChange(ListChangeEvent event) { + ListDiff diff = event.diff; + ListDiffEntry[] differences = diff.getDifferences(); + for (int i = 0; i < differences.length; i++) { + ListDiffEntry listDiffEntry = differences[i]; + Binding binding = (Binding) listDiffEntry.getElement(); + if (listDiffEntry.isAddition()) { + addBinding(binding); + } else { + removeBinding(binding); + } + } + } + }; + + public void addDbc(DataBindingContext dbc) { + IObservableList bindings = dbc.getBindings(); + for (Iterator iterator = bindings.iterator(); iterator.hasNext();) { + Binding binding = (Binding) iterator.next(); + addBinding(binding); + } + bindings.addListChangeListener(bindingsListener); + } + + private void addBinding(final Binding binding) { + if (binding.getTarget() instanceof ISWTObservable) { + ISWTObservable target = (ISWTObservable) binding.getTarget(); + if (target.getWidget() instanceof Control) { + Control control = (Control) target.getWidget(); + final ControlDecoration decoration = new ControlDecoration( + control, DECORATION_POSITION); + + binding.getValidationStatus().addValueChangeListener( + new IValueChangeListener() { + public void handleValueChange(ValueChangeEvent event) { + updateDecoration(decoration, binding); + } + }); + + binding.getValidationStatus().addStaleListener( + new IStaleListener() { + public void handleStale(StaleEvent staleEvent) { + updateDecoration(decoration, binding); + } + }); + + bindingToDecorationMap.put(binding, decoration); + updateDecoration(decoration, binding); + } + } + } + + private void removeBinding(Binding binding) { + ControlDecoration decoration = (ControlDecoration) bindingToDecorationMap + .get(binding); + if (decoration != null) { + decoration.hide(); + decoration.setDescriptionText(null); + + bindingToDecorationMap.remove(binding); + } + } + + private void updateDecoration(ControlDecoration decoration, Binding binding) { + IObservableValue validation = binding.getValidationStatus(); + if (validation.isStale()) { + showPendingUpdate(decoration); + return; + } + + IStatus validationStatus = (IStatus) validation.getValue(); + if (validationStatus.isOK()) { + decoration.hide(); + } else { + decoration.show(); + decoration.setImage(getStatusImage(validationStatus)); + decoration.setDescriptionText(validationStatus.getMessage()); + } + } + + private void showPendingUpdate(final ControlDecoration decoration) { + decoration.show(); + decoration.setImage(getImage(FieldDecorationRegistry.DEC_INFORMATION)); + decoration.setDescriptionText("Pending update..."); + } + + private Image getImage(String key) { + return FieldDecorationRegistry.getDefault().getFieldDecoration(key) + .getImage(); + } + + private Image getStatusImage(IStatus status) { + if (status.matches(IStatus.ERROR)) { + return getImage(FieldDecorationRegistry.DEC_ERROR); + } else if (status.matches(IStatus.WARNING)) { + return getImage(FieldDecorationRegistry.DEC_WARNING); + } else if (status.matches(IStatus.INFO)) { + return getImage(FieldDecorationRegistry.DEC_INFORMATION); + } + return null; + } +}