Implement round-trip validation with RequestFactory and the Editor framework.
Make the decorator panel much more focused to eliminate excessive generic parameterization.
Patch by: bobv
Review by: rjrjr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8781 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
index bf0cfc5..2dc6f11 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/DynaTableRf.java
@@ -64,10 +64,9 @@
       }
     });
 
-    final DynaTableRequestFactory requests = 
-      GWT.create(DynaTableRequestFactory.class);
+    final DynaTableRequestFactory requests = GWT.create(DynaTableRequestFactory.class);
     requests.init(eventBus);
-    
+
     // Add remote logging handler
     RequestFactoryLogHandler.LoggingRequestProvider provider =
       new RequestFactoryLogHandler.LoggingRequestProvider() {
@@ -78,7 +77,7 @@
     Logger.getLogger("").addHandler(
         new RequestFactoryLogHandler(provider, Level.WARNING,
             "WireActivityLogger"));
-    
+
     FavoritesManager manager = new FavoritesManager();
     PersonEditorWorkflow.register(eventBus, requests, manager);
 
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/PersonEditorWorkflow.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/PersonEditorWorkflow.java
index 6abdaf8..ff38cd5 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/PersonEditorWorkflow.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/PersonEditorWorkflow.java
@@ -106,22 +106,22 @@
   @UiHandler("save")
   void onSave(ClickEvent e) {
     // MOVE TO ACTIVITY END
-    RequestObject<Void> request = editorDriver.<Void> flush();
+    final RequestObject<Void> request = editorDriver.<Void> flush();
     if (editorDriver.hasErrors()) {
-      dialog.setText("Errors detected");
+      dialog.setText("Errors detected locally");
       return;
     }
-    dialog.hide();
     request.fire(new Receiver<Void>() {
       @Override
       public void onSuccess(Void response, Set<SyncResult> syncResults) {
+        dialog.hide();
       }
 
       @Override
       public void onViolation(Set<Violation> errors) {
-        for (Violation error : errors) {
-          System.out.println(error.getPath() + " " + error.getMessage());
-        }
+        dialog.setText("Errors detected on the server");
+        request.clearUsed();
+        editorDriver.setViolations(errors);
       }
     });
   }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.java
index afffd74..56d6936 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.java
@@ -17,13 +17,11 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.editor.client.Editor;
-import com.google.gwt.editor.client.adapters.ValueBoxEditor;
-import com.google.gwt.editor.client.ui.EditorErrorPanel;
+import com.google.gwt.editor.client.ui.ValueBoxEditorDecorator;
 import com.google.gwt.sample.dynatablerf.shared.AddressProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.Widget;
 
 /**
@@ -34,13 +32,13 @@
   }
 
   @UiField
-  TextBox street;
+  ValueBoxEditorDecorator<String> street;
   @UiField
-  TextBox city;
+  ValueBoxEditorDecorator<String> city;
   @UiField
-  TextBox state;
+  ValueBoxEditorDecorator<String> state;
   @UiField
-  EditorErrorPanel<Integer, ValueBoxEditor<Integer>> zip;
+  ValueBoxEditorDecorator<Integer> zip;
 
   public AddressEditor() {
     initWidget(GWT.<Binder> create(Binder.class).createAndBindUi(this));
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.ui.xml b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.ui.xml
index 6e3aea0..4db5bc7 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.ui.xml
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/AddressEditor.ui.xml
@@ -6,20 +6,36 @@
   <g:HTMLPanel>
     <div class="{style.rightAlign}">
       Street:
-      <g:TextBox ui:field="street" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="street"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextBox stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
       City:
-      <g:TextBox ui:field="city" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="city"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextBox stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
       State:
-      <g:TextBox ui:field="state" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="state"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextBox stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
       Zip:
-      <e:EditorErrorPanel ui:field="zip" stylePrimaryName="{style.editField}">
-        <e:contents>
-          <a:IntegerBox stylePrimaryName="{style.editField}"/>
-        </e:contents>
-      </e:EditorErrorPanel>
+      <e:ValueBoxEditorDecorator ui:field="zip"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <a:IntegerBox stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
     </div>
   </g:HTMLPanel>
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.java
index 502a370..d398b2f 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.java
@@ -19,12 +19,12 @@
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.ui.ValueBoxEditorDecorator;
 import com.google.gwt.sample.dynatablerf.shared.PersonProxy;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.TextArea;
-import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Focusable;
 import com.google.gwt.user.client.ui.Widget;
 
 /**
@@ -38,13 +38,16 @@
   AddressEditor address;
 
   @UiField
-  TextBox name;
+  ValueBoxEditorDecorator<String> description;
 
   @UiField
-  TextBox description;
+  ValueBoxEditorDecorator<String> name;
 
   @UiField
-  TextArea note;
+  ValueBoxEditorDecorator<String> note;
+
+  @UiField
+  Focusable nameBox;
 
   public PersonEditor() {
     initWidget(GWT.<Binder> create(Binder.class).createAndBindUi(this));
@@ -53,7 +56,7 @@
   public void focus() {
     Scheduler.get().scheduleDeferred(new ScheduledCommand() {
       public void execute() {
-        name.setFocus(true);
+        nameBox.setFocus(true);
       }
     });
   }
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.ui.xml b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.ui.xml
index 2844495..59df545 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.ui.xml
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/widgets/PersonEditor.ui.xml
@@ -1,18 +1,34 @@
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder' xmlns:g='urn:import:com.google.gwt.user.client.ui'
-  xmlns:dt='urn:import:com.google.gwt.sample.dynatablerf.client.widgets'>
+  xmlns:dt='urn:import:com.google.gwt.sample.dynatablerf.client.widgets'
+  xmlns:e='urn:import:com.google.gwt.editor.client.ui'>
   <ui:style src="../common.css">
     
   </ui:style>
   <g:HTMLPanel>
     <div class="{style.rightAlign}">
       Name:
-      <g:TextBox ui:field="name" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="name"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextBox ui:field="nameBox" stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
       Description:
-      <g:TextBox ui:field="description" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="description"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextBox stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
       Note:
-      <g:TextArea ui:field="note" stylePrimaryName="{style.editField}" />
+      <e:ValueBoxEditorDecorator ui:field="note"
+        stylePrimaryName="{style.editField}">
+        <e:valuebox>
+          <g:TextArea stylePrimaryName="{style.editField}" />
+        </e:valuebox>
+      </e:ValueBoxEditorDecorator>
       <br />
     </div>
     Address:
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java
index 62bec08..e7b3580 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/domain/Person.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.sample.dynatablerf.server.SchoolCalendarService;
 
-import javax.validation.Valid;
 import javax.validation.constraints.DecimalMin;
 import javax.validation.constraints.NotNull;
 import javax.validation.constraints.Size;
@@ -39,14 +38,13 @@
   }
 
   @NotNull
-  @Valid
   private final Address address = new Address();
 
   @NotNull
   private String description = "DESC";
 
   @NotNull
-  @Size(min = 1, message = "Persons must have names")
+  @Size(min = 2, message = "Persons aren't just characters")
   private String name;
 
   @NotNull
@@ -56,7 +54,6 @@
   @DecimalMin("0")
   private Integer version = 0;
 
-  @NotNull
   private String note;
 
   public Person() {
diff --git a/user/src/com/google/gwt/editor/client/Editor.java b/user/src/com/google/gwt/editor/client/Editor.java
index ddc0732..a9584bb 100644
--- a/user/src/com/google/gwt/editor/client/Editor.java
+++ b/user/src/com/google/gwt/editor/client/Editor.java
@@ -48,6 +48,15 @@
  */
 public interface Editor<T> {
   /**
+   * Tells the Editor framework to ignore an Editor accessor.
+   */
+  @Documented
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(value = {ElementType.FIELD, ElementType.METHOD})
+  public @interface Ignore {
+  }
+
+  /**
    * Maps a composite Editor's component Editors into the data-model.
    */
   @Documented
diff --git a/user/src/com/google/gwt/editor/client/adapters/ValueBoxEditor.java b/user/src/com/google/gwt/editor/client/adapters/ValueBoxEditor.java
index a16880f..6f367e4 100644
--- a/user/src/com/google/gwt/editor/client/adapters/ValueBoxEditor.java
+++ b/user/src/com/google/gwt/editor/client/adapters/ValueBoxEditor.java
@@ -15,9 +15,6 @@
  */
 package com.google.gwt.editor.client.adapters;
 
-import com.google.gwt.editor.client.EditorDelegate;
-import com.google.gwt.editor.client.HasEditorDelegate;
-import com.google.gwt.editor.client.LeafValueEditor;
 import com.google.gwt.user.client.ui.ValueBoxBase;
 
 import java.text.ParseException;
@@ -29,18 +26,17 @@
  * 
  * @param <T> the type of value to be edited
  */
-public class ValueBoxEditor<T> implements LeafValueEditor<T>,
-    HasEditorDelegate<T> {
+public class ValueBoxEditor<T> extends TakesValueEditor<T> {
 
   public static <T> ValueBoxEditor<T> of(ValueBoxBase<T> valueBox) {
     return new ValueBoxEditor<T>(valueBox);
   }
 
-  private EditorDelegate<T> delegate;
-  private ValueBoxBase<T> peer;
+  private final ValueBoxBase<T> peer;
   private T value;
 
   protected ValueBoxEditor(ValueBoxBase<T> peer) {
+    super(peer);
     this.peer = peer;
   }
 
@@ -54,15 +50,13 @@
     try {
       value = peer.getValueOrThrow();
     } catch (ParseException e) {
-      delegate.recordError(e.getMessage(), peer.getText(), e);
+      // TODO i18n
+      getDelegate().recordError("Bad value (" + peer.getText() + ")",
+          peer.getText(), e);
     }
     return value;
   }
 
-  public void setDelegate(EditorDelegate<T> delegate) {
-    this.delegate = delegate;
-  }
-
   public void setValue(T value) {
     peer.setValue(this.value = value);
   }
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 dc1ae5e..7857c2b 100644
--- a/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/impl/AbstractEditorDelegate.java
@@ -27,6 +27,7 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -43,15 +44,20 @@
 
   private class Chain<R, S extends Editor<R>> implements
       CompositeEditor.EditorChain<R, S> {
+    private DelegateMap delegateMap;
     private Map<S, AbstractEditorDelegate<R, S>> map = new LinkedHashMap<S, AbstractEditorDelegate<R, S>>();
 
+    public Chain(DelegateMap delegateMap) {
+      this.delegateMap = delegateMap;
+    }
+
     public void attach(R object, S subEditor) {
       AbstractEditorDelegate<R, S> subDelegate = createComposedDelegate();
       map.put(subEditor, subDelegate);
       @SuppressWarnings("unchecked")
       Editor<Object> temp = (Editor<Object>) subEditor;
       initializeSubDelegate(subDelegate, path
-          + composedEditor.getPathElement(temp), object, subEditor);
+          + composedEditor.getPathElement(temp), object, subEditor, delegateMap);
     }
 
     public void detach(S subEditor) {
@@ -82,11 +88,16 @@
 
   protected CompositeEditor<T, Object, Editor<Object>> composedEditor;
   protected Chain<Object, Editor<Object>> editorChain;
-  protected List<EditorError> errors = new ArrayList<EditorError>();
+  protected List<EditorError> errors;
   protected HasEditorErrors<T> hasEditorErrors;
   protected LeafValueEditor<T> leafValueEditor;
   protected String path;
   /**
+   * A map of local paths to sub-editors that do not have EditorDelegates (and
+   * will not be in the DelegateMap).
+   */
+  protected Map<String, Editor<?>> simpleEditors;
+  /**
    * This field avoids needing to repeatedly cast {@link #editor}.
    */
   protected ValueAwareEditor<T> valueAwareEditor;
@@ -97,6 +108,9 @@
 
   public abstract T ensureMutable(T object);
 
+  /**
+   * Flushes both data and errors.
+   */
   public void flush(List<EditorError> errorAccumulator) {
     try {
       if (valueAwareEditor != null) {
@@ -119,39 +133,15 @@
         editorChain.collectErrors();
       }
     } finally {
-      if (hasEditorErrors != null) {
-        // Allow higher-priority co-editor to spy on errors
-        for (Iterator<EditorError> it = errorAccumulator.iterator(); it.hasNext();) {
-          EditorError error = it.next();
-          if (error.getAbsolutePath().startsWith(getPath())) {
-            errors.add(error);
-            it.remove();
-          }
-        }
-        // Include the trailing dot
-        int length = getPath().length();
-        int pathPrefixLength = length == 0 ? 0 : (length + 1);
-        for (EditorError error : errors) {
-          ((SimpleError) error).setPathPrefixLength(pathPrefixLength);
-        }
-        // Give all of the errors to the handler and use a new local accumulator
-        hasEditorErrors.showErrors(Collections.unmodifiableList(errors));
-
-        for (EditorError error : errors) {
-          if (!error.isConsumed()) {
-            errorAccumulator.add(error);
-          }
-        }
-
-        // Reset local error list
-        errors = new ArrayList<EditorError>();
-      } else {
-        errorAccumulator.addAll(errors);
-        errors.clear();
-      }
+      showErrors(errorAccumulator);
     }
   }
 
+  public void flushErrors(List<EditorError> errorAccumulator) {
+    flushSubEditorErrors(errorAccumulator);
+    showErrors(errorAccumulator);
+  }
+
   public abstract T getObject();
 
   public String getPath() {
@@ -163,13 +153,20 @@
     errors.add(error);
   }
 
+  public void recordError(String message, Object value, Object userData,
+      String extraPath) {
+    EditorError error = new SimpleError(this, message, value, userData,
+        extraPath);
+    errors.add(error);
+  }
+
   public abstract HandlerRegistration subscribe();
 
   protected String appendPath(String path) {
     return appendPath(this.path, path);
   }
 
-  protected abstract void attachSubEditors();
+  protected abstract void attachSubEditors(DelegateMap delegates);
 
   /**
    * Only implemented by delegates for a {@link CompositeEditor}.
@@ -178,14 +175,24 @@
     throw new IllegalStateException();
   }
 
+  protected abstract void flushSubEditorErrors(
+      List<EditorError> errorAccumulator);
+
   protected abstract void flushSubEditors(List<EditorError> errorAccumulator);
 
   protected abstract E getEditor();
 
-  protected void initialize(String pathSoFar, T object, E editor) {
+  protected Editor<?> getSimpleEditor(String declaredPath) {
+    return simpleEditors.get(declaredPath);
+  }
+
+  protected void initialize(String pathSoFar, T object, E editor,
+      DelegateMap map) {
     this.path = pathSoFar;
     setEditor(editor);
     setObject(object);
+    errors = new ArrayList<EditorError>();
+    simpleEditors = new HashMap<String, Editor<?>>();
 
     // Set up pre-casted fields to access the editor
     if (editor instanceof HasEditorErrors<?>) {
@@ -203,7 +210,7 @@
         @SuppressWarnings("unchecked")
         CompositeEditor<T, Object, Editor<Object>> temp = (CompositeEditor<T, Object, Editor<Object>>) editor;
         composedEditor = temp;
-        editorChain = new Chain<Object, Editor<Object>>();
+        editorChain = new Chain<Object, Editor<Object>>(map);
         composedEditor.setEditorChain(editorChain);
       }
     }
@@ -222,7 +229,7 @@
       valueAwareEditor.setValue(object);
     }
     if (object != null) {
-      attachSubEditors();
+      attachSubEditors(map);
     }
   }
 
@@ -231,9 +238,47 @@
    */
   protected abstract <R, S extends Editor<R>> void initializeSubDelegate(
       AbstractEditorDelegate<R, S> subDelegate, String path, R object,
-      S subEditor);
+      S subEditor, DelegateMap map);
 
   protected abstract void setEditor(E editor);
 
   protected abstract void setObject(T object);
+
+  /**
+   * @param errorAccumulator an out parameter to which unhandled EditorErrors
+   *          will be added.
+   */
+  private void showErrors(List<EditorError> errorAccumulator) {
+    assert errors != errorAccumulator;
+    if (hasEditorErrors != null) {
+      // Allow higher-priority co-editor to spy on errors
+      for (Iterator<EditorError> it = errorAccumulator.iterator(); it.hasNext();) {
+        EditorError error = it.next();
+        if (error.getAbsolutePath().startsWith(getPath())) {
+          errors.add(error);
+          it.remove();
+        }
+      }
+      // Include the trailing dot
+      int length = getPath().length();
+      int pathPrefixLength = length == 0 ? 0 : (length + 1);
+      for (EditorError error : errors) {
+        ((SimpleError) error).setPathPrefixLength(pathPrefixLength);
+      }
+      // Give all of the errors to the handler and use a new local accumulator
+      hasEditorErrors.showErrors(Collections.unmodifiableList(errors));
+
+      for (EditorError error : errors) {
+        if (!error.isConsumed()) {
+          errorAccumulator.add(error);
+        }
+      }
+
+      // Reset local error list
+      errors = new ArrayList<EditorError>();
+    } else {
+      errorAccumulator.addAll(errors);
+      errors.clear();
+    }
+  }
 }
\ No newline at end of file
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 7fb7268..5bfc2f4 100644
--- a/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
+++ b/user/src/com/google/gwt/editor/client/impl/AbstractSimpleBeanEditorDriver.java
@@ -33,6 +33,7 @@
     implements SimpleBeanEditorDriver<T, E> {
 
   private SimpleBeanEditorDelegate<T, E> delegate;
+  private DelegateMap delegateMap = new DelegateMap(DelegateMap.IDENTITY);
   private E editor;
   private List<EditorError> errors;
   private T object;
@@ -41,7 +42,8 @@
     checkEditor();
     this.object = object;
     delegate = createDelegate();
-    delegate.initialize("", object, editor);
+    delegate.initialize("", object, editor, delegateMap);
+    delegateMap.put(object, delegate);
   }
 
   public T flush() {
@@ -65,6 +67,13 @@
 
   protected abstract SimpleBeanEditorDelegate<T, E> createDelegate();
 
+  /**
+   * Visible for testing.
+   */
+  DelegateMap getDelegateMap() {
+    return delegateMap;
+  }
+
   private void checkDelegate() {
     if (delegate == null) {
       throw new IllegalStateException("Must call edit() first");
diff --git a/user/src/com/google/gwt/editor/client/impl/DelegateMap.java b/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
new file mode 100644
index 0000000..f20ee51
--- /dev/null
+++ b/user/src/com/google/gwt/editor/client/impl/DelegateMap.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.editor.client.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Allows fast traversal of an Editor hierarchy.
+ */
+public class DelegateMap {
+  /**
+   * 
+   */
+  public interface KeyMethod {
+    Object key(Object object);
+  }
+
+  public static final KeyMethod IDENTITY = new KeyMethod() {
+    public Object key(Object object) {
+      return object;
+    }
+  };
+
+  private final Map<Object, List<AbstractEditorDelegate<?, ?>>> map = new HashMap<Object, List<AbstractEditorDelegate<?, ?>>>();
+  private final Map<String, List<AbstractEditorDelegate<?, ?>>> paths = new HashMap<String, List<AbstractEditorDelegate<?, ?>>>();
+  private final KeyMethod keyMethod;
+
+  public DelegateMap(KeyMethod key) {
+    this.keyMethod = key;
+  }
+
+  public List<AbstractEditorDelegate<?, ?>> get(Object object) {
+    Object key = keyMethod.key(object);
+    return key == null ? null : map.get(key);
+  }
+
+  /**
+   * Returns a list of Editors available at a particular absolute path.
+   */
+  public List<AbstractEditorDelegate<?, ?>> getPath(String path) {
+    return paths.get(path);
+  }
+
+  /**
+   * Accesses the delegate map without using the KeyMethod.
+   */
+  public List<AbstractEditorDelegate<?, ?>> getRaw(Object key) {
+    return map.get(key);
+  }
+
+  public <T> void put(T object, AbstractEditorDelegate<T, ?> delegate) {
+    {
+      List<AbstractEditorDelegate<?, ?>> list = paths.get(delegate.getPath());
+      if (list == null) {
+        list = new ArrayList<AbstractEditorDelegate<?, ?>>();
+        paths.put(delegate.getPath(), list);
+      }
+      list.add(delegate);
+    }
+    Object key = keyMethod.key(object);
+    if (key == null) {
+      return;
+    }
+    {
+      List<AbstractEditorDelegate<?, ?>> list = map.get(key);
+      if (list == null) {
+        list = new ArrayList<AbstractEditorDelegate<?, ?>>();
+        map.put(key, list);
+      }
+      list.add(delegate);
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/editor/client/impl/SimpleBeanEditorDelegate.java b/user/src/com/google/gwt/editor/client/impl/SimpleBeanEditorDelegate.java
index bc3d6a4..df0dcf5 100644
--- a/user/src/com/google/gwt/editor/client/impl/SimpleBeanEditorDelegate.java
+++ b/user/src/com/google/gwt/editor/client/impl/SimpleBeanEditorDelegate.java
@@ -38,8 +38,8 @@
   }
 
   @Override
-  public void initialize(String pathSoFar, T object, E editor) {
-    super.initialize(pathSoFar, object, editor);
+  public void initialize(String pathSoFar, T object, E editor, DelegateMap map) {
+    super.initialize(pathSoFar, object, editor, map);
   }
 
   @Override
@@ -50,8 +50,8 @@
   @Override
   protected <R, S extends Editor<R>> void initializeSubDelegate(
       AbstractEditorDelegate<R, S> subDelegate, String path, R object,
-      S subEditor) {
+      S subEditor, DelegateMap map) {
     ((SimpleBeanEditorDelegate<R, S>) subDelegate).initialize(path, object,
-        subEditor);
+        subEditor, map);
   }
 }
diff --git a/user/src/com/google/gwt/editor/client/impl/SimpleError.java b/user/src/com/google/gwt/editor/client/impl/SimpleError.java
index 2b0db0f..e2cf710 100644
--- a/user/src/com/google/gwt/editor/client/impl/SimpleError.java
+++ b/user/src/com/google/gwt/editor/client/impl/SimpleError.java
@@ -39,6 +39,19 @@
     this.userData = userData;
   }
 
+  /**
+   * Used to construct an error for an Editor that does not have a delegate.
+   */
+  SimpleError(AbstractEditorDelegate<?, ?> delegate, String message,
+      Object value, Object userData, String extraPath) {
+    assert extraPath != null && extraPath.length() > 0;
+    this.absolutePath = delegate.getPath() + extraPath;
+    this.editor = delegate.getSimpleEditor(extraPath);
+    this.message = message;
+    this.value = value;
+    this.userData = userData;
+  }
+
   public String getAbsolutePath() {
     return absolutePath;
   }
diff --git a/user/src/com/google/gwt/editor/client/ui/EditorErrorPanel.java b/user/src/com/google/gwt/editor/client/ui/ValueBoxEditorDecorator.java
similarity index 62%
rename from user/src/com/google/gwt/editor/client/ui/EditorErrorPanel.java
rename to user/src/com/google/gwt/editor/client/ui/ValueBoxEditorDecorator.java
index 136052d..9132016 100644
--- a/user/src/com/google/gwt/editor/client/ui/EditorErrorPanel.java
+++ b/user/src/com/google/gwt/editor/client/ui/ValueBoxEditorDecorator.java
@@ -18,30 +18,47 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.DivElement;
 import com.google.gwt.dom.client.Style.Display;
-import com.google.gwt.editor.client.Editor;
 import com.google.gwt.editor.client.EditorError;
 import com.google.gwt.editor.client.HasEditorErrors;
 import com.google.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.adapters.ValueBoxEditor;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiChild;
 import com.google.gwt.uibinder.client.UiConstructor;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.ValueBoxBase;
 import com.google.gwt.user.client.ui.Widget;
 
 import java.util.List;
 
 /**
- * A simple decorator to display an Editor's EditorErrors in conjunction with a
- * Widget.
+ * A simple decorator to display leaf widgets with an error message.
+ * <p>
+ * <h3>Use in UiBinder Templates</h3>
+ * <p>
+ * The decorator may have exactly one ValueBoxBase added though an
+ * <code>&lt;e:valuebox></code> child tag.
+ * <p>
+ * For example:
+ * <pre>
+ * &#64;UiField
+ * ValueBoxEditorDecorator<String> name;
+ * </pre>
+ * <pre>
+ * &lt;e:ValueBoxEditorDecorator ui:field='name'>
+ *   &lt;e:valuebox>
+ *     &lt;g:TextBox />
+ *   &lt;/e:valuebox>
+ * &lt;/e:ValueBoxEditorDecorator>
+ * </pre>
  * 
- * @param <T> the type of data being editod
- * @param <E> the type of Editor
+ * @param <T> the type of data being edited
  */
-public class EditorErrorPanel<T, E extends Editor<T>> extends Composite
-    implements HasEditorErrors<T>, IsEditor<E> {
-  interface Binder extends UiBinder<Widget, EditorErrorPanel<?, ?>> {
+public class ValueBoxEditorDecorator<T> extends Composite implements
+    HasEditorErrors<T>, IsEditor<ValueBoxEditor<T>> {
+  interface Binder extends UiBinder<Widget, ValueBoxEditorDecorator<?>> {
     Binder BINDER = GWT.create(Binder.class);
   }
 
@@ -51,45 +68,36 @@
   @UiField
   DivElement errorLabel;
 
-  private E editor;
+  private ValueBoxEditor<T> editor;
 
   @UiConstructor
-  public EditorErrorPanel() {
+  public ValueBoxEditorDecorator() {
     initWidget(Binder.BINDER.createAndBindUi(this));
   }
 
-  public EditorErrorPanel(Widget widget, E editor) {
+  public ValueBoxEditorDecorator(ValueBoxBase<T> widget,
+      ValueBoxEditor<T> editor) {
     this();
-    setWidget(widget);
-    setEditor(editor);
+    contents.add(widget);
+    this.editor = editor;
   }
 
-  public E asEditor() {
+  public ValueBoxEditor<T> asEditor() {
     return editor;
   }
 
-  public void setEditor(E editor) {
+  public void setEditor(ValueBoxEditor<T> editor) {
     this.editor = editor;
   }
 
   /**
-   * Set the widget that the EditorPanel will display. If the widget is also an
-   * {@link Editor} or implements {@link IsEditor}, this method will
+   * Set the widget that the EditorPanel will display. This method will
    * automatically call {@link #setEditor}.
    */
-  @UiChild(limit = 1, tagname = "contents")
-  public void setWidget(Widget widget) {
+  @UiChild(limit = 1, tagname = "valuebox")
+  public void setValueBox(ValueBoxBase<T> widget) {
     contents.add(widget);
-
-    if (widget instanceof Editor<?>) {
-      @SuppressWarnings("unchecked")
-      E isEditor = (E) widget;
-      setEditor(isEditor);
-    } else if (widget instanceof IsEditor<?>) {
-      @SuppressWarnings("unchecked")
-      E isEditor = ((IsEditor<E>) widget).asEditor();
-      setEditor(isEditor);
-    }
+    setEditor(widget.asEditor());
   }
 
   /**
@@ -98,18 +106,19 @@
    * passed into {@link #setEditor()}.
    */
   public void showErrors(List<EditorError> errors) {
-    if (errors.isEmpty()) {
-      errorLabel.setInnerText("");
-      errorLabel.getStyle().setDisplay(Display.NONE);
-      return;
-    }
-
     StringBuilder sb = new StringBuilder();
     for (EditorError error : errors) {
       if (error.getEditor().equals(editor)) {
         sb.append("\n").append(error.getMessage());
       }
     }
+
+    if (sb.length() == 0) {
+      errorLabel.setInnerText("");
+      errorLabel.getStyle().setDisplay(Display.NONE);
+      return;
+    }
+
     errorLabel.setInnerText(sb.substring(1));
     errorLabel.getStyle().setDisplay(Display.INLINE_BLOCK);
   }
diff --git a/user/src/com/google/gwt/editor/client/ui/EditorErrorPanel.ui.xml b/user/src/com/google/gwt/editor/client/ui/ValueBoxEditorDecorator.ui.xml
similarity index 100%
rename from user/src/com/google/gwt/editor/client/ui/EditorErrorPanel.ui.xml
rename to user/src/com/google/gwt/editor/client/ui/ValueBoxEditorDecorator.ui.xml
diff --git a/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java b/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
index 9aa6ed9..dc2a0ce 100644
--- a/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
+++ b/user/src/com/google/gwt/editor/rebind/AbstractEditorDriverGenerator.java
@@ -25,6 +25,7 @@
 import com.google.gwt.dev.util.Name;
 import com.google.gwt.dev.util.Name.BinaryName;
 import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.impl.DelegateMap;
 import com.google.gwt.editor.rebind.model.EditorData;
 import com.google.gwt.editor.rebind.model.EditorModel;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
@@ -159,7 +160,8 @@
       }
 
       // For each entity property, create a sub-delegate and initialize
-      sw.println("protected void attachSubEditors() {");
+      sw.println("protected void attachSubEditors(%s delegateMap) {",
+          DelegateMap.class.getCanonicalName());
       sw.indent();
       for (EditorData d : data) {
         String subDelegateType = getEditorDelegate(d);
@@ -168,11 +170,16 @@
         if (d.isDelegateRequired()) {
           sw.println("%s = new %s();", delegateFields.get(d), subDelegateType);
           writeDelegateInitialization(sw, d, delegateFields);
+          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.getGetterName(), d.getBeanOwnerGuard("getObject()"));
+          // simpleEditor.put("some.path", editor.simpleEditor());
+          sw.println("simpleEditors.put(\"%s\", editor.%s);",
+              d.getDeclaredPath(), d.getSimpleExpression());
         }
         sw.outdent();
         sw.println("}");
@@ -226,6 +233,20 @@
       sw.outdent();
       sw.println("}");
 
+      // Flush each sub-delegate
+      sw.println("protected void flushSubEditorErrors(%s errorAccumulator) {",
+          List.class.getCanonicalName());
+      sw.indent();
+      for (EditorData d : data) {
+        if (d.isDelegateRequired()) {
+          // if (fooDelegate != null) fooDelegate.flushErrors(accumulator);
+          sw.println("if (%1$s != null) %1$s.flushErrors(errorAccumulator);",
+              delegateFields.get(d));
+        }
+      }
+      sw.outdent();
+      sw.println("}");
+
       sw.println("public static void traverseEditor(%s editor,"
           + " String prefix, %s<String> paths) {",
           editor.getQualifiedSourceName(), List.class.getName());
diff --git a/user/src/com/google/gwt/editor/rebind/SimpleBeanEditorDriverGenerator.java b/user/src/com/google/gwt/editor/rebind/SimpleBeanEditorDriverGenerator.java
index e2d9b21..1a2200b 100644
--- a/user/src/com/google/gwt/editor/rebind/SimpleBeanEditorDriverGenerator.java
+++ b/user/src/com/google/gwt/editor/rebind/SimpleBeanEditorDriverGenerator.java
@@ -56,7 +56,8 @@
     // fooDelegate.initialize(appendPath("foo"), getObject().getFoo(),
     // editor.fooEditor);
     sw.println("%s.initialize(appendPath(\"%s\"), getObject()%s.%s(),"
-        + " editor.%s);", delegateFields.get(d), d.getPropertyName(),
-        d.getBeanOwnerExpression(), d.getGetterName(), d.getSimpleExpression());
+        + " editor.%s, delegateMap);", delegateFields.get(d),
+        d.getPropertyName(), d.getBeanOwnerExpression(), d.getGetterName(),
+        d.getSimpleExpression());
   }
 }
diff --git a/user/src/com/google/gwt/editor/rebind/model/EditorModel.java b/user/src/com/google/gwt/editor/rebind/model/EditorModel.java
index 13b8f0d..f58d086 100644
--- a/user/src/com/google/gwt/editor/rebind/model/EditorModel.java
+++ b/user/src/com/google/gwt/editor/rebind/model/EditorModel.java
@@ -320,7 +320,8 @@
 
     for (JClassType type : editorType.getFlattenedSupertypeHierarchy()) {
       for (JField field : type.getFields()) {
-        if (field.isPrivate() || field.isStatic()) {
+        if (field.isPrivate() || field.isStatic()
+            || field.getAnnotation(Editor.Ignore.class) != null) {
           continue;
         }
         JType fieldClassType = field.getType();
@@ -330,7 +331,8 @@
         }
       }
       for (JMethod method : type.getMethods()) {
-        if (method.isPrivate() || method.isStatic()) {
+        if (method.isPrivate() || method.isStatic()
+            || method.getAnnotation(Editor.Ignore.class) != null) {
           continue;
         }
         JType methodReturnType = method.getReturnType();
diff --git a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
index 2f04c7c..4179cbd 100644
--- a/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/RequestFactoryEditorDriver.java
@@ -21,6 +21,7 @@
 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;
 
 import java.util.List;
 
@@ -87,4 +88,15 @@
    * RequestFactory should be provided.
    */
   void initialize(EventBus eventBus, RequestFactory requestFactory, E editor);
+
+  /**
+   * Show Violations returned from an attempt to submit a request. The
+   * violations will be converted into {@link EditorError} objects whose
+   * {@link EditorError#getUserData() getUserData()} method can be used to
+   * access the original Violation object.
+   * 
+   * @return <code>true</code> if there were any unconsumed EditorErrors which
+   *         can be retrieved from {@link #getErrors()}
+   */
+  boolean setViolations(Iterable<Violation> errors);
 }
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 9aaf8e8..ce6a8f3 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/AbstractRequestFactoryEditorDriver.java
@@ -17,11 +17,14 @@
 
 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.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;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -35,7 +38,17 @@
 public abstract class AbstractRequestFactoryEditorDriver<R extends EntityProxy, E extends Editor<R>>
     implements RequestFactoryEditorDriver<R, E> {
 
+  private static final DelegateMap.KeyMethod PROXY_ID_KEY = new DelegateMap.KeyMethod() {
+    public Object key(Object object) {
+      if (object instanceof EntityProxy) {
+        return ((EntityProxy) object).stableId();
+      }
+      return null;
+    }
+  };
+
   private RequestFactoryEditorDelegate<R, E> delegate;
+  private DelegateMap delegateMap = new DelegateMap(PROXY_ID_KEY);
   private E editor;
   private EventBus eventBus;
   private List<EditorError> errors;
@@ -48,16 +61,19 @@
     this.saveRequest = saveRequest;
     delegate = createDelegate();
     delegate.initialize(eventBus, requestFactory, "", object, editor,
-        saveRequest);
+        delegateMap, saveRequest);
+    delegateMap.put(object, delegate);
   }
 
-  @SuppressWarnings("unchecked")
   public <T> RequestObject<T> flush() {
     checkDelegate();
     checkSaveRequest();
     errors = new ArrayList<EditorError>();
     delegate.flush(errors);
-    return (RequestObject<T>) saveRequest;
+
+    @SuppressWarnings("unchecked")
+    RequestObject<T> toReturn = (RequestObject<T>) saveRequest;
+    return toReturn;
   }
 
   public List<EditorError> getErrors() {
@@ -81,6 +97,49 @@
     traverseEditors(paths);
   }
 
+  public boolean setViolations(Iterable<Violation> violations) {
+    checkDelegate();
+
+    // For each violation
+    for (Violation error : violations) {
+
+      /*
+       * Find the delegates that are attached to the object. Use getRaw() here
+       * since the violation doesn't include an EntityProxy reference
+       */
+      List<AbstractEditorDelegate<?, ?>> delegateList = delegateMap.getRaw(error.getProxyId());
+      if (delegateList != null) {
+
+        // For each delegate editing some record...
+        for (AbstractEditorDelegate<?, ?> baseDelegate : delegateList) {
+
+          // compute its base path in the hierarchy...
+          String basePath = baseDelegate.getPath();
+
+          // and the absolute path of the leaf editor receiving the error.
+          String absolutePath = (basePath.length() > 0 ? basePath + "." : "")
+              + error.getPath();
+
+          // Find the leaf editor's delegate.
+          List<AbstractEditorDelegate<?, ?>> leafDelegates = delegateMap.getPath(absolutePath);
+          if (leafDelegates != null) {
+            // Only attach the error to the first delegate in a co-editor chain.
+            leafDelegates.get(0).recordError(error.getMessage(), null, error);
+          } else {
+            // No EditorDelegate to attach it to, stick it on the base.
+            baseDelegate.recordError(error.getMessage(), null, error,
+                error.getPath());
+          }
+        }
+      }
+    }
+
+    // Flush the errors, which will take care of co-editor chains.
+    errors = new ArrayList<EditorError>();
+    delegate.flushErrors(errors);
+    return !errors.isEmpty();
+  }
+
   protected abstract RequestFactoryEditorDelegate<R, E> createDelegate();
 
   protected E getEditor() {
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 7a7bb18..011bb58 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryEditorDelegate.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.editor.client.Editor;
 import com.google.gwt.editor.client.impl.AbstractEditorDelegate;
+import com.google.gwt.editor.client.impl.DelegateMap;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.requestfactory.shared.EntityProxy;
@@ -53,11 +54,12 @@
   }
 
   public void initialize(EventBus eventBus, RequestFactory factory,
-      String pathSoFar, P object, E editor, RequestObject<?> editRequest) {
+      String pathSoFar, P object, E editor, DelegateMap delegateMap,
+      RequestObject<?> editRequest) {
     this.eventBus = eventBus;
     this.factory = factory;
     this.request = editRequest;
-    super.initialize(pathSoFar, object, editor);
+    super.initialize(pathSoFar, object, editor, delegateMap);
   }
 
   @Override
@@ -73,8 +75,8 @@
   @Override
   protected <R, S extends Editor<R>> void initializeSubDelegate(
       AbstractEditorDelegate<R, S> subDelegate, String path, R object,
-      S subEditor) {
+      S subEditor, DelegateMap delegateMap) {
     ((RequestFactoryEditorDelegate<R, S>) subDelegate).initialize(eventBus,
-        factory, path, object, subEditor, request);
+        factory, path, object, subEditor, delegateMap, request);
   }
 }
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 62c9203..452ea84 100644
--- a/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
+++ b/user/src/com/google/gwt/requestfactory/client/testing/MockRequestFactoryEditorDriver.java
@@ -22,6 +22,7 @@
 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;
 
 import java.util.Collections;
 import java.util.List;
@@ -37,10 +38,10 @@
     implements RequestFactoryEditorDriver<P, E> {
   private static final String[] EMPTY_STRING = new String[0];
 
-  private RequestObject<?> saveRequest;
   private EventBus eventBus;
   private E editor;
   private P proxy;
+  private RequestObject<?> saveRequest;
   private RequestFactory requestFactory;
 
   /**
@@ -124,4 +125,11 @@
     this.requestFactory = requestFactory;
     this.editor = editor;
   }
+
+  /**
+   * A no-op method that always returns false.
+   */
+  public boolean setViolations(Iterable<Violation> errors) {
+    return false;
+  }
 }
diff --git a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryEditorDriverGenerator.java b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryEditorDriverGenerator.java
index 23d7add..e36214f 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryEditorDriverGenerator.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryEditorDriverGenerator.java
@@ -89,8 +89,9 @@
   protected void writeDelegateInitialization(SourceWriter sw, EditorData d,
       Map<EditorData, String> delegateFields) {
     sw.println("%s.initialize(eventBus, factory, "
-        + "appendPath(\"%s\"), getObject()%s.%s()," + " editor.%s, request);",
-        delegateFields.get(d), d.getPropertyName(), d.getBeanOwnerExpression(),
-        d.getGetterName(), d.getSimpleExpression());
+        + "appendPath(\"%s\"), getObject()%s.%s()," + " editor.%s,"
+        + " delegateMap, request);", delegateFields.get(d),
+        d.getPropertyName(), d.getBeanOwnerExpression(), d.getGetterName(),
+        d.getSimpleExpression());
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/ValueBoxBase.java b/user/src/com/google/gwt/user/client/ui/ValueBoxBase.java
index bd83c3c..ed1dd09 100644
--- a/user/src/com/google/gwt/user/client/ui/ValueBoxBase.java
+++ b/user/src/com/google/gwt/user/client/ui/ValueBoxBase.java
@@ -18,7 +18,6 @@
 import com.google.gwt.core.client.GWT;
 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.ValueBoxEditor;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
@@ -67,7 +66,7 @@
 @SuppressWarnings("deprecation")
 public class ValueBoxBase<T> extends FocusWidget implements
     SourcesChangeEvents, HasChangeHandlers, HasName, HasDirectionEstimator,
-    HasValue<T>, AutoDirectionHandler.Target, IsEditor<LeafValueEditor<T>> {
+    HasValue<T>, AutoDirectionHandler.Target, IsEditor<ValueBoxEditor<T>> {
 
   private static TextBoxImpl impl = GWT.create(TextBoxImpl.class);
 
@@ -124,7 +123,7 @@
    * may override this method to provide custom error-handling when using the
    * Editor framework.
    */
-  public LeafValueEditor<T> asEditor() {
+  public ValueBoxEditor<T> asEditor() {
     return ValueBoxEditor.of(this);
   }
 
diff --git a/user/test/com/google/gwt/editor/EditorSuite.java b/user/test/com/google/gwt/editor/EditorSuite.java
index 5c3bffe..a4d615c 100644
--- a/user/test/com/google/gwt/editor/EditorSuite.java
+++ b/user/test/com/google/gwt/editor/EditorSuite.java
@@ -18,6 +18,7 @@
 import com.google.gwt.editor.client.EditorErrorTest;
 import com.google.gwt.editor.client.SimpleBeanEditorTest;
 import com.google.gwt.editor.client.adapters.ListEditorWrapperTest;
+import com.google.gwt.editor.client.impl.DelegateMapTest;
 import com.google.gwt.editor.rebind.model.EditorModelTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
@@ -31,6 +32,7 @@
   public static Test suite() {
     GWTTestSuite suite = new GWTTestSuite(
         "Test suite for core Editor functions");
+    suite.addTestSuite(DelegateMapTest.class);
     suite.addTestSuite(EditorModelTest.class);
     suite.addTestSuite(EditorErrorTest.class);
     suite.addTestSuite(ListEditorWrapperTest.class);
diff --git a/user/test/com/google/gwt/editor/client/AddressEditor.java b/user/test/com/google/gwt/editor/client/AddressEditor.java
index 8fa108d..33862a6 100644
--- a/user/test/com/google/gwt/editor/client/AddressEditor.java
+++ b/user/test/com/google/gwt/editor/client/AddressEditor.java
@@ -21,6 +21,6 @@
  * Simple editor used by multiple tests.
  */
 public class AddressEditor implements Editor<Address> {
-  SimpleEditor<String> city = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
-  SimpleEditor<String> street = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
+  public SimpleEditor<String> city = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
+  public SimpleEditor<String> street = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/editor/client/PersonEditor.java b/user/test/com/google/gwt/editor/client/PersonEditor.java
index e6336e9..61c8b0c 100644
--- a/user/test/com/google/gwt/editor/client/PersonEditor.java
+++ b/user/test/com/google/gwt/editor/client/PersonEditor.java
@@ -21,8 +21,8 @@
  * Simple editor used by multiple tests.
  */
 class PersonEditor implements Editor<Person> {
-  AddressEditor addressEditor = new AddressEditor();
-  SimpleEditor<String> name = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
+  public AddressEditor addressEditor = new AddressEditor();
+  public SimpleEditor<String> name = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
   @Path("manager.name")
-  SimpleEditor<String> managerName = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
+  public SimpleEditor<String> managerName = SimpleEditor.of(SimpleBeanEditorTest.UNINITIALIZED);
 }
\ No newline at end of file
diff --git a/user/test/com/google/gwt/editor/client/impl/DelegateMapTest.java b/user/test/com/google/gwt/editor/client/impl/DelegateMapTest.java
new file mode 100644
index 0000000..3c4c704
--- /dev/null
+++ b/user/test/com/google/gwt/editor/client/impl/DelegateMapTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.editor.client.impl;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.editor.client.Address;
+import com.google.gwt.editor.client.AddressEditor;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.Person;
+import com.google.gwt.editor.client.SimpleBeanEditorDriver;
+import com.google.gwt.editor.client.adapters.SimpleEditor;
+import com.google.gwt.junit.client.GWTTestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 
+ */
+public class DelegateMapTest extends GWTTestCase {
+  class AddressCoEditorView extends AddressEditor implements
+      IsEditor<AddressEditor> {
+    private AddressEditor addressEditor = new AddressEditor();
+
+    public AddressEditor asEditor() {
+      return addressEditor;
+    }
+  }
+
+  class PersonEditorWithCoAddressEditorView implements Editor<Person> {
+    AddressCoEditorView addressEditor = new AddressCoEditorView();
+    SimpleEditor<String> name = SimpleEditor.of("uninitialized");
+    @Path("manager.name")
+    SimpleEditor<String> managerName = SimpleEditor.of("uninitialized");
+  }
+
+  interface PersonEditorWithCoAddressEditorViewDriver extends
+      SimpleBeanEditorDriver<Person, PersonEditorWithCoAddressEditorView> {
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.editor.Editor";
+  }
+
+  private AbstractSimpleBeanEditorDriver<Person, PersonEditorWithCoAddressEditorView> driver;
+  private PersonEditorWithCoAddressEditorView editor;
+  private DelegateMap map;
+  private Person person;
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    Address a = new Address();
+    a.setCity("city");
+    a.setStreet("street");
+
+    Person m = new Person();
+    m.setName("manager");
+
+    person = new Person();
+    person.setName("name");
+    person.setAddress(a);
+    person.setManager(m);
+
+    editor = new PersonEditorWithCoAddressEditorView();
+    driver = GWT.create(PersonEditorWithCoAddressEditorViewDriver.class);
+    driver.initialize(editor);
+    driver.edit(person);
+
+    map = driver.getDelegateMap();
+  }
+
+  public void test() {
+    // Test by-object
+    assertEquals(Arrays.asList(editor), editors(map, person));
+    assertEquals(Arrays.asList(editor.addressEditor.addressEditor,
+        editor.addressEditor), editors(map, person.getAddress()));
+
+    // Test by-path
+    assertEquals(Arrays.asList(editor), editors(map, ""));
+    assertEquals(Arrays.asList(editor.addressEditor.addressEditor,
+        editor.addressEditor), editors(map, "address"));
+  }
+
+  public void testSimplePath() {
+    assertSame(editor.name, map.get(person).get(0).getSimpleEditor("name"));
+    assertSame(editor.managerName, map.get(person).get(0).getSimpleEditor(
+        "manager.name"));
+    // Only simple editors
+    assertNull(map.get(person).get(0).getSimpleEditor("address"));
+  }
+
+  private List<Editor<?>> editors(DelegateMap map, Object o) {
+    List<Editor<?>> toReturn = new ArrayList<Editor<?>>();
+    for (AbstractEditorDelegate<?, ?> delegate : map.get(o)) {
+      toReturn.add(delegate.getEditor());
+    }
+    return toReturn;
+  }
+
+  private List<Editor<?>> editors(DelegateMap map, String path) {
+    List<Editor<?>> toReturn = new ArrayList<Editor<?>>();
+    for (AbstractEditorDelegate<?, ?> delegate : map.getPath(path)) {
+      toReturn.add(delegate.getEditor());
+    }
+    return toReturn;
+  }
+}
diff --git a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
index 66e4482..e7bdf64 100644
--- a/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
+++ b/user/test/com/google/gwt/editor/rebind/model/EditorModelTest.java
@@ -42,6 +42,7 @@
 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;
 import com.google.gwt.user.client.TakesValue;
 import com.google.gwt.user.client.ui.HasText;
 
@@ -599,6 +600,7 @@
         code.append("SimpleEditor<String> readonly;\n");
         code.append("public static SimpleEditor ignoredStatic;\n");
         code.append("private SimpleEditor<String> ignoredPrivate;\n");
+        code.append("@Editor.Ignore public SimpleEditor<String> ignoredPublic;\n");
         code.append("}");
         return code;
       }
@@ -614,6 +616,7 @@
         code.append("protected abstract SimpleEditor<String> readonlyEditor();\n");
         code.append("public static SimpleEditor<String> ignoredStatic() {return null;}\n");
         code.append("private SimpleEditor<String> ignoredPrivate() {return null;}\n");
+        code.append("@Editor.Ignore public abstract SimpleEditor<String> ignoredPublic();\n");
         code.append("}");
         return code;
       }
@@ -767,6 +770,7 @@
         new EmptyMockJavaResource(HasEditorErrors.class),
         new RealJavaResource(HasText.class),
         new RealJavaResource(IsEditor.class),
+        new EmptyMockJavaResource(Iterable.class),
         new RealJavaResource(LeafValueEditor.class),
         new EmptyMockJavaResource(Property.class),
         new EmptyMockJavaResource(EntityProxy.class),
@@ -775,7 +779,8 @@
         new EmptyMockJavaResource(RequestObject.class),
         new RealJavaResource(SimpleEditor.class),
         new RealJavaResource(TakesValue.class),
-        new EmptyMockJavaResource(ValueAwareEditor.class),}));
+        new EmptyMockJavaResource(ValueAwareEditor.class),
+        new EmptyMockJavaResource(Violation.class),}));
     toReturn.addAll(Arrays.asList(JavaResourceBase.getStandardResources()));
     return toReturn;
   }
diff --git a/user/test/com/google/gwt/requestfactory/client/EditorTest.java b/user/test/com/google/gwt/requestfactory/client/EditorTest.java
index 7d950db..aa0bdca 100644
--- a/user/test/com/google/gwt/requestfactory/client/EditorTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/EditorTest.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorError;
+import com.google.gwt.editor.client.HasEditorErrors;
 import com.google.gwt.editor.client.adapters.SimpleEditor;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.SimpleEventBus;
@@ -26,23 +28,27 @@
 import com.google.gwt.requestfactory.shared.SimpleFooProxy;
 import com.google.gwt.requestfactory.shared.SimpleRequestFactory;
 import com.google.gwt.requestfactory.shared.SyncResult;
+import com.google.gwt.requestfactory.shared.Violation;
 
 import java.util.Arrays;
+import java.util.List;
 import java.util.Set;
 
 /**
- * Integration test of the Editor framework.
+ * Integration test of the Editor framework. Only tests for
+ * RequestFactory-specific features belong here; all other tests should use the
+ * SimpleBeanEditorDriver to make the tests simpler.
  */
 public class EditorTest extends GWTTestCase {
-  interface SimpleFooDriver extends
-      RequestFactoryEditorDriver<SimpleFooProxy, SimpleFooEditor> {
-  }
-
   static class SimpleBarEditor implements Editor<SimpleBarProxy> {
     protected final SimpleEditor<String> userName = SimpleEditor.of();
   }
 
-  static class SimpleFooEditor implements Editor<SimpleFooProxy> {
+  interface SimpleFooDriver extends
+      RequestFactoryEditorDriver<SimpleFooProxy, SimpleFooEditor> {
+  }
+
+  static class SimpleFooEditor implements HasEditorErrors<SimpleFooProxy> {
     /**
      * Test field-based access.
      */
@@ -56,6 +62,12 @@
 
     private final SimpleBarEditor barEditor = new SimpleBarEditor();
 
+    List<EditorError> errors;
+
+    public void showErrors(List<EditorError> errors) {
+      this.errors = errors;
+    }
+
     /**
      * Test method-based access with path override.
      */
@@ -68,6 +80,13 @@
   private EventBus eventBus;
   private SimpleRequestFactory factory;
 
+  private static final int TEST_TIMEOUT = 5000;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.requestfactory.RequestFactorySuite";
+  }
+
   @Override
   public void gwtSetUp() {
     factory = GWT.create(SimpleRequestFactory.class);
@@ -87,13 +106,6 @@
     });
   }
 
-  @Override
-  public String getModuleName() {
-    return "com.google.gwt.requestfactory.RequestFactorySuite";
-  }
-
-  private static final int TEST_TIMEOUT = 5000;
-
   public void test() {
     delayTestFinish(TEST_TIMEOUT);
     final SimpleFooEditor editor = new SimpleFooEditor();
@@ -131,4 +143,50 @@
           }
         });
   }
