Make DynaTableRf use a ListEditor for the favorites.
Fix potential NPE's in AED.Chain.
Widen RequestFactoryEditorDriver's type bound to allow it to drive more than just EntityProxy types.
Patch by: bobv
Review by: rjrjr

Review at http://gwt-code-reviews.appspot.com/888801


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8810 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/FavoritesWidget.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/FavoritesWidget.java
index 997ed6d..605cc69 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/FavoritesWidget.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/FavoritesWidget.java
@@ -16,6 +16,8 @@
 package com.google.gwt.sample.dynatablerf.client.widgets;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.requestfactory.client.RequestFactoryEditorDriver;
@@ -34,8 +36,10 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Set;
 
 /**
@@ -47,22 +51,48 @@
   interface Binder extends UiBinder<Widget, FavoritesWidget> {
   }
 
-  interface Driver extends RequestFactoryEditorDriver<PersonProxy, NameLabel> {
+  interface Driver extends RequestFactoryEditorDriver<List<PersonProxy>, //
+      ListEditor<PersonProxy, NameLabel>> {
   }
 
   interface Style extends CssResource {
     String favorite();
   }
 
+  /**
+   * This is used by a ListEditor.
+   */
+  private class NameLabelSource extends EditorSource<NameLabel> {
+    @Override
+    public NameLabel create(int index) {
+      NameLabel label = new NameLabel(eventBus);
+      label.setStylePrimaryName(style.favorite());
+      container.insert(label, index);
+      return label;
+    }
+
+    @Override
+    public void dispose(NameLabel subEditor) {
+      subEditor.removeFromParent();
+      subEditor.cancelSubscription();
+    }
+
+    @Override
+    public void setIndex(NameLabel editor, int index) {
+      container.insert(editor, index);
+    }
+  }
+
   @UiField
   FlowPanel container;
 
   @UiField
   Style style;
+
+  private final List<PersonProxy> displayed;
   private final EventBus eventBus;
   private final RequestFactory factory;
   private FavoritesManager manager;
-  private final Map<EntityProxyId, NameLabel> map = new HashMap<EntityProxyId, NameLabel>();
   private HandlerRegistration subscription;
 
   public FavoritesWidget(EventBus eventBus, RequestFactory factory,
@@ -70,7 +100,27 @@
     this.eventBus = eventBus;
     this.factory = factory;
     this.manager = manager;
+
+    // Create the UI
     initWidget(GWT.<Binder> create(Binder.class).createAndBindUi(this));
+
+    // Create the driver which manages the data-bound widgets
+    Driver driver = GWT.<Driver> create(Driver.class);
+
+    // Use a ListEditor that uses our NameLabelSource
+    ListEditor<PersonProxy, NameLabel> editor = ListEditor.of(new NameLabelSource());
+
+    // Configure the driver
+    ListEditor<PersonProxy, NameLabel> listEditor = editor;
+    driver.initialize(eventBus, factory, listEditor);
+
+    /*
+     * Notice the backing list is essentially anonymous.
+     */
+    driver.display(new ArrayList<PersonProxy>());
+
+    // Modifying this list triggers widget creation and destruction
+    displayed = listEditor.getList();
   }
 
   @Override
@@ -103,21 +153,16 @@
     }
 
     if (event.isFavorite()) {
-      if (!map.containsKey(person.stableId())) {
-        NameLabel label = new NameLabel(eventBus);
-        Driver driver = GWT.create(Driver.class);
-        driver.initialize(eventBus, factory, label);
-        driver.edit(person, null);
-        label.setStylePrimaryName(style.favorite());
-
-        container.add(label);
-        map.put(person.stableId(), label);
-      }
+      displayed.add(person);
     } else {
-      NameLabel toRemove = map.remove(person.stableId());
-      if (toRemove != null) {
-        container.remove(toRemove);
-      }
+      displayed.remove(person);
     }
+
+    // Sorting the list of PersonProxies will also change the UI display
+    Collections.sort(displayed, new Comparator<PersonProxy>() {
+      public int compare(PersonProxy o1, PersonProxy o2) {
+        return o1.getName().compareTo(o2.getName());
+      }
+    });
   }
 }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/NameLabel.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/NameLabel.java
