Implement an isDirty() method for Editor framework drivers.
Issue 5881.
Patch by: bobv
Review by: rjrjr
Review at http://gwt-code-reviews.appspot.com/1306801
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9575 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/editor/client/EditorDelegate.java b/user/src/com/google/gwt/editor/client/EditorDelegate.java
index 87abeb3..8009ebb 100644
--- a/user/src/com/google/gwt/editor/client/EditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/EditorDelegate.java
@@ -29,7 +29,7 @@
public interface EditorDelegate<T> {
/**
* Returns the Editor's path, relative to the root object.
- *
+ *
* @return the path as a String
*/
String getPath();
@@ -50,6 +50,22 @@
void recordError(String message, Object value, Object userData);
/**
+ * Toggle the dirty-state flag for the Editor.
+ * <p>
+ * The dirty state of an Editor will be automatically cleared any time the
+ * Driver's {@code edit()} or {@code flush()} methods are called.
+ * <p>
+ * The dirty state will be automatically calculated for
+ * {@link LeafValueEditor} instances based on an {@link Object#equals(Object)}
+ * comparison of {@link LeafValueEditor#getValue()} and the value last passed
+ * to {@link LeafValueEditor#setValue(Object)}, however a clean state can be
+ * overridden by calling {@code setDirty(true)}.
+ *
+ * @param dirty the dirty state of the Editor
+ */
+ void setDirty(boolean dirty);
+
+ /**
* Register for notifications if object being edited is updated. Not all
* backends support subscriptions and will return <code>null</code>.
* <p>
diff --git a/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java b/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
index 5b5168a..017dcb5 100644
--- a/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
+++ b/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
@@ -84,6 +84,14 @@
void initialize(E editor);
/**
+ * Returns {@code true} if any of the Editors in the hierarchy have been
+ * modified relative to the last value passed into {@link #edit(Object)}.
+ *
+ * @see EditorDelegate#setDirty(boolean)
+ */
+ boolean isDirty();
+
+ /**
* Show {@link ConstraintViolation ConstraintViolations} generated through a
* {@link javax.validation.Validator Validator}. The violations will be
* converted into {@link EditorError} objects whose
diff --git a/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java b/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
index ee85f62..ca3c96b 100644
--- a/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
@@ -104,9 +104,15 @@
}
protected CompositeEditor<T, Object, Editor<Object>> composedEditor;
+ protected boolean dirty;
protected Chain<Object, Editor<Object>> editorChain;
protected List<EditorError> errors;
protected HasEditorErrors<T> hasEditorErrors;
+ protected T lastLeafValue;
+ /**
+ * Records values last set into any sub-editors that are leaves.
+ */
+ protected Map<String, Object> lastLeafValues;
protected LeafValueEditor<T> leafValueEditor;
protected String path;
/**
@@ -119,10 +125,6 @@
*/
protected ValueAwareEditor<T> valueAwareEditor;
- public AbstractEditorDelegate() {
- super();
- }
-
/**
* Flushes both data and errors.
*/
@@ -162,6 +164,18 @@
return path;
}
+ public boolean isDirty() {
+ if (dirty) {
+ return true;
+ }
+ if (leafValueEditor != null) {
+ if (!equals(lastLeafValue, leafValueEditor.getValue())) {
+ return true;
+ }
+ }
+ return isDirtyCheckLeaves();
+ }
+
public void recordError(String message, Object value, Object userData) {
EditorError error = new SimpleError(this, message, value, userData);
errors.add(error);
@@ -175,8 +189,10 @@
}
public void refresh(T object) {
+ dirty = false;
setObject(ensureMutable(object));
if (leafValueEditor != null) {
+ lastLeafValue = object;
leafValueEditor.setValue(object);
} else if (valueAwareEditor != null) {
valueAwareEditor.setValue(object);
@@ -184,6 +200,10 @@
refreshEditors();
}
+ public void setDirty(boolean dirty) {
+ this.dirty = dirty;
+ }
+
public abstract HandlerRegistration subscribe();
protected String appendPath(String path) {
@@ -206,6 +226,20 @@
return object;
}
+ /**
+ * Utility method used by generated subtypes that handles null vs. non-null
+ * comparisons.
+ */
+ protected boolean equals(Object a, Object b) {
+ if (a == b) {
+ return true;
+ }
+ if (a != null && a.equals(b)) {
+ return true;
+ }
+ return false;
+ }
+
protected abstract void flushSubEditorErrors(
List<EditorError> errorAccumulator);
@@ -217,6 +251,11 @@
return simpleEditors.get(declaredPath);
}
+ /**
+ * Returns {@code true} if the editor contains leaf editors without delegates.
+ */
+ protected abstract boolean hasSubEditorsWithoutDelegates();
+
protected void initialize(String pathSoFar, T object, E editor,
DelegateMap map) {
this.path = pathSoFar;
@@ -224,6 +263,9 @@
setObject(ensureMutable(object));
errors = new ArrayList<EditorError>();
simpleEditors = new HashMap<String, Editor<?>>();
+ if (hasSubEditorsWithoutDelegates()) {
+ lastLeafValues = new HashMap<String, Object>();
+ }
// Set up pre-casted fields to access the editor
if (editor instanceof HasEditorErrors<?>) {
@@ -252,6 +294,7 @@
* happened, only set the value and don't descend into any sub-Editors.
*/
if (leafValueEditor != null) {
+ lastLeafValue = object;
leafValueEditor.setValue(object);
return;
}
@@ -273,6 +316,13 @@
S subEditor, DelegateMap map);
/**
+ * Returns {@code true} if any leaf sub-editors are dirty.
+ *
+ * @see #lastLeafValues
+ */
+ protected abstract boolean isDirtyCheckLeaves();
+
+ /**
* Refresh all of the sub-editors.
*/
protected abstract void refreshEditors();
diff --git a/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java b/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
index 55330b8..802f86b 100644
--- a/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
+++ b/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
@@ -67,6 +67,15 @@
this.editor = editor;
}
+ public boolean isDirty() {
+ for (AbstractEditorDelegate<?, ?> d : delegateMap) {
+ if (d.isDirty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public boolean setConstraintViolations(
final Iterable<ConstraintViolation<?>> violations) {
checkDelegate();
diff --git a/user/src/com/google/gwt/editor/client/impl/DelegateMap.java b/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
index f20ee51..fa336ab 100644
--- a/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
+++ b/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
@@ -17,13 +17,14 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Allows fast traversal of an Editor hierarchy.
*/
-public class DelegateMap {
+public class DelegateMap implements Iterable<AbstractEditorDelegate<?, ?>> {
/**
*
*/
@@ -31,6 +32,47 @@
Object key(Object object);
}
+ private static class MapIterator implements
+ Iterator<AbstractEditorDelegate<?, ?>> {
+ private AbstractEditorDelegate<?, ?> next;
+ private Iterator<AbstractEditorDelegate<?, ?>> list;
+ private Iterator<List<AbstractEditorDelegate<?, ?>>> values;
+
+ public MapIterator(DelegateMap map) {
+ values = map.map.values().iterator();
+ next();
+ }
+
+ public boolean hasNext() {
+ return next != null;
+ }
+
+ public AbstractEditorDelegate<?, ?> next() {
+ AbstractEditorDelegate<?, ?> toReturn = next;
+
+ if (list != null && list.hasNext()) {
+ // Simple case, just advance the pointer
+ next = list.next();
+ } else {
+ // Uninitialized, or current list exhausted
+ next = null;
+ while (values.hasNext()) {
+ // Find the next non-empty iterator
+ list = values.next().iterator();
+ if (list.hasNext()) {
+ next = list.next();
+ break;
+ }
+ }
+ }
+ return toReturn;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
public static final KeyMethod IDENTITY = new KeyMethod() {
public Object key(Object object) {
return object;
@@ -64,6 +106,10 @@
return map.get(key);
}
+ public Iterator<AbstractEditorDelegate<?, ?>> iterator() {
+ return new MapIterator(this);
+ }
+
public <T> void put(T object, AbstractEditorDelegate<T, ?> delegate) {
{
List<AbstractEditorDelegate<?, ?>> list = paths.get(delegate.getPath());
diff --git a/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java b/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
index 3706468..ba90c7e 100644
--- a/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
@@ -29,6 +29,7 @@
}
};
+ private boolean dirty;
private String path = "";
/**
@@ -39,12 +40,28 @@
}
/**
+ * Returns {@code false} or the last value passed to
+ * {@link #setDirty(boolean)}.
+ */
+ public boolean isDirty() {
+ return dirty;
+ }
+
+ /**
* No-op.
*/
public void recordError(String message, Object value, Object userData) {
}
/**
+ * Records the value of {@code dirty} which can be retrieved from
+ * {@link #isDirty()}.
+ */
+ public void setDirty(boolean dirty) {
+ this.dirty = dirty;
+ }
+
+ /**
* Controls the return value of {@link #getPath()}.
*/
public void setPath(String path) {
diff --git a/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java b/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
index 01bb3f1..c098255 100644
--- a/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
+++ b/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
@@ -88,6 +88,13 @@
}
/**
+ * Returns {@code false}.
+ */
+ public boolean isDirty() {
+ return false;
+ }
+
+ /**
* A no-op method that always returns false.
*/
public boolean setConstraintViolations(
diff --git a/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java b/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
index 56579a6..e7ff8b7 100644
--- a/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
+++ b/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
@@ -173,13 +173,22 @@
sw.println("delegateMap.put(%1$s.getObject(), %1$s);",
delegateFields.get(d));
} else if (d.isLeafValueEditor()) {
- // if (can().access().without().npe()) { editor.subEditor.setValue() }
- sw.println("if (%4$s) editor.%1$s.setValue(getObject()%2$s%3$s);",
- d.getSimpleExpression(), d.getBeanOwnerExpression(),
- d.getGetterExpression(), d.getBeanOwnerGuard("getObject()"));
- // simpleEditor.put("some.path", editor.simpleEditor());
+ // if (can().access().without().npe()) {
+ sw.println("if (%s) {", d.getBeanOwnerGuard("getObject()"));
+ sw.indent();
+ // Bar value = getObject()....;
+ sw.println("%s value = getObject()%s%s;",
+ d.getEditedType().getQualifiedSourceName(),
+ d.getBeanOwnerExpression(), d.getGetterExpression());
+ // editor.subEditor.setValue(value);
+ sw.println("editor.%s.setValue(value);", d.getSimpleExpression());
+ // simpleEditors.put("foo.bar", editor.subEditor);
sw.println("simpleEditors.put(\"%s\", editor.%s);",
d.getDeclaredPath(), d.getSimpleExpression());
+ // lastLeafValues.put("foo.bar", value);
+ sw.println("lastLeafValues.put(\"%s\", value);", d.getDeclaredPath());
+ sw.outdent();
+ sw.println("}");
}
sw.outdent();
sw.println("}");
@@ -247,6 +256,39 @@
sw.outdent();
sw.println("}");
+ sw.println("protected boolean hasSubEditorsWithoutDelegates() {");
+ boolean hasSubEditorsWithoutDelegates = false;
+ for (EditorData d : data) {
+ if (!d.isDelegateRequired()) {
+ hasSubEditorsWithoutDelegates = true;
+ break;
+ }
+ }
+ sw.indentln("return %s;", hasSubEditorsWithoutDelegates ? "true"
+ : "false");
+ sw.println("}");
+
+ // isDirty() traversal method for sub-editors without delegates
+ sw.println("protected boolean isDirtyCheckLeaves() {");
+ sw.indent();
+ if (hasSubEditorsWithoutDelegates) {
+ for (EditorData d : data) {
+ if (!d.isDelegateRequired()) {
+ // if (editor.subEditor != null &&
+ sw.println("if (editor.%s != null &&", d.getSimpleExpression());
+ // !equals(editor.sub.getValue(), lastLeafValues.get("foo.bar"))) {
+ sw.indentln(
+ "!equals(editor.%s.getValue(), lastLeafValues.get(\"%s\"))) {",
+ d.getSimpleExpression(), d.getDeclaredPath());
+ sw.indentln("return true;");
+ sw.println("}");
+ }
+ }
+ }
+ sw.println("return false;");
+ sw.outdent();
+ sw.println("}");
+
// Reset the data being displayed
sw.println("protected void refreshEditors() {",
DelegateMap.class.getCanonicalName());
@@ -269,13 +311,24 @@
// if (editor.subEditor != null) {
sw.println("if (editor.%s != null) {", d.getSimpleExpression());
sw.indent();
- // if (can().access().without().npe()) { editor.subEditor.setValue() }
- sw.println("if (%4$s) editor.%1$s.setValue(getObject()%2$s%3$s);",
- d.getSimpleExpression(), d.getBeanOwnerExpression(),
- d.getGetterExpression(), d.getBeanOwnerGuard("getObject()"));
- // else { editor.subEditor.setValue(null); }
- sw.println("else { editor.%s.setValue(null); }",
- d.getSimpleExpression());
+ // if (can().access().without().npe()) {
+ sw.println("if (%s) {", d.getBeanOwnerGuard("getObject()"));
+ sw.indent();
+ // Bar value = getObject()....;
+ sw.println("%s value = getObject()%s%s;",
+ d.getEditedType().getQualifiedSourceName(),
+ d.getBeanOwnerExpression(), d.getGetterExpression());
+ // editor.subEditor.setValue(value);
+ sw.println("editor.%s.setValue(value);", d.getSimpleExpression());
+ // lastLeafValues.put("foo.bar", value);
+ sw.println("lastLeafValues.put(\"%s\", value);", d.getDeclaredPath());
+ sw.outdent();
+ sw.println("} else {");
+ sw.indent();
+ sw.println("editor.%s.setValue(null);", d.getSimpleExpression());
+ sw.println("lastLeafValues.put(\"%s\", null);", d.getDeclaredPath());
+ sw.outdent();
+ sw.println("}");
sw.outdent();
sw.println("}");
}
diff --git a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
index cf20de9..9d0ee99 100644
--- a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
@@ -139,6 +139,14 @@
void initialize(E editor);
/**
+ * Returns {@code true} if any of the Editors in the hierarchy have been
+ * modified relative to the last value passed into {@link #edit(Object)}.
+ *
+ * @see com.google.gwt.editor.client.EditorDelegate#setDirty(boolean)
+ */
+ boolean isDirty();
+
+ /**
* Show {@link ConstraintViolation ConstraintViolations} generated through a
* JSR 303 Validator. The violations will be converted into
* {@link EditorError} objects whose {@link EditorError#getUserData()
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
index 416c984..672c2c8 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
@@ -17,6 +17,7 @@
import com.google.gwt.editor.client.Editor;
import com.google.gwt.editor.client.EditorError;
+import com.google.gwt.editor.client.impl.AbstractEditorDelegate;
import com.google.gwt.editor.client.impl.DelegateMap;
import com.google.gwt.editor.client.impl.SimpleViolation;
import com.google.gwt.event.shared.EventBus;
@@ -193,6 +194,15 @@
initialize(requestFactory.getEventBus(), requestFactory, editor);
}
+ public boolean isDirty() {
+ for (AbstractEditorDelegate<?, ?> d : delegateMap) {
+ if (d.isDirty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public boolean setConstraintViolations(
Iterable<ConstraintViolation<?>> violations) {
return doSetViolations(SimpleViolation.iterableFromConstrantViolations(violations));
diff --git a/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
index e7bf389..f870386 100644
--- a/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
@@ -142,6 +142,13 @@
}
/**
+ * Returns {@code false}.
+ */
+ public boolean isDirty() {
+ return false;
+ }
+
+ /**
* A no-op method that always returns false.
*/
public boolean setConstraintViolations(
diff --git a/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java b/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
index b2e55bd..0dec642 100644
--- a/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
+++ b/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
@@ -118,6 +118,19 @@
SimpleBeanEditorDriver<Person, PersonEditorWithCoAddressEditorView> {
}
+ class PersonEditorWithDelegate extends PersonEditor implements
+ HasEditorDelegate<Person> {
+ EditorDelegate<Person> delegate;
+
+ public void setDelegate(EditorDelegate<Person> delegate) {
+ this.delegate = delegate;
+ }
+ }
+
+ interface PersonEditorWithDelegateDriver extends
+ SimpleBeanEditorDriver<Person, PersonEditorWithDelegate> {
+ }
+
class PersonEditorWithLeafAddressEditor implements Editor<Person> {
LeafAddressEditor addressEditor = new LeafAddressEditor();
SimpleEditor<String> name = SimpleEditor.of(UNINITIALIZED);
@@ -304,6 +317,76 @@
assertEquals("Should see this", person.getName());
}
+ public void testDirty() {
+ PersonEditor editor = new PersonEditor();
+ PersonEditorDriver driver = GWT.create(PersonEditorDriver.class);
+ driver.initialize(editor);
+ driver.edit(person);
+
+ // Freshly-initialized should not be dirty
+ assertFalse(driver.isDirty());
+
+ // Changing the Person object should not affect the dirty status
+ person.setName("blah");
+ assertFalse(driver.isDirty());
+
+ editor.addressEditor.city.setValue("Foo");
+ assertTrue(driver.isDirty());
+
+ // Reset to original value
+ editor.addressEditor.city.setValue("City");
+ assertFalse(driver.isDirty());
+
+ // Try a null value
+ editor.managerName.setValue(null);
+ assertTrue(driver.isDirty());
+ }
+
+ public void testDirtyWithDelegate() {
+ PersonEditorWithDelegate editor = new PersonEditorWithDelegate();
+ PersonEditorWithDelegateDriver driver = GWT.create(PersonEditorWithDelegateDriver.class);
+ driver.initialize(editor);
+ driver.edit(person);
+
+ // Freshly-initialized should not be dirty
+ assertFalse(driver.isDirty());
+
+ // Use the delegate to toggle the state
+ editor.delegate.setDirty(true);
+ assertTrue(driver.isDirty());
+
+ // Use the delegate to clear the state
+ editor.delegate.setDirty(false);
+ assertFalse(driver.isDirty());
+
+ // Check that the delegate has no influence over values
+ editor.addressEditor.city.setValue("edited");
+ assertTrue(driver.isDirty());
+ editor.delegate.setDirty(false);
+ assertTrue(driver.isDirty());
+ editor.delegate.setDirty(true);
+ assertTrue(driver.isDirty());
+ }
+
+ public void testDirtyWithOptionalEditor() {
+ AddressEditor addressEditor = new AddressEditor();
+ PersonEditorWithOptionalAddressEditor editor = new PersonEditorWithOptionalAddressEditor(addressEditor);
+ PersonEditorWithOptionalAddressDriver driver = GWT.create(PersonEditorWithOptionalAddressDriver.class);
+ driver.initialize(editor);
+ driver.edit(person);
+
+ // Freshly-initialized should not be dirty
+ assertFalse(driver.isDirty());
+
+ // Change the instance being edited
+ Address a = new Address();
+ editor.address.setValue(a);
+ assertTrue(driver.isDirty());
+
+ // Check restoration works
+ editor.address.setValue(personAddress);
+ assertFalse(driver.isDirty());
+ }
/**
* Test the use of the IsEditor interface that allows a view object to
* encapsulate its Editor.