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

Reply via email to