index d9063b1..3bea339 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/NameLabel.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/NameLabel.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.editor.client.EditorDelegate;
 import com.google.gwt.editor.client.ValueAwareEditor;
-import com.google.gwt.editor.client.adapters.HasTextEditor;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.shared.EventBus;
@@ -32,15 +31,14 @@
  * the displayed object.
  */
 class NameLabel extends Composite implements ValueAwareEditor<PersonProxy> {
-  private final Label label = new Label();
-  final HasTextEditor nameEditor = HasTextEditor.of(label);
+  final Label nameEditor = new Label();
   private PersonProxy person;
   private HandlerRegistration subscription;
 
   public NameLabel(final EventBus eventBus) {
-    initWidget(label);
+    initWidget(nameEditor);
 
-    label.addClickHandler(new ClickHandler() {
+    nameEditor.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
         eventBus.fireEvent(new EditPersonEvent(person));
       }
@@ -55,6 +53,7 @@
   }
 
   public void setDelegate(EditorDelegate<PersonProxy> delegate) {
+    assert subscription == null;
     subscription = delegate.subscribe();
   }
 
@@ -63,11 +62,10 @@
   }
 
   /**
-   * Unhook event notifications when not attached to the DOM.
+   * Unhook event notifications when being permanently disposed of by
+   * FavoritesWidget.
    */
-  @Override
-  protected void onUnload() {
-    super.onUnload();
+  protected void cancelSubscription() {
     if (subscription != null) {
       subscription.removeHandler();
     }
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 e200642..5af1c44 100644
--- a/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
@@ -52,21 +52,38 @@
     }
 
     public void attach(R object, S subEditor) {
-      AbstractEditorDelegate<R, S> subDelegate = createComposedDelegate();
-      map.put(subEditor, subDelegate);
+      AbstractEditorDelegate<R, S> subDelegate = map.get(subEditor);
+
       @SuppressWarnings("unchecked")
       Editor<Object> temp = (Editor<Object>) subEditor;
-      initializeSubDelegate(subDelegate, path
-          + composedEditor.getPathElement(temp), object, subEditor, delegateMap);
+      String subPath = path + composedEditor.getPathElement(temp);
+
+      if (subDelegate == null) {
+        subDelegate = createComposedDelegate();
+        map.put(subEditor, subDelegate);
+        initializeSubDelegate(subDelegate, subPath, object, subEditor,
+            delegateMap);
+      } else {
+        subDelegate.path = subPath;
+        subDelegate.refresh(object);
+      }
     }
 
     public void detach(S subEditor) {
-      map.remove(subEditor).flush(errors);
+      AbstractEditorDelegate<R, S> subDelegate = map.remove(subEditor);
+      if (subDelegate != null && subDelegate.shouldFlush()) {
+        subDelegate.flush(errors);
+      }
     }
 
     public R getValue(S subEditor) {
       AbstractEditorDelegate<R, S> subDelegate = map.get(subEditor);
-      subDelegate.flush(errors);
+      if (subDelegate == null) {
+        return null;
+      }
+      if (subDelegate.shouldFlush()) {
+        subDelegate.flush(errors);
+      }
       return subDelegate.getObject();
     }
 
@@ -261,6 +278,14 @@
   protected abstract void setObject(T object);
 
   /**
+   * Indicates whether or not calls to {@link #flush} are expected as part of
+   * normal operation.
+   */
+  protected boolean shouldFlush() {
+    return true;
+  }
+
+  /**
    * Collect all paths being edited.
    */
   protected abstract void traverse(List<String> paths);
diff --git a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
index 4179cbd..1611d1e 100644
--- a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
@@ -18,7 +18,6 @@
 import com.google.gwt.editor.client.Editor;
 import com.google.gwt.editor.client.EditorError;
 import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestObject;
 import com.google.gwt.requestfactory.shared.Violation;
@@ -53,7 +52,12 @@
  * @param <E> the type of Editor that will edit the Record
  * @see com.google.gwt.requestfactory.client.testing.MockRequestFactoryEditorDriver
  */
-public interface RequestFactoryEditorDriver<P extends EntityProxy, E extends Editor<? super P>> {
+public interface RequestFactoryEditorDriver<P, E extends Editor<? super P>> {
+  /**
+   * Initialize the Editor and its sub-editors with data for display-only mode.
+   */
+  void display(P proxy);
+
   /**
    * Initialize the Editor and its sub-editors with data.
    */
@@ -65,6 +69,8 @@
    * in a depth-first manner.
    * 
    * @return the RequestObject passed into {@link #edit}
+   * @throws IllegalStateException if {@link #edit(Object, RequestObject)} has
+   *           not been called with a non-null {@link RequestObject}
    */
   <T> RequestObject<T> flush();
 
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 ce6a8f3..35ae178 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
@@ -35,7 +35,7 @@
  * @param <R> the type of Record
  * @param <E> the type of Editor
  */
-public abstract class AbstractRequestFactoryEditorDriver<R extends EntityProxy, E extends Editor<R>>
+public abstract class AbstractRequestFactoryEditorDriver<R, E extends Editor<R>>
     implements RequestFactoryEditorDriver<R, E> {
 
   private static final DelegateMap.KeyMethod PROXY_ID_KEY = new DelegateMap.KeyMethod() {
@@ -56,6 +56,10 @@
   private RequestFactory requestFactory;
   private RequestObject<?> saveRequest;
 
+  public void display(R object) {
+    edit(object, null);
+  }
+
   public void edit(R object, RequestObject<?> saveRequest) {
     checkEditor();
     this.saveRequest = saveRequest;
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java
index 80382cc..4ae8773 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java
@@ -121,4 +121,9 @@
     ((RequestFactoryEditorDelegate<R, S>) subDelegate).initialize(eventBus,
         factory, path, object, subEditor, delegateMap, request);
   }
+
+  @Override
+  protected boolean shouldFlush() {
+    return request != null;
+  }
 }
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 452ea84..3dbe060 100644
--- a/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
@@ -19,7 +19,6 @@
 import com.google.gwt.editor.client.EditorError;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.requestfactory.client.RequestFactoryEditorDriver;
-import com.google.gwt.requestfactory.shared.EntityProxy;
 import com.google.gwt.requestfactory.shared.RequestFactory;
 import com.google.gwt.requestfactory.shared.RequestObject;
 import com.google.gwt.requestfactory.shared.Violation;
@@ -34,8 +33,8 @@
  * @param <P> the Proxy type being edited
  * @param <E> the Editor type
  */
-public class MockRequestFactoryEditorDriver<P extends EntityProxy, E extends Editor<P>>
-    implements RequestFactoryEditorDriver<P, E> {
+public class MockRequestFactoryEditorDriver<P, E extends Editor<P>> implements
+    RequestFactoryEditorDriver<P, E> {
   private static final String[] EMPTY_STRING = new String[0];
 
   private EventBus eventBus;
@@ -47,6 +46,13 @@
   /**
    * Records its arguments.
    */
+  public void display(P proxy) {
+    this.proxy = proxy;
+  }
+
+  /**
+   * Records its arguments.
+   */
   public void edit(P proxy, RequestObject<?> saveRequest) {
     this.proxy = proxy;
     this.saveRequest = saveRequest;
diff --git a/user/src/com/google/gwt/user/client/ui/Label.java b/user/src/com/google/gwt/user/client/ui/Label.java
index 9099a7f..340cafa 100644
--- a/user/src/com/google/gwt/user/client/ui/Label.java
+++ b/user/src/com/google/gwt/user/client/ui/Label.java
@@ -17,6 +17,9 @@
 
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.LeafValueEditor;
+import com.google.gwt.editor.client.adapters.HasTextEditor;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.DoubleClickEvent;
@@ -64,7 +67,7 @@
 public class Label extends Widget implements HasDirectionalText, HasWordWrap,
     HasDirection, HasClickHandlers, HasDoubleClickHandlers, SourcesClickEvents,
     SourcesMouseEvents, HasAllMouseHandlers, HasDirectionEstimator,
-    HasAutoHorizontalAlignment {
+    HasAutoHorizontalAlignment, IsEditor<LeafValueEditor<String>> {
 
   /**
    * Creates a Label widget that wraps an existing &lt;div&gt; or &lt;span&gt;
@@ -261,6 +264,10 @@
     ListenerWrapper.WrappedMouseWheelListener.add(this, listener);
   }
 
+  public LeafValueEditor<String> asEditor() {
+    return HasTextEditor.of(this);
+  }
+
   /**
    * {@inheritDoc}
    */