+
+  public void testViolations() {
+    delayTestFinish(TEST_TIMEOUT);
+    final SimpleFooEditor editor = new SimpleFooEditor();
+
+    final SimpleFooDriver driver = GWT.create(SimpleFooDriver.class);
+    driver.initialize(eventBus, factory, editor);
+
+    factory.simpleFooRequest().findSimpleFooById(0L).with(driver.getPaths()).fire(
+        new Receiver<SimpleFooProxy>() {
+          public void onSuccess(SimpleFooProxy response,
+              Set<SyncResult> syncResults) {
+            driver.edit(response,
+                factory.simpleFooRequest().persistAndReturnSelf(response).with(
+                    driver.getPaths()));
+            // Set to an illegal value
+            editor.userName.setValue("");
+
+            driver.<SimpleFooProxy> flush().fire(
+                new Receiver<SimpleFooProxy>() {
+                  @Override
+                  public void onSuccess(SimpleFooProxy response,
+                      Set<SyncResult> syncResults) {
+                    fail("Expected errors");
+                  }
+
+                  @Override
+                  public void onViolation(Set<Violation> errors) {
+                    assertEquals(1, errors.size());
+                    Violation v = errors.iterator().next();
+
+                    driver.setViolations(errors);
+                    assertEquals(1, editor.errors.size());
+                    EditorError error = editor.errors.get(0);
+                    assertEquals("userName", error.getAbsolutePath());
+                    assertSame(editor.userName, error.getEditor());
+                    assertTrue(error.getMessage().length() > 0);
+                    assertEquals("userName", error.getPath());
+                    assertSame(v, error.getUserData());
+                    assertNull(error.getValue());
+                    finishTest();
+                  }
+                });
+          }
+        });
+  }
 }