AutoBeans improvements.
Support for arbitrarily complex parameterizations of List, Set, and Map property accessors.
Simplify logic in AutoBeanCodex by assembling a chain of Coders to handle parameterized types.
Support chained setter methods in AutoBean interfaces.
Patch by: bobv
Review by: rjrjr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9703 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
index 0adbd02..3c4efdf 100644
--- a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
+++ b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
@@ -16,11 +16,11 @@
 package com.google.gwt.autobean.rebind;
 
 import com.google.gwt.autobean.client.impl.AbstractAutoBeanFactory;
-import com.google.gwt.autobean.rebind.model.JBeanMethod;
 import com.google.gwt.autobean.rebind.model.AutoBeanFactoryMethod;
 import com.google.gwt.autobean.rebind.model.AutoBeanFactoryModel;
 import com.google.gwt.autobean.rebind.model.AutoBeanMethod;
 import com.google.gwt.autobean.rebind.model.AutoBeanType;
+import com.google.gwt.autobean.rebind.model.JBeanMethod;
 import com.google.gwt.autobean.shared.AutoBean;
 import com.google.gwt.autobean.shared.AutoBeanFactory;
 import com.google.gwt.autobean.shared.AutoBeanUtils;
@@ -30,6 +30,7 @@
 import com.google.gwt.autobean.shared.AutoBeanVisitor.PropertyContext;
 import com.google.gwt.autobean.shared.impl.AbstractAutoBean;
 import com.google.gwt.autobean.shared.impl.AbstractAutoBean.OneShotContext;
+import com.google.gwt.autobean.shared.impl.AbstractPropertyContext;
 import com.google.gwt.core.client.impl.WeakMapping;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.GeneratorContext;
@@ -39,6 +40,7 @@
 import com.google.gwt.core.ext.typeinfo.JEnumConstant;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JParameter;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
 import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
 import com.google.gwt.core.ext.typeinfo.JType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
@@ -103,6 +105,19 @@
     return factory.getCreatedClassName();
   }
 
+  /**
+   * Flattens a parameterized type into a simple list of types.
+   */
+  private void createTypeList(List<JType> accumulator, JType type) {
+    accumulator.add(type);
+    JParameterizedType hasParams = type.isParameterized();
+    if (hasParams != null) {
+      for (JClassType arg : hasParams.getTypeArgs()) {
+        createTypeList(accumulator, arg);
+      }
+    }
+  }
+
   private String getBaseMethodDeclaration(JMethod jmethod) {
     // Foo foo, Bar bar, Baz baz
     StringBuilder parameters = new StringBuilder();
@@ -254,9 +269,13 @@
         }
           break;
         case SET:
+        case SET_BUILDER:
           // values.put("foo", parameter);
           sw.println("values.put(\"%s\", %s);", method.getPropertyName(),
               jmethod.getParameters()[0].getName());
+          if (JBeanMethod.SET_BUILDER.equals(method.getAction())) {
+            sw.println("return this;");
+          }
           break;
         case CALL:
           // return com.example.Owner.staticMethod(Outer.this, param,
@@ -490,6 +509,7 @@
           sw.println("return toReturn;");
           break;
         case SET:
