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());