Revision: 9575
Author: gwt.mirror...@gmail.com
Date: Thu Jan 20 09:22:57 2011
Log: 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
http://code.google.com/p/google-web-toolkit/source/detail?r=9575
Modified:
/trunk/user/src/com/google/gwt/editor/client/EditorDelegate.java
/trunk/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
/trunk/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
/trunk/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
/trunk/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
/trunk/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
/trunk/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
/trunk/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
/trunk/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
/trunk/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
=======================================
--- /trunk/user/src/com/google/gwt/editor/client/EditorDelegate.java Thu
Oct 14 09:50:44 2010
+++ /trunk/user/src/com/google/gwt/editor/client/EditorDelegate.java Thu
Jan 20 09:22:57 2011
@@ -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();
@@ -49,6 +49,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>.
=======================================
---
/trunk/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/editor/client/SimpleBeanEditorDriver.java
Thu Jan 20 09:22:57 2011
@@ -83,6 +83,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
=======================================
---
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
Thu Dec 9 12:12:52 2010
+++
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
Thu Jan 20 09:22:57 2011
@@ -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.
*/
@@ -161,6 +163,18 @@
public String getPath() {
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);
@@ -175,14 +189,20 @@
}
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);
}
refreshEditors();
}
+
+ public void setDirty(boolean dirty) {
+ this.dirty = dirty;
+ }
public abstract HandlerRegistration subscribe();
@@ -205,6 +225,20 @@
protected <Q> Q ensureMutable(Q object) {
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);
@@ -216,6 +250,11 @@
protected Editor<?> getSimpleEditor(String declaredPath) {
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) {
@@ -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;
}
@@ -272,6 +315,13 @@
AbstractEditorDelegate<R, S> subDelegate, String path, R object,
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.
*/
=======================================
---
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
Thu Jan 20 09:22:57 2011
@@ -66,6 +66,15 @@
public void initialize(E editor) {
this.editor = editor;
}
+
+ public boolean isDirty() {
+ for (AbstractEditorDelegate<?, ?> d : delegateMap) {
+ if (d.isDirty()) {
+ return true;
+ }
+ }
+ return false;
+ }
public boolean setConstraintViolations(
final Iterable<ConstraintViolation<?>> violations) {
=======================================
--- /trunk/user/src/com/google/gwt/editor/client/impl/DelegateMap.java Wed
Sep 15 02:26:39 2010
+++ /trunk/user/src/com/google/gwt/editor/client/impl/DelegateMap.java Thu
Jan 20 05:44:43 2011
@@ -17,19 +17,61 @@
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<?, ?>>
{
/**
*
*/
public interface KeyMethod {
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) {
@@ -63,6 +105,10 @@
public List<AbstractEditorDelegate<?, ?>> getRaw(Object key) {
return map.get(key);
}
+
+ public Iterator<AbstractEditorDelegate<?, ?>> iterator() {
+ return new MapIterator(this);
+ }
public <T> void put(T object, AbstractEditorDelegate<T, ?> delegate) {
{
=======================================
---
/trunk/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
Fri Sep 24 12:10:50 2010
+++
/trunk/user/src/com/google/gwt/editor/client/testing/MockEditorDelegate.java
Thu Jan 20 05:44:43 2011
@@ -29,6 +29,7 @@
}
};
+ private boolean dirty;
private String path = "";
/**
@@ -37,12 +38,28 @@
public String getPath() {
return path;
}
+
+ /**
+ * 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()}.
=======================================
---
/trunk/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/editor/client/testing/MockSimpleBeanEditorDriver.java
Thu Jan 20 05:44:43 2011
@@ -86,6 +86,13 @@
public void initialize(E editor) {
this.editor = editor;
}
+
+ /**
+ * Returns {@code false}.
+ */
+ public boolean isDirty() {
+ return false;
+ }
/**
* A no-op method that always returns false.
=======================================
---
/trunk/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
Thu Dec 9 12:12:52 2010
+++
/trunk/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
Thu Jan 20 05:44:43 2011
@@ -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("}");
}
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
Thu Jan 20 05:44:43 2011
@@ -138,6 +138,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
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
Thu Jan 20 05:44:43 2011
@@ -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;
@@ -192,6 +193,15 @@
public void initialize(RequestFactory requestFactory, E editor) {
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) {
=======================================
---
/trunk/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
Tue Jan 18 08:32:58 2011
+++
/trunk/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
Thu Jan 20 05:44:43 2011
@@ -140,6 +140,13 @@
public void initialize(RequestFactory requestFactory, E editor) {
this.initialize(requestFactory.getEventBus(), requestFactory, editor);
}
+
+ /**
+ * Returns {@code false}.
+ */
+ public boolean isDirty() {
+ return false;
+ }
/**
* A no-op method that always returns false.
=======================================
--- /trunk/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
Thu Dec 9 12:12:52 2010
+++ /trunk/user/test/com/google/gwt/editor/client/SimpleBeanEditorTest.java
Thu Jan 20 05:44:43 2011
@@ -117,6 +117,19 @@
interface PersonEditorWithCoAddressEditorViewDriver extends
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();
@@ -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.
--
http://groups.google.com/group/Google-Web-Toolkit-Contributors