Bug 286444 UI model and propagation of context values: add #modify()
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/context/IEclipseContext.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/context/IEclipseContext.java
index 4ed88ce..56dea9b 100644
--- a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/context/IEclipseContext.java
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/context/IEclipseContext.java
@@ -174,6 +174,11 @@
 	/**
 	 * Modifies the value to be associated with the given name.
 	 * <p>
+	 * The value has to be declared as modifiable by the original context before this method can be
+	 * used. If the variable with this name has not been declared as modifiable, an
+	 * {@link IllegalArgumentException} will be thrown.
+	 * </p>
+	 * <p>
 	 * The value is modified in the context in which it has been previously set. If none of the
 	 * contexts on the parent chain have a value set for the name, the value will be set in this
 	 * context.
@@ -184,7 +189,18 @@
 	 * @param value
 	 *            The value to be stored, or a {@link ContextFunction} that can return the stored
 	 *            value.
+	 * @throws IllegalArgumentException
+	 *             if the variable has not been declared as modifiable
 	 */
 	public void modify(String name, Object value);
 
+	/**
+	 * Declares the named value as modifiable by descendants of this context. If the value does not
+	 * exist in this context, a <code>null</code> value added for the name.
+	 * 
+	 * @param name
+	 *            the name to be declared as modifiable by descendants
+	 */
+	public void declareModifiable(String name);
+
 }
diff --git a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/internal/context/EclipseContext.java b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/internal/context/EclipseContext.java
index 4ff31a1..7d270e9 100644
--- a/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/internal/context/EclipseContext.java
+++ b/bundles/org.eclipse.e4.core.services/src/org/eclipse/e4/core/services/internal/context/EclipseContext.java
@@ -10,6 +10,7 @@
  *******************************************************************************/
 package org.eclipse.e4.core.services.internal.context;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -230,6 +231,8 @@
 
 	private final IEclipseContextStrategy strategy;
 
+	private ArrayList modifiable;
+
 	public EclipseContext(IEclipseContext parent, IEclipseContextStrategy strategy) {
 		this.strategy = strategy;
 		set(IContextConstants.PARENT, parent);
@@ -469,6 +472,10 @@
 	public boolean internalModify(String name, Object value) {
 		boolean containsKey = localValues.containsKey(name);
 		if (containsKey) {
+			if (!checkModifiable(name)) {
+				String tmp = "Variable " + name + " is not modifiable in the context " + toString();
+				throw new IllegalArgumentException(tmp);
+			}
 			Object oldValue = localValues.put(name, value);
 			if (value != oldValue) {
 				if (DEBUG_VERBOSE)
@@ -504,4 +511,26 @@
 			computation.addDependency(this, name);
 		}
 	}
+
+	public void declareModifiable(String name) {
+		if (name == null)
+			return;
+		if (modifiable == null)
+			modifiable = new ArrayList(3);
+		modifiable.add(name);
+		if (localValues.containsKey(name))
+			return;
+		localValues.put(name, null);
+	}
+
+	private boolean checkModifiable(String name) {
+		if (modifiable == null)
+			return false;
+		for (Iterator i = modifiable.iterator(); i.hasNext();) {
+			String candidate = (String) i.next();
+			if (candidate.equals(name))
+				return true;
+		}
+		return false;
+	}
 }
diff --git a/tests/org.eclipse.e4.core.tests.services/src/org/eclipse/e4/core/services/internal/context/EclipseContextTest.java b/tests/org.eclipse.e4.core.tests.services/src/org/eclipse/e4/core/services/internal/context/EclipseContextTest.java
index 52a9ef0..21b7c61 100644
--- a/tests/org.eclipse.e4.core.tests.services/src/org/eclipse/e4/core/services/internal/context/EclipseContextTest.java
+++ b/tests/org.eclipse.e4.core.tests.services/src/org/eclipse/e4/core/services/internal/context/EclipseContextTest.java
@@ -229,6 +229,10 @@
 		parent.set("b", "b2");
 		grandParent.set("c", "c3");
 
+		child.declareModifiable("a");
+		parent.declareModifiable("b");
+		grandParent.declareModifiable("c");
+
 		// test pre-conditions
 		assertNull(grandParent.get("b"));
 		assertEquals("b2", parent.get("b"));
@@ -266,6 +270,35 @@
 		assertFalse(grandParent.containsKey("a"));
 		assertEquals("aNew", child.get("a"));
 		assertNull(parent.get("a"));
+
+		// test access rules
+		child.set("aNo", "a1");
+		parent.set("bNo", "b2");
+		grandParent.set("cNo", "c3");
+
+		boolean exception = false;
+		try {
+			child.modify("bNo", "new");
+		} catch (IllegalArgumentException e) {
+			exception = true;
+		}
+		assertTrue(exception);
+
+		exception = false;
+		try {
+			grandParent.modify("cNo", "new");
+		} catch (IllegalArgumentException e) {
+			exception = true;
+		}
+		assertTrue(exception);
+
+		exception = false;
+		try {
+			child.modify("aNo", "new");
+		} catch (IllegalArgumentException e) {
+			exception = true;
+		}
+		assertTrue(exception);
 	}
 
 	public void testRemoveValueComputationOnDispose() {