Released patch for Bug 238909 [DataBinding] Correctly set the staleness state of the validation status observable of the MultiValidator
diff --git a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/validation/MultiValidator.java b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/validation/MultiValidator.java
index 385d549..3c459e1 100644
--- a/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/validation/MultiValidator.java
+++ b/bundles/org.eclipse.core.databinding/src/org/eclipse/core/databinding/validation/MultiValidator.java
@@ -9,21 +9,26 @@
  *     Matthew Hall - initial API and implementation (bug 218269)
  *     Boris Bokowski - bug 218269
  *     Matthew Hall - bug 237884, 240590
+ *     Ovidio Mallo - bug 238909
  ******************************************************************************/
 
 package org.eclipse.core.databinding.validation;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Iterator;
 import java.util.List;
 
 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.IStaleListener;
 import org.eclipse.core.databinding.observable.ObservableTracker;
 import org.eclipse.core.databinding.observable.Observables;
 import org.eclipse.core.databinding.observable.Realm;
+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;
@@ -31,8 +36,9 @@
 import org.eclipse.core.databinding.observable.list.WritableList;
 import org.eclipse.core.databinding.observable.map.IObservableMap;
 import org.eclipse.core.databinding.observable.set.IObservableSet;
+import org.eclipse.core.databinding.observable.value.AbstractObservableValue;
 import org.eclipse.core.databinding.observable.value.IObservableValue;
-import org.eclipse.core.databinding.observable.value.WritableValue;
+import org.eclipse.core.internal.databinding.Util;
 import org.eclipse.core.internal.databinding.observable.ValidatedObservableList;
 import org.eclipse.core.internal.databinding.observable.ValidatedObservableMap;
 import org.eclipse.core.internal.databinding.observable.ValidatedObservableSet;
@@ -117,7 +123,7 @@
  */
 public abstract class MultiValidator extends ValidationStatusProvider {
 	private Realm realm;
-	private IObservableValue validationStatus;
+	private ValidationStatusObservableValue validationStatus;
 	private IObservableValue unmodifiableValidationStatus;
 	private WritableList targets;
 	private IObservableList unmodifiableTargets;
@@ -127,23 +133,31 @@
 		public void handleListChange(ListChangeEvent event) {
 			event.diff.accept(new ListDiffVisitor() {
 				public void handleAdd(int index, Object element) {
-					((IObservable) element)
-							.addChangeListener(dependencyListener);
+					IObservable dependency = (IObservable) element;
+					dependency.addChangeListener(dependencyListener);
+					dependency.addStaleListener(dependencyListener);
 				}
 
 				public void handleRemove(int index, Object element) {
-					((IObservable) element)
-							.removeChangeListener(dependencyListener);
+					IObservable dependency = (IObservable) element;
+					dependency.removeChangeListener(dependencyListener);
+					dependency.removeStaleListener(dependencyListener);
 				}
 			});
 		}
 	};
 
-	private IChangeListener dependencyListener = new IChangeListener() {
+	private class DependencyListener implements IChangeListener, IStaleListener {
 		public void handleChange(ChangeEvent event) {
 			revalidate();
 		}
-	};
+
+		public void handleStale(StaleEvent staleEvent) {
+			validationStatus.makeStale();
+		}
+	}
+
+	private DependencyListener dependencyListener = new DependencyListener();
 
 	/**
 	 * Constructs a MultiValidator on the default realm.
@@ -162,8 +176,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);
@@ -197,21 +210,26 @@
 	}
 
 	private void revalidate() {
+		class ValidationRunnable implements Runnable {
+			IStatus validationResult;
+
+			public void run() {
+				try {
+					validationResult = validate();
+					if (validationResult == null)
+						validationResult = ValidationStatus.ok();
+				} catch (RuntimeException e) {
+					// Usually an NPE as dependencies are init'ed
+					validationResult = ValidationStatus
+							.error(e.getMessage(), e);
+				}
+			}
+		}
+
+		ValidationRunnable validationRunnable = new ValidationRunnable();
 		final IObservable[] dependencies = ObservableTracker.runAndMonitor(
-				new Runnable() {
-					public void run() {
-						try {
-							IStatus status = validate();
-							if (status == null)
-								status = ValidationStatus.ok();
-							setStatus(status);
-						} catch (RuntimeException e) {
-							// Usually an NPE as dependencies are
-							// init'ed
-							setStatus(ValidationStatus.error(e.getMessage(), e));
-						}
-					}
-				}, null, null);
+				validationRunnable, null, null);
+
 		ObservableTracker.runAndIgnore(new Runnable() {
 			public void run() {
 				List newTargets = new ArrayList(Arrays.asList(dependencies));
@@ -228,14 +246,9 @@
 				targets.addAll(newTargets);
 			}
 		});
-	}
 
-	private void setStatus(final IStatus status) {
-		ObservableTracker.runAndIgnore(new Runnable() {
-			public void run() {
-				validationStatus.setValue(status);
-			}
-		});
+		// Once the dependencies are up-to-date, we set the new status.
+		validationStatus.setValue(validationRunnable.validationResult);
 	}
 
 	/**
@@ -401,4 +414,60 @@
 		super.dispose();
 	}
 
+	private class ValidationStatusObservableValue extends
+			AbstractObservableValue {
+		private Object value = ValidationStatus.ok();
+
+		private boolean stale = false;
+
+		public ValidationStatusObservableValue(Realm realm) {
+			super(realm);
+		}
+
+		protected Object doGetValue() {
+			return value;
+		}
+
+		protected void doSetValue(Object value) {
+			boolean oldStale = stale;
+
+			// Update the staleness state by checking whether any of the current
+			// dependencies is stale.
+			stale = false;
+			for (Iterator iter = targets.iterator(); iter.hasNext();) {
+				IObservable dependency = (IObservable) iter.next();
+				if (dependency.isStale()) {
+					stale = true;
+					break;
+				}
+			}
+
+			Object oldValue = this.value;
+			this.value = value;
+
+			// If either becoming non-stale or setting a new value, we must fire
+			// a value change event.
+			if ((oldStale && !stale) || !Util.equals(oldValue, value)) {
+				fireValueChange(Diffs.createValueDiff(oldValue, value));
+			} else if (!oldStale && stale) {
+				fireStale();
+			}
+		}
+
+		void makeStale() {
+			if (!stale) {
+				stale = true;
+				fireStale();
+			}
+		}
+
+		public boolean isStale() {
+			ObservableTracker.getterCalled(this);
+			return stale;
+		}
+
+		public Object getValueType() {
+			return IStatus.class;
+		}
+	}
 }
diff --git a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java
index 9457db9..2dc8c6f 100644
--- a/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java
+++ b/tests/org.eclipse.jface.tests.databinding/src/org/eclipse/core/tests/databinding/validation/MultiValidatorTest.java
@@ -8,16 +8,19 @@
  * Contributors:
  *     Matthew Hall - initial API and implementation (bug 218269)
  *     Matthew Hall - bug 237884
- *     Ovidio Mallo - bug 240590
+ *     Ovidio Mallo - bugs 240590, 238909
  ******************************************************************************/
 
 package org.eclipse.core.tests.databinding.validation;
 
 import org.eclipse.core.databinding.DataBindingContext;
 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.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;
@@ -28,16 +31,17 @@
 import org.eclipse.core.runtime.AssertionFailedException;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.jface.databinding.conformance.util.CurrentRealm;
+import org.eclipse.jface.databinding.conformance.util.ValueChangeEventTracker;
 import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase;
 
 public class MultiValidatorTest extends AbstractDefaultRealmTestCase {
-	private WritableValue dependency;
+	private DependencyObservableValue dependency;
 	private MultiValidator validator;
 	private IObservableValue validationStatus;
 
 	protected void setUp() throws Exception {
 		super.setUp();
-		dependency = new WritableValue(null, IStatus.class);
+		dependency = new DependencyObservableValue(null, IStatus.class);
 		validator = new MultiValidator() {
 			protected IStatus validate() {
 				return (IStatus) dependency.getValue();
@@ -142,7 +146,7 @@
 	}
 
 	public void testBug237884_Comment3_ValidationStatusAsDependencyCausesStackOverflow() {
-		dependency = new WritableValue(new Object(), Object.class);
+		dependency = new DependencyObservableValue(new Object(), Object.class);
 		validator = new MultiValidator() {
 			private int counter;
 
@@ -213,4 +217,129 @@
 		// dependency set (the validator's targets).
 		assertFalse(validator.getTargets().contains(noDependency));
 	}
+
+	public void testValidationStaleness() {
+		ValueChangeEventTracker validationChangeCounter = ValueChangeEventTracker
+				.observe(validationStatus);
+
+		StaleCounter validationStaleCounter = new StaleCounter();
+		validationStatus.addStaleListener(validationStaleCounter);
+
+		// Assert initial state.
+		assertFalse(validationStatus.isStale());
+		assertEquals(0, validationChangeCounter.count);
+		assertEquals(0, validationStaleCounter.count);
+
+		// Change to a stale state.
+		dependency.setStale(true);
+		assertTrue(validationStatus.isStale());
+		assertEquals(0, validationChangeCounter.count);
+		assertEquals(1, validationStaleCounter.count); // +1
+
+		// The validation status is already stale so even if it gets another
+		// stale event from its dependencies, it should not propagate that
+		// event.
+		dependency.fireStale();
+		assertTrue(validationStatus.isStale());
+		assertEquals(0, validationChangeCounter.count);
+		assertEquals(1, validationStaleCounter.count);
+
+		// Change the validation status while remaining stale.
+		dependency.setValue(ValidationStatus.error("e1"));
+		assertTrue(validationStatus.isStale());
+		assertEquals(1, validationChangeCounter.count); // +1
+		assertEquals(1, validationStaleCounter.count);
+
+		// Move back to a non-stale state.
+		dependency.setStale(false);
+		assertFalse(dependency.isStale());
+		assertFalse(validationStatus.isStale());
+		assertEquals(2, validationChangeCounter.count); // +1
+		assertEquals(1, validationStaleCounter.count);
+	}
+
+	public void testStatusValueChangeWhileValidationStale() {
+		// Change to a stale state.
+		dependency.setStale(true);
+		assertTrue(validationStatus.isStale());
+
+		// Even if the validation is stale, we want the current value to be
+		// tracked.
+		dependency.setValue(ValidationStatus.error("e1"));
+		assertTrue(validationStatus.isStale());
+		assertEquals(dependency.getValue(), validationStatus.getValue());
+		dependency.setValue(ValidationStatus.error("e2"));
+		assertTrue(validationStatus.isStale());
+		assertEquals(dependency.getValue(), validationStatus.getValue());
+	}
+
+	public void testValidationStatusBecomesStaleThroughNewDependency() {
+		final DependencyObservableValue nonStaleDependency = new DependencyObservableValue(
+				ValidationStatus.ok(), IStatus.class);
+		nonStaleDependency.setStale(false);
+
+		final DependencyObservableValue staleDependency = new DependencyObservableValue(
+				ValidationStatus.ok(), IStatus.class);
+		staleDependency.setStale(true);
+
+		validator = new MultiValidator() {
+			protected IStatus validate() {
+				if (nonStaleDependency.getValue() != null) {
+					return (IStatus) nonStaleDependency.getValue();
+				}
+				return (IStatus) staleDependency.getValue();
+			}
+		};
+		validationStatus = validator.getValidationStatus();
+
+		assertFalse(validationStatus.isStale());
+
+		StaleCounter validationStaleCounter = new StaleCounter();
+		validationStatus.addStaleListener(validationStaleCounter);
+		assertEquals(0, validationStaleCounter.count);
+
+		// Setting the status of the non-stale dependency to null leads to the
+		// new stale dependency being accessed which in turn should trigger a
+		// stale event.
+		nonStaleDependency.setValue(null);
+		assertTrue(validationStatus.isStale());
+		assertEquals(1, validationStaleCounter.count);
+	}
+
+	private static class DependencyObservableValue extends WritableValue {
+		private boolean stale = false;
+
+		public DependencyObservableValue(Object initialValue, Object valueType) {
+			super(initialValue, valueType);
+		}
+
+		public boolean isStale() {
+			ObservableTracker.getterCalled(this);
+			return stale;
+		}
+
+		public void setStale(boolean stale) {
+			if (this.stale != stale) {
+				this.stale = stale;
+				if (stale) {
+					fireStale();
+				} else {
+					fireValueChange(Diffs.createValueDiff(doGetValue(),
+							doGetValue()));
+				}
+			}
+		}
+
+		protected void fireStale() {
+			super.fireStale();
+		}
+	}
+
+	private static class StaleCounter implements IStaleListener {
+		int count;
+
+		public void handleStale(StaleEvent event) {
+			count++;
+		}
+	}
 }