Convert AutoBeans to use JSOs as their backing store with lazy reification of values. Use JsonSplittable in DevMode to avoid JSNI overhead. Review at http://gwt-code-reviews.appspot.com/1407802 Patch by: bobv Review by: rice git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9955 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java index 11f6998..5eb9763 100644 --- a/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java +++ b/dev/core/src/com/google/gwt/dev/shell/CompilingClassLoader.java
@@ -535,7 +535,7 @@ */ JClassType implementingType = typeOracle.getSingleJsoImpl(intfMethod.getEnclosingType()); - if (implementingType == null) { + if (implementingType == null || implementingType.isAnnotationPresent(GwtScriptOnly.class)) { /* * This means that there is no concrete implementation of the * interface by a JSO. Any implementation that might be created by a
diff --git a/tools/api-checker/config/gwt22_23userApi.conf b/tools/api-checker/config/gwt22_23userApi.conf index 88cb8f5..7041d0b 100644 --- a/tools/api-checker/config/gwt22_23userApi.conf +++ b/tools/api-checker/config/gwt22_23userApi.conf
@@ -16,7 +16,9 @@ :**/tools/**\ :com/google/gwt/regexp/shared/**\ :com/google/gwt/autobean/**/impl/**\ +:com/google/gwt/autobean/shared/ValueCodex.java\ :com/google/gwt/autobean/shared/ValueCodexHelper.java\ +:com/google/gwt/autobean/shared/AutoBeanCodex.java\ :com/google/gwt/core/client/impl/WeakMapping.java\ :com/google/gwt/core/ext/**\ :com/google/gwt/dev/*.java\
diff --git a/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java b/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java index 0310304..f8f9e3e 100644 --- a/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java +++ b/user/src/com/google/gwt/autobean/client/impl/AbstractAutoBeanFactory.java
@@ -46,7 +46,7 @@ /** * EnumMap support. */ - public <E extends Enum<E>> E getEnum(Class<E> clazz, String token) { + public <E extends Enum<?>> E getEnum(Class<E> clazz, String token) { maybeInitializeEnumMap(); List<Enum<?>> list = stringsToEnumsMap.get(token); if (list == null) {
diff --git a/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java b/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java index 73a077a..620c9ec 100644 --- a/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java +++ b/user/src/com/google/gwt/autobean/client/impl/ClientPropertyContext.java
@@ -18,6 +18,8 @@ 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 com.google.gwt.autobean.shared.AutoBeanVisitor.PropertyContext; +import com.google.gwt.autobean.shared.impl.AbstractAutoBean; import com.google.gwt.core.client.JavaScriptObject; import java.util.List; @@ -27,18 +29,20 @@ /** * Provides base methods for generated implementations of PropertyContext. */ -public final class ClientPropertyContext implements CollectionPropertyContext, MapPropertyContext { +public final class ClientPropertyContext implements PropertyContext, CollectionPropertyContext, + MapPropertyContext { /** * A reference to an instance setter method. */ public static final class Setter extends JavaScriptObject { /** - * Create a trivial Setter that calls {@link Map#put(Object, Object)}. + * Create a trivial Setter that calls {@link AbstractAutoBean#setProperty()} + * . */ - public static native Setter mapSetter(Map<String, Object> values, String key) /*-{ + public static native Setter beanSetter(AbstractAutoBean<?> bean, String key) /*-{ return function(value) { - values.@java.util.Map::put(*)(key, value); + bean.@com.google.gwt.autobean.shared.impl.AbstractAutoBean::setProperty(*)(key, value); }; }-*/;
diff --git a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java index 6a86993..f6d3f3f 100644 --- a/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java +++ b/user/src/com/google/gwt/autobean/client/impl/JsoSplittable.java
@@ -16,8 +16,11 @@ package com.google.gwt.autobean.client.impl; import com.google.gwt.autobean.shared.Splittable; +import com.google.gwt.autobean.shared.impl.HasSplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; +import com.google.gwt.core.client.GwtScriptOnly; import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsonUtils; import java.util.ArrayList; import java.util.Collections; @@ -25,91 +28,106 @@ /** * Implements the EntityCodex.Splittable interface using a raw JavaScriptObject. + * <p> + * A string value represented by a JsoSplittable can't use the string object + * directly, since {@code String.prototype} is overridden, so instead a + * temporary wrapper object is used to encapsulate the string data. */ -public final class JsoSplittable extends JavaScriptObject implements Splittable { - /** - * This type is used because we can't treat Strings as JSOs. - */ - public static class StringSplittable implements Splittable { - private final String value; +@GwtScriptOnly +public final class JsoSplittable extends JavaScriptObject implements Splittable, HasSplittable { + public static native JsoSplittable create() /*-{ + return {}; + }-*/; - public StringSplittable(String value) { - this.value = value; - } - - public String asString() { - return value; - } - - public Splittable get(int index) { - throw new UnsupportedOperationException(); - } - - public Splittable get(String key) { - throw new UnsupportedOperationException(); - } - - public String getPayload() { - return StringQuoter.quote(value); - } - - public List<String> getPropertyKeys() { - return Collections.emptyList(); - } - - public boolean isIndexed() { - return false; - } - - public boolean isKeyed() { - return false; - } - - public boolean isNull(int index) { - throw new UnsupportedOperationException(); - } - - public boolean isNull(String key) { - throw new UnsupportedOperationException(); - } - - public boolean isString() { - return true; - } - - public int size() { - return 0; - } + public static Splittable create(boolean value) { + return create0(value); } - public static Splittable create(Object object) { - if (object instanceof String) { - return new StringSplittable((String) object); - } - return create0(object); + public static Splittable create(double value) { + return create0(value); } - private static native Splittable create0(Object object) /*-{ + public static Splittable create(String value) { + return create0(value); + } + + public static native JsoSplittable createIndexed() /*-{ + return []; + }-*/; + + public static native JsoSplittable nullValue() /*-{ + return null; + }-*/; + + private static native Splittable create0(boolean object) /*-{ return object; }-*/; + private static native Splittable create0(double object) /*-{ + return object; + }-*/; + + private static native Splittable create0(String object) /*-{ + return { + __s : object + }; + }-*/; + + private static native boolean stringifyFastSupported() /*-{ + return $wnd.JSON && $wnd.JSON.stringify; + }-*/; + protected JsoSplittable() { } - public native String asString() /*-{ - return String(this); + public native boolean asBoolean() /*-{ + return this == true; + }-*/;; + + public native double asNumber() /*-{ + return Number(this); }-*/; - public Splittable get(int index) { - return create(get0(index)); + public void assign(Splittable parent, int index) { + if (isString()) { + assign0(parent, index, asString()); + } else { + assign0(parent, index, this); + } } - public Splittable get(String key) { - return create(get0(key)); + public void assign(Splittable parent, String index) { + if (isString()) { + assign0(parent, index, asString()); + } else { + assign0(parent, index, this); + } + } + + public native String asString() /*-{ + return this.__s; + }-*/; + + public Splittable deepCopy() { + return StringQuoter.split(getPayload()); + } + + public JsoSplittable get(int index) { + return getRaw(index); + } + + public JsoSplittable get(String key) { + return getRaw(key); } public String getPayload() { - throw new UnsupportedOperationException("Cannot convert JsoSplittable to payload"); + if (isString()) { + return JsonUtils.escapeValue(asString()); + } + if (stringifyFastSupported()) { + return stringifyFast(); + } + return stringifySlow(); } public List<String> getPropertyKeys() { @@ -118,12 +136,28 @@ return Collections.unmodifiableList(toReturn); } + public native Object getReified(String key) /*-{ + return this.__reified && this.__reified[':' + key]; + }-*/; + + public Splittable getSplittable() { + return this; + } + + public native boolean isBoolean() /*-{ + return typeof (this) == 'boolean' || this instanceof Boolean; + }-*/; + + public native boolean isFunction() /*-{ + return typeof this == 'function'; + }-*/; + public native boolean isIndexed() /*-{ return this instanceof Array; }-*/; public boolean isKeyed() { - return !isString() && !isIndexed(); + return this != NULL && !isString() && !isIndexed() && !isFunction(); } public native boolean isNull(int index) /*-{ @@ -134,20 +168,50 @@ return this[key] == null; }-*/; + public native boolean isNumber() /*-{ + return typeof (this) == 'number' || this instanceof Number; + }-*/; + + public native boolean isReified(String key) /*-{ + return !!(this.__reified && this.__reified.hasOwnProperty(':' + key)); + }-*/; + public native boolean isString() /*-{ - return typeof (this) == 'string' || this instanceof String; + return this && this.__s != null; + }-*/; + + public native boolean isUndefined(String key) /*-{ + return this[key] === undefined; + }-*/; + + public native void setReified(String key, Object object) /*-{ + // Use a function object so native JSON.stringify will ignore + (this.__reified || (this.__reified = function() { + }))[':' + key] = object; + }-*/; + + public native void setSize(int size) /*-{ + this.length = size; }-*/; public native int size() /*-{ return this.length; }-*/; - private native Object get0(int index) /*-{ - return Object(this[index]); + private native void assign0(Splittable parent, int index, Splittable value) /*-{ + parent[index] = value; }-*/; - private native Object get0(String key) /*-{ - return Object(this[key]); + private native void assign0(Splittable parent, int index, String value) /*-{ + parent[index] = value; + }-*/; + + private native void assign0(Splittable parent, String index, Splittable value) /*-{ + parent[index] = value; + }-*/; + + private native void assign0(Splittable parent, String index, String value) /*-{ + parent[index] = value; }-*/; private native void getPropertyKeys0(List<String> list) /*-{ @@ -157,4 +221,83 @@ } } }-*/; + + private native JsoSplittable getRaw(int index) /*-{ + _ = this[index]; + if (_ == null) { + return null; + } + if (typeof _ == 'string' || _ instanceof String) { + return @com.google.gwt.autobean.client.impl.JsoSplittable::create(Ljava/lang/String;)(_); + } + return Object(_); + }-*/; + + private native JsoSplittable getRaw(String index) /*-{ + _ = this[index]; + if (_ == null) { + return null; + } + if (typeof _ == 'string' || _ instanceof String) { + return @com.google.gwt.autobean.client.impl.JsoSplittable::create(Ljava/lang/String;)(_); + } + return Object(_); + }-*/; + + private native String stringifyFast() /*-{ + return $wnd.JSON.stringify(this); + }-*/; + + private String stringifySlow() { + StringBuilder sb = new StringBuilder(); + stringifySlow(sb); + return sb.toString(); + } + + private void stringifySlow(StringBuilder sb) { + if (this == NULL) { + sb.append("null"); + return; + } + if (isBoolean()) { + sb.append(asBoolean()); + return; + } + if (isNumber()) { + sb.append(asNumber()); + return; + } + if (isString()) { + sb.append(JsonUtils.escapeValue(asString())); + return; + } + if (isIndexed()) { + sb.append("["); + for (int i = 0, j = size(); i < j; i++) { + if (i > 0) { + sb.append(","); + } + get(i).stringifySlow(sb); + } + sb.append("]"); + return; + } + + sb.append("{"); + boolean needsComma = false; + for (String key : getPropertyKeys()) { + if (needsComma) { + sb.append(","); + } else { + needsComma = true; + } + JsoSplittable value = get(key); + if (!value.isFunction()) { + sb.append(JsonUtils.escapeValue(key)); + sb.append(":"); + value.stringifySlow(sb); + } + } + sb.append("}"); + } }
diff --git a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java index a07758d..1ec4f78 100644 --- a/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java +++ b/user/src/com/google/gwt/autobean/rebind/AutoBeanFactoryGenerator.java
@@ -27,6 +27,7 @@ import com.google.gwt.autobean.shared.AutoBeanFactory; import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.autobean.shared.AutoBeanVisitor; +import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.impl.AbstractAutoBean; import com.google.gwt.autobean.shared.impl.AbstractAutoBean.OneShotContext; import com.google.gwt.core.client.JavaScriptObject; @@ -185,28 +186,16 @@ AutoBeanFactory.class.getCanonicalName()); } - // Clone constructor - // public FooIntfAutoBean(FooIntfoAutoBean toClone, boolean deepClone) { - sw.println("public %1$s(%1$s toClone, boolean deep) {", type.getSimpleSourceName()); - sw.indentln("super(toClone, deep);"); - sw.println("}"); - // Wrapping constructor // public FooIntfAutoBean(AutoBeanFactory factory, FooIntfo wrapped) { sw.println("public %s(%s factory, %s wrapped) {", type.getSimpleSourceName(), AutoBeanFactory.class.getCanonicalName(), type.getPeerType().getQualifiedSourceName()); - sw.indentln("super(factory, wrapped);"); + sw.indentln("super(wrapped, factory);"); sw.println("}"); // public FooIntf as() {return shim;} sw.println("public %s as() {return shim;}", type.getPeerType().getQualifiedSourceName()); - // public FooIntfAutoBean clone(boolean deep) { - sw.println("public %s clone(boolean deep) {", type.getQualifiedSourceName()); - // return new FooIntfAutoBean(this, deep); - sw.indentln("return new %s(this, deep);", type.getSimpleSourceName()); - sw.println("}"); - // public Class<Intf> getType() {return Intf.class;} sw.println("public Class<%1$s> getType() {return %1$s.class;}", ModelUtils.ensureBaseType( type.getPeerType()).getQualifiedSourceName()); @@ -230,45 +219,49 @@ // return new FooIntf() { sw.println("return new %s() {", type.getPeerType().getQualifiedSourceName()); sw.indent(); + sw.println("private final %s data = %s.this.data;", Splittable.class.getCanonicalName(), type + .getQualifiedSourceName()); for (AutoBeanMethod method : type.getMethods()) { JMethod jmethod = method.getMethod(); + JType returnType = jmethod.getReturnType(); sw.println("public %s {", getBaseMethodDeclaration(jmethod)); sw.indent(); switch (method.getAction()) { case GET: { - // Must handle de-boxing primitive types - JPrimitiveType primitive = jmethod.getReturnType().isPrimitive(); - if (primitive != null) { - // Object toReturn = values.get("foo"); - sw.println("Object toReturn = values.get(\"%s\");", method.getPropertyName()); - sw.println("if (toReturn == null) {"); - // return 0; - sw.indentln("return %s;", primitive.getUninitializedFieldExpression()); - sw.println("} else {"); - // return (BoxedType) toReturn; - sw.indentln("return (%s) toReturn;", primitive.getQualifiedBoxedSourceName()); - sw.println("}"); + String castType; + if (returnType.isPrimitive() != null) { + castType = returnType.isPrimitive().getQualifiedBoxedSourceName(); + // Boolean toReturn = getOrReify("foo"); + sw.println("%s toReturn = getOrReify(\"%s\");", castType, method.getPropertyName()); + // return toReturn == null ? false : toReturn; + sw.println("return toReturn == null ? %s : toReturn;", returnType.isPrimitive() + .getUninitializedFieldExpression()); + } else if (returnType.equals(context.getTypeOracle().findType( + Splittable.class.getCanonicalName()))) { + sw.println("return data.isNull(\"%1$s\") ? null : data.get(\"%1$s\");", method + .getPropertyName()); } else { - // return (ReturnType) values.get(\"foo\"); - sw.println("return (%s) values.get(\"%s\");", ModelUtils - .getQualifiedBaseSourceName(jmethod.getReturnType()), method.getPropertyName()); + // return (ReturnType) values.getOrReify(\"foo\"); + castType = ModelUtils.getQualifiedBaseSourceName(returnType); + sw.println("return (%s) getOrReify(\"%s\");", castType, method.getPropertyName()); } } break; case SET: - case SET_BUILDER: - // values.put("foo", parameter); - sw.println("values.put(\"%s\", %s);", method.getPropertyName(), - jmethod.getParameters()[0].getName()); + case SET_BUILDER: { + JParameter param = jmethod.getParameters()[0]; + // setProperty("foo", parameter); + sw.println("setProperty(\"%s\", %s);", method.getPropertyName(), param.getName()); if (JBeanMethod.SET_BUILDER.equals(method.getAction())) { sw.println("return this;"); } break; + } case CALL: // return com.example.Owner.staticMethod(Outer.this, param, // param); JMethod staticImpl = method.getStaticImpl(); - if (!jmethod.getReturnType().equals(JPrimitiveType.VOID)) { + if (!returnType.equals(JPrimitiveType.VOID)) { sw.print("return "); } sw.print("%s.%s(%s.this", staticImpl.getEnclosingType().getQualifiedSourceName(), @@ -506,19 +499,15 @@ sw.println("public %s {", getBaseMethodDeclaration(jmethod)); sw.indent(); - // Use explicit enclosing this reference to avoid method conflicts - sw.println("%s.this.checkWrapped();", type.getSimpleSourceName()); - switch (method.getAction()) { case GET: /* * The getter call will ensure that any non-value return type is * definitely wrapped by an AutoBean instance. */ - // Foo toReturn=FooAutoBean.this.get("getFoo", getWrapped().getFoo()); - sw.println("%s toReturn = %3$s.this.get(\"%2$s\", getWrapped().%2$s());", ModelUtils - .getQualifiedBaseSourceName(jmethod.getReturnType()), methodName, type - .getSimpleSourceName()); + sw.println("%s toReturn = %s.this.getWrapped().%s();", ModelUtils + .getQualifiedBaseSourceName(jmethod.getReturnType()), type.getSimpleSourceName(), + methodName); // Non-value types might need to be wrapped writeReturnWrapper(sw, type, method); @@ -526,7 +515,6 @@ break; case SET: case SET_BUILDER: - sw.println("%s.this.checkFrozen();", type.getSimpleSourceName()); // getWrapped().setFoo(foo); sw.println("%s.this.getWrapped().%s(%s);", type.getSimpleSourceName(), methodName, parameters[0].getName()); @@ -596,7 +584,6 @@ sw.println("%s propertyContext;", ClientPropertyContext.class.getCanonicalName()); // Local variable ref cleans up emitted js sw.println("%1$s as = as();", type.getPeerType().getQualifiedSourceName()); - sw.println("%s<String, Object> values = this.values;", Map.class.getCanonicalName()); for (AutoBeanMethod method : type.getMethods()) { if (!method.getAction().equals(JBeanMethod.GET)) { @@ -662,9 +649,9 @@ referencedSetters.add(setter); } else { // Create a function that will update the values map - // CPContext.mapSetter(values, "foo"); - sw.println("%s.mapSetter(values, \"%s\"),", ClientPropertyContext.Setter.class - .getCanonicalName(), method.getPropertyName()); + // CPContext.beanSetter(FooBeanImpl.this, "foo"); + sw.println("%s.beanSetter(%s.this, \"%s\"),", ClientPropertyContext.Setter.class + .getCanonicalName(), type.getSimpleSourceName(), method.getPropertyName()); } } if (typeList.size() == 1) {
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 132393f..32b4a1b 100644 --- a/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java +++ b/user/src/com/google/gwt/autobean/server/impl/BeanMethod.java
@@ -16,6 +16,7 @@ package com.google.gwt.autobean.server.impl; import com.google.gwt.autobean.shared.AutoBean; +import com.google.gwt.autobean.shared.AutoBean.PropertyName; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -38,8 +39,7 @@ } @Override - Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) - throws Throwable { + Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) throws Throwable { if (CALL.matches(handler, method)) { return CALL.invoke(handler, method, args); } @@ -58,7 +58,7 @@ @Override public String inferName(Method method) { String name = method.getName(); - if (name.startsWith(IS_PREFIX)) { + if (name.startsWith(IS_PREFIX) && !method.isAnnotationPresent(PropertyName.class)) { Class<?> returnType = method.getReturnType(); if (Boolean.TYPE.equals(returnType) || Boolean.class.equals(returnType)) { return decapitalize(name.substring(2)); @@ -70,7 +70,7 @@ @Override Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) { String propertyName = inferName(method); - Object toReturn = handler.getBean().getValues().get(propertyName); + Object toReturn = handler.getBean().getOrReify(propertyName); if (toReturn == null && method.getReturnType().isPrimitive()) { toReturn = TypeUtils.getDefaultPrimitiveValue(method.getReturnType()); } @@ -80,15 +80,14 @@ @Override boolean matches(SimpleBeanHandler<?> handler, Method method) { Class<?> returnType = method.getReturnType(); - if (method.getParameterTypes().length != 0 - || Void.TYPE.equals(returnType)) { + if (method.getParameterTypes().length != 0 || Void.TYPE.equals(returnType)) { return false; } String name = method.getName(); if (Boolean.TYPE.equals(returnType) || Boolean.class.equals(returnType)) { - if (name.startsWith(IS_PREFIX) && name.length() > 2 - || name.startsWith(HAS_PREFIX) && name.length() > 3) { + if (name.startsWith(IS_PREFIX) && name.length() > 2 || name.startsWith(HAS_PREFIX) + && name.length() > 3) { return true; } } @@ -101,7 +100,7 @@ SET { @Override Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) { - handler.getBean().getValues().put(inferName(method), args[0]); + handler.getBean().setProperty(inferName(method), args[0]); return null; } @@ -109,8 +108,7 @@ boolean matches(SimpleBeanHandler<?> handler, Method method) { String name = method.getName(); return name.startsWith(SET_PREFIX) && name.length() > 3 - && method.getParameterTypes().length == 1 - && method.getReturnType().equals(Void.TYPE); + && method.getParameterTypes().length == 1 && method.getReturnType().equals(Void.TYPE); } }, /** @@ -121,15 +119,15 @@ SET_BUILDER { @Override Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) { - handler.getBean().getValues().put(inferName(method), args[0]); - return handler.getBean().as(); + ProxyAutoBean<?> bean = handler.getBean(); + bean.setProperty(inferName(method), args[0]); + return bean.as(); } @Override boolean matches(SimpleBeanHandler<?> handler, Method method) { String name = method.getName(); - return name.startsWith(SET_PREFIX) - && name.length() > 3 + return name.startsWith(SET_PREFIX) && name.length() > 3 && method.getParameterTypes().length == 1 && method.getReturnType().isAssignableFrom(method.getDeclaringClass()); } @@ -144,8 +142,7 @@ } @Override - Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) - throws Throwable { + Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) throws Throwable { if (args == null) { args = EMPTY_OBJECT; } @@ -190,8 +187,9 @@ continue; } // Check the AutoBean parameterization of the 0th argument - Class<?> foundAutoBean = TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization( - AutoBean.class, found.getGenericParameterTypes()[0])); + Class<?> foundAutoBean = + TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(AutoBean.class, found + .getGenericParameterTypes()[0])); if (!foundAutoBean.isAssignableFrom(autoBeanType)) { continue; } @@ -205,8 +203,8 @@ } /** - * Private equivalent of Introspector.decapitalize(String) - * since java.beans.Introspector is not available in Android 2.2. + * Private equivalent of Introspector.decapitalize(String) since + * java.beans.Introspector is not available in Android 2.2. */ private static String decapitalize(String name) { if (name == null) { @@ -223,6 +221,10 @@ } public String inferName(Method method) { + PropertyName prop = method.getAnnotation(PropertyName.class); + if (prop != null) { + return prop.value(); + } return decapitalize(method.getName().substring(3)); } @@ -236,11 +238,11 @@ /** * Invoke the method. */ - abstract Object invoke(SimpleBeanHandler<?> handler, Method method, - Object[] args) throws Throwable; + abstract Object invoke(SimpleBeanHandler<?> handler, Method method, Object[] args) + throws Throwable; /** * Determine if the method maches the given type. */ abstract boolean matches(SimpleBeanHandler<?> handler, Method method); -} +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java b/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java index f17e879..c1f4be9 100644 --- a/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java +++ b/user/src/com/google/gwt/autobean/server/impl/BeanPropertyContext.java
@@ -16,20 +16,19 @@ package com.google.gwt.autobean.server.impl; import java.lang.reflect.Method; -import java.util.Map; /** * A property context that allows setters to be called on a simple peer, * regardless of whether or not the interface actually has a setter. */ class BeanPropertyContext extends MethodPropertyContext { + private final ProxyAutoBean<?> bean; private final String propertyName; - private final Map<String, Object> map; public BeanPropertyContext(ProxyAutoBean<?> bean, Method getter) { super(getter); + this.bean = bean; propertyName = BeanMethod.GET.inferName(getter); - map = bean.getPropertyMap(); } @Override @@ -42,6 +41,6 @@ Class<?> maybeAutobox = TypeUtils.maybeAutobox(getType()); assert value == null || maybeAutobox.isInstance(value) : value.getClass().getCanonicalName() + " is not assignable to " + maybeAutobox.getCanonicalName(); - map.put(propertyName, maybeAutobox.cast(value)); + bean.setProperty(propertyName, maybeAutobox.cast(value)); } }
diff --git a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java index 51e7902..04b77a6 100644 --- a/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java +++ b/user/src/com/google/gwt/autobean/server/impl/JsonSplittable.java
@@ -16,21 +16,39 @@ package com.google.gwt.autobean.server.impl; import com.google.gwt.autobean.shared.Splittable; +import com.google.gwt.autobean.shared.impl.HasSplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; /** * Uses the org.json packages to slice and dice request payloads. */ -public class JsonSplittable implements Splittable { +public class JsonSplittable implements Splittable, HasSplittable { + + /** + * Ensures that the same JsonSplittable will be returned for a given backing + * JSONObject. + */ + private static final Map<Object, Reference<JsonSplittable>> canonical = + new WeakHashMap<Object, Reference<JsonSplittable>>(); + + public static JsonSplittable create() { + return new JsonSplittable(new JSONObject()); + } + public static Splittable create(String payload) { try { switch (payload.charAt(0)) { @@ -39,8 +57,8 @@ case '[': return new JsonSplittable(new JSONArray(payload)); case '"': - return new JsonSplittable( - new JSONArray("[" + payload + "]").getString(0)); + return new JsonSplittable(new JSONArray("[" + payload + "]").getString(0)); + case '-': case '0': case '1': case '2': @@ -51,19 +69,31 @@ case '7': case '8': case '9': - return new JsonSplittable(payload); + return new JsonSplittable(Double.parseDouble(payload)); + case 't': + case 'f': + return new JsonSplittable(Boolean.parseBoolean(payload)); + case 'n': + return null; default: - throw new RuntimeException("Could not parse payload: payload[0] = " - + payload.charAt(0)); + throw new RuntimeException("Could not parse payload: payload[0] = " + payload.charAt(0)); } } catch (JSONException e) { throw new RuntimeException("Could not parse payload", e); } } + public static Splittable createIndexed() { + return new JsonSplittable(new JSONArray()); + } + + public static Splittable createNull() { + return new JsonSplittable(); + } + /** - * Private equivalent of org.json.JSONObject.getNames(JSONObject) - * since that method is not available in Android 2.2. + * Private equivalent of org.json.JSONObject.getNames(JSONObject) since that + * method is not available in Android 2.2. Used to represent a null value. */ private static String[] getNames(JSONObject json) { int length = json.length(); @@ -79,20 +109,38 @@ return names; } - private final JSONArray array; - private final JSONObject obj; - private final String string; + private JSONArray array; + private Boolean bool; + /** + * Used to represent a null value. + */ + private boolean isNull; + private Double number; + private JSONObject obj; + private String string; + private final Map<String, Object> reified = new HashMap<String, Object>(); + + /** + * Constructor for a null value. + */ + private JsonSplittable() { + isNull = true; + } + + private JsonSplittable(boolean value) { + this.bool = value; + } + + private JsonSplittable(double value) { + this.number = value; + } private JsonSplittable(JSONArray array) { this.array = array; - this.obj = null; - this.string = null; } private JsonSplittable(JSONObject obj) { - this.array = null; this.obj = obj; - this.string = null; } private JsonSplittable(String string) { @@ -101,10 +149,38 @@ this.string = string; } + public boolean asBoolean() { + return bool; + } + + public double asNumber() { + return number; + } + + public void assign(Splittable parent, int index) { + try { + ((JsonSplittable) parent).array.put(index, value()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + + public void assign(Splittable parent, String propertyName) { + try { + ((JsonSplittable) parent).obj.put(propertyName, value()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + public String asString() { return string; } + public Splittable deepCopy() { + return create(getPayload()); + } + public Splittable get(int index) { try { return makeSplittable(array.get(index)); @@ -122,6 +198,9 @@ } public String getPayload() { + if (isNull) { + return "null"; + } if (obj != null) { return obj.toString(); } @@ -131,6 +210,12 @@ if (string != null) { return StringQuoter.quote(string); } + if (number != null) { + return String.valueOf(number); + } + if (bool != null) { + return String.valueOf(bool); + } throw new RuntimeException("No data in this JsonSplittable"); } @@ -143,6 +228,18 @@ } } + public Object getReified(String key) { + return reified.get(key); + } + + public Splittable getSplittable() { + return this; + } + + public boolean isBoolean() { + return bool != null; + } + public boolean isIndexed() { return array != null; } @@ -160,10 +257,39 @@ return !obj.has(key) || obj.isNull(key); } + public boolean isNumber() { + return number != null; + } + + public boolean isReified(String key) { + return reified.containsKey(key); + } + public boolean isString() { return string != null; } + public boolean isUndefined(String key) { + return !obj.has(key); + } + + public void setReified(String key, Object object) { + reified.put(key, object); + } + + public void setSize(int size) { + // This is terrible, but there's no API support for resizing or splicing + JSONArray newArray = new JSONArray(); + for (int i = 0; i < size; i++) { + try { + newArray.put(i, array.get(i)); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } + array = newArray; + } + public int size() { return array.length(); } @@ -173,23 +299,53 @@ */ @Override public String toString() { - if (obj != null) { - return obj.toString(); - } else if (array != null) { - return array.toString(); - } else if (string != null) { - return string; - } - return "<Uninitialized>"; + return getPayload(); } - private JsonSplittable makeSplittable(Object object) { - if (object instanceof JSONObject) { - return new JsonSplittable((JSONObject) object); + private synchronized JsonSplittable makeSplittable(Object object) { + if (JSONObject.NULL.equals(object)) { + return null; } - if (object instanceof JSONArray) { - return new JsonSplittable((JSONArray) object); + Reference<JsonSplittable> ref = canonical.get(object); + JsonSplittable seen = ref == null ? null : ref.get(); + if (seen == null) { + if (object instanceof JSONObject) { + seen = new JsonSplittable((JSONObject) object); + } else if (object instanceof JSONArray) { + seen = new JsonSplittable((JSONArray) object); + } else if (object instanceof String) { + seen = new JsonSplittable(object.toString()); + } else if (object instanceof Number) { + seen = new JsonSplittable(((Number) object).doubleValue()); + } else if (object instanceof Boolean) { + seen = new JsonSplittable((Boolean) object); + } else { + throw new RuntimeException("Unhandled type " + object.getClass()); + } + canonical.put(object, new WeakReference<JsonSplittable>(seen)); } - return new JsonSplittable(object.toString()); + return seen; } -} + + private Object value() { + if (isNull) { + return null; + } + if (obj != null) { + return obj; + } + if (array != null) { + return array; + } + if (string != null) { + return string; + } + if (number != null) { + return number; + } + if (bool != null) { + return bool; + } + throw new RuntimeException("No data"); + } +} \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java b/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java index 4f8e3be..de65b5e 100644 --- a/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java +++ b/user/src/com/google/gwt/autobean/server/impl/ProxyAutoBean.java
@@ -66,15 +66,14 @@ Class<?>... extraInterfaces) { Class<?>[] intfs; if (extraInterfaces == null) { - intfs = new Class<?>[] {intf}; + intfs = new Class<?>[]{intf}; } else { intfs = new Class<?>[extraInterfaces.length + 1]; intfs[0] = intf; System.arraycopy(extraInterfaces, 0, intfs, 1, extraInterfaces.length); } - return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs, - handler)); + return intf.cast(Proxy.newProxyInstance(intf.getClassLoader(), intfs, handler)); } private static Data calculateData(Class<?> beanType) { @@ -117,13 +116,11 @@ private final Class<T> beanType; private final Configuration configuration; private final Data data; - private final T shim; // These constructors mirror the generated constructors. @SuppressWarnings("unchecked") - public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, - Configuration configuration) { + public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration) { super(factory); this.beanType = (Class<T>) beanType; this.configuration = configuration; @@ -132,36 +129,20 @@ } @SuppressWarnings("unchecked") - public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, - Configuration configuration, T toWrap) { - super(factory, toWrap); - if (Proxy.isProxyClass(toWrap.getClass())) { - System.out.println("blah"); - } + public ProxyAutoBean(AutoBeanFactory factory, Class<?> beanType, Configuration configuration, + T toWrap) { + super(toWrap, factory); this.beanType = (Class<T>) beanType; this.configuration = configuration; this.data = calculateData(beanType); this.shim = createShim(); } - private ProxyAutoBean(ProxyAutoBean<T> toClone, boolean deep) { - super(toClone, deep); - this.beanType = toClone.beanType; - this.configuration = toClone.configuration; - this.data = toClone.data; - this.shim = createShim(); - } - @Override public T as() { return shim; } - @Override - public AutoBean<T> clone(boolean deep) { - return new ProxyAutoBean<T>(this, deep); - } - public Configuration getConfiguration() { return configuration; } @@ -194,9 +175,13 @@ super.checkWrapped(); } + /** + * Not used in this implementation. Instead, the simple implementation is + * created lazily in {@link #getWrapped()}. + */ @Override protected T createSimplePeer() { - return ProxyAutoBean.makeProxy(beanType, new SimpleBeanHandler<T>(this)); + return null; } /** @@ -208,10 +193,11 @@ } /** - * Allow access by {@link BeanMethod}. + * Allow access by BeanMethod. */ - protected Map<String, Object> getValues() { - return values; + @Override + protected <V> V getOrReify(String propertyName) { + return super.<V> getOrReify(propertyName); } /** @@ -219,6 +205,9 @@ */ @Override protected T getWrapped() { + if (wrapped == null && isUsingSimplePeer()) { + wrapped = (T) ProxyAutoBean.makeProxy(beanType, new SimpleBeanHandler<T>(this)); + } return super.getWrapped(); } @@ -230,6 +219,11 @@ super.set(method, value); } + @Override + protected void setProperty(String propertyName, Object value) { + super.setProperty(propertyName, value); + } + // TODO: Port to model-based when class-based TypeOracle is available. @Override protected void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx) { @@ -257,8 +251,9 @@ } // Create the context used for the property visitation - MethodPropertyContext x = isUsingSimplePeer() ? new BeanPropertyContext( - this, getter) : new GetterPropertyContext(this, getter); + MethodPropertyContext x = + isUsingSimplePeer() ? new BeanPropertyContext(this, getter) : new GetterPropertyContext( + this, getter); switch (propertyType) { case VALUE: { @@ -313,13 +308,8 @@ return beanType; } - Map<String, Object> getPropertyMap() { - return values; - } - private T createShim() { - T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this, - getWrapped())); + T toReturn = ProxyAutoBean.makeProxy(beanType, new ShimHandler<T>(this, getWrapped())); WeakMapping.set(toReturn, AutoBean.class.getName(), this); return toReturn; }
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 23c190d..dc920a7 100644 --- a/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java +++ b/user/src/com/google/gwt/autobean/server/impl/ShimHandler.java
@@ -19,6 +19,7 @@ import com.google.gwt.autobean.shared.AutoBeanUtils; import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; @@ -31,11 +32,9 @@ class ShimHandler<T> implements InvocationHandler { private final ProxyAutoBean<T> bean; private final Method interceptor; - private final T toWrap; public ShimHandler(ProxyAutoBean<T> bean, T toWrap) { this.bean = bean; - this.toWrap = toWrap; Method maybe = null; for (Class<?> clazz : bean.getConfiguration().getCategories()) { @@ -67,36 +66,35 @@ return bean.getWrapped().hashCode(); } - public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { method.setAccessible(true); Object toReturn; String name = method.getName(); - bean.checkWrapped(); method.setAccessible(true); - if (BeanMethod.OBJECT.matches(method)) { - return method.invoke(this, args); - } else if (BeanMethod.GET.matches(method)) { - toReturn = method.invoke(toWrap, args); - toReturn = bean.get(name, toReturn); - } else if (BeanMethod.SET.matches(method) - || BeanMethod.SET_BUILDER.matches(method)) { - bean.checkFrozen(); - toReturn = method.invoke(toWrap, args); - bean.set(name, args[0]); - } else { - // XXX How should freezing and calls work together? - // bean.checkFrozen(); - toReturn = method.invoke(toWrap, args); - bean.call(name, toReturn, args); - } - Class<?> intf = method.getReturnType(); - if (!Object.class.equals(intf)) { - // XXX Need to deal with resolving generic T return types - toReturn = maybeWrap(intf, toReturn); - } - if (interceptor != null) { - toReturn = interceptor.invoke(null, bean, toReturn); + try { + if (BeanMethod.OBJECT.matches(method)) { + return method.invoke(this, args); + } else if (BeanMethod.GET.matches(method)) { + toReturn = method.invoke(bean.getWrapped(), args); + toReturn = bean.get(name, toReturn); + } else if (BeanMethod.SET.matches(method) || BeanMethod.SET_BUILDER.matches(method)) { + toReturn = method.invoke(bean.getWrapped(), args); + bean.set(name, args[0]); + } else { + // XXX How should freezing and calls work together? + toReturn = method.invoke(bean.getWrapped(), args); + bean.call(name, toReturn, args); + } + Class<?> intf = method.getReturnType(); + if (!Object.class.equals(intf)) { + // XXX Need to deal with resolving generic T return types + toReturn = maybeWrap(intf, toReturn); + } + if (interceptor != null) { + toReturn = interceptor.invoke(null, bean, toReturn); + } + } catch (InvocationTargetException e) { + throw e.getCause(); } return toReturn; } @@ -110,9 +108,11 @@ if (toReturn == null) { return null; } - if (TypeUtils.isValueType(intf) - || TypeUtils.isValueType(toReturn.getClass()) - || AutoBeanUtils.getAutoBean(toReturn) != null + AutoBean<?> returnBean = AutoBeanUtils.getAutoBean(toReturn); + if (returnBean != null) { + return returnBean.as(); + } + if (TypeUtils.isValueType(intf) || TypeUtils.isValueType(toReturn.getClass()) || bean.getConfiguration().getNoWrap().contains(intf)) { return toReturn; } @@ -124,8 +124,8 @@ */ return toReturn; } - ProxyAutoBean<Object> newBean = new ProxyAutoBean<Object>( - bean.getFactory(), intf, bean.getConfiguration(), toReturn); + ProxyAutoBean<Object> newBean = + new ProxyAutoBean<Object>(bean.getFactory(), intf, bean.getConfiguration(), toReturn); return newBean.as(); } } \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java b/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java index d25468d..766d26b 100644 --- a/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java +++ b/user/src/com/google/gwt/autobean/server/impl/SimpleBeanHandler.java
@@ -37,8 +37,7 @@ /** * Delegates most work to {@link BeanMethod}. */ - public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { for (BeanMethod type : BeanMethod.values()) { if (type.matches(this, method)) { Object toReturn = type.invoke(this, method, args); @@ -47,12 +46,12 @@ } throw new RuntimeException("Unhandled invocation " + method.getName()); } - + /** * For debugging use only. */ @Override public String toString() { - return bean.getPropertyMap().toString(); + return bean.getSplittable().getPayload(); } } \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java index cf769c1..2503696 100644 --- a/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java +++ b/user/src/com/google/gwt/autobean/server/impl/TypeUtils.java
@@ -164,8 +164,9 @@ return false; } - public static Class<?> maybeAutobox(Class<?> domainType) { - Class<?> autoBoxType = AUTOBOX_MAP.get(domainType); + public static <V> Class<V> maybeAutobox(Class<V> domainType) { + @SuppressWarnings("unchecked") + Class<V> autoBoxType = (Class<V>) AUTOBOX_MAP.get(domainType); return autoBoxType == null ? domainType : autoBoxType; }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBean.java b/user/src/com/google/gwt/autobean/shared/AutoBean.java index 8b8613f..3af5128 100644 --- a/user/src/com/google/gwt/autobean/shared/AutoBean.java +++ b/user/src/com/google/gwt/autobean/shared/AutoBean.java
@@ -58,16 +58,21 @@ T as(); /** - * Creates a copy of the AutoBean. + * This method always throws an {@link UnsupportedOperationException}. The + * implementation of this method in previous releases was not sufficiently + * robust and there are no further uses of this method within the GWT code + * base. Furthermore, there are many different semantics that can be applied + * to a cloning process that cannot be adequately addressed with a single + * implementation. * <p> - * If the AutoBean has tags, the tags will be copied into the cloned AutoBean. - * If any of the tag values are AutoBeans, they will not be cloned, regardless - * of the value of <code>deep</code>. + * A simple clone of an acyclic datastructure can be created by using + * {@link AutoBeanCodex} to encode and decode the root object. Other cloning + * algorithms are best implemented by using an {@link AutoBeanVisitor}. * - * @param deep indicates if all referenced AutoBeans should be cloned - * @return a copy of this {@link AutoBean} - * @throws IllegalStateException if the AutoBean is a wrapper type + * @throws UnsupportedOperationException + * @deprecated with no replacement */ + @Deprecated AutoBean<T> clone(boolean deep); /**
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java index b7e4d52..9797984 100644 --- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java +++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -15,21 +15,10 @@ */ 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.AutoBeanCodexImpl; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; import com.google.gwt.autobean.shared.impl.StringQuoter; -import java.util.ArrayList; -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; -import java.util.Stack; - /** * Utility methods for encoding an AutoBean graph into a JSON-compatible string. * This codex intentionally does not preserve object identity, nor does it @@ -38,363 +27,16 @@ public class AutoBeanCodex { /** - * Describes a means of encoding or decoding a particular type of data to or - * from a wire format representation. + * Decode an AutoBeanCodex payload. + * + * @param <T> the expected return type + * @param factory an AutoBeanFactory capable of producing {@code AutoBean<T>} + * @param clazz the expected return type + * @param data a payload previously generated by {@link #encode(AutoBean)} + * @return an AutoBean containing the payload contents */ - interface Coder { - Object decode(Splittable data); - - 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 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 { - stack.push(new ObjectCoder(type)); - } - } - - public Coder getCoder() { - assert stack.size() == 1 : "Incorrect size: " + stack.size(); - return stack.pop(); - } - } - - class CollectionCoder implements Coder { - private final Coder elementDecoder; - private final Class<?> type; - - public CollectionCoder(Class<?> type, Coder elementDecoder) { - this.elementDecoder = elementDecoder; - this.type = type; - } - - 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 { - // Should not reach here - throw new RuntimeException(type.getName()); - } - 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; - } - - public void encode(StringBuilder sb, Object value) { - if (value == null) { - sb.append("null"); - return; - } - - Iterator<?> it = ((Collection<?>) value).iterator(); - sb.append("["); - if (it.hasNext()) { - elementDecoder.encode(sb, it.next()); - while (it.hasNext()) { - sb.append(","); - elementDecoder.encode(sb, it.next()); - } - } - sb.append("]"); - } - } - - class EnumCoder<E extends Enum<E>> implements Coder { - private final Class<E> type; - - public EnumCoder(Class<E> type) { - this.type = type; - } - - public Object decode(Splittable data) { - return enumMap.getEnum(type, data.asString()); - } - - public void encode(StringBuilder sb, Object value) { - if (value == null) { - sb.append("null"); - } - sb.append(StringQuoter.quote(enumMap.getToken((Enum<?>) value))); - } - } - - /** - * Used to stop processing. - */ - static class HaltException extends RuntimeException { - public HaltException(RuntimeException cause) { - super(cause); - } - - @Override - public RuntimeException getCause() { - return (RuntimeException) super.getCause(); - } - } - - 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 AutoBeanCodex(factory).doDecode(clazz, data); + return AutoBeanCodexImpl.doDecode(EncodeState.forDecode(factory), clazz, data); } /** @@ -404,7 +46,8 @@ * @param factory an AutoBeanFactory capable of producing {@code AutoBean<T>} * @param clazz the expected return type * @param payload a payload string previously generated by - * {@link #encode(AutoBean)} + * {@link #encode(AutoBean)}{@link Splittable#getPayload() + * .getPayload()}. * @return an AutoBean containing the payload contents */ public static <T> AutoBean<T> decode(AutoBeanFactory factory, Class<T> clazz, String payload) { @@ -420,7 +63,7 @@ * @param bean the target AutoBean */ public static void decodeInto(Splittable data, AutoBean<?> bean) { - new AutoBeanCodex(bean.getFactory()).doDecodeInto(data, bean); + AutoBeanCodexImpl.doDecodeInto(EncodeState.forDecode(bean.getFactory()), data, bean); } /** @@ -432,42 +75,12 @@ */ public static Splittable encode(AutoBean<?> bean) { if (bean == null) { - return LazySplittable.NULL; + return Splittable.NULL; } StringBuilder sb = new StringBuilder(); - new AutoBeanCodex(bean.getFactory()).doEncode(sb, bean); - return new LazySplittable(sb.toString()); - } - - 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(); - } + EncodeState state = EncodeState.forEncode(bean.getFactory(), sb); + AutoBeanCodexImpl.doEncode(state, bean); + return StringQuoter.split(sb.toString()); } }
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java index e617ddf..ca8de60 100644 --- a/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java +++ b/user/src/com/google/gwt/autobean/shared/AutoBeanUtils.java
@@ -314,8 +314,7 @@ */ private static boolean sameOrEquals(Collection<?> collection, Collection<?> otherCollection, Map<PendingComparison, Comparison> pending, Map<Object, Object> pairs) { - if (collection.size() != otherCollection.size() - || !collection.getClass().equals(otherCollection.getClass())) { + if (collection.size() != otherCollection.size()) { return false; }
diff --git a/user/src/com/google/gwt/autobean/shared/Splittable.java b/user/src/com/google/gwt/autobean/shared/Splittable.java index 65a2286..9011fc7 100644 --- a/user/src/com/google/gwt/autobean/shared/Splittable.java +++ b/user/src/com/google/gwt/autobean/shared/Splittable.java
@@ -15,6 +15,8 @@ */ package com.google.gwt.autobean.shared; +import com.google.gwt.autobean.shared.impl.StringQuoter; + import java.util.List; /** @@ -24,11 +26,41 @@ */ public interface Splittable { /** + * A value that represents {@code null}. + */ + Splittable NULL = StringQuoter.nullValue(); + + /** + * Returns a boolean representation of the data; + */ + boolean asBoolean(); + + /** + * Returns a numeric representation of the data. + */ + double asNumber(); + + /** + * Assign the splittable to the specified index of the {@code parent} object. + */ + void assign(Splittable parent, int index); + + /** + * Assign the splittable to the named property of the {@code parent} object. + */ + void assign(Splittable parent, String propertyName); + + /** * Returns a string representation of the data. */ String asString(); /** + * Clones the Splittable, ignoring cycles and tags. + */ + Splittable deepCopy(); + + /** * Returns the nth element of a list. */ Splittable get(int index); @@ -50,6 +82,16 @@ List<String> getPropertyKeys(); /** + * Returns a value previously set with {@link #setReified(String, Object)}. + */ + Object getReified(String key); + + /** + * Returns {@code true} if the value of the Splittable is a boolean. + */ + boolean isBoolean(); + + /** * Returns {@code} true if {@link #size()} and {@link #get(int)} can be * expected to return meaningful values. */ @@ -62,23 +104,49 @@ boolean isKeyed(); /** - * Indicates if the nth element of a list is null. + * Indicates if the nth element of a list is null or undefined. */ boolean isNull(int index); /** - * Indicates if the named property is null. + * Indicates if the named property is null or undefined. */ boolean isNull(String key); /** + * Returns {@code true} if the value of the Splittable is numeric. + */ + boolean isNumber(); + + /** + * Returns {@code true} if {@link #setReified(String, Object)} has been called + * with the given key. + */ + boolean isReified(String key); + + /** * Returns {@code} true if {@link #asString()} can be expected to return a * meaningful value. */ boolean isString(); /** - * Returns the size of the list. + * Returns {@code true} if the value of the key is undefined. + */ + boolean isUndefined(String key); + + /** + * Associates a tag value with the Splittable. + */ + void setReified(String key, Object object); + + /** + * Resets the length of an indexed Splittable. + */ + void setSize(int i); + + /** + * Returns the size of an indexed Splittable. */ int size(); } \ No newline at end of file
diff --git a/user/src/com/google/gwt/autobean/shared/ValueCodex.java b/user/src/com/google/gwt/autobean/shared/ValueCodex.java index b5e103d..8234ee6 100644 --- a/user/src/com/google/gwt/autobean/shared/ValueCodex.java +++ b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
@@ -15,7 +15,6 @@ */ package com.google.gwt.autobean.shared; -import com.google.gwt.autobean.shared.impl.LazySplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; import java.math.BigDecimal; @@ -38,13 +37,13 @@ } @Override - public BigDecimal decode(Class<?> clazz, String value) { - return new BigDecimal(value); + public BigDecimal decode(Class<?> clazz, Splittable value) { + return new BigDecimal(value.asString()); } @Override - public String toJsonExpression(Object value) { - return StringQuoter.quote(((BigDecimal) value).toString()); + public Splittable encode(Object value) { + return StringQuoter.create(((BigDecimal) value).toString()); } }, BIG_INTEGER(BigInteger.class) { @@ -54,31 +53,46 @@ } @Override - public BigInteger decode(Class<?> clazz, String value) { - return new BigInteger(value); + public BigInteger decode(Class<?> clazz, Splittable value) { + return new BigInteger(value.asString()); } @Override - public String toJsonExpression(Object value) { - return StringQuoter.quote(((BigInteger) value).toString()); + public Splittable encode(Object value) { + return StringQuoter.create(((BigInteger) value).toString()); } }, BOOLEAN(Boolean.class, boolean.class, false) { @Override - public Boolean decode(Class<?> clazz, String value) { - return Boolean.valueOf(value); + public Boolean decode(Class<?> clazz, Splittable value) { + return value.asBoolean(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Boolean) value); } }, BYTE(Byte.class, byte.class, (byte) 0) { @Override - public Byte decode(Class<?> clazz, String value) { - return Byte.valueOf(value); + public Byte decode(Class<?> clazz, Splittable value) { + return (byte) value.asNumber(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Byte) value); } }, CHARACTER(Character.class, char.class, (char) 0) { @Override - public Character decode(Class<?> clazz, String value) { - return value.charAt(0); + public Character decode(Class<?> clazz, Splittable value) { + return value.asString().charAt(0); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create(String.valueOf((Character) value)); } }, DATE(Date.class) { @@ -88,80 +102,111 @@ } @Override - public Date decode(Class<?> clazz, String value) { - return StringQuoter.tryParseDate(value); + public Date decode(Class<?> clazz, Splittable value) { + return StringQuoter.tryParseDate(value.asString()); } @Override - public String toJsonExpression(Object value) { - return String.valueOf(((Date) value).getTime()); + public Splittable encode(Object value) { + return StringQuoter.create(String.valueOf(((Date) value).getTime())); } }, DOUBLE(Double.class, double.class, 0d) { @Override - public Double decode(Class<?> clazz, String value) { - return Double.valueOf(value); + public Double decode(Class<?> clazz, Splittable value) { + return value.asNumber(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Double) value); } }, ENUM(Enum.class) { @Override - public Enum<?> decode(Class<?> clazz, String value) { - return (Enum<?>) clazz.getEnumConstants()[Integer.valueOf(value)]; + public Enum<?> decode(Class<?> clazz, Splittable value) { + return (Enum<?>) clazz.getEnumConstants()[(int) value.asNumber()]; } @Override - public String toJsonExpression(Object value) { - return String.valueOf(((Enum<?>) value).ordinal()); + public Splittable encode(Object value) { + return StringQuoter.create(((Enum<?>) value).ordinal()); } }, FLOAT(Float.class, float.class, 0f) { @Override - public Float decode(Class<?> clazz, String value) { - return Float.valueOf(value); + public Float decode(Class<?> clazz, Splittable value) { + return (float) value.asNumber(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Float) value); } }, INTEGER(Integer.class, int.class, 0) { @Override - public Integer decode(Class<?> clazz, String value) { - return Integer.valueOf(value); + public Integer decode(Class<?> clazz, Splittable value) { + return Integer.valueOf((int) value.asNumber()); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Integer) value); } }, LONG(Long.class, long.class, 0L) { @Override - public Long decode(Class<?> clazz, String value) { - return Long.valueOf(value); + public Long decode(Class<?> clazz, Splittable value) { + return Long.parseLong(value.asString()); } @Override - public String toJsonExpression(Object value) { - // Longs cannot be expressed as a JS double - if (value instanceof Long) { - return StringQuoter.quote(String.valueOf(value)); - } else { - throw new IllegalArgumentException("value should be a Long"); - } + public Splittable encode(Object value) { + return StringQuoter.create(String.valueOf((Long) value)); } }, SHORT(Short.class, short.class, (short) 0) { @Override - public Short decode(Class<?> clazz, String value) { - return Short.valueOf(value); + public Short decode(Class<?> clazz, Splittable value) { + return (short) value.asNumber(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((Short) value); } }, STRING(String.class) { @Override - public String decode(Class<?> clazz, String value) { + public String decode(Class<?> clazz, Splittable value) { + return value.asString(); + } + + @Override + public Splittable encode(Object value) { + return StringQuoter.create((String) value); + } + }, + SPLITTABLE(Splittable.class) { + @Override + public Splittable decode(Class<?> clazz, Splittable value) { return value; } @Override - public String toJsonExpression(Object value) { - return StringQuoter.quote((String) value); + public Splittable encode(Object value) { + return (Splittable) value; } }, VOID(Void.class, void.class, null) { @Override - public Void decode(Class<?> clazz, String value) { + public Void decode(Class<?> clazz, Splittable value) { + return null; + } + + @Override + public Splittable encode(Object value) { return null; } }; @@ -190,7 +235,9 @@ return false; } - public abstract Object decode(Class<?> clazz, String value); + public abstract Object decode(Class<?> clazz, Splittable value); + + public abstract Splittable encode(Object value); public Object getDefaultValue() { return defaultValue; @@ -203,10 +250,6 @@ public Class<?> getType() { return type; } - - public String toJsonExpression(Object value) { - return String.valueOf(value); - } } private static final Set<Class<?>> ALL_VALUE_TYPES; @@ -237,19 +280,23 @@ return ValueCodexHelper.canDecode(clazz); } + @SuppressWarnings("unchecked") public static <T> T decode(Class<T> clazz, Splittable split) { - if (split == null || split == LazySplittable.NULL) { + if (split == null || split == Splittable.NULL) { return null; } - return decode(clazz, split.asString()); + return (T) getTypeOrDie(clazz).decode(clazz, split); } - @SuppressWarnings("unchecked") + /** + * No callers in GWT codebase. + * + * @deprecated use {@link #decode(Class, Splittable)} instead. + * @throws UnsupportedOperationException + */ + @Deprecated public static <T> T decode(Class<T> clazz, String string) { - if (string == null) { - return null; - } - return (T) getTypeOrDie(clazz).decode(clazz, string); + throw new UnsupportedOperationException(); } /** @@ -258,14 +305,14 @@ */ public static Splittable encode(Class<?> clazz, Object obj) { if (obj == null) { - return LazySplittable.NULL; + return Splittable.NULL; } - return new LazySplittable(getTypeOrDie(clazz).toJsonExpression(obj)); + return getTypeOrDie(clazz).encode(obj); } public static Splittable encode(Object obj) { if (obj == null) { - return LazySplittable.NULL; + return Splittable.NULL; } Type t = findType(obj.getClass()); // Try upcasting @@ -280,7 +327,7 @@ if (t == null) { throw new UnsupportedOperationException(obj.getClass().getName()); } - return new LazySplittable(t.toJsonExpression(obj)); + return t.encode(obj); } /**
diff --git a/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java b/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java index ad2dc20..d45da29 100644 --- a/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java +++ b/user/src/com/google/gwt/autobean/shared/impl/AbstractAutoBean.java
@@ -20,6 +20,9 @@ import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.autobean.shared.AutoBeanVisitor; import com.google.gwt.autobean.shared.AutoBeanVisitor.Context; +import com.google.gwt.autobean.shared.Splittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; import com.google.gwt.core.client.impl.WeakMapping; import java.util.HashMap; @@ -32,7 +35,7 @@ * * @param <T> the wrapper type */ -public abstract class AbstractAutoBean<T> implements AutoBean<T> { +public abstract class AbstractAutoBean<T> implements AutoBean<T>, HasSplittable { /** * Used to avoid cycles when visiting. */ @@ -44,64 +47,50 @@ } } + public static final String UNSPLITTABLE_VALUES_KEY = "__unsplittableValues"; protected static final Object[] EMPTY_OBJECT = new Object[0]; /** * Used by {@link #createSimplePeer()}. */ - protected final Map<String, Object> values; - + protected Splittable data; + protected T wrapped; private final AutoBeanFactory factory; private boolean frozen; - /** * Lazily initialized by {@link #setTag(String, Object)} because not all * instances will make use of tags. */ private Map<String, Object> tags; private final boolean usingSimplePeer; - private T wrapped; /** * Constructor that will use a generated simple peer. */ protected AbstractAutoBean(AutoBeanFactory factory) { + this(factory, StringQuoter.createSplittable()); + } + + /** + * Constructor that will use a generated simple peer, backed with existing + * data. + */ + protected AbstractAutoBean(AutoBeanFactory factory, Splittable data) { + this.data = data; this.factory = factory; usingSimplePeer = true; - values = new HashMap<String, Object>(); + wrapped = createSimplePeer(); } /** - * Clone constructor. + * Constructor that wraps an existing object. The parameters on this method + * are reversed to avoid conflicting with the other two-arg constructor for + * {@code AutoBean<Splittable>} instances. */ - protected AbstractAutoBean(AbstractAutoBean<T> toClone, boolean deep) { - this.factory = toClone.factory; - if (!toClone.usingSimplePeer) { - throw new IllegalStateException("Cannot clone wrapped bean"); - } - if (toClone.tags != null) { - tags = new HashMap<String, Object>(toClone.tags); - } - usingSimplePeer = true; - values = new HashMap<String, Object>(toClone.values); - - if (deep) { - for (Map.Entry<String, Object> entry : values.entrySet()) { - AutoBean<?> auto = AutoBeanUtils.getAutoBean(entry.getValue()); - if (auto != null) { - entry.setValue(auto.clone(true).as()); - } - } - } - } - - /** - * Constructor that wraps an existing object. - */ - protected AbstractAutoBean(AutoBeanFactory factory, T wrapped) { + protected AbstractAutoBean(T wrapped, AutoBeanFactory factory) { this.factory = factory; usingSimplePeer = false; - values = null; + data = null; this.wrapped = wrapped; // Used by AutoBeanUtils @@ -114,17 +103,31 @@ public abstract T as(); - public abstract AutoBean<T> clone(boolean deep); + public AutoBean<T> clone(boolean deep) { + throw new UnsupportedOperationException(); + } public AutoBeanFactory getFactory() { return factory; } + public Splittable getSplittable() { + return data; + } + @SuppressWarnings("unchecked") public <Q> Q getTag(String tagName) { return tags == null ? null : (Q) tags.get(tagName); } + /** + * Indicates that the value returned from {@link #getSplittable()} may not + * contain all of the data encapsulated by the AutoBean. + */ + public boolean hasUnsplittableValues() { + return data.isReified(UNSPLITTABLE_VALUES_KEY); + } + public boolean isFrozen() { return frozen; } @@ -133,6 +136,16 @@ return !usingSimplePeer; } + public void setData(Splittable data) { + assert data != null : "null data"; + this.data = data; + /* + * The simple peer aliases the data object from the enclosing bean to avoid + * needing to call up the this.this$0 chain. + */ + wrapped = createSimplePeer(); + } + public void setFrozen(boolean frozen) { this.frozen = frozen; } @@ -208,11 +221,30 @@ return AutoBeanUtils.<W, W> getAutoBean(obj).as(); } - protected T getWrapped() { - if (wrapped == null) { - assert usingSimplePeer : "checkWrapped should have failed"; - wrapped = createSimplePeer(); + /** + * Native getters and setters for primitive properties are generated for each + * type to ensure inlining. + */ + protected <Q> Q getOrReify(String propertyName) { + checkWrapped(); + if (data.isReified(propertyName)) { + @SuppressWarnings("unchecked") + Q temp = (Q) data.getReified(propertyName); + return temp; } + if (data.isNull(propertyName)) { + return null; + } + data.setReified(propertyName, null); + Coder coder = AutoBeanCodexImpl.doCoderFor(this, propertyName); + @SuppressWarnings("unchecked") + Q toReturn = (Q) coder.decode(EncodeState.forDecode(factory), data.get(propertyName)); + data.setReified(propertyName, toReturn); + return toReturn; + } + + protected T getWrapped() { + checkWrapped(); return wrapped; } @@ -233,6 +265,27 @@ protected void set(String method, Object value) { } - protected abstract void traverseProperties(AutoBeanVisitor visitor, - OneShotContext ctx); + protected void setProperty(String propertyName, Object value) { + checkWrapped(); + checkFrozen(); + data.setReified(propertyName, value); + if (value == null) { + Splittable.NULL.assign(data, propertyName); + return; + } + Coder coder = AutoBeanCodexImpl.doCoderFor(this, propertyName); + Splittable backing = coder.extractSplittable(EncodeState.forDecode(factory), value); + if (backing == null) { + /* + * External data type, such as an ArrayList or a concrete implementation + * of a setter's interface type. This means that a slow serialization pass + * is necessary. + */ + data.setReified(UNSPLITTABLE_VALUES_KEY, true); + } else { + backing.assign(data, propertyName); + } + } + + protected abstract void traverseProperties(AutoBeanVisitor visitor, OneShotContext ctx); }
diff --git a/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java b/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java new file mode 100644 index 0000000..88cd297 --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/AutoBeanCodexImpl.java
@@ -0,0 +1,607 @@ +/* + * 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.AutoBean; +import com.google.gwt.autobean.shared.AutoBeanFactory; +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.autobean.shared.Splittable; +import com.google.gwt.autobean.shared.ValueCodex; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Stack; + +/** + * Contains the implementation details of AutoBeanCodex. This type was factored + * out of AutoBeanCodex so that various implementation details can be accessed + * without polluting a public API. + */ +public class AutoBeanCodexImpl { + + /** + * Describes a means of encoding or decoding a particular type of data to or + * from a wire format representation. Any given instance of a Coder should be + * stateless; any state required for operation must be maintained in an + * {@link EncodeState}. + */ + public interface Coder { + Object decode(EncodeState state, Splittable data); + + void encode(EncodeState state, Object value); + + Splittable extractSplittable(EncodeState state, Object value); + } + + /** + * Contains transient state for Coder operation. + */ + public static class EncodeState { + /** + * Constructs a state object used for decoding payloads. + */ + public static EncodeState forDecode(AutoBeanFactory factory) { + return new EncodeState(factory, null); + } + + /** + * Constructs a state object used for encoding payloads. + */ + public static EncodeState forEncode(AutoBeanFactory factory, StringBuilder sb) { + return new EncodeState(factory, sb); + } + + /** + * Constructs a "stateless" state for testing Coders that do not require + * AutoBean implementation details. + */ + public static EncodeState forTesting() { + return new EncodeState(null, null); + } + + final EnumMap enumMap; + final AutoBeanFactory factory; + final StringBuilder sb; + final Stack<AutoBean<?>> seen; + + private EncodeState(AutoBeanFactory factory, StringBuilder sb) { + this.factory = factory; + enumMap = factory instanceof EnumMap ? (EnumMap) factory : null; + this.sb = sb; + this.seen = sb == null ? null : new Stack<AutoBean<?>>(); + } + } + + /** + * Dynamically creates a Coder that is capable of operating on a particular + * parameterization of a datastructure (e.g. {@code Map<String, List<String>>} + * ). + */ + static class CoderCreator extends ParameterizationVisitor { + private Stack<Coder> stack = new Stack<Coder>(); + + @Override + public void endVisitType(Class<?> type) { + if (List.class.equals(type) || Set.class.equals(type)) { + stack.push(collectionCoder(type, stack.pop())); + } else if (Map.class.equals(type)) { + // Note that the parameters are passed in reverse order + stack.push(mapCoder(stack.pop(), stack.pop())); + } else if (Splittable.class.equals(type)) { + stack.push(splittableCoder()); + } else if (type.getEnumConstants() != null) { + @SuppressWarnings(value = {"unchecked"}) + Class<Enum<?>> enumType = (Class<Enum<?>>) type; + stack.push(enumCoder(enumType)); + } else if (ValueCodex.canDecode(type)) { + stack.push(valueCoder(type)); + } else { + stack.push(objectCoder(type)); + } + } + + public Coder getCoder() { + assert stack.size() == 1 : "Incorrect size: " + stack.size(); + return stack.pop(); + } + } + + /** + * Constructs one of the lightweight collection types. + */ + static class CollectionCoder implements Coder { + private final Coder elementDecoder; + private final Class<?> type; + + public CollectionCoder(Class<?> type, Coder elementDecoder) { + this.elementDecoder = elementDecoder; + this.type = type; + } + + public Object decode(EncodeState state, Splittable data) { + Collection<Object> collection; + if (List.class.equals(type)) { + collection = new SplittableList<Object>(data, elementDecoder, state); + } else if (Set.class.equals(type)) { + collection = new SplittableSet<Object>(data, elementDecoder, state); + } else { + // Should not reach here + throw new RuntimeException(type.getName()); + } + return collection; + } + + public void encode(EncodeState state, Object value) { + if (value == null) { + state.sb.append("null"); + return; + } + + Iterator<?> it = ((Collection<?>) value).iterator(); + state.sb.append("["); + if (it.hasNext()) { + elementDecoder.encode(state, it.next()); + while (it.hasNext()) { + state.sb.append(","); + elementDecoder.encode(state, it.next()); + } + } + state.sb.append("]"); + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return tryExtractSplittable(value); + } + } + + /** + * Produces enums. + * + * @param <E> + */ + static class EnumCoder<E extends Enum<?>> implements Coder { + private final Class<E> type; + + public EnumCoder(Class<E> type) { + this.type = type; + } + + public Object decode(EncodeState state, Splittable data) { + return state.enumMap.getEnum(type, data.asString()); + } + + public void encode(EncodeState state, Object value) { + if (value == null) { + state.sb.append("null"); + return; + } + state.sb.append(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value))); + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return StringQuoter.split(StringQuoter.quote(state.enumMap.getToken((Enum<?>) value))); + } + } + + /** + * Used to stop processing. + */ + static class HaltException extends RuntimeException { + public HaltException(RuntimeException cause) { + super(cause); + } + + @Override + public RuntimeException getCause() { + return (RuntimeException) super.getCause(); + } + } + + /** + * Constructs one of the lightweight Map types, depending on the key type. + */ + static 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(EncodeState state, Splittable data) { + Map<Object, Object> toReturn; + if (data.isIndexed()) { + assert data.size() == 2 : "Wrong data size: " + data.size(); + toReturn = new SplittableComplexMap<Object, Object>(data, keyDecoder, valueDecoder, state); + } else { + toReturn = new SplittableSimpleMap<Object, Object>(data, keyDecoder, valueDecoder, state); + } + return toReturn; + } + + public void encode(EncodeState state, Object value) { + if (value == null) { + state.sb.append("null"); + return; + } + + Map<?, ?> map = (Map<?, ?>) value; + boolean isSimpleMap = keyDecoder instanceof ValueCoder; + if (isSimpleMap) { + boolean first = true; + state.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 (first) { + first = false; + } else { + state.sb.append(","); + } + + keyDecoder.encode(state, mapKey); + state.sb.append(":"); + if (mapValue == null) { + // Null values must be preserved + state.sb.append("null"); + } else { + valueDecoder.encode(state, mapValue); + } + } + state.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()); + } + state.sb.append("["); + collectionCoder(List.class, keyDecoder).encode(state, keys); + state.sb.append(","); + collectionCoder(List.class, valueDecoder).encode(state, values); + state.sb.append("]"); + } + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return tryExtractSplittable(value); + } + } + + /** + * Recurses into {@link AutoBeanCodexImpl}. + */ + static class ObjectCoder implements Coder { + private final Class<?> type; + + public ObjectCoder(Class<?> type) { + this.type = type; + } + + public Object decode(EncodeState state, Splittable data) { + AutoBean<?> bean = doDecode(state, type, data); + return bean == null ? null : bean.as(); + } + + public void encode(EncodeState state, Object value) { + if (value == null) { + state.sb.append("null"); + return; + } + doEncode(state, AutoBeanUtils.getAutoBean(value)); + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return tryExtractSplittable(value); + } + } + + static class PropertyCoderCreator extends AutoBeanVisitor { + private AutoBean<?> bean; + + @Override + public boolean visit(AutoBean<?> bean, Context ctx) { + this.bean = bean; + return true; + } + + @Override + public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, + PropertyContext ctx) { + maybeCreateCoder(propertyName, ctx); + return false; + } + + @Override + public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) { + maybeCreateCoder(propertyName, ctx); + return false; + } + + private void maybeCreateCoder(String propertyName, PropertyContext ctx) { + CoderCreator creator = new CoderCreator(); + ctx.accept(creator); + coderFor.put(key(bean, propertyName), creator.getCoder()); + } + } + + /** + * Extracts properties from a bean and turns them into JSON text. + */ + static class PropertyGetter extends AutoBeanVisitor { + private boolean first = true; + private final EncodeState state; + + public PropertyGetter(EncodeState state) { + this.state = state; + } + + @Override + public void endVisit(AutoBean<?> bean, Context ctx) { + state.sb.append("}"); + state.seen.pop(); + } + + @Override + public boolean visit(AutoBean<?> bean, Context ctx) { + if (state.seen.contains(bean)) { + throw new HaltException(new UnsupportedOperationException("Cycles not supported")); + } + state.seen.push(bean); + state.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 { + state.sb.append(","); + } + state.sb.append(StringQuoter.quote(propertyName)); + state.sb.append(":"); + decoder.encode(state, value); + } + } + + /** + * Populates beans with data extracted from an evaluated JSON payload. + */ + static class PropertySetter extends AutoBeanVisitor { + private Splittable data; + private EncodeState state; + + public void decodeInto(EncodeState state, Splittable data, AutoBean<?> bean) { + this.data = data; + this.state = state; + 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(state, data.get(propertyName)); + ctx.set(propertyValue); + } + } + } + + /** + * A passthrough Coder. + */ + static class SplittableCoder implements Coder { + static final Coder INSTANCE = new SplittableCoder(); + + public Object decode(EncodeState state, Splittable data) { + return data; + } + + public void encode(EncodeState state, Object value) { + if (value == null) { + state.sb.append("null"); + return; + } + state.sb.append(((Splittable) value).getPayload()); + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return (Splittable) value; + } + } + + /** + * Delegates to ValueCodex. + */ + static 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(EncodeState state, Splittable propertyValue) { + if (propertyValue == null || propertyValue == Splittable.NULL) { + return ValueCodex.getUninitializedFieldValue(type); + } + return ValueCodex.decode(type, propertyValue); + } + + public void encode(EncodeState state, Object value) { + state.sb.append(ValueCodex.encode(type, value).getPayload()); + } + + public Splittable extractSplittable(EncodeState state, Object value) { + return ValueCodex.encode(type, value); + } + } + + /** + * A map of AutoBean interface+property names to the Coder for that property. + */ + private static final Map<String, Coder> coderFor = new HashMap<String, Coder>(); + /** + * A map of types to a Coder that handles the type. + */ + private static final Map<Class<?>, Coder> coders = new HashMap<Class<?>, Coder>(); + + public static Coder collectionCoder(Class<?> type, Coder elementCoder) { + return new CollectionCoder(type, elementCoder); + } + + public static Coder doCoderFor(AutoBean<?> bean, String propertyName) { + String key = key(bean, propertyName); + Coder toReturn = coderFor.get(key); + if (toReturn == null) { + bean.accept(new PropertyCoderCreator()); + toReturn = coderFor.get(key); + if (toReturn == null) { + throw new IllegalArgumentException(propertyName); + } + } + return toReturn; + } + + public static <T> AutoBean<T> doDecode(EncodeState state, Class<T> clazz, Splittable data) { + @SuppressWarnings("unchecked") + AutoBean<T> toReturn = (AutoBean<T>) data.getReified(AutoBeanCodexImpl.class.getName()); + if (toReturn != null) { + return toReturn; + } + toReturn = state.factory.create(clazz); + data.setReified(AutoBeanCodexImpl.class.getName(), toReturn); + if (toReturn == null) { + throw new IllegalArgumentException(clazz.getName()); + } + ((AbstractAutoBean<T>) toReturn).setData(data); + return toReturn; + } + + public static void doDecodeInto(EncodeState state, Splittable data, AutoBean<?> bean) { + new PropertySetter().decodeInto(state, data, bean); + } + + public static void doEncode(EncodeState state, AutoBean<?> bean) { + PropertyGetter e = new PropertyGetter(state); + try { + bean.accept(e); + } catch (HaltException ex) { + throw ex.getCause(); + } + } + + public static <E extends Enum<?>> Coder enumCoder(Class<E> type) { + Coder toReturn = coders.get(type); + if (toReturn == null) { + toReturn = new EnumCoder<E>(type); + coders.put(type, toReturn); + } + return toReturn; + } + + public static Coder mapCoder(Coder valueCoder, Coder keyCoder) { + return new MapCoder(valueCoder, keyCoder); + } + + public static Coder objectCoder(Class<?> type) { + Coder toReturn = coders.get(type); + if (toReturn == null) { + toReturn = new ObjectCoder(type); + coders.put(type, toReturn); + } + return toReturn; + } + + public static Coder splittableCoder() { + return SplittableCoder.INSTANCE; + } + + public static Coder valueCoder(Class<?> type) { + Coder toReturn = coders.get(type); + if (toReturn == null) { + toReturn = new ValueCoder(type); + coders.put(type, toReturn); + } + return toReturn; + } + + static Splittable tryExtractSplittable(Object value) { + AutoBean<?> bean = AutoBeanUtils.getAutoBean(value); + if (bean != null) { + value = bean; + } + if (bean instanceof HasSplittable) { + return ((HasSplittable) bean).getSplittable(); + } + return null; + } + + private static String key(AutoBean<?> bean, String propertyName) { + return bean.getType().getName() + ":" + propertyName; + } +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java index 3b53d1b..0a3d298 100644 --- a/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java +++ b/user/src/com/google/gwt/autobean/shared/impl/EnumMap.java
@@ -27,7 +27,7 @@ Class<? extends Enum<?>>[] value(); } - <E extends Enum<E>> E getEnum(Class<E> clazz, String token); + <E extends Enum<?>> E getEnum(Class<E> clazz, String token); String getToken(Enum<?> e); }
diff --git a/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java b/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java new file mode 100644 index 0000000..7ae4a33 --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/HasSplittable.java
@@ -0,0 +1,26 @@ +/* + * 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.Splittable; + +/** + * Allows reified type facades to return their underlying Splittable + * datastructure. + */ +public interface HasSplittable { + Splittable getSplittable(); +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java b/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java deleted file mode 100644 index 2fb4749..0000000 --- a/user/src/com/google/gwt/autobean/shared/impl/LazySplittable.java +++ /dev/null
@@ -1,92 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.gwt.autobean.shared.impl; - -import com.google.gwt.autobean.shared.Splittable; - -import java.util.List; - -/** - * Holds a string payload with the expectation that the object will be used only - * for creating a larger payload. - */ -public class LazySplittable implements Splittable { - public static final Splittable NULL = new LazySplittable("null"); - - private final String payload; - private Splittable split; - - public LazySplittable(String payload) { - this.payload = payload; - } - - public String asString() { - maybeSplit(); - return split.asString(); - } - - public Splittable get(int index) { - maybeSplit(); - return split.get(index); - } - - public Splittable get(String key) { - maybeSplit(); - return split.get(key); - } - - public String getPayload() { - return payload; - } - - public List<String> getPropertyKeys() { - maybeSplit(); - return split.getPropertyKeys(); - } - - public boolean isIndexed() { - return payload.charAt(0) == '['; - } - - public boolean isKeyed() { - return payload.charAt(0) == '{'; - } - - public boolean isNull(int index) { - maybeSplit(); - return split.isNull(index); - } - - public boolean isNull(String key) { - maybeSplit(); - return split.isNull(key); - } - - public boolean isString() { - return payload.charAt(0) == '\"'; - } - - public int size() { - maybeSplit(); - return split.size(); - } - - private void maybeSplit() { - if (split == null) { - split = StringQuoter.split(payload); - } - } -}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java new file mode 100644 index 0000000..4225512 --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableComplexMap.java
@@ -0,0 +1,200 @@ +/* + * 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.Splittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; + +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +/** + * A Map implementation for complex keys. + * + * @param <K> the key type + * @param <V> the value type + */ +public class SplittableComplexMap<K, V> implements Map<K, V>, HasSplittable { + private final Splittable data; + private final List<K> keys; + private final List<V> values; + + public SplittableComplexMap(Splittable data, Coder keyCoder, Coder valueCoder, EncodeState state) { + this.data = data; + this.keys = new SplittableList<K>(data.get(0), keyCoder, state); + this.values = new SplittableList<V>(data.get(1), valueCoder, state); + assert this.keys.size() == this.values.size(); + } + + public void clear() { + // Trigger ConcurrentModificationExceptions for any outstanding Iterators + keys.clear(); + values.clear(); + } + + public boolean containsKey(Object key) { + return keys.contains(key); + } + + public boolean containsValue(Object value) { + return values.contains(value); + } + + public Set<java.util.Map.Entry<K, V>> entrySet() { + return new AbstractSet<Map.Entry<K, V>>() { + + @Override + public Iterator<java.util.Map.Entry<K, V>> iterator() { + return new Iterator<Map.Entry<K, V>>() { + Iterator<K> keyIt = keys.iterator(); + ListIterator<V> valueIt = values.listIterator(); + + public boolean hasNext() { + assert keyIt.hasNext() == valueIt.hasNext(); + return keyIt.hasNext(); + } + + public java.util.Map.Entry<K, V> next() { + return new Map.Entry<K, V>() { + final K key = keyIt.next(); + final V value = valueIt.next(); + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + valueIt.set(value); + return value; + } + }; + } + + public void remove() { + keyIt.remove(); + valueIt.remove(); + } + }; + } + + @Override + public int size() { + return keys.size(); + } + }; + } + + public V get(Object key) { + int idx = keys.indexOf(key); + if (idx == -1) { + return null; + } + return values.get(idx); + } + + public Splittable getSplittable() { + return data; + } + + public boolean isEmpty() { + return keys.isEmpty(); + } + + public Set<K> keySet() { + return new AbstractSet<K>() { + @Override + public Iterator<K> iterator() { + return keys.iterator(); + } + + @Override + public int size() { + return keys.size(); + } + }; + } + + public V put(K key, V value) { + int idx = keys.indexOf(key); + if (idx == -1) { + keys.add(key); + values.add(value); + return null; + } + return values.set(idx, value); + } + + public void putAll(Map<? extends K, ? extends V> m) { + for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public V remove(Object key) { + int idx = keys.indexOf(key); + if (idx == -1) { + return null; + } + keys.remove(idx); + return values.remove(idx); + } + + public int size() { + return keys.size(); + } + + public Collection<V> values() { + return new AbstractCollection<V>() { + @Override + public Iterator<V> iterator() { + return new Iterator<V>() { + final Iterator<K> keyIt = keys.iterator(); + final Iterator<V> valueIt = values.iterator(); + + public boolean hasNext() { + return keyIt.hasNext(); + } + + public V next() { + keyIt.next(); + return valueIt.next(); + } + + public void remove() { + keyIt.remove(); + valueIt.remove(); + } + }; + } + + @Override + public int size() { + return keys.size(); + } + }; + } +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java new file mode 100644 index 0000000..0a7df50 --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableList.java
@@ -0,0 +1,114 @@ +/* + * 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.Splittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; + +import java.util.AbstractList; + +/** + * A list implementation that lazily reifies its constituent elements. + * + * @param <E> the element type + */ +public class SplittableList<E> extends AbstractList<E> implements HasSplittable { + static <Q> Q reify(EncodeState state, Splittable data, int index, Coder coder) { + if (data.isNull(index)) { + return null; + } + @SuppressWarnings("unchecked") + Q toReturn = (Q) coder.decode(state, data.get(index)); + data.setReified(String.valueOf(index), toReturn); + return toReturn; + } + + static void set(EncodeState state, Splittable data, int index, Coder coder, Object value) { + data.setReified(String.valueOf(index), value); + if (value == null) { + Splittable.NULL.assign(data, index); + return; + } + Splittable backing = coder.extractSplittable(state, value); + if (backing == null) { + /* + * External data type, such as an ArrayList or a concrete implementation + * of a setter's interface type. This means that a slow serialization pass + * is necessary. + */ + data.setReified(AbstractAutoBean.UNSPLITTABLE_VALUES_KEY, true); + } else { + backing.assign(data, index); + } + } + + private Splittable data; + private final Coder elementCoder; + private final EncodeState state; + + public SplittableList(Splittable data, Coder elementCoder, EncodeState state) { + assert data.isIndexed() : "Expecting indexed data"; + this.data = data; + this.elementCoder = elementCoder; + this.state = state; + } + + @Override + public void add(int index, E element) { + set(state, data, index, elementCoder, element); + } + + @Override + public E get(int index) { + if (data.isReified(String.valueOf(index))) { + @SuppressWarnings("unchecked") + E toReturn = (E) data.getReified(String.valueOf(index)); + return toReturn; + } + // javac generics bug + return SplittableList.<E> reify(state, data, index, elementCoder); + } + + public Splittable getSplittable() { + return data; + } + + @Override + public E remove(int index) { + E toReturn = get(index); + // XXX This is terrible, use Array.splice + int newSize = data.size() - 1; + for (int i = index; i < newSize; i++) { + data.get(i + 1).assign(data, i); + data.setReified(String.valueOf(i), data.getReified(String.valueOf(i + 1))); + } + data.setSize(newSize); + return toReturn; + } + + @Override + public E set(int index, E element) { + E previous = get(index); + set(state, data, index, elementCoder, element); + return previous; + } + + @Override + public int size() { + return data.size(); + } +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java new file mode 100644 index 0000000..fbd3f5d --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableSet.java
@@ -0,0 +1,71 @@ +/* + * 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.Splittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; + +import java.util.AbstractSet; +import java.util.Iterator; + +/** + * This type is optimized for the read-only case and has {@code O(n)} insertion + * / lookup performance since computing hashcodes for the elements would require + * up-front reification. + * + * @param <E> the element type + */ +public class SplittableSet<E> extends AbstractSet<E> implements HasSplittable { + private SplittableList<E> data; + + public SplittableSet(Splittable data, Coder elementCoder, EncodeState state) { + this.data = new SplittableList<E>(data, elementCoder, state); + } + + @Override + public boolean add(E e) { + if (!data.contains(e)) { + data.add(e); + return true; + } + return false; + } + + @Override + public void clear() { + data.clear(); + } + + public Splittable getSplittable() { + return data.getSplittable(); + } + + @Override + public Iterator<E> iterator() { + return data.iterator(); + } + + @Override + public boolean remove(Object o) { + return data.remove(o); + } + + @Override + public int size() { + return data.size(); + } +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java b/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java new file mode 100644 index 0000000..edfeff4 --- /dev/null +++ b/user/src/com/google/gwt/autobean/shared/impl/SplittableSimpleMap.java
@@ -0,0 +1,255 @@ +/* + * 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.Splittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; + +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A Map implementation for regular JSON maps with value-type keys. + * + * @param <K> the key type + * @param <V> the value type + */ +public class SplittableSimpleMap<K, V> implements Map<K, V>, HasSplittable { + private final Splittable data; + private final Coder keyCoder; + private final EncodeState state; + private final Coder valueCoder; + /** + * Don't hang the reified data from {@link #data} since we can't tell the + * __reified field from the actual data. + */ + private Splittable reified = StringQuoter.createSplittable(); + + public SplittableSimpleMap(Splittable data, Coder keyCoder, Coder valueCoder, EncodeState state) { + this.data = data; + this.keyCoder = keyCoder; + this.state = state; + this.valueCoder = valueCoder; + } + + public void clear() { + for (String key : data.getPropertyKeys()) { + Splittable.NULL.assign(data, key); + reified.setReified(key, null); + } + } + + public boolean containsKey(Object key) { + String encodedKey = encodedKey(key); + return !data.isUndefined(encodedKey) || reified.isReified(encodedKey); + } + + public boolean containsValue(Object value) { + return values().contains(value); + } + + public Set<java.util.Map.Entry<K, V>> entrySet() { + return new AbstractSet<Map.Entry<K, V>>() { + final List<String> keys = data.getPropertyKeys(); + + @Override + public Iterator<java.util.Map.Entry<K, V>> iterator() { + return new Iterator<Map.Entry<K, V>>() { + Iterator<String> keyIterator = keys.iterator(); + String encodedKey; + + public boolean hasNext() { + return keyIterator.hasNext(); + } + + public java.util.Map.Entry<K, V> next() { + encodedKey = keyIterator.next(); + return new Map.Entry<K, V>() { + @SuppressWarnings("unchecked") + final K key = (K) keyCoder.decode(state, StringQuoter.split(StringQuoter + .quote(encodedKey))); + @SuppressWarnings("unchecked") + final V value = (V) valueCoder.decode(state, data.get(encodedKey)); + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V newValue) { + return put(key, newValue); + } + }; + } + + public void remove() { + Splittable.NULL.assign(data, encodedKey); + reified.setReified(encodedKey, null); + } + }; + } + + @Override + public int size() { + return keys.size(); + } + }; + } + + public V get(Object key) { + String encodedKey = encodedKey(key); + return getRaw(encodedKey); + } + + public Splittable getSplittable() { + return data; + } + + public boolean isEmpty() { + return data.getPropertyKeys().isEmpty(); + } + + public Set<K> keySet() { + return new AbstractSet<K>() { + final List<String> keys = data.getPropertyKeys(); + + @Override + public Iterator<K> iterator() { + return new Iterator<K>() { + final Iterator<String> it = keys.iterator(); + String lastEncodedKey; + + public boolean hasNext() { + return it.hasNext(); + } + + public K next() { + lastEncodedKey = it.next(); + @SuppressWarnings("unchecked") + K toReturn = + (K) keyCoder.decode(state, StringQuoter.split(StringQuoter.quote(lastEncodedKey))); + return toReturn; + } + + public void remove() { + Splittable.NULL.assign(data, lastEncodedKey); + reified.setReified(lastEncodedKey, null); + } + }; + } + + @Override + public int size() { + return keys.size(); + } + }; + } + + public V put(K key, V value) { + V toReturn = get(key); + String encodedKey = encodedKey(key); + reified.setReified(encodedKey, value); + Splittable encodedValue = valueCoder.extractSplittable(state, value); + if (encodedValue == null) { + // External datastructure + reified.setReified(AbstractAutoBean.UNSPLITTABLE_VALUES_KEY, true); + } else { + encodedValue.assign(data, encodedKey); + } + return toReturn; + } + + public void putAll(Map<? extends K, ? extends V> m) { + for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public V remove(Object key) { + V toReturn = get(key); + String encodedKey = encodedKey(key); + reified.setReified(encodedKey, null); + Splittable.NULL.assign(data, encodedKey); + return toReturn; + } + + public int size() { + return data.getPropertyKeys().size(); + } + + public Collection<V> values() { + return new AbstractCollection<V>() { + final List<String> keys = data.getPropertyKeys(); + + @Override + public Iterator<V> iterator() { + return new Iterator<V>() { + final Iterator<String> it = keys.iterator(); + String lastEncodedKey; + + public boolean hasNext() { + return it.hasNext(); + } + + public V next() { + lastEncodedKey = it.next(); + return getRaw(lastEncodedKey); + } + + public void remove() { + Splittable.NULL.assign(data, lastEncodedKey); + reified.setReified(lastEncodedKey, null); + } + }; + } + + @Override + public int size() { + return keys.size(); + } + }; + } + + private String encodedKey(Object key) { + return keyCoder.extractSplittable(state, key).asString(); + } + + private V getRaw(String encodedKey) { + if (reified.isReified(encodedKey)) { + @SuppressWarnings("unchecked") + V toReturn = (V) reified.getReified(encodedKey); + return toReturn; + } + // Both undefined or an explicit null should return null here + if (data.isNull(encodedKey)) { + return null; + } + Splittable value = data.get(encodedKey); + @SuppressWarnings("unchecked") + V toReturn = (V) valueCoder.decode(state, value); + reified.setReified(encodedKey, toReturn); + return toReturn; + } +}
diff --git a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java index 899ed52..559e2af 100644 --- a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java +++ b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -31,12 +31,36 @@ */ public class StringQuoter { private static final String ISO8601_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSz"; - private static final DateFormat ISO8601 = new SimpleDateFormat( - ISO8601_PATTERN, Locale.getDefault()); + private static final DateFormat ISO8601 = new SimpleDateFormat(ISO8601_PATTERN, Locale + .getDefault()); private static final String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z"; - private static final DateFormat RFC2822 = new SimpleDateFormat( - RFC2822_PATTERN, Locale.getDefault()); + private static final DateFormat RFC2822 = new SimpleDateFormat(RFC2822_PATTERN, Locale + .getDefault()); + + public static Splittable create(boolean value) { + return JsonSplittable.create(String.valueOf(value)); + } + + public static Splittable create(double value) { + return JsonSplittable.create(String.valueOf(value)); + } + + public static Splittable create(String value) { + return JsonSplittable.create(quote(value)); + } + + public static Splittable createIndexed() { + return JsonSplittable.createIndexed(); + } + + public static Splittable createSplittable() { + return JsonSplittable.create(); + } + + public static Splittable nullValue() { + return JsonSplittable.createNull(); + } /** * Create a quoted JSON string.
diff --git a/user/src/com/google/gwt/core/client/JsonUtils.java b/user/src/com/google/gwt/core/client/JsonUtils.java index e44bb35..7088266 100644 --- a/user/src/com/google/gwt/core/client/JsonUtils.java +++ b/user/src/com/google/gwt/core/client/JsonUtils.java
@@ -60,17 +60,17 @@ try { return JSON.parse(json); } catch (e) { - return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e); + return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json); } } else { if (!@com.google.gwt.core.client.JsonUtils::safeToEval(Ljava/lang/String;)(json)) { - return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Illegal character in JSON string"); + return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Illegal character in JSON string", json); } json = @com.google.gwt.core.client.JsonUtils::escapeJsonForEval(Ljava/lang/String;)(json); try { return eval('(' + json + ')'); } catch (e) { - return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e); + return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json); } } }-*/; @@ -110,12 +110,12 @@ try { return eval('(' + escaped + ')'); } catch (e) { - return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(Ljava/lang/String;)("Error parsing JSON: " + e); + return @com.google.gwt.core.client.JsonUtils::throwIllegalArgumentException(*)("Error parsing JSON: " + e, json); } }-*/; - static void throwIllegalArgumentException(String message) { - throw new IllegalArgumentException(message); + static void throwIllegalArgumentException(String message, String data) { + throw new IllegalArgumentException(message + "\n" + data); } private static native String escapeChar(String c) /*-{
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java index 6487320..429dd6f 100644 --- a/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java +++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/AbstractRequestContext.java
@@ -26,8 +26,8 @@ import com.google.gwt.autobean.shared.AutoBeanVisitor; import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.ValueCodex; +import com.google.gwt.autobean.shared.impl.AbstractAutoBean; import com.google.gwt.autobean.shared.impl.EnumMap; -import com.google.gwt.autobean.shared.impl.LazySplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; import com.google.gwt.event.shared.UmbrellaException; import com.google.web.bindery.requestfactory.shared.BaseProxy; @@ -36,13 +36,13 @@ import com.google.web.bindery.requestfactory.shared.EntityProxyId; import com.google.web.bindery.requestfactory.shared.Receiver; import com.google.web.bindery.requestfactory.shared.RequestContext; +import com.google.web.bindery.requestfactory.shared.RequestTransport.TransportReceiver; import com.google.web.bindery.requestfactory.shared.ServerFailure; -import com.google.web.bindery.requestfactory.shared.ValueProxy; import com.google.web.bindery.requestfactory.shared.Violation; import com.google.web.bindery.requestfactory.shared.WriteOperation; -import com.google.web.bindery.requestfactory.shared.RequestTransport.TransportReceiver; import com.google.web.bindery.requestfactory.shared.impl.posers.DatePoser; import com.google.web.bindery.requestfactory.shared.messages.IdMessage; +import com.google.web.bindery.requestfactory.shared.messages.IdMessage.Strength; import com.google.web.bindery.requestfactory.shared.messages.InvocationMessage; import com.google.web.bindery.requestfactory.shared.messages.JsonRpcRequest; import com.google.web.bindery.requestfactory.shared.messages.MessageFactory; @@ -51,7 +51,6 @@ import com.google.web.bindery.requestfactory.shared.messages.ResponseMessage; import com.google.web.bindery.requestfactory.shared.messages.ServerFailureMessage; import com.google.web.bindery.requestfactory.shared.messages.ViolationMessage; -import com.google.web.bindery.requestfactory.shared.messages.IdMessage.Strength; import java.util.ArrayList; import java.util.Collection; @@ -67,8 +66,7 @@ /** * Base implementations for RequestContext services. */ -public abstract class AbstractRequestContext implements RequestContext, - EntityCodex.EntitySource { +public abstract class AbstractRequestContext implements RequestContext, EntityCodex.EntitySource { /** * Allows the payload dialect to be injected into the AbstractRequestContext * without the caller needing to be concerned with how the implementation @@ -110,8 +108,7 @@ * DialectImpl interface and restored to to AbstractRequestContext. */ if (!invocations.isEmpty()) { - throw new RuntimeException( - "Only one invocation per request, pending backend support"); + throw new RuntimeException("Only one invocation per request, pending backend support"); } invocations.add(request); for (Object arg : request.getRequestData().getOrderedParameters()) { @@ -152,20 +149,23 @@ if (!raw.isNull("error")) { Splittable error = raw.get("error"); - ServerFailure failure = new ServerFailure( - error.get("message").asString(), error.get("code").asString(), - payload, true); + ServerFailure failure = + new ServerFailure(error.get("message").asString(), error.get("code").asString(), + payload, true); fail(receiver, failure); return; } Splittable result = raw.get("result"); @SuppressWarnings("unchecked") - Class<BaseProxy> target = (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType(); + Class<BaseProxy> target = + (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType(); SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target); AutoBean<BaseProxy> bean = createProxy(target, id); - AutoBeanCodex.decodeInto(result, bean); + // XXX expose this as a proper API + ((AbstractAutoBean<?>) bean).setData(result); + // AutoBeanCodex.decodeInto(result, bean); if (callback != null) { callback.onSuccess(bean.as()); @@ -178,9 +178,8 @@ Splittable encode(Object obj) { Splittable value; if (obj == null) { - return LazySplittable.NULL; - } else if (obj.getClass().isEnum() - && getAutoBeanFactory() instanceof EnumMap) { + return Splittable.NULL; + } else if (obj.getClass().isEnum() && getAutoBeanFactory() instanceof EnumMap) { value = ValueCodex.encode(((EnumMap) getAutoBeanFactory()).getToken((Enum<?>) obj)); } else if (ValueCodex.canDecode(obj.getClass())) { value = ValueCodex.encode(obj); @@ -233,13 +232,13 @@ } public void processPayload(final Receiver<Void> receiver, String payload) { - ResponseMessage response = AutoBeanCodex.decode( - MessageFactoryHolder.FACTORY, ResponseMessage.class, payload).as(); + ResponseMessage response = + AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ResponseMessage.class, payload).as(); if (response.getGeneralFailure() != null) { ServerFailureMessage failure = response.getGeneralFailure(); - ServerFailure fail = new ServerFailure(failure.getMessage(), - failure.getExceptionType(), failure.getStackTrace(), - failure.isFatal()); + ServerFailure fail = + new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure + .getStackTrace(), failure.isFatal()); fail(receiver, fail); return; @@ -266,13 +265,12 @@ if (response.getStatusCodes().get(i)) { invocations.get(i).onSuccess(response.getInvocationResults().get(i)); } else { - ServerFailureMessage failure = AutoBeanCodex.decode( - MessageFactoryHolder.FACTORY, ServerFailureMessage.class, - response.getInvocationResults().get(i)).as(); + ServerFailureMessage failure = + AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, ServerFailureMessage.class, + response.getInvocationResults().get(i)).as(); invocations.get(i).onFail( - new ServerFailure(failure.getMessage(), - failure.getExceptionType(), failure.getStackTrace(), - failure.isFatal())); + new ServerFailure(failure.getMessage(), failure.getExceptionType(), failure + .getStackTrace(), failure.isFatal())); } } catch (Throwable t) { if (causes == null) { @@ -368,12 +366,14 @@ * Objects are placed into this map by being passed into {@link #edit} or as * an invocation argument. */ - private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies = new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>(); + private final Map<SimpleProxyId<?>, AutoBean<? extends BaseProxy>> editedProxies = + new LinkedHashMap<SimpleProxyId<?>, AutoBean<? extends BaseProxy>>(); /** * A map that contains the canonical instance of an entity to return in the * return graph, since this is built from scratch. */ - private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies = new HashMap<SimpleProxyId<?>, AutoBean<?>>(); + private final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies = + new HashMap<SimpleProxyId<?>, AutoBean<?>>(); /** * A map that allows us to handle the case where the server has sent back an @@ -381,12 +381,12 @@ * client will need to swap out the request-local ids with a regular * client-allocated id. */ - private final Map<Integer, SimpleProxyId<?>> syntheticIds = new HashMap<Integer, SimpleProxyId<?>>(); + private final Map<Integer, SimpleProxyId<?>> syntheticIds = + new HashMap<Integer, SimpleProxyId<?>>(); private final DialectImpl dialect; - protected AbstractRequestContext(AbstractRequestFactory factory, - Dialect dialect) { + protected AbstractRequestContext(AbstractRequestFactory factory, Dialect dialect) { this.requestFactory = factory; this.dialect = dialect.create(this); } @@ -405,12 +405,10 @@ /** * Creates a new proxy with an assigned ID. */ - public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, - SimpleProxyId<T> id) { + public <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id) { AutoBean<T> created = getAutoBeanFactory().create(clazz); if (created == null) { - throw new IllegalArgumentException("Unknown proxy type " - + clazz.getName()); + throw new IllegalArgumentException("Unknown proxy type " + clazz.getName()); } created.setTag(STABLE_ID, id); return created; @@ -481,10 +479,9 @@ /** * EntityCodex support. */ - public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( - Splittable serializedProxyId) { - IdMessage ref = AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, - IdMessage.class, serializedProxyId).as(); + public <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(Splittable serializedProxyId) { + IdMessage ref = + AutoBeanCodex.decode(MessageFactoryHolder.FACTORY, IdMessage.class, serializedProxyId).as(); @SuppressWarnings("unchecked") SimpleProxyId<Q> id = (SimpleProxyId<Q>) getId(ref); return getProxyForReturnPayloadGraph(id); @@ -566,8 +563,7 @@ protected void fail(Receiver<Void> receiver, ServerFailure failure) { reuse(); Set<Throwable> causes = null; - for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>( - invocations)) { + for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) { try { request.onFail(failure); } catch (Throwable t) { @@ -606,8 +602,7 @@ protected void violation(final Receiver<Void> receiver, Set<Violation> errors) { reuse(); Set<Throwable> causes = null; - for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>( - invocations)) { + for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(invocations)) { try { request.onViolation(errors); } catch (Throwable t) { @@ -640,16 +635,14 @@ if (Strength.SYNTHETIC.equals(op.getStrength())) { return allocateSyntheticId(op.getTypeToken(), op.getSyntheticId()); } - return requestFactory.getId(op.getTypeToken(), op.getServerId(), - op.getClientId()); + return requestFactory.getId(op.getTypeToken(), op.getServerId(), op.getClientId()); } /** * Creates or retrieves a new canonical AutoBean to represent the given id in * the returned payload. */ - <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph( - SimpleProxyId<Q> id) { + <Q extends BaseProxy> AutoBean<Q> getProxyForReturnPayloadGraph(SimpleProxyId<Q> id) { @SuppressWarnings("unchecked") AutoBean<Q> bean = (AutoBean<Q>) returnedProxies.get(id); if (bean == null) { @@ -665,8 +658,8 @@ * Create a single OperationMessage that encapsulates the state of a proxy * AutoBean. */ - AutoBean<OperationMessage> makeOperationMessage( - SimpleProxyId<BaseProxy> stableId, AutoBean<?> proxyBean, boolean useDelta) { + AutoBean<OperationMessage> makeOperationMessage(SimpleProxyId<BaseProxy> stableId, + AutoBean<?> proxyBean, boolean useDelta) { // The OperationMessages describes operations on exactly one entity AutoBean<OperationMessage> toReturn = MessageFactoryHolder.FACTORY.operation(); @@ -710,8 +703,9 @@ Map<String, Object> diff = Collections.emptyMap(); if (isEntityType(stableId.getProxyClass())) { // Compute what's changed on the client - diff = useDelta ? AutoBeanUtils.diff(parent, proxyBean) - : AutoBeanUtils.getAllProperties(proxyBean); + diff = + useDelta ? AutoBeanUtils.diff(parent, proxyBean) : AutoBeanUtils + .getAllProperties(proxyBean); } else if (isValueType(stableId.getProxyClass())) { // Send everything diff = AutoBeanUtils.getAllProperties(proxyBean); @@ -720,8 +714,7 @@ if (!diff.isEmpty()) { Map<String, Splittable> propertyMap = new HashMap<String, Splittable>(); for (Map.Entry<String, Object> entry : diff.entrySet()) { - propertyMap.put(entry.getKey(), - EntityCodex.encode(this, entry.getValue())); + propertyMap.put(entry.getKey(), EntityCodex.encode(this, entry.getValue())); } operation.setPropertyMap(propertyMap); } @@ -735,8 +728,8 @@ * @param returnRecord the JSON map containing property/value pairs * @param operations the WriteOperation eventns to broadcast over the EventBus */ - <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, - OperationMessage op, WriteOperation... operations) { + <Q extends BaseProxy> Q processReturnOperation(SimpleProxyId<Q> id, OperationMessage op, + WriteOperation... operations) { AutoBean<Q> toMutate = getProxyForReturnPayloadGraph(id); toMutate.setTag(Constants.VERSION_PROPERTY_B64, op.getVersion()); @@ -746,15 +739,16 @@ // Apply updates toMutate.accept(new AutoBeanVisitor() { @Override - public boolean visitReferenceProperty(String propertyName, - AutoBean<?> value, PropertyContext ctx) { + public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, + PropertyContext ctx) { if (ctx.canSet()) { if (properties.containsKey(propertyName)) { Splittable raw = properties.get(propertyName); - Class<?> elementType = ctx instanceof CollectionPropertyContext - ? ((CollectionPropertyContext) ctx).getElementType() : null; - Object decoded = EntityCodex.decode(AbstractRequestContext.this, - ctx.getType(), elementType, raw); + Class<?> elementType = + ctx instanceof CollectionPropertyContext ? ((CollectionPropertyContext) ctx) + .getElementType() : null; + Object decoded = + EntityCodex.decode(AbstractRequestContext.this, ctx.getType(), elementType, raw); ctx.set(decoded); } } @@ -762,8 +756,7 @@ } @Override - public boolean visitValueProperty(String propertyName, Object value, - PropertyContext ctx) { + public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) { if (ctx.canSet()) { if (properties.containsKey(propertyName)) { Splittable raw = properties.get(propertyName); @@ -799,8 +792,8 @@ continue; } requestFactory.getEventBus().fireEventFromSource( - new EntityProxyChange<EntityProxy>((EntityProxy) proxy, - writeOperation), id.getProxyClass()); + new EntityProxyChange<EntityProxy>((EntityProxy) proxy, writeOperation), + id.getProxyClass()); } } return proxy; @@ -811,8 +804,8 @@ * * @see #syntheticIds */ - private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId( - String typeToken, int syntheticId) { + private <Q extends BaseProxy> SimpleProxyId<Q> allocateSyntheticId(String typeToken, + int syntheticId) { @SuppressWarnings("unchecked") SimpleProxyId<Q> toReturn = (SimpleProxyId<Q>) syntheticIds.get(syntheticId); if (toReturn == null) { @@ -861,19 +854,25 @@ * Shallow-clones an autobean and makes duplicates of the collection types. A * regular {@link AutoBean#clone} won't duplicate reference properties. */ - private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections( - AutoBean<T> toClone) { - AutoBean<T> clone = toClone.clone(false); + private <T extends BaseProxy> AutoBean<T> cloneBeanAndCollections(final AutoBean<T> toClone) { + AutoBean<T> clone = toClone.getFactory().create(toClone.getType()); + clone.setTag(STABLE_ID, toClone.getTag(STABLE_ID)); + clone.setTag(Constants.VERSION_PROPERTY_B64, toClone.getTag(Constants.VERSION_PROPERTY_B64)); /* * Take ownership here to prevent cycles in value objects from overflowing * the stack. */ takeOwnership(clone); clone.accept(new AutoBeanVisitor() { + final Map<String, Object> values = AutoBeanUtils.getAllProperties(toClone); @Override - public boolean visitCollectionProperty(String propertyName, - AutoBean<Collection<?>> value, CollectionPropertyContext ctx) { + public boolean visitCollectionProperty(String propertyName, AutoBean<Collection<?>> value, + CollectionPropertyContext ctx) { + // javac generics bug + value = + AutoBeanUtils.<Collection<?>, Collection<?>> getAutoBean((Collection<?>) values + .get(propertyName)); if (value != null) { Collection<Object> collection; if (List.class == ctx.getType()) { @@ -885,20 +884,20 @@ throw new IllegalArgumentException(ctx.getType().getName()); } - if (isValueType(ctx.getElementType())) { + if (isValueType(ctx.getElementType()) || isEntityType(ctx.getElementType())) { /* - * Value proxies must be cloned upfront, since the value is replaced - * outright. + * Proxies must be edited up-front so that the elements in the + * collection have stable identity. */ for (Object o : value.as()) { if (o == null) { collection.add(null); } else { - collection.add(editProxy((ValueProxy) o)); + collection.add(editProxy((BaseProxy) o)); } } } else { - // For entities and simple values, just alias the values + // For simple values, just copy the values collection.addAll(value.as()); } @@ -908,21 +907,30 @@ } @Override - public boolean visitReferenceProperty(String propertyName, - AutoBean<?> value, PropertyContext ctx) { + public boolean visitReferenceProperty(String propertyName, AutoBean<?> value, + PropertyContext ctx) { + value = AutoBeanUtils.getAutoBean(values.get(propertyName)); if (value != null) { - if (isValueType(ctx.getType())) { + if (isValueType(ctx.getType()) || isEntityType(ctx.getType())) { /* * Value proxies must be cloned upfront, since the value is replaced * outright. */ @SuppressWarnings("unchecked") - AutoBean<ValueProxy> valueBean = (AutoBean<ValueProxy>) value; + AutoBean<BaseProxy> valueBean = (AutoBean<BaseProxy>) value; ctx.set(editProxy(valueBean.as())); + } else { + ctx.set(value.as()); } } return false; } + + @Override + public boolean visitValueProperty(String propertyName, Object value, PropertyContext ctx) { + ctx.set(values.get(propertyName)); + return false; + } }); return clone; } @@ -988,8 +996,7 @@ } // Parameter values or references - List<Splittable> parameters = new ArrayList<Splittable>( - data.getOrderedParameters().length); + List<Splittable> parameters = new ArrayList<Splittable>(data.getOrderedParameters().length); for (Object param : data.getOrderedParameters()) { parameters.add(EntityCodex.encode(this, param)); } @@ -1008,8 +1015,8 @@ private List<OperationMessage> makePayloadOperations() { List<OperationMessage> operations = new ArrayList<OperationMessage>(); for (AutoBean<? extends BaseProxy> currentView : editedProxies.values()) { - OperationMessage operation = makeOperationMessage( - BaseProxyCategory.stableId(currentView), currentView, true).as(); + OperationMessage operation = + makeOperationMessage(BaseProxyCategory.stableId(currentView), currentView, true).as(); operations.add(operation); } return operations; @@ -1080,7 +1087,7 @@ */ private <T extends BaseProxy> T takeOwnership(AutoBean<T> bean) { editedProxies.put(stableId(bean), bean); - bean.setTag(REQUEST_CONTEXT, AbstractRequestContext.this); + bean.setTag(REQUEST_CONTEXT, this); return bean.as(); } }
diff --git a/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java b/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java index 5566589..8aca80f 100644 --- a/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java +++ b/user/src/com/google/web/bindery/requestfactory/shared/impl/EntityCodex.java
@@ -19,7 +19,6 @@ import com.google.gwt.autobean.shared.AutoBeanUtils; import com.google.gwt.autobean.shared.Splittable; import com.google.gwt.autobean.shared.ValueCodex; -import com.google.gwt.autobean.shared.impl.LazySplittable; import com.google.gwt.autobean.shared.impl.StringQuoter; import com.google.web.bindery.requestfactory.shared.BaseProxy; import com.google.web.bindery.requestfactory.shared.EntityProxyId; @@ -42,8 +41,7 @@ * Expects an encoded * {@link com.google.web.bindery.requestfactory.shared.messages.IdMessage}. */ - <Q extends BaseProxy> AutoBean<Q> getBeanForPayload( - Splittable serializedIdMessage); + <Q extends BaseProxy> AutoBean<Q> getBeanForPayload(Splittable serializedIdMessage); /** * Should return an encoded @@ -59,9 +57,9 @@ /** * Collection support is limited to value types and resolving ids. */ - public static Object decode(EntitySource source, Class<?> type, - Class<?> elementType, Splittable split) { - if (split == null || split == LazySplittable.NULL) { + public static Object decode(EntitySource source, Class<?> type, Class<?> elementType, + Splittable split) { + if (split == null || split == Splittable.NULL) { return null; } @@ -99,8 +97,7 @@ return collection; } - if (source.isEntityType(type) || source.isValueType(type) - || EntityProxyId.class.equals(type)) { + if (source.isEntityType(type) || source.isValueType(type) || EntityProxyId.class.equals(type)) { return source.getBeanForPayload(split).as(); } @@ -111,8 +108,8 @@ /** * Collection support is limited to value types and resolving ids. */ - public static Object decode(EntitySource source, Class<?> type, - Class<?> elementType, String jsonPayload) { + public static Object decode(EntitySource source, Class<?> type, Class<?> elementType, + String jsonPayload) { Splittable split = StringQuoter.split(jsonPayload); return decode(source, type, elementType, split); } @@ -122,7 +119,7 @@ */ public static Splittable encode(EntitySource source, Object value) { if (value == null) { - return LazySplittable.NULL; + return Splittable.NULL; } if (value instanceof Poser<?>) { @@ -146,7 +143,7 @@ } } toReturn.append(']'); - return new LazySplittable(toReturn.toString()); + return StringQuoter.split(toReturn.toString()); } if (value instanceof BaseProxy) {
diff --git a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java index cb331b6..bfd0300 100644 --- a/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java +++ b/user/super/com/google/gwt/autobean/super/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -17,6 +17,8 @@ import com.google.gwt.autobean.client.impl.JsoSplittable; import com.google.gwt.autobean.shared.Splittable; +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.GwtScriptOnly; import com.google.gwt.core.client.JavaScriptException; import com.google.gwt.core.client.JsDate; import com.google.gwt.core.client.JsonUtils; @@ -27,18 +29,44 @@ /** * This a super-source version with a client-only implementation. */ +@GwtScriptOnly public class StringQuoter { + public static Splittable create(boolean value) { + return JsoSplittable.create(value); + } + + public static Splittable create(double value) { + return JsoSplittable.create(value); + } + + public static Splittable create(String value) { + return JsoSplittable.create(value); + } + + public static Splittable createIndexed() { + return JsoSplittable.createIndexed(); + } + + public static Splittable createSplittable() { + return JsoSplittable.create(); + } + + public static Splittable nullValue() { + return JsoSplittable.nullValue(); + } + public static String quote(String raw) { return JsonUtils.escapeValue(raw); } public static Splittable split(String payload) { - boolean isString = payload.charAt(0) == '\"'; - if (isString) { + char c = payload.charAt(0); + boolean isSimple = c != '{' && c != '['; + if (isSimple) { payload = "[" + payload + "]"; } - Splittable toReturn = JsoSplittable.create(JsonUtils.safeEval(payload)); - if (isString) { + Splittable toReturn = JsonUtils.safeEval(payload).<JsoSplittable> cast(); + if (isSimple) { toReturn = toReturn.get(0); } return toReturn;
diff --git a/user/test/com/google/gwt/autobean/AutoBeanSuite.java b/user/test/com/google/gwt/autobean/AutoBeanSuite.java index 491e91c..f8f8d6e 100644 --- a/user/test/com/google/gwt/autobean/AutoBeanSuite.java +++ b/user/test/com/google/gwt/autobean/AutoBeanSuite.java
@@ -18,7 +18,9 @@ import com.google.gwt.autobean.client.AutoBeanTest; import com.google.gwt.autobean.server.AutoBeanCodexJreTest; import com.google.gwt.autobean.server.AutoBeanJreTest; +import com.google.gwt.autobean.server.SplittableJreTest; import com.google.gwt.autobean.shared.AutoBeanCodexTest; +import com.google.gwt.autobean.shared.SplittableTest; import com.google.gwt.junit.tools.GWTTestSuite; import junit.framework.Test; @@ -34,6 +36,8 @@ suite.addTestSuite(AutoBeanCodexTest.class); suite.addTestSuite(AutoBeanJreTest.class); suite.addTestSuite(AutoBeanTest.class); + suite.addTestSuite(SplittableJreTest.class); + suite.addTestSuite(SplittableTest.class); return suite; } }
diff --git a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java index cc61684..e83e46d 100644 --- a/user/test/com/google/gwt/autobean/client/AutoBeanTest.java +++ b/user/test/com/google/gwt/autobean/client/AutoBeanTest.java
@@ -263,46 +263,6 @@ assertEquals("Blah", more.as().getString()); } - public void testClone() { - AutoBean<Intf> a1 = factory.intf(); - - Intf i1 = a1.as(); - i1.setInt(42); - - AutoBean<Intf> a2 = a1.clone(false); - Intf i2 = a2.as(); - - // Copies have the same values - assertNotSame(i1, i2); - assertFalse(i1.equals(i2)); - assertEquals(i1.getInt(), i2.getInt()); - assertTrue(AutoBeanUtils.deepEquals(a1, a2)); - - // Cloned instances do not affect one another - i1.setInt(41); - assertEquals(41, i1.getInt()); - assertEquals(42, i2.getInt()); - } - - public void testCloneDeep() { - AutoBean<OtherIntf> a1 = factory.otherIntf(); - OtherIntf o1 = a1.as(); - - o1.setIntf(factory.intf().as()); - o1.getIntf().setInt(42); - - AutoBean<OtherIntf> a2 = a1.clone(true); - assertTrue(AutoBeanUtils.deepEquals(a1, a2)); - - OtherIntf o2 = a2.as(); - - assertNotSame(o1.getIntf(), o2.getIntf()); - assertEquals(o1.getIntf().getInt(), o2.getIntf().getInt()); - - o1.getIntf().setInt(41); - assertEquals(42, o2.getIntf().getInt()); - } - public void testDiff() { AutoBean<Intf> a1 = factory.intf(); AutoBean<Intf> a2 = factory.intf(); @@ -315,24 +275,6 @@ assertEquals(42, diff.get("int")); } - /** - * Tests that lists are in fact aliased between AutoBeans after a shallow - * clone. - */ - public void testDiffWithCloneAndListMutation() { - AutoBean<HasList> a1 = factory.hasList(); - List<Intf> list = new ArrayList<Intf>(); - list.add(factory.intf().as()); - a1.as().setList(list); - - AutoBean<HasList> a2 = a1.clone(false); - assertTrue(AutoBeanUtils.diff(a1, a2).isEmpty()); - - // Lists are aliased between AutoBeans - assertSame(a1.as().getList(), a2.as().getList()); - assertEquals(a1.as().getList(), a2.as().getList()); - } - public void testDiffWithListPropertyAssignment() { AutoBean<HasList> a1 = factory.hasList(); AutoBean<HasList> a2 = factory.hasList(); @@ -435,14 +377,14 @@ } @Override - public void endVisitCollectionProperty(String propertyName, - AutoBean<Collection<?>> value, CollectionPropertyContext ctx) { + public void endVisitCollectionProperty(String propertyName, AutoBean<Collection<?>> value, + CollectionPropertyContext ctx) { check(propertyName, ctx); } @Override - public void endVisitMapProperty(String propertyName, - AutoBean<Map<?, ?>> value, MapPropertyContext ctx) { + public void endVisitMapProperty(String propertyName, AutoBean<Map<?, ?>> value, + MapPropertyContext ctx) { check(propertyName, ctx); } @@ -458,14 +400,12 @@ } else if ("listOfMap".equals(propertyName)) { // List<Map<String, Intf>> assertEquals(List.class.getName() + "<" + Map.class.getName() + "<" - + String.class.getName() + "," + Intf.class.getName() + ">>", - sb.toString()); + + 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()); + + String.class.getName() + "," + String.class.getName() + ">," + List.class.getName() + + "<" + List.class.getName() + "<" + Intf.class.getName() + ">>>", sb.toString()); } else { throw new RuntimeException(propertyName); } @@ -512,8 +452,8 @@ boolean seenOther; @Override - public void endVisitReferenceProperty(String propertyName, - AutoBean<?> value, PropertyContext ctx) { + public void endVisitReferenceProperty(String propertyName, AutoBean<?> value, + PropertyContext ctx) { if ("hasBoolean".equals(propertyName)) { assertSame(hasBoolean, value); assertEquals(HasBoolean.class, ctx.getType()); @@ -529,8 +469,7 @@ } @Override - public void endVisitValueProperty(String propertyName, Object value, - PropertyContext ctx) { + public void endVisitValueProperty(String propertyName, Object value, PropertyContext ctx) { if ("int".equals(propertyName)) { assertEquals(42, value); assertEquals(int.class, ctx.getType());
diff --git a/user/test/com/google/gwt/autobean/server/SplittableJreTest.java b/user/test/com/google/gwt/autobean/server/SplittableJreTest.java new file mode 100644 index 0000000..af1c93a --- /dev/null +++ b/user/test/com/google/gwt/autobean/server/SplittableJreTest.java
@@ -0,0 +1,28 @@ +/* + * 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.server; + +import com.google.gwt.autobean.shared.SplittableTest; + +/** + * A JRE-only version of SplittableTest. + */ +public class SplittableJreTest extends SplittableTest { + @Override + public String getModuleName() { + return null; + } +}
diff --git a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java index 8e686b8..43bfb35 100644 --- a/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java +++ b/user/test/com/google/gwt/autobean/shared/AutoBeanCodexTest.java
@@ -200,7 +200,7 @@ assertEquals(MyEnum.BAR, map.getEnum(MyEnum.class, "BAR")); assertEquals(MyEnum.BAZ, map.getEnum(MyEnum.class, "quux")); - List<MyEnum> arrayValue = Arrays.asList(MyEnum.FOO, MyEnum.BAR, MyEnum.BAZ); + List<MyEnum> arrayValue = Arrays.asList(MyEnum.FOO, MyEnum.BAR, null, MyEnum.BAZ); Map<MyEnum, Integer> mapValue = new HashMap<MyEnum, Integer>(); mapValue.put(MyEnum.FOO, 0); mapValue.put(MyEnum.BAR, 1); @@ -219,6 +219,13 @@ assertEquals(MyEnum.BAZ, decoded.as().getEnum()); assertEquals(arrayValue, decoded.as().getEnums()); assertEquals(mapValue, decoded.as().getMap()); + + assertEquals(MyEnum.BAZ, AutoBeanUtils.getAllProperties(bean).get("enum")); + bean.as().setEnum(null); + assertNull(bean.as().getEnum()); + assertNull(AutoBeanUtils.getAllProperties(bean).get("enum")); + decoded = checkEncode(bean); + assertNull(decoded.as().getEnum()); } /** @@ -229,14 +236,13 @@ EnumMap map = (EnumMap) f; assertEquals("FOO_LIST", map.getToken(EnumReachableThroughList.FOO_LIST)); assertEquals("FOO_KEY", map.getToken(EnumReachableThroughMapKey.FOO_KEY)); - assertEquals("FOO_VALUE", - map.getToken(EnumReachableThroughMapValue.FOO_VALUE)); - assertEquals(EnumReachableThroughList.FOO_LIST, - map.getEnum(EnumReachableThroughList.class, "FOO_LIST")); - assertEquals(EnumReachableThroughMapKey.FOO_KEY, - map.getEnum(EnumReachableThroughMapKey.class, "FOO_KEY")); - assertEquals(EnumReachableThroughMapValue.FOO_VALUE, - map.getEnum(EnumReachableThroughMapValue.class, "FOO_VALUE")); + assertEquals("FOO_VALUE", map.getToken(EnumReachableThroughMapValue.FOO_VALUE)); + assertEquals(EnumReachableThroughList.FOO_LIST, map.getEnum(EnumReachableThroughList.class, + "FOO_LIST")); + assertEquals(EnumReachableThroughMapKey.FOO_KEY, map.getEnum(EnumReachableThroughMapKey.class, + "FOO_KEY")); + assertEquals(EnumReachableThroughMapValue.FOO_VALUE, map.getEnum( + EnumReachableThroughMapValue.class, "FOO_VALUE")); } public void testMap() { @@ -267,8 +273,7 @@ } assertEquals(5, complex.size()); for (Map.Entry<Simple, Simple> entry : complex.entrySet()) { - assertEquals(entry.getKey().getString(), - String.valueOf(entry.getValue().getInt())); + assertEquals(entry.getKey().getString(), String.valueOf(entry.getValue().getInt())); } } @@ -282,7 +287,8 @@ 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>>(); + 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(); @@ -316,8 +322,8 @@ AutoBean<HasSimple> decodedBean2 = checkEncode(bean2); assertNotNull(decodedBean2.as().getSimple()); - assertTrue(AutoBeanUtils.diff(bean, - AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple())).isEmpty()); + assertTrue(AutoBeanUtils.diff(bean, AutoBeanUtils.getAutoBean(decodedBean2.as().getSimple())) + .isEmpty()); AutoBean<HasList> bean3 = f.hasList(); bean3.as().setIntList(Arrays.asList(1, 2, 3, null, 4, 5)); @@ -325,8 +331,7 @@ AutoBean<HasList> decodedBean3 = checkEncode(bean3); assertNotNull(decodedBean3.as().getIntList()); - assertEquals(Arrays.asList(1, 2, 3, null, 4, 5), - decodedBean3.as().getIntList()); + assertEquals(Arrays.asList(1, 2, 3, null, 4, 5), decodedBean3.as().getIntList()); assertNotNull(decodedBean3.as().getList()); assertEquals(1, decodedBean3.as().getList().size()); assertTrue(AutoBeanUtils.diff(bean, @@ -339,30 +344,25 @@ 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)); + 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")); + Map<Splittable, Splittable> testMap = + Collections.singletonMap(ValueCodex.encode("12345"), ValueCodex.encode("5678")); bean.as().setSplittableMap(testMap); AutoBean<HasSplittable> decoded = checkEncode(bean); Splittable toDecode = decoded.as().getSimple(); - AutoBean<Simple> decodedSimple = AutoBeanCodex.decode(f, Simple.class, - toDecode); + 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()); + 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()); assertNull(list.get(1)); - assertEquals("Simple", - AutoBeanCodex.decode(f, Simple.class, list.get(2)).as().getString()); + assertEquals("Simple", AutoBeanCodex.decode(f, Simple.class, list.get(2)).as().getString()); } @Override
diff --git a/user/test/com/google/gwt/autobean/shared/SplittableTest.java b/user/test/com/google/gwt/autobean/shared/SplittableTest.java new file mode 100644 index 0000000..17fd846 --- /dev/null +++ b/user/test/com/google/gwt/autobean/shared/SplittableTest.java
@@ -0,0 +1,318 @@ +/* + * 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; + +import com.google.gwt.autobean.client.impl.JsoSplittable; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.Coder; +import com.google.gwt.autobean.shared.impl.AutoBeanCodexImpl.EncodeState; +import com.google.gwt.autobean.shared.impl.SplittableList; +import com.google.gwt.autobean.shared.impl.SplittableSimpleMap; +import com.google.gwt.autobean.shared.impl.StringQuoter; +import com.google.gwt.core.client.GWT; +import com.google.gwt.junit.client.GWTTestCase; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * Tests for the underlying Splittable implementation. This test class is not + * indicative of code that users would write, it's simply doing spot-checks of + * functionality that AbstractAutoBean depends on. + */ +public class SplittableTest extends GWTTestCase { + + /** + * + */ + private static final EncodeState testState = EncodeState.forTesting(); + + @Override + public String getModuleName() { + return "com.google.gwt.autobean.AutoBean"; + } + + public void testBasicProperties() { + Splittable data = StringQuoter.split("{\"a\":true, \"b\":3, \"c\":\"string\", \"d\":null}"); + assertTrue("isBoolean", data.get("a").isBoolean()); + assertTrue("asBoolean", data.get("a").asBoolean()); + assertTrue("isNumber", data.get("b").isNumber()); + assertEquals(3.0, data.get("b").asNumber()); + assertTrue("isString", data.get("c").isString()); + assertEquals("string", data.get("c").asString()); + assertTrue("isNull", data.isNull("d")); + assertNull("should be null", data.get("d")); + } + + /** + * Splittables are implemented by a couple of different concrete types. We'll + * use this method to make sure that the correct implementation type is being + * used in various circumstances. + */ + public void testImplementationChoice() { + Splittable s = StringQuoter.split("[1,false,\"true\"]"); + if (GWT.isScript()) { + assertTrue("s should be JsoSplittable", s instanceof JsoSplittable); + assertTrue("s[0] should be JsoSplittable", s.get(0) instanceof JsoSplittable); + assertTrue("s[1] should be JsoSplittable", s.get(1) instanceof JsoSplittable); + assertTrue("s[2] should be JsoSplittable", s.get(2) instanceof JsoSplittable); + } else { + // Using the same types in both pure-JRE and DevMode to avoid JSNI + // overhead + assertTrue("s should be JsonSplittable", s.getClass().getName().endsWith("JsonSplittable")); + assertTrue("s[0] should be JsonSplittable", s.get(0).getClass().getName().endsWith( + "JsonSplittable")); + assertTrue("s[1] should be JsonSplittable", s.get(1).getClass().getName().endsWith( + "JsonSplittable")); + assertTrue("s[2] should be JsonSplittable", s.get(2).getClass().getName().endsWith( + "JsonSplittable")); + } + } + + public void testIndexed() { + Splittable s = StringQuoter.createIndexed(); + assertTrue(s.isIndexed()); + assertFalse(s.isKeyed()); + assertFalse(s.isString()); + assertEquals(0, s.size()); + + string("foo").assign(s, 0); + string("bar").assign(s, 1); + string("baz").assign(s, 2); + + assertEquals(3, s.size()); + assertEquals("[\"foo\",\"bar\",\"baz\"]", s.getPayload()); + + string("quux").assign(s, 1); + assertEquals("[\"foo\",\"quux\",\"baz\"]", s.getPayload()); + + Splittable s2 = s.deepCopy(); + assertNotSame(s, s2); + assertEquals(s.size(), s2.size()); + for (int i = 0, j = s.size(); i < j; i++) { + assertEquals(s.get(i).asString(), s2.get(i).asString()); + } + + s.setSize(2); + assertEquals(2, s.size()); + assertEquals("[\"foo\",\"quux\"]", s.getPayload()); + + // Make sure reified values aren't in the payload + Object o = new Object(); + s.setReified("reified", o); + assertFalse(s.getPayload().contains("reified")); + assertFalse(s.getPayload().contains("__s")); + assertSame(o, s.getReified("reified")); + } + + public void testKeyed() { + Splittable s = StringQuoter.createSplittable(); + assertFalse(s.isIndexed()); + assertTrue(s.isKeyed()); + assertFalse(s.isString()); + assertTrue(s.getPropertyKeys().isEmpty()); + + string("bar").assign(s, "foo"); + string("quux").assign(s, "baz"); + + // Actual iteration order is undefined + assertEquals(new HashSet<String>(Arrays.asList("foo", "baz")), new HashSet<String>(s + .getPropertyKeys())); + + assertFalse(s.isNull("foo")); + assertTrue(s.isNull("bar")); + + assertEquals("bar", s.get("foo").asString()); + assertEquals("quux", s.get("baz").asString()); + + String payload = s.getPayload(); + assertTrue(payload.startsWith("{")); + assertTrue(payload.endsWith("}")); + assertTrue(payload.contains("\"foo\":\"bar\"")); + assertTrue(payload.contains("\"baz\":\"quux\"")); + + Splittable s2 = s.deepCopy(); + assertNotSame(s, s2); + assertEquals("bar", s2.get("foo").asString()); + assertEquals("quux", s2.get("baz").asString()); + + // Make sure reified values aren't in the payload + Object o = new Object(); + s.setReified("reified", o); + assertFalse("Should not see reified in " + s.getPayload(), s.getPayload().contains("reified")); + assertFalse("Should not see __s in " + s.getPayload(), s.getPayload().contains("__s")); + assertSame(o, s.getReified("reified")); + } + + public void testNested() { + Splittable s = StringQuoter.split("{\"a\":{\"foo\":\"bar\"}}"); + Splittable a = s.get("a"); + assertNotNull(a); + assertEquals("bar", a.get("foo").asString()); + assertSame(a, s.get("a")); + assertEquals(a, s.get("a")); + } + + /** + * Tests attributes of the {@link Splittable#NULL} field. + */ + public void testNull() { + Splittable n = Splittable.NULL; + if (GWT.isScript()) { + assertNull(n); + } else { + assertNotNull(n); + } + assertFalse("boolean", n.isBoolean()); + assertFalse("indexed", n.isIndexed()); + assertFalse("keyed", n.isKeyed()); + assertFalse("string", n.isString()); + assertEquals("null", n.getPayload()); + } + + /** + * Extra tests in here due to potential to confuse {@code false} and + * {@code null} values. + */ + public void testSplittableListBoolean() { + Coder boolCoder = AutoBeanCodexImpl.valueCoder(Boolean.class); + Splittable s = StringQuoter.createIndexed(); + bool(false).assign(s, 0); + assertFalse("0 should not be null", s.isNull(0)); + assertTrue("s[0] should be a boolean", s.get(0).isBoolean()); + assertFalse("s[0] should be false", s.get(0).asBoolean()); + assertNotNull("Null decode", ValueCodex.decode(Boolean.class, s.get(0))); + Object decodedBoolean = boolCoder.decode(testState, s.get(0)); + assertNotNull("decode should not return null", decodedBoolean); + assertFalse("decoded value should be false", (Boolean) decodedBoolean); + + bool(true).assign(s, 1); + assertTrue("s[1] should be a boolean", s.get(1).isBoolean()); + assertTrue("s[1] should be true", s.get(1).asBoolean()); + assertTrue("boolCoder 1", (Boolean) boolCoder.decode(testState, s.get(1))); + + Splittable.NULL.assign(s, 2); + assertTrue("3 should be null", s.isNull(3)); + assertEquals("payload", "[false,true,null]", s.getPayload()); + List<Boolean> boolList = new SplittableList<Boolean>(s, boolCoder, testState); + assertEquals("boolList", Arrays.<Boolean> asList(false, true, null), boolList); + } + + /** + * Extra tests in here due to potential to confuse 0 and {@code null} values. + */ + public void testSplittableListNumbers() { + Coder intCoder = AutoBeanCodexImpl.valueCoder(Integer.class); + Coder doubleCoder = AutoBeanCodexImpl.valueCoder(Double.class); + Splittable s = StringQuoter.createIndexed(); + number(0).assign(s, 0); + assertFalse("0 should not be null", s.isNull(0)); + assertTrue("s[0] should be a number", s.get(0).isNumber()); + assertNotNull("Null decode", ValueCodex.decode(Integer.class, s.get(0))); + Object decodedInt = intCoder.decode(testState, s.get(0)); + assertNotNull("decode should not return null", decodedInt); + assertEquals("intCoder 0", Integer.valueOf(0), decodedInt); + assertEquals("doubleCoder 0", Double.valueOf(0), doubleCoder.decode(testState, s.get(0))); + + number(3.141592).assign(s, 1); + assertEquals("intCoder 1", Integer.valueOf(3), intCoder.decode(testState, s.get(1))); + assertEquals("doubleCoder 1", Double.valueOf(3.141592), doubleCoder.decode(testState, s.get(1))); + + number(42).assign(s, 2); + Splittable.NULL.assign(s, 3); + assertTrue("3 should be null", s.isNull(3)); + assertEquals("payload", "[0,3.141592,42,null]", s.getPayload()); + List<Double> doubleList = new SplittableList<Double>(s, doubleCoder, testState); + assertEquals(Double.valueOf(0), doubleList.get(0)); + assertEquals("doubleList", Arrays.<Double> asList(0d, 3.141592, 42d, null), doubleList); + + // Don't share backing data between lists + s = StringQuoter.split("[0,3.141592,42,null]"); + List<Integer> intList = new SplittableList<Integer>(s, intCoder, testState); + assertEquals("intList", Arrays.<Integer> asList(0, 3, 42, null), intList); + } + + public void testSplittableListString() { + Splittable data = StringQuoter.split("[\"Hello\",\"World\"]"); + SplittableList<String> list = + new SplittableList<String>(data, AutoBeanCodexImpl.valueCoder(String.class), testState); + assertEquals(2, list.size()); + assertEquals(Arrays.asList("Hello", "World"), list); + list.set(0, "Goodbye"); + assertEquals(Arrays.asList("Goodbye", "World"), list); + assertEquals("[\"Goodbye\",\"World\"]", data.getPayload()); + list.remove(0); + assertEquals(Arrays.asList("World"), list); + assertEquals("[\"World\"]", data.getPayload()); + list.add("Wide"); + list.add("Web"); + assertEquals(Arrays.asList("World", "Wide", "Web"), list); + assertEquals("[\"World\",\"Wide\",\"Web\"]", data.getPayload()); + } + + public void testSplittableMapStringString() { + Splittable data = StringQuoter.split("{\"foo\":\"bar\",\"baz\":\"quux\",\"isNull\":null}"); + assertTrue("isNull should be null", data.isNull("isNull")); + assertFalse("isNull should not be undefined", data.isUndefined("isNull")); + Map<String, String> map = + new SplittableSimpleMap<String, String>(data, AutoBeanCodexImpl.valueCoder(String.class), + AutoBeanCodexImpl.valueCoder(String.class), testState); + assertEquals(3, map.size()); + assertEquals("bar", map.get("foo")); + assertEquals("quux", map.get("baz")); + assertTrue("Map should have isNull key", map.containsKey("isNull")); + assertNull(map.get("isNull")); + assertFalse("Map should not have unknown key", map.containsKey("unknown")); + + map.put("bar", "foo2"); + assertEquals("foo2", map.get("bar")); + String payload = data.getPayload(); + assertTrue(payload.contains("\"bar\":\"foo2\"")); + assertTrue(payload.contains("\"isNull\":null")); + } + + public void testString() { + Splittable s = string("Hello '\" World!"); + assertFalse(s.isIndexed()); + assertFalse(s.isKeyed()); + assertTrue(s.isString()); + assertEquals("Hello '\" World!", s.asString()); + assertEquals("\"Hello '\\\" World!\"", s.getPayload()); + } + + public void testStringEmpty() { + Splittable s = string(""); + assertFalse(s.isIndexed()); + assertFalse(s.isKeyed()); + assertTrue(s.isString()); + assertEquals("", s.asString()); + assertEquals("\"\"", s.getPayload()); + } + + private Splittable bool(boolean value) { + return StringQuoter.split(String.valueOf(value)); + } + + private Splittable number(double number) { + return StringQuoter.split(String.valueOf(number)); + } + + private Splittable string(String value) { + return StringQuoter.split(StringQuoter.quote(value)); + } +}