+        case SET_BUILDER:
           sw.println("%s.this.checkFrozen();", type.getSimpleSourceName());
           // getWrapped().setFoo(foo);
           sw.println("%s.this.getWrapped().%s(%s);",
@@ -497,6 +517,9 @@
           // FooAutoBean.this.set("setFoo", foo);
           sw.println("%s.this.set(\"%s\", %s);", type.getSimpleSourceName(),
               methodName, parameters[0].getName());
+          if (JBeanMethod.SET_BUILDER.equals(method.getAction())) {
+            sw.println("return this;");
+          }
           break;
         case CALL:
           // XXX How should freezing and calls work together?
@@ -567,7 +590,9 @@
       // If it's not a simple bean type, try to find a real setter method
       if (!type.isSimpleBean()) {
         for (AutoBeanMethod maybeSetter : type.getMethods()) {
-          if (maybeSetter.getAction().equals(JBeanMethod.SET)
+          boolean isASetter = maybeSetter.getAction().equals(JBeanMethod.SET)
+              || maybeSetter.getAction().equals(JBeanMethod.SET_BUILDER);
+          if (isASetter
               && maybeSetter.getPropertyName().equals(method.getPropertyName())) {
             setter = maybeSetter;
             break;
@@ -598,6 +623,11 @@
         propertyContextType = PropertyContext.class;
       }
 
+      // Map<List<Foo>, Bar> --> Map, List, Foo, Bar
+      List<JType> typeList = new ArrayList<JType>();
+      createTypeList(typeList, method.getMethod().getReturnType());
+      assert typeList.size() > 0;
+
       /*
        * Make the PropertyContext that lets us call the setter. We allow
        * multiple methods to be bound to the same property (e.g. to allow JSON
@@ -606,30 +636,45 @@
        */
       String propertyContextName = names.createName("_"
           + method.getPropertyName() + "PropertyContext");
-      sw.println("class %s implements %s {", propertyContextName,
+      sw.println("class %s extends %s implements %s {", propertyContextName,
+          AbstractPropertyContext.class.getCanonicalName(),
           propertyContextType.getCanonicalName());
       sw.indent();
-      sw.println("public boolean canSet() { return %s; }", type.isSimpleBean()
-          || setter != null);
-      if (method.isCollection()) {
-        // Will return the collection's element type or null if not a collection
-        sw.println(
-            "public Class<?> getElementType() { return %s.class; }",
-            ModelUtils.ensureBaseType(method.getElementType()).getQualifiedSourceName());
-      } else if (method.isMap()) {
-        // Will return the map's value type
-        sw.println(
-            "public Class<?> getValueType() { return %s.class; }",
-            ModelUtils.ensureBaseType(method.getValueType()).getQualifiedSourceName());
-        // Will return the map's key type
-        sw.println(
-            "public Class<?> getKeyType() { return %s.class; }",
-            ModelUtils.ensureBaseType(method.getKeyType()).getQualifiedSourceName());
+      sw.println("%s() {", propertyContextName);
+      sw.indent();
+      sw.print("super(new Class<?>[] {");
+      boolean first = true;
+      for (JType lit : typeList) {
+        if (first) {
+          first = false;
+        } else {
+          sw.print(", ");
+        }
+        sw.print("%s.class",
+            ModelUtils.ensureBaseType(lit).getQualifiedSourceName());
       }
-      // Return the property type
-      sw.println(
-          "public Class<?> getType() { return %s.class; }",
-          ModelUtils.ensureBaseType(method.getMethod().getReturnType()).getQualifiedSourceName());
+      sw.println("}, new int[] {");
+      first = true;
+      for (JType lit : typeList) {
+        if (first) {
+          first = false;
+        } else {
+          sw.print(", ");
+        }
+        JParameterizedType hasParam = lit.isParameterized();
+        if (hasParam == null) {
+          sw.print("0");
+        } else {
+          sw.print(String.valueOf(hasParam.getTypeArgs().length));
+        }
+      }
+      sw.println("});");
+      sw.outdent();
+      sw.println("}");
+      // Base method returns true.
+      if (!type.isSimpleBean() && setter == null) {
+        sw.println("public boolean canSet() { return false; }");
+      }
       sw.println("public void set(Object obj) { ");
       if (setter != null) {
         // Prefer the setter if one exists
@@ -666,7 +711,7 @@
       sw.println("visitor.endVisit%sProperty(\"%s\", value, %s);", visitMethod,
           method.getPropertyName(), propertyContextName);
       sw.outdent();
-      sw.print("}");
+      sw.println("}");
     }
     sw.outdent();
     sw.println("}");
diff --git a/user/src/com/google/gwt/autobean/rebind/model/AutoBeanMethod.java b/user/src/com/google/gwt/autobean/rebind/model/AutoBeanMethod.java
index 516ce3e..d75e819 100644
--- a/user/src/com/google/gwt/autobean/rebind/model/AutoBeanMethod.java
+++ b/user/src/com/google/gwt/autobean/rebind/model/AutoBeanMethod.java
@@ -40,7 +40,8 @@
 
     public AutoBeanMethod build() {
       if (toReturn.action.equals(JBeanMethod.GET)
-          || toReturn.action.equals(JBeanMethod.SET)) {
+          || toReturn.action.equals(JBeanMethod.SET)
+          || toReturn.action.equals(JBeanMethod.SET_BUILDER)) {
         PropertyName annotation = toReturn.method.getAnnotation(PropertyName.class);
         if (annotation != null) {
           toReturn.propertyName = annotation.value();
diff --git a/user/src/com/google/gwt/autobean/rebind/model/JBeanMethod.java b/user/src/com/google/gwt/autobean/rebind/model/JBeanMethod.java
index e403530..9880b61 100644
--- a/user/src/com/google/gwt/autobean/rebind/model/JBeanMethod.java
+++ b/user/src/com/google/gwt/autobean/rebind/model/JBeanMethod.java
@@ -20,6 +20,7 @@
 import static com.google.gwt.autobean.server.impl.BeanMethod.IS_PREFIX;
 import static com.google.gwt.autobean.server.impl.BeanMethod.SET_PREFIX;
 
+import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.JMethod;
 import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
 import com.google.gwt.core.ext.typeinfo.JType;
@@ -95,6 +96,24 @@
       return false;
     }
   },
+  SET_BUILDER {
+    @Override
+    public boolean matches(JMethod method) {
+      JClassType returnClass = method.getReturnType().isClassOrInterface();
+      if (returnClass == null
+          || !returnClass.isAssignableFrom(method.getEnclosingType())) {
+        return false;
+      }
+      if (method.getParameters().length != 1) {
+        return false;
+      }
+      String name = method.getName();
+      if (name.startsWith(SET_PREFIX) && name.length() > 3) {
+        return true;
+      }
+      return false;
+    }
+  },
   CALL {
     /**
      * Matches all leftover methods.
diff --git a/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java b/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
index 6cdf277..59e90ed 100644
--- a/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
+++ b/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
@@ -115,6 +115,27 @@
     }
   },
   /**
+   * A setter that returns a type assignable from the interface in which the
+   * method is declared to support chained, builder-pattern setters. For
+   * example, {@code foo.setBar(1).setBaz(42)}.
+   */
+  SET_BUILDER {
+    @Override
+    Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) {
+      handler.getBean().getValues().put(inferName(method), args[0]);
+      return handler.getBean().as();
+    }
+
+    @Override
+    boolean matches(SimpleBeanHandler<?> handler, Method method) {
+      String name = method.getName();
+      return name.startsWith(SET_PREFIX)
+          && name.length() > 3
+          && method.getParameterTypes().length == 1
+          && method.getReturnType().isAssignableFrom(method.getDeclaringClass());
+    }
+  },
+  /**
    * Domain methods.
    */
   CALL {
diff --git a/user/src/com/google/gwt/autobean/server/impl/GetterPropertyContext.java b/user/src/com/google/gwt/autobean/server/impl/GetterPropertyContext.java
index 7e61307..ba7814d 100644
--- a/user/src/com/google/gwt/autobean/server/impl/GetterPropertyContext.java
+++ b/user/src/com/google/gwt/autobean/server/impl/GetterPropertyContext.java
@@ -34,7 +34,7 @@
     Method found = null;
     String name = BeanMethod.GET.inferName(getter);
     for (Method m : getter.getDeclaringClass().getMethods()) {
-      if (BeanMethod.SET.matches(m)) {
+      if (BeanMethod.SET.matches(m) || BeanMethod.SET_BUILDER.matches(m)) {
         if (BeanMethod.SET.inferName(m).equals(name)
             && getter.getReturnType().isAssignableFrom(m.getParameterTypes()[0])) {
           found = m;
diff --git a/user/src/com/google/gwt/autobean/server/impl/MethodPropertyContext.java b/user/src/com/google/gwt/autobean/server/impl/MethodPropertyContext.java
index b88d44b..efbf8f7 100644
--- a/user/src/com/google/gwt/autobean/server/impl/MethodPropertyContext.java
+++ b/user/src/com/google/gwt/autobean/server/impl/MethodPropertyContext.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.autobean.shared.AutoBeanVisitor.CollectionPropertyContext;
 import com.google.gwt.autobean.shared.AutoBeanVisitor.MapPropertyContext;
+import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
 
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
@@ -31,9 +32,10 @@
 abstract class MethodPropertyContext implements CollectionPropertyContext,
     MapPropertyContext {
   private static class Data {
+    Class<?> elementType;
+    Type genericType;
     Class<?> keyType;
     Class<?> valueType;
-    Class<?> elementType;
     Class<?> type;
   }
 
@@ -41,7 +43,6 @@
    * Save prior instances in order to decrease the amount of data computed.
    */
   private static final Map<Method, Data> cache = new WeakHashMap<Method, Data>();
-
   private final Data data;
 
   public MethodPropertyContext(Method getter) {
@@ -53,6 +54,7 @@
       }
 
       this.data = new Data();
+      data.genericType = getter.getGenericReturnType();
       data.type = getter.getReturnType();
       // Compute collection element type
       if (Collection.class.isAssignableFrom(getType())) {
@@ -69,6 +71,10 @@
     }
   }
 
+  public void accept(ParameterizationVisitor visitor) {
+    traverse(visitor, data.genericType);
+  }
+
   public abstract boolean canSet();
 
   public Class<?> getElementType() {
@@ -88,4 +94,18 @@
   }
 
   public abstract void set(Object value);
+
+  private void traverse(ParameterizationVisitor visitor, Type type) {
+    Class<?> base = TypeUtils.ensureBaseType(type);
+    if (visitor.visitType(base)) {
+      Type[] params = TypeUtils.getParameterization(base, type);
+      for (Type t : params) {
+        if (visitor.visitParameter()) {
+          traverse(visitor, t);
+        }
+        visitor.endVisitParameter();
+      }
+    }
+    visitor.endVisitType(base);
+  }
 }
diff --git a/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java b/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
index d2930d6..23c190d 100644
--- a/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
+++ b/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
@@ -18,7 +18,6 @@
 import com.google.gwt.autobean.shared.AutoBean;
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 
-import java.lang.reflect.Array;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
@@ -80,7 +79,8 @@
     } else if (BeanMethod.GET.matches(method)) {
       toReturn = method.invoke(toWrap, args);
       toReturn = bean.get(name, toReturn);
-    } else if (BeanMethod.SET.matches(method)) {
+    } else if (BeanMethod.SET.matches(method)
+        || BeanMethod.SET_BUILDER.matches(method)) {
       bean.checkFrozen();
       toReturn = method.invoke(toWrap, args);
       bean.set(name, args[0]);
@@ -117,12 +117,11 @@
       return toReturn;
     }
     if (toReturn.getClass().isArray()) {
-      for (int i = 0, j = Array.getLength(toReturn); i < j; i++) {
-        Object value = Array.get(toReturn, i);
-        if (value != null) {
-          Array.set(toReturn, i, maybeWrap(value.getClass(), value));
-        }
-      }
+      /*
+       * We can't reliably wrap arrays, but the only time we typically see an
+       * array is with toArray() call on a collection, since arrays aren't
+       * supported property types.
+       */
       return toReturn;
     }
     ProxyAutoBean<Object> newBean = new ProxyAutoBean<Object>(
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
index 2ae2f0e..ec564d6 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.autobean.shared;
 
+import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
 import com.google.gwt.autobean.shared.impl.EnumMap;
 import com.google.gwt.autobean.shared.impl.LazySplittable;
 import com.google.gwt.autobean.shared.impl.StringQuoter;
@@ -23,6 +24,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -34,455 +36,113 @@
  * encode cycles, but it will detect them.
  */
 public class AutoBeanCodex {
-  static class Decoder extends AutoBeanVisitor {
-    private final Stack<AutoBean<?>> beanStack = new Stack<AutoBean<?>>();
-    private final Stack<Splittable> dataStack = new Stack<Splittable>();
-    private AutoBean<?> bean;
-    private Splittable data;
-    private final AutoBeanFactory factory;
 
-    public Decoder(AutoBeanFactory factory) {
-      this.factory = factory;
-    }
+  /**
+   * Describes a means of encoding or decoding a particular type of data to or
+   * from a wire format representation.
+   */
+  interface Coder {
+    Object decode(Splittable data);
 
-    @SuppressWarnings("unchecked")
-    public <T> AutoBean<T> decode(Splittable data, Class<T> type) {
-      push(data, type);
-      bean.accept(this);
-      return (AutoBean<T>) pop();
-    }
+    void encode(StringBuilder sb, Object value);
+  }
+
+  /**
+   * Creates a Coder that is capable of operating on a particular
+   * parameterization of a datastructure (e.g. {@code Map<String, List<String>>}
+   * ).
+   */
+  class CoderCreator extends ParameterizationVisitor {
+    private Stack<Coder> stack = new Stack<Coder>();
 
     @Override
-    public boolean visitCollectionProperty(String propertyName,
-        AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
-      if (data.isNull(propertyName)) {
-        return false;
-      }
-
-      Collection<Object> collection;
-      if (List.class.equals(ctx.getType())) {
-        collection = new ArrayList<Object>();
-      } else if (Set.class.equals(ctx.getType())) {
-        collection = new HashSet<Object>();
+    public void endVisitType(Class<?> type) {
+      if (List.class.equals(type) || Set.class.equals(type)) {
+        stack.push(new CollectionCoder(type, stack.pop()));
+      } else if (Map.class.equals(type)) {
+        // Note that the parameters are passed in reverse order
+        stack.push(new MapCoder(stack.pop(), stack.pop()));
+      } else if (Splittable.class.equals(type)) {
+        stack.push(new SplittableDecoder());
+      } else if (type.getEnumConstants() != null) {
+        @SuppressWarnings(value = {"rawtypes", "unchecked"})
+        EnumCoder decoder = new EnumCoder(type);
+        stack.push(decoder);
+      } else if (ValueCodex.canDecode(type)) {
+        stack.push(new ValueCoder(type));
       } else {
-        throw new UnsupportedOperationException("Only List and Set supported");
+        stack.push(new ObjectCoder(type));
       }
-
-      boolean isValue = ValueCodex.canDecode(ctx.getElementType());
-      boolean isEncoded = Splittable.class.equals(ctx.getElementType());
-      Splittable listData = data.get(propertyName);
-      for (int i = 0, j = listData.size(); i < j; i++) {
-        if (listData.isNull(i)) {
-          collection.add(null);
-        } else {
-          if (isValue) {
-            collection.add(decodeValue(ctx.getElementType(), listData.get(i)));
-          } else if (isEncoded) {
-            collection.add(listData.get(i));
-          } else {
-            collection.add(decode(listData.get(i), ctx.getElementType()).as());
-          }
-        }
-      }
-      ctx.set(collection);
-      return false;
     }
 
-    @Override
-    public boolean visitMapProperty(String propertyName,
-        AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
-      if (data.isNull(propertyName)) {
-        return false;
-      }
-
-      Map<?, ?> map;
-      if (ValueCodex.canDecode(ctx.getKeyType())) {
-        map = decodeValueKeyMap(data.get(propertyName), ctx.getKeyType(),
-            ctx.getValueType());
-      } else {
-        map = decodeObjectKeyMap(data.get(propertyName), ctx.getKeyType(),
-            ctx.getValueType());
-      }
-      ctx.set(map);
-      return false;
-    }
-
-    @Override
-    public boolean visitReferenceProperty(String propertyName,
-        AutoBean<?> value, PropertyContext ctx) {
-      if (data.isNull(propertyName)) {
-        return false;
-      }
-
-      if (Splittable.class.equals(ctx.getType())) {
-        ctx.set(data.get(propertyName));
-        return false;
-      }
-
-      push(data.get(propertyName), ctx.getType());
-      bean.accept(this);
-      ctx.set(pop().as());
-      return false;
-    }
-
-    @Override
-    public boolean visitValueProperty(String propertyName, Object value,
-        PropertyContext ctx) {
-      if (!data.isNull(propertyName)) {
-        Object object;
-        Splittable propertyValue = data.get(propertyName);
-        Class<?> type = ctx.getType();
-        object = decodeValue(type, propertyValue);
-        ctx.set(object);
-      }
-      return false;
-    }
-
-    private Map<?, ?> decodeObjectKeyMap(Splittable map, Class<?> keyType,
-        Class<?> valueType) {
-      boolean isEncodedKey = Splittable.class.equals(keyType);
-      boolean isEncodedValue = Splittable.class.equals(valueType);
-      boolean isValueValue = Splittable.class.equals(valueType);
-
-      Splittable keyList = map.get(0);
-      Splittable valueList = map.get(1);
-      assert keyList.size() == valueList.size();
-
-      Map<Object, Object> toReturn = new HashMap<Object, Object>(keyList.size());
-      for (int i = 0, j = keyList.size(); i < j; i++) {
-        Object key;
-        if (isEncodedKey) {
-          key = keyList.get(i);
-        } else {
-          key = decode(keyList.get(i), keyType).as();
-        }
-
-        Object value;
-        if (valueList.isNull(i)) {
-          value = null;
-        } else if (isEncodedValue) {
-          value = keyList.get(i);
-        } else if (isValueValue) {
-          value = decodeValue(valueType, keyList.get(i));
-        } else {
-          value = decode(valueList.get(i), valueType).as();
-        }
-
-        toReturn.put(key, value);
-      }
-      return toReturn;
-    }
-
-    private Object decodeValue(Class<?> type, Splittable propertyValue) {
-      return decodeValue(type, propertyValue.asString());
-    }
-
-    private Object decodeValue(Class<?> type, String propertyValue) {
-      Object object;
-      if (type.isEnum() && bean.getFactory() instanceof EnumMap) {
-        // The generics kind of get in the way here
-        @SuppressWarnings({"unchecked", "rawtypes"})
-        Class<Enum> enumType = (Class<Enum>) type;
-        @SuppressWarnings("unchecked")
-        Enum<?> e = ((EnumMap) bean.getFactory()).getEnum(enumType,
-            propertyValue);
-        object = e;
-      } else {
-        object = ValueCodex.decode(type, propertyValue);
-      }
-      return object;
-    }
-
-    private Map<?, ?> decodeValueKeyMap(Splittable map, Class<?> keyType,
-        Class<?> valueType) {
-      Map<Object, Object> toReturn = new HashMap<Object, Object>();
-
-      boolean isEncodedValue = Splittable.class.equals(valueType);
-      boolean isValueValue = ValueCodex.canDecode(valueType);
-      for (String encodedKey : map.getPropertyKeys()) {
-        Object key = decodeValue(keyType, encodedKey);
-        Object value;
-        if (map.isNull(encodedKey)) {
-          value = null;
-        } else if (isEncodedValue) {
-          value = map.get(encodedKey);
-        } else if (isValueValue) {
-          value = decodeValue(valueType, map.get(encodedKey));
-        } else {
-          value = decode(map.get(encodedKey), valueType).as();
-        }
-        toReturn.put(key, value);
-      }
-
-      return toReturn;
-    }
-
-    private AutoBean<?> pop() {
-      dataStack.pop();
-      if (dataStack.isEmpty()) {
-        data = null;
-      } else {
-        data = dataStack.peek();
-      }
-      AutoBean<?> toReturn = beanStack.pop();
-      if (beanStack.isEmpty()) {
-        bean = null;
-      } else {
-        bean = beanStack.peek();
-      }
-      return toReturn;
-    }
-
-    private void push(Splittable data, Class<?> type) {
-      this.data = data;
-      bean = factory.create(type);
-      if (bean == null) {
-        throw new IllegalArgumentException(
-            "The AutoBeanFactory cannot create a " + type.getName());
-      }
-      dataStack.push(data);
-      beanStack.push(bean);
+    public Coder getCoder() {
+      assert stack.size() == 1 : "Incorrect size: " + stack.size();
+      return stack.pop();
     }
   }
 
-  static class Encoder extends AutoBeanVisitor {
-    private EnumMap enumMap;
-    private Set<AutoBean<?>> seen = new HashSet<AutoBean<?>>();
-    private Stack<StringBuilder> stack = new Stack<StringBuilder>();
-    private StringBuilder sb;
+  class CollectionCoder implements Coder {
+    private final Coder elementDecoder;
+    private final Class<?> type;
 
-    public Encoder(AutoBeanFactory factory) {
-      if (factory instanceof EnumMap) {
-        enumMap = (EnumMap) factory;
-      }
+    public CollectionCoder(Class<?> type, Coder elementDecoder) {
+      this.elementDecoder = elementDecoder;
+      this.type = type;
     }
 
-    @Override
-    public void endVisit(AutoBean<?> bean, Context ctx) {
-      if (sb.length() == 0) {
-        // No properties
-        sb.append("{");
+    public Object decode(Splittable data) {
+      Collection<Object> collection;
+      if (List.class.equals(type)) {
+        collection = new ArrayList<Object>();
+      } else if (Set.class.equals(type)) {
+        collection = new HashSet<Object>();
       } else {
-        sb.setCharAt(0, '{');
+        // Should not reach here
+        throw new RuntimeException(type.getName());
       }
-      sb.append("}");
+      for (int i = 0, j = data.size(); i < j; i++) {
+        Object element = data.isNull(i) ? null
+            : elementDecoder.decode(data.get(i));
+        collection.add(element);
+      }
+      return collection;
     }
 
-    @Override
-    public void endVisitReferenceProperty(String propertyName,
-        AutoBean<?> value, PropertyContext ctx) {
-      StringBuilder popped = pop();
-      if (popped.length() > 0) {
-        sb.append(",\"").append(propertyName).append("\":").append(
-            popped.toString());
-      }
-    }
-
-    @Override
-    public boolean visitCollectionProperty(String propertyName,
-        AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
-      push(new StringBuilder());
-
+    public void encode(StringBuilder sb, Object value) {
       if (value == null) {
-        return false;
+        sb.append("null");
+        return;
       }
 
-      Collection<?> collection = value.as();
-      if (collection.isEmpty()) {
-        sb.append("[]");
-        return false;
-      }
-
-      if (ValueCodex.canDecode(ctx.getElementType())) {
-        for (Object element : collection) {
-          sb.append(",").append(
-              encodeValue(ctx.getElementType(), element).getPayload());
-        }
-      } else {
-        boolean isEncoded = Splittable.class.equals(ctx.getElementType());
-        for (Object element : collection) {
+      Iterator<?> it = ((Collection<?>) value).iterator();
+      sb.append("[");
+      if (it.hasNext()) {
+        elementDecoder.encode(sb, it.next());
+        while (it.hasNext()) {
           sb.append(",");
-          if (element == null) {
-            sb.append("null");
-          } else if (isEncoded) {
-            sb.append(((Splittable) element).getPayload());
-          } else {
-            encodeToStringBuilder(sb, element);
-          }
+          elementDecoder.encode(sb, it.next());
         }
       }
-      sb.setCharAt(0, '[');
       sb.append("]");
-      return false;
+    }
+  }
+
+  class EnumCoder<E extends Enum<E>> implements Coder {
+    private final Class<E> type;
+
+    public EnumCoder(Class<E> type) {
+      this.type = type;
     }
 
-    @Override
-    public boolean visitMapProperty(String propertyName,
-        AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
-      push(new StringBuilder());
+    public Object decode(Splittable data) {
+      return enumMap.getEnum(type, data.asString());
+    }
 
+    public void encode(StringBuilder sb, Object value) {
       if (value == null) {
-        return false;
+        sb.append("null");
       }
-
-      Map<?, ?> map = value.as();
-      if (map.isEmpty()) {
-        sb.append("{}");
-        return false;
-      }
-
-      Class<?> keyType = ctx.getKeyType();
-      Class<?> valueType = ctx.getValueType();
-      boolean isEncodedKey = Splittable.class.equals(keyType);
-      boolean isEncodedValue = Splittable.class.equals(valueType);
-      boolean isValueKey = ValueCodex.canDecode(keyType);
-      boolean isValueValue = ValueCodex.canDecode(valueType);
-
-      if (isValueKey) {
-        writeValueKeyMap(map, keyType, valueType, isEncodedValue, isValueValue);
-      } else {
-        writeObjectKeyMap(map, valueType, isEncodedKey, isEncodedValue,
-            isValueValue);
-      }
-
-      return false;
-    }
-
-    @Override
-    public boolean visitReferenceProperty(String propertyName,
-        AutoBean<?> value, PropertyContext ctx) {
-      push(new StringBuilder());
-
-      if (value == null) {
-        return false;
-      }
-
-      if (Splittable.class.equals(ctx.getType())) {
-        sb.append(((Splittable) value.as()).getPayload());
-        return false;
-      }
-
-      if (seen.contains(value)) {
-        haltOnCycle();
-      }
-
-      return true;
-    }
-
-    @Override
-    public boolean visitValueProperty(String propertyName, Object value,
-        PropertyContext ctx) {
-      // Skip primitive types whose values are uninteresting.
-      Class<?> type = ctx.getType();
-      Object blankValue = ValueCodex.getUninitializedFieldValue(type);
-      if (value == blankValue || value != null && value.equals(blankValue)) {
-        return false;
-      }
-
-      // Special handling for enums if we have an obfuscation map
-      Splittable split;
-      split = encodeValue(type, value);
-      sb.append(",\"").append(propertyName).append("\":").append(
-          split.getPayload());
-      return false;
-    }
-
-    StringBuilder pop() {
-      StringBuilder toReturn = stack.pop();
-      sb = stack.peek();
-      return toReturn;
-    }
-
-    void push(StringBuilder sb) {
-      stack.push(sb);
-      this.sb = sb;
-    }
-
-    private void encodeToStringBuilder(StringBuilder accumulator, Object value) {
-      push(new StringBuilder());
-      AutoBean<?> bean = AutoBeanUtils.getAutoBean(value);
-      if (!seen.add(bean)) {
-        haltOnCycle();
-      }
-      bean.accept(this);
-      accumulator.append(pop().toString());
-      seen.remove(bean);
-    }
-
-    /**
-     * Encodes a value, with special handling for enums to allow the field name
-     * to be overridden.
-     */
-    private Splittable encodeValue(Class<?> expectedType, Object value) {
-      Splittable split;
-      if (value instanceof Enum<?> && enumMap != null) {
-        split = ValueCodex.encode(String.class,
-            enumMap.getToken((Enum<?>) value));
-      } else {
-        split = ValueCodex.encode(expectedType, value);
-      }
-      return split;
-    }
-
-    private void haltOnCycle() {
-      throw new HaltException(new UnsupportedOperationException(
-          "Cycle detected"));
-    }
-
-    /**
-     * Writes a map JSON literal where the keys are object types. This is
-     * encoded as a list of two lists, since it's possible that two distinct
-     * objects have the same encoded form.
-     */
-    private void writeObjectKeyMap(Map<?, ?> map, Class<?> valueType,
-        boolean isEncodedKey, boolean isEncodedValue, boolean isValueValue) {
-      StringBuilder keys = new StringBuilder();
-      StringBuilder values = new StringBuilder();
-
-      for (Map.Entry<?, ?> entry : map.entrySet()) {
-        if (isEncodedKey) {
-          keys.append(",").append(((Splittable) entry.getKey()).getPayload());
-        } else {
-          encodeToStringBuilder(keys.append(","), entry.getKey());
-        }
-
-        if (isEncodedValue) {
-          values.append(",").append(
-              ((Splittable) entry.getValue()).getPayload());
-        } else if (isValueValue) {
-          values.append(",").append(
-              encodeValue(valueType, entry.getValue()).getPayload());
-        } else {
-          encodeToStringBuilder(values.append(","), entry.getValue());
-        }
-      }
-      keys.setCharAt(0, '[');
-      keys.append("]");
-      values.setCharAt(0, '[');
-      values.append("]");
-
-      sb.append("[").append(keys.toString()).append(",").append(
-          values.toString()).append("]");
-    }
-
-    /**
-     * Writes a map JSON literal where the keys are value types.
-     */
-    private void writeValueKeyMap(Map<?, ?> map, Class<?> keyType,
-        Class<?> valueType, boolean isEncodedValue, boolean isValueValue) {
-      for (Map.Entry<?, ?> entry : map.entrySet()) {
-        sb.append(",").append(encodeValue(keyType, entry.getKey()).getPayload()).append(
-            ":");
-        if (isEncodedValue) {
-          sb.append(((Splittable) entry.getValue()).getPayload());
-        } else if (isValueValue) {
-          sb.append(encodeValue(valueType, entry.getValue()).getPayload());
-        } else {
-          encodeToStringBuilder(sb, entry.getValue());
-        }
-      }
-      sb.setCharAt(0, '{');
-      sb.append("}");
+      sb.append(StringQuoter.quote(enumMap.getToken((Enum<?>) value)));
     }
   }
 
@@ -500,9 +160,250 @@
     }
   }
 
+  class MapCoder implements Coder {
+    private final Coder keyDecoder;
+    private final Coder valueDecoder;
+
+    /**
+     * Parameters in reversed order to accommodate stack-based setup.
+     */
+    public MapCoder(Coder valueDecoder, Coder keyDecoder) {
+      this.keyDecoder = keyDecoder;
+      this.valueDecoder = valueDecoder;
+    }
+
+    public Object decode(Splittable data) {
+      Map<Object, Object> toReturn = new HashMap<Object, Object>();
+      if (data.isIndexed()) {
+        assert data.size() == 2 : "Wrong data size: " + data.size();
+        Splittable keys = data.get(0);
+        Splittable values = data.get(1);
+        for (int i = 0, j = keys.size(); i < j; i++) {
+          Object key = keys.isNull(i) ? null : keyDecoder.decode(keys.get(i));
+          Object value = values.isNull(i) ? null
+              : valueDecoder.decode(values.get(i));
+          toReturn.put(key, value);
+        }
+      } else {
+        ValueCoder keyValueDecoder = (ValueCoder) keyDecoder;
+        for (String rawKey : data.getPropertyKeys()) {
+          Object key = keyValueDecoder.decode(rawKey);
+          Object value = data.isNull(rawKey) ? null
+              : valueDecoder.decode(data.get(rawKey));
+          toReturn.put(key, value);
+        }
+      }
+      return toReturn;
+    }
+
+    public void encode(StringBuilder sb, Object value) {
+      if (value == null) {
+        sb.append("null");
+        return;
+      }
+
+      Map<?, ?> map = (Map<?, ?>) value;
+      boolean isSimpleMap = keyDecoder instanceof ValueCoder;
+      if (isSimpleMap) {
+        boolean first = true;
+        sb.append("{");
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+          Object mapKey = entry.getKey();
+          if (mapKey == null) {
+            // A null key in a simple map is meaningless
+            continue;
+          }
+          Object mapValue = entry.getValue();
+          if (mapValue == null) {
+            // A null value can be ignored
+            continue;
+          }
+
+          if (first) {
+            first = false;
+          } else {
+            sb.append(",");
+          }
+
+          keyDecoder.encode(sb, mapKey);
+          sb.append(":");
+          valueDecoder.encode(sb, mapValue);
+        }
+        sb.append("}");
+      } else {
+        List<Object> keys = new ArrayList<Object>(map.size());
+        List<Object> values = new ArrayList<Object>(map.size());
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+          keys.add(entry.getKey());
+          values.add(entry.getValue());
+        }
+        sb.append("[");
+        new CollectionCoder(List.class, keyDecoder).encode(sb, keys);
+        sb.append(",");
+        new CollectionCoder(List.class, valueDecoder).encode(sb, values);
+        sb.append("]");
+      }
+    }
+  }
+
+  class ObjectCoder implements Coder {
+    private final Class<?> type;
+
+    public ObjectCoder(Class<?> type) {
+      this.type = type;
+    }
+
+    public Object decode(Splittable data) {
+      AutoBean<?> bean = doDecode(type, data);
+      return bean == null ? null : bean.as();
+    }
+
+    public void encode(StringBuilder sb, Object value) {
+      if (value == null) {
+        sb.append("null");
+        return;
+      }
+      doEncode(sb, AutoBeanUtils.getAutoBean(value));
+    }
+  }
+
+  /**
+   * Extracts properties from a bean and turns them into JSON text.
+   */
+  class PropertyGetter extends AutoBeanVisitor {
+    private boolean first = true;
+    private final StringBuilder sb;
+
+    public PropertyGetter(StringBuilder sb) {
+      this.sb = sb;
+    }
+
+    @Override
+    public void endVisit(AutoBean<?> bean, Context ctx) {
+      sb.append("}");
+      seen.pop();
+    }
+
+    @Override
+    public boolean visit(AutoBean<?> bean, Context ctx) {
+      if (seen.contains(bean)) {
+        throw new HaltException(new UnsupportedOperationException(
+            "Cycles not supported"));
+      }
+      seen.push(bean);
+      sb.append("{");
+      return true;
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName,
+        AutoBean<?> value, PropertyContext ctx) {
+      if (value != null) {
+        encodeProperty(propertyName, value.as(), ctx);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value,
+        PropertyContext ctx) {
+      if (value != null
+          && !value.equals(ValueCodex.getUninitializedFieldValue(ctx.getType()))) {
+        encodeProperty(propertyName, value, ctx);
+      }
+      return false;
+    }
+
+    private void encodeProperty(String propertyName, Object value,
+        PropertyContext ctx) {
+      CoderCreator pd = new CoderCreator();
+      ctx.accept(pd);
+      Coder decoder = pd.getCoder();
+      if (first) {
+        first = false;
+      } else {
+        sb.append(",");
+      }
+      sb.append(StringQuoter.quote(propertyName));
+      sb.append(":");
+      decoder.encode(sb, value);
+    }
+  }
+
+  /**
+   * Populates beans with data extracted from an evaluated JSON payload.
+   */
+  class PropertySetter extends AutoBeanVisitor {
+    private Splittable data;
+
+    public void decodeInto(Splittable data, AutoBean<?> bean) {
+      this.data = data;
+      bean.accept(this);
+    }
+
+    @Override
+    public boolean visitReferenceProperty(String propertyName,
+        AutoBean<?> value, PropertyContext ctx) {
+      decodeProperty(propertyName, ctx);
+      return false;
+    }
+
+    @Override
+    public boolean visitValueProperty(String propertyName, Object value,
+        PropertyContext ctx) {
+      decodeProperty(propertyName, ctx);
+      return false;
+    }
+
+    protected void decodeProperty(String propertyName, PropertyContext ctx) {
+      if (!data.isNull(propertyName)) {
+        CoderCreator pd = new CoderCreator();
+        ctx.accept(pd);
+        Coder decoder = pd.getCoder();
+        Object propertyValue = decoder.decode(data.get(propertyName));
+        ctx.set(propertyValue);
+      }
+    }
+  }
+
+  class SplittableDecoder implements Coder {
+    public Object decode(Splittable data) {
+      return data;
+    }
+
+    public void encode(StringBuilder sb, Object value) {
+      if (value == null) {
+        sb.append("null");
+        return;
+      }
+      sb.append(((Splittable) value).getPayload());
+    }
+  }
+
+  class ValueCoder implements Coder {
+    private final Class<?> type;
+
+    public ValueCoder(Class<?> type) {
+      assert type.getEnumConstants() == null : "Should use EnumTypeCodex";
+      this.type = type;
+    }
+
+    public Object decode(Splittable propertyValue) {
+      return decode(propertyValue.asString());
+    }
+
+    public Object decode(String propertyValue) {
+      return ValueCodex.decode(type, propertyValue);
+    }
+
+    public void encode(StringBuilder sb, Object value) {
+      sb.append(ValueCodex.encode(value).getPayload());
+    }
+  }
+
   public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz,
       Splittable data) {
-    return new Decoder(factory).decode(data, clazz);
+    return new AutoBeanCodex(factory).doDecode(clazz, data);
   }
 
   /**
@@ -534,21 +435,38 @@
     }
 
     StringBuilder sb = new StringBuilder();
-    encodeForJsoPayload(sb, bean);
+    new AutoBeanCodex(bean.getFactory()).doEncode(sb, bean);
     return new LazySplittable(sb.toString());
   }
 
-  // ["prop",value,"prop",value, ...]
-  private static void encodeForJsoPayload(StringBuilder sb, AutoBean<?> bean) {
-    Encoder e = new Encoder(bean.getFactory());
-    e.push(sb);
+  private final EnumMap enumMap;
+  private final AutoBeanFactory factory;
+  private final Stack<AutoBean<?>> seen = new Stack<AutoBean<?>>();
+
+  private AutoBeanCodex(AutoBeanFactory factory) {
+    this.factory = factory;
+    this.enumMap = factory instanceof EnumMap ? (EnumMap) factory : null;
+  }
+
+  <T> AutoBean<T> doDecode(Class<T> clazz, Splittable data) {
+    AutoBean<T> toReturn = factory.create(clazz);
+    if (toReturn == null) {
+      throw new IllegalArgumentException(clazz.getName());
+    }
+    doDecodeInto(data, toReturn);
+    return toReturn;
+  }
+
+  void doDecodeInto(Splittable data, AutoBean<?> bean) {
+    new PropertySetter().decodeInto(data, bean);
+  }
+
+  void doEncode(StringBuilder sb, AutoBean<?> bean) {
+    PropertyGetter e = new PropertyGetter(sb);
     try {
       bean.accept(e);
     } catch (HaltException ex) {
       throw ex.getCause();
     }
   }
-
-  private AutoBeanCodex() {
-  }
 }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanVisitor.java b/user/src/com/google/gwt/autobean/shared/AutoBeanVisitor.java
index 49e81dc..39358ac 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanVisitor.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanVisitor.java
@@ -62,10 +62,73 @@
   }
 
   /**
+   * The ParameterizationVisitor provides access to more complete type
+   * information than a simple class literal can provide.
+   * <p>
+   * The order of traversal reflects the declared parameterization of the
+   * property. For example, a {@code Map<String, List<Foo>>} would be traversed
+   * via the following sequence:
+   * 
+   * <pre>
+   * visitType(Map.class);
+   *   visitParameter();
+   *     visitType(String.class);
+   *     endVisitType(String.class);
+   *   endVisitParameter();
+   *   visitParameter();
+   *     visitType(List.class);
+   *       visitParameter();
+   *         visitType(Foo.class);
+   *         endVisitType(Foo.class);
+   *       endParameter();
+   *     endVisitType(List.class);
+   *   endVisitParameter();
+   * endVisitType(Map.class);
+   * </pre>
+   */
+  public static class ParameterizationVisitor {
+    /**
+     * Called when finished with a type parameter.
+     */
+    public void endVisitParameter() {
+    }
+
+    /**
+     * Called when finished with a type.
+     */
+    public void endVisitType(Class<?> type) {
+    }
+
+    /**
+     * Called when visiting a type parameter.
+     * 
+     * @return {@code true} if the type parameter should be visited
+     */
+    public boolean visitParameter() {
+      return true;
+    }
+
+    /**
+     * Called when visiting a possibly parameterized type.
+     * 
+     * @return {@code true} if the type should be visited
+     */
+    public boolean visitType(Class<?> type) {
+      return true;
+    }
+  }
+
+  /**
    * Allows properties to be reset.
    */
   public interface PropertyContext {
     /**
+     * Allows deeper inspection of the declared parameterization of the
+     * property.
+     */
+    void accept(ParameterizationVisitor visitor);
+
+    /**
      * Indicates if the {@link #set} method will succeed.
      * 
      * @return {@code true} if the property can be set
diff --git a/user/src/com/google/gwt/autobean/shared/impl/AbstractPropertyContext.java b/user/src/com/google/gwt/autobean/shared/impl/AbstractPropertyContext.java
new file mode 100644
index 0000000..b04b0e9
--- /dev/null
+++ b/user/src/com/google/gwt/autobean/shared/impl/AbstractPropertyContext.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2011 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.autobean.shared.impl;
+
+import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
+import com.google.gwt.autobean.shared.AutoBeanVisitor.PropertyContext;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides base methods for generated implementations of PropertyContext.
+ */
+public abstract class AbstractPropertyContext implements PropertyContext {
+
+  private final Class<?>[] types;
+  private final int[] paramCounts;
+
+  protected AbstractPropertyContext(Class<?>[] types, int[] paramCounts) {
+    this.types = types;
+    this.paramCounts = paramCounts;
+  }
+
+  public void accept(ParameterizationVisitor visitor) {
+    traverse(visitor, 0);
+  }
+
+  public boolean canSet() {
+    return true;
+  }
+
+  /**
+   * @see com.google.gwt.autobean.shared.AutoBeanVisitor.CollectionPropertyContext#getElementType()
+   */
+  public Class<?> getElementType() {
+    assert types.length >= 2;
+    assert List.class.equals(types[0]) || Set.class.equals(types[0]);
+    return types[1];
+  }
+
+  /**
+   * @see com.google.gwt.autobean.shared.AutoBeanVisitor.MapPropertyContext#getKeyType()
+   */
+  public Class<?> getKeyType() {
+    assert types.length >= 2;
+    assert Map.class.equals(types[0]);
+    return types[1];
+  }
+
+  public Class<?> getType() {
+    return types[0];
+  }
+
+  /**
+   * @see com.google.gwt.autobean.shared.AutoBeanVisitor.MapPropertyContext#getValueType()
+   */
+  public Class<?> getValueType() {
+    assert types.length >= 2;
+    assert Map.class.equals(types[0]);
+    return types[2];
+  }
+
+  private int traverse(ParameterizationVisitor visitor, int count) {
+    Class<?> type = types[count];
+    int paramCount = paramCounts[count];
+    ++count;
+    if (visitor.visitType(type)) {
+      for (int i = 0; i < paramCount; i++) {
+        if (visitor.visitParameter()) {
+          count = traverse(visitor, count);
+        }
+        visitor.endVisitParameter();
+      }
+    }
+    visitor.endVisitType(type);
+    return count;
+  }
+}
diff --git a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
index 759b93c..cc61684 100644
--- a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
+++ b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
@@ -20,12 +20,15 @@
 import com.google.gwt.autobean.shared.AutoBeanFactory.Category;
 import com.google.gwt.autobean.shared.AutoBeanUtils;
 import com.google.gwt.autobean.shared.AutoBeanVisitor;
+import com.google.gwt.autobean.shared.AutoBeanVisitor.ParameterizationVisitor;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Stack;
 
 /**
  * Tests runtime behavior of AutoBean framework.
@@ -58,8 +61,14 @@
 
     AutoBean<HasCall> hasCall();
 
+    AutoBean<HasChainedSetters> hasChainedSetters();
+
     AutoBean<HasList> hasList();
 
+    AutoBean<HasComplexTypes> hasListOfList();
+
+    AutoBean<HasMoreChainedSetters> hasMoreChainedSetters();
+
     AutoBean<Intf> intf();
 
     AutoBean<Intf> intf(RealIntf wrapped);
@@ -68,29 +77,55 @@
   }
 
   interface HasBoolean {
-    boolean isIs();
-
     boolean getGet();
 
     boolean hasHas();
 
-    void setIs(boolean value);
+    boolean isIs();
 
     void setGet(boolean value);
 
     void setHas(boolean value);
+
+    void setIs(boolean value);
   }
 
   interface HasCall {
     int add(int a, int b);
   }
 
+  interface HasChainedSetters {
+    int getInt();
+
+    String getString();
+
+    HasChainedSetters setInt(int value);
+
+    HasChainedSetters setString(String value);
+  }
+
+  interface HasComplexTypes {
+    List<List<Intf>> getList();
+
+    List<Map<String, Intf>> getListOfMap();
+
+    Map<Map<String, String>, List<List<Intf>>> getMap();
+  }
+
   interface HasList {
     List<Intf> getList();
 
     void setList(List<Intf> list);
   }
 
+  interface HasMoreChainedSetters extends HasChainedSetters {
+    boolean isBoolean();
+
+    HasMoreChainedSetters setBoolean(boolean value);
+
+    HasMoreChainedSetters setInt(int value);
+  }
+
   interface Intf {
     int getInt();
 
@@ -102,15 +137,15 @@
   }
 
   interface OtherIntf {
-    Intf getIntf();
-
     HasBoolean getHasBoolean();
 
+    Intf getIntf();
+
     UnreferencedInFactory getUnreferenced();
 
-    void setIntf(Intf intf);
-
     void setHasBoolean(HasBoolean value);
+
+    void setIntf(Intf intf);
   }
 
   static class RealIntf implements Intf {
@@ -151,6 +186,41 @@
   interface UnreferencedInFactory {
   }
 
+  private static class ParameterizationTester extends ParameterizationVisitor {
+    private final StringBuilder sb;
+    private Stack<Boolean> isOpen = new Stack<Boolean>();
+
+    private ParameterizationTester(StringBuilder sb) {
+      this.sb = sb;
+    }
+
+    @Override
+    public void endVisitType(Class<?> type) {
+      if (isOpen.pop()) {
+        sb.append(">");
+      }
+    }
+
+    @Override
+    public boolean visitParameter() {
+      if (isOpen.peek()) {
+        sb.append(",");
+      } else {
+        sb.append("<");
+        isOpen.pop();
+        isOpen.push(true);
+      }
+      return true;
+    }
+
+    @Override
+    public boolean visitType(Class<?> type) {
+      sb.append(type.getName());
+      isOpen.push(false);
+      return true;
+    }
+  }
+
   protected Factory factory;
 
   @Override
@@ -180,6 +250,19 @@
     assertEquals(6, CallImpl.seen);
   }
 
+  public void testChainedSetters() {
+    AutoBean<HasChainedSetters> bean = factory.hasChainedSetters();
+    bean.as().setInt(42).setString("Blah");
+    assertEquals(42, bean.as().getInt());
+    assertEquals("Blah", bean.as().getString());
+
+    AutoBean<HasMoreChainedSetters> more = factory.hasMoreChainedSetters();
+    more.as().setInt(42).setBoolean(true).setString("Blah");
+    assertEquals(42, more.as().getInt());
+    assertTrue(more.as().isBoolean());
+    assertEquals("Blah", more.as().getString());
+  }
+
   public void testClone() {
     AutoBean<Intf> a1 = factory.intf();
 
@@ -341,6 +424,55 @@
     assertNotNull(AutoBeanUtils.getAutoBean(retrieved));
   }
 
+  public void testParameterizationVisitor() {
+    AutoBean<HasComplexTypes> auto = factory.hasListOfList();
+    auto.accept(new AutoBeanVisitor() {
+      int count = 0;
+
+      @Override
+      public void endVisit(AutoBean<?> bean, Context ctx) {
+        assertEquals(3, count);
+      }
+
+      @Override
+      public void endVisitCollectionProperty(String propertyName,
+          AutoBean<Collection<?>> value, CollectionPropertyContext ctx) {
+        check(propertyName, ctx);
+      }
+
+      @Override
+      public void endVisitMapProperty(String propertyName,
+          AutoBean<Map<?, ?>> value, MapPropertyContext ctx) {
+        check(propertyName, ctx);
+      }
+
+      private void check(String propertyName, PropertyContext ctx) {
+        count++;
+        StringBuilder sb = new StringBuilder();
+        ctx.accept(new ParameterizationTester(sb));
+
+        if ("list".equals(propertyName)) {
+          // List<List<Intf>>
+          assertEquals(List.class.getName() + "<" + List.class.getName() + "<"
+              + Intf.class.getName() + ">>", sb.toString());
+        } else if ("listOfMap".equals(propertyName)) {
+          // List<Map<String, Intf>>
+          assertEquals(List.class.getName() + "<" + Map.class.getName() + "<"
+              + String.class.getName() + "," + Intf.class.getName() + ">>",
+              sb.toString());
+        } else if ("map".equals(propertyName)) {
+          // Map<Map<String, String>, List<List<Intf>>>
+          assertEquals(Map.class.getName() + "<" + Map.class.getName() + "<"
+              + String.class.getName() + "," + String.class.getName() + ">,"
+              + List.class.getName() + "<" + List.class.getName() + "<"
+              + Intf.class.getName() + ">>>", sb.toString());
+        } else {
+          throw new RuntimeException(propertyName);
+        }
+      }
+    });
+  }
+
   /**
    * Make sure primitive properties can be returned.
    */
diff --git a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
index 798bdce..8e686b8 100644
--- a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
+++ b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
@@ -34,7 +34,7 @@
    * Protected so that the JRE-only test can instantiate instances.
    */
   protected interface Factory extends AutoBeanFactory {
-    AutoBean<HasAutoBean> hasAutoBean();
+    AutoBean<HasSplittable> hasAutoBean();
 
     AutoBean<HasCycle> hasCycle();
 
@@ -65,20 +65,6 @@
     FOO_VALUE
   }
 
-  interface HasAutoBean {
-    Splittable getSimple();
-
-    List<Splittable> getSimpleList();
-
-    Splittable getString();
-
-    void setSimple(Splittable simple);
-
-    void setSimpleList(List<Splittable> simple);
-
-    void setString(Splittable s);
-  }
-
   /**
    * Used to test that cycles are detected.
    */
@@ -119,10 +105,14 @@
   interface HasMap {
     Map<Simple, Simple> getComplexMap();
 
+    Map<Map<String, String>, Map<String, String>> getNestedMap();
+
     Map<String, Simple> getSimpleMap();
 
     void setComplexMap(Map<Simple, Simple> map);
 
+    void setNestedMap(Map<Map<String, String>, Map<String, String>> map);
+
     void setSimpleMap(Map<String, Simple> map);
   }
 
@@ -132,6 +122,24 @@
     void setSimple(Simple s);
   }
 
+  interface HasSplittable {
+    Splittable getSimple();
+
+    List<Splittable> getSimpleList();
+
+    Map<Splittable, Splittable> getSplittableMap();
+
+    Splittable getString();
+
+    void setSimple(Splittable simple);
+
+    void setSimpleList(List<Splittable> simple);
+
+    void setSplittableMap(Map<Splittable, Splittable> map);
+
+    void setString(Splittable s);
+  }
+
   enum MyEnum {
     FOO, BAR,
     // The eclipse formatter wants to put this annotation inline
@@ -264,6 +272,26 @@
     }
   }
 
+  /**
+   * Verify that arbitrarily complicated Maps of Maps work.
+   */
+  public void testNestedMap() {
+    Map<String, String> key = new HashMap<String, String>();
+    key.put("a", "b");
+
+    Map<String, String> value = new HashMap<String, String>();
+    value.put("c", "d");
+
+    Map<Map<String, String>, Map<String, String>> test = new HashMap<Map<String, String>, Map<String, String>>();
+    test.put(key, value);
+
+    AutoBean<HasMap> bean = f.hasMap();
+    bean.as().setNestedMap(test);
+
+    AutoBean<HasMap> decoded = checkEncode(bean);
+    assertEquals(1, decoded.as().getNestedMap().size());
+  }
+
   public void testNull() {
     AutoBean<Simple> bean = f.simple();
     AutoBean<Simple> decodedBean = checkEncode(bean);
@@ -308,20 +336,27 @@
   public void testSplittable() {
     AutoBean<Simple> simple = f.simple();
     simple.as().setString("Simple");
-    AutoBean<HasAutoBean> bean = f.hasAutoBean();
+    AutoBean<HasSplittable> bean = f.hasAutoBean();
     bean.as().setSimple(AutoBeanCodex.encode(simple));
     bean.as().setString(ValueCodex.encode("Hello ['\"] world"));
     List<Splittable> testList = Arrays.asList(AutoBeanCodex.encode(simple),
         null, AutoBeanCodex.encode(simple));
     bean.as().setSimpleList(testList);
+    Map<Splittable, Splittable> testMap = Collections.singletonMap(
+        ValueCodex.encode("12345"), ValueCodex.encode("5678"));
+    bean.as().setSplittableMap(testMap);
 
-    AutoBean<HasAutoBean> decoded = checkEncode(bean);
+    AutoBean<HasSplittable> decoded = checkEncode(bean);
     Splittable toDecode = decoded.as().getSimple();
     AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class,
         toDecode);
     assertEquals("Simple", decodedSimple.as().getString());
     assertEquals("Hello ['\"] world",
         ValueCodex.decode(String.class, decoded.as().getString()));
+    assertEquals("12345",
+        decoded.as().getSplittableMap().keySet().iterator().next().asString());
+    assertEquals("5678",
+        decoded.as().getSplittableMap().values().iterator().next().asString());
 
     List<Splittable> list = decoded.as().getSimpleList();
     assertEquals(3, list.size());