Initial add of JSON-RPC support to RequestFactory.
Patch by: bobv
Review by: rjrjr
Review at http://gwt-code-reviews.appspot.com/1355804
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9741 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBean.java b/user/src/com/google/gwt/autobean/shared/AutoBean.java
index a51c22b..8b8613f 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBean.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBean.java
@@ -37,7 +37,7 @@
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
- @Target(value = {ElementType.METHOD, ElementType.FIELD})
+ @Target(value = {ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface PropertyName {
String value();
}
diff --git a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
index ec564d6..bcded2c 100644
--- a/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/AutoBeanCodex.java
@@ -423,6 +423,17 @@
}
/**
+ * Copy data from a {@link Splittable} into an AutoBean. Unset values in the
+ * Splittable will not nullify data that already exists in the AutoBean.
+ *
+ * @param data the source data to copy
+ * @param bean the target AutoBean
+ */
+ public static void decodeInto(Splittable data, AutoBean<?> bean) {
+ new AutoBeanCodex(bean.getFactory()).doDecodeInto(data, bean);
+ }
+
+ /**
* Encodes an AutoBean. The actual payload contents can be retrieved through
* {@link Splittable#getPayload()}.
*
diff --git a/user/src/com/google/gwt/autobean/shared/ValueCodex.java b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
index ca867eb..b4fb321 100644
--- a/user/src/com/google/gwt/autobean/shared/ValueCodex.java
+++ b/user/src/com/google/gwt/autobean/shared/ValueCodex.java
@@ -89,7 +89,7 @@
@Override
public Date decode(Class<?> clazz, String value) {
- return new Date(Long.valueOf(value));
+ return StringQuoter.tryParseDate(value);
}
@Override
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 15ce275..899ed52 100644
--- a/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
+++ b/user/src/com/google/gwt/autobean/shared/impl/StringQuoter.java
@@ -20,10 +20,24 @@
import org.json.JSONObject;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
/**
* This class has a super-source version with a client-only implementation.
*/
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 String RFC2822_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z";
+ private static final DateFormat RFC2822 = new SimpleDateFormat(
+ RFC2822_PATTERN, Locale.getDefault());
+
/**
* Create a quoted JSON string.
*/
@@ -34,4 +48,27 @@
public static Splittable split(String payload) {
return JsonSplittable.create(payload);
}
+
+ /**
+ * Attempt to parse an ISO-8601 date format. May return {@code null} if the
+ * input cannot be parsed.
+ */
+ public static Date tryParseDate(String date) {
+ try {
+ return new Date(Long.parseLong(date));
+ } catch (NumberFormatException ignored) {
+ }
+ if (date.endsWith("Z")) {
+ date = date.substring(0, date.length() - 1) + "+0000";
+ }
+ try {
+ return ISO8601.parse(date);
+ } catch (ParseException ignored) {
+ }
+ try {
+ return RFC2822.parse(date);
+ } catch (ParseException ignored) {
+ }
+ return null;
+ }
}
diff --git a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
index 89c992a..d8ff630 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/RequestFactoryGenerator.java
@@ -15,7 +15,9 @@
*/
package com.google.gwt.requestfactory.rebind;
+import com.google.gwt.autobean.rebind.model.JBeanMethod;
import com.google.gwt.autobean.shared.AutoBean;
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
import com.google.gwt.autobean.shared.AutoBeanFactory;
import com.google.gwt.autobean.shared.AutoBeanFactory.Category;
import com.google.gwt.autobean.shared.AutoBeanFactory.NoWrap;
@@ -37,8 +39,10 @@
import com.google.gwt.requestfactory.rebind.model.RequestFactoryModel;
import com.google.gwt.requestfactory.rebind.model.RequestMethod;
import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.JsonRpcContent;
import com.google.gwt.requestfactory.shared.impl.AbstractRequest;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory;
@@ -145,15 +149,16 @@
SourceWriter sw = factory.createSourceWriter(context, pw);
// Constructor that accepts the parent RequestFactory
- sw.println("public %s(%s requestFactory) {super(requestFactory);}",
+ sw.println(
+ "public %s(%s requestFactory) {super(requestFactory, %s.%s);}",
method.getSimpleSourceName(),
- AbstractRequestFactory.class.getCanonicalName());
+ AbstractRequestFactory.class.getCanonicalName(),
+ Dialect.class.getCanonicalName(), method.getDialect().name());
// Write each Request method
for (RequestMethod request : method.getRequestMethods()) {
JMethod jmethod = request.getDeclarationMethod();
- String operation = jmethod.getEnclosingType().getQualifiedBinaryName()
- + "::" + jmethod.getName();
+ String operation = request.getOperation();
// foo, bar, baz
StringBuilder parameterArray = new StringBuilder();
@@ -223,12 +228,54 @@
returnTypeBaseQualifiedName, elementType);
sw.println("}");
+ /*
+ * Only support extra properties in JSON-RPC payloads. Could add this to
+ * standard requests to provide out-of-band data.
+ */
+ if (method.getDialect().equals(Dialect.JSON_RPC)) {
+ for (JMethod setter : request.getExtraSetters()) {
+ PropertyName propertyNameAnnotation = setter.getAnnotation(PropertyName.class);
+ String propertyName = propertyNameAnnotation == null
+ ? JBeanMethod.SET.inferName(setter)
+ : propertyNameAnnotation.value();
+ String maybeReturn = JBeanMethod.SET_BUILDER.matches(setter)
+ ? "return this;" : "";
+ sw.println(
+ "%s { getRequestData().setNamedParameter(\"%s\", %s); %s}",
+ setter.getReadableDeclaration(false, false, false, false, true),
+ propertyName, setter.getParameters()[0].getName(), maybeReturn);
+ }
+ }
+
// end class X{}
sw.outdent();
sw.println("}");
// Instantiate, enqueue, and return
sw.println("X x = new X();");
+
+ if (request.getApiVersion() != null) {
+ sw.println("x.getRequestData().setApiVersion(\"%s\");",
+ Generator.escape(request.getApiVersion()));
+ }
+
+ // JSON-RPC payloads send their parameters in a by-name fashion
+ if (method.getDialect().equals(Dialect.JSON_RPC)) {
+ for (JParameter param : jmethod.getParameters()) {
+ PropertyName annotation = param.getAnnotation(PropertyName.class);
+ String propertyName = annotation == null ? param.getName()
+ : annotation.value();
+ boolean isContent = param.isAnnotationPresent(JsonRpcContent.class);
+ if (isContent) {
+ sw.println("x.getRequestData().setRequestContent(%s);",
+ param.getName());
+ } else {
+ sw.println("x.getRequestData().setNamedParameter(\"%s\", %s);",
+ propertyName, param.getName());
+ }
+ }
+ }
+
// See comment in AbstractRequest.using(EntityProxy)
if (!request.isInstance()) {
sw.println("addInvocation(x);");
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
index 9922620..ae6ce92 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/ContextMethod.java
@@ -17,6 +17,8 @@
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
import java.util.Collections;
import java.util.List;
@@ -47,6 +49,8 @@
toReturn.packageName = returnClass.getPackage().getName();
toReturn.simpleSourceName = returnClass.getName().replace('.', '_')
+ "Impl";
+ toReturn.dialect = returnClass.isAnnotationPresent(JsonRpcService.class)
+ ? Dialect.JSON_RPC : Dialect.STANDARD;
}
public void setRequestMethods(List<RequestMethod> requestMethods) {
@@ -54,6 +58,7 @@
}
}
+ private Dialect dialect;
private String interfaceName;
private String methodName;
private String packageName;
@@ -63,6 +68,10 @@
private ContextMethod() {
}
+ public Dialect getDialect() {
+ return dialect;
+ }
+
/**
* The qualified source name of the RequestContext sub-interface (i.e., the
* return type of the method declaration).
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
index 1e3419b..d634624 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestFactoryModel.java
@@ -16,6 +16,7 @@
package com.google.gwt.requestfactory.rebind.model;
import com.google.gwt.autobean.rebind.model.JBeanMethod;
+import com.google.gwt.autobean.shared.Splittable;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
@@ -29,6 +30,8 @@
import com.google.gwt.requestfactory.rebind.model.RequestMethod.CollectionType;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.InstanceRequest;
+import com.google.gwt.requestfactory.shared.JsonRpcProxy;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
import com.google.gwt.requestfactory.shared.ProxyFor;
import com.google.gwt.requestfactory.shared.ProxyForName;
import com.google.gwt.requestfactory.shared.Request;
@@ -60,6 +63,10 @@
instanceRequestInterface.getSimpleSourceName());
}
+ static String noSettersAllowed(JMethod found) {
+ return String.format("Optional setters not allowed here: ", found.getName());
+ }
+
static String poisonedMessage() {
return "Unable to create RequestFactoryModel model due to previous errors";
}
@@ -71,6 +78,7 @@
private final JClassType instanceRequestInterface;
private final JClassType listInterface;
private final TreeLogger logger;
+ private final JClassType mapInterface;
private final TypeOracle oracle;
/**
* This map prevents cyclic type dependencies from overflowing the stack.
@@ -81,10 +89,12 @@
*/
private final Map<JClassType, EntityProxyModel> peers = new LinkedHashMap<JClassType, EntityProxyModel>();
private boolean poisoned;
- private final JClassType setInterface;
private final JClassType requestContextInterface;
private final JClassType requestFactoryInterface;
private final JClassType requestInterface;
+ private final JClassType setInterface;
+ private final JClassType splittableType;
+
private final JClassType valueProxyInterface;
public RequestFactoryModel(TreeLogger logger, JClassType factoryType)
@@ -96,10 +106,12 @@
entityProxyInterface = oracle.findType(EntityProxy.class.getCanonicalName());
instanceRequestInterface = oracle.findType(InstanceRequest.class.getCanonicalName());
listInterface = oracle.findType(List.class.getCanonicalName());
- setInterface = oracle.findType(Set.class.getCanonicalName());
+ mapInterface = oracle.findType(Map.class.getCanonicalName());
requestContextInterface = oracle.findType(RequestContext.class.getCanonicalName());
requestFactoryInterface = oracle.findType(RequestFactory.class.getCanonicalName());
requestInterface = oracle.findType(Request.class.getCanonicalName());
+ setInterface = oracle.findType(Set.class.getCanonicalName());
+ splittableType = oracle.findType(Splittable.class.getCanonicalName());
valueProxyInterface = oracle.findType(ValueProxy.class.getCanonicalName());
for (JMethod method : factoryType.getOverridableMethods()) {
@@ -165,9 +177,12 @@
JClassType contextType) throws UnableToCompleteException {
Service serviceAnnotation = contextType.getAnnotation(Service.class);
ServiceName serviceNameAnnotation = contextType.getAnnotation(ServiceName.class);
- if (serviceAnnotation == null && serviceNameAnnotation == null) {
- poison("RequestContext subtype %s is missing a @%s annotation",
- contextType.getQualifiedSourceName(), Service.class.getSimpleName());
+ JsonRpcService jsonRpcAnnotation = contextType.getAnnotation(JsonRpcService.class);
+ if (serviceAnnotation == null && serviceNameAnnotation == null
+ && jsonRpcAnnotation == null) {
+ poison("RequestContext subtype %s is missing a @%s or @%s annotation",
+ contextType.getQualifiedSourceName(), Service.class.getSimpleName(),
+ JsonRpcService.class.getSimpleName());
return;
}
@@ -181,7 +196,8 @@
RequestMethod.Builder methodBuilder = new RequestMethod.Builder();
methodBuilder.setDeclarationMethod(method);
- if (!validateContextMethodAndSetDataType(methodBuilder, method)) {
+ if (!validateContextMethodAndSetDataType(methodBuilder, method,
+ jsonRpcAnnotation != null)) {
continue;
}
@@ -227,12 +243,12 @@
// Get the server domain object type
ProxyFor proxyFor = entityProxyType.getAnnotation(ProxyFor.class);
ProxyForName proxyForName = entityProxyType.getAnnotation(ProxyForName.class);
- if (proxyFor == null && proxyForName == null) {
- poison("The %s type does not have a @%s or @%s annotation",
+ JsonRpcProxy jsonRpcProxy = entityProxyType.getAnnotation(JsonRpcProxy.class);
+ if (proxyFor == null && proxyForName == null && jsonRpcProxy == null) {
+ poison("The %s type does not have a @%s, @%s, or @%s annotation",
entityProxyType.getQualifiedSourceName(),
- ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName());
- // early exit, because further processing causes NPEs in numerous spots
- die(poisonedMessage());
+ ProxyFor.class.getSimpleName(), ProxyForName.class.getSimpleName(),
+ JsonRpcProxy.class.getSimpleName());
}
// Look at the methods declared on the EntityProxy
@@ -259,7 +275,8 @@
propertyName, previouslySeen.getName(), method.getName());
}
- } else if (JBeanMethod.SET.matches(method)) {
+ } else if (JBeanMethod.SET.matches(method)
+ || JBeanMethod.SET_BUILDER.matches(method)) {
transportedType = method.getParameters()[0].getType();
} else if (name.equals("stableId")
@@ -293,7 +310,7 @@
* Examine a RequestContext method to see if it returns a transportable type.
*/
private boolean validateContextMethodAndSetDataType(
- RequestMethod.Builder methodBuilder, JMethod method)
+ RequestMethod.Builder methodBuilder, JMethod method, boolean allowSetters)
throws UnableToCompleteException {
JClassType requestReturnType = method.getReturnType().isInterface();
JClassType invocationReturnType;
@@ -333,6 +350,17 @@
&& paramsOk;
}
+ // Validate any extra properties on the request type
+ for (JMethod maybeSetter : requestReturnType.getInheritableMethods()) {
+ if (JBeanMethod.SET.matches(maybeSetter)
+ || JBeanMethod.SET_BUILDER.matches(maybeSetter)) {
+ if (allowSetters) {
+ methodBuilder.addExtraSetter(maybeSetter);
+ } else {
+ poison(noSettersAllowed(maybeSetter));
+ }
+ }
+ }
return validateTransportableType(methodBuilder, invocationReturnType, true);
}
@@ -354,7 +382,8 @@
}
}
- if (ModelUtils.isValueType(oracle, transportedClass)) {
+ if (ModelUtils.isValueType(oracle, transportedClass)
+ || splittableType.equals(transportedClass)) {
// Simple values, like Integer and String
methodBuilder.setValueType(true);
} else if (entityProxyInterface.isAssignableFrom(transportedClass)
@@ -383,6 +412,28 @@
collectionInterface, transportedClass)[0];
methodBuilder.setCollectionElementType(elementType);
validateTransportableType(methodBuilder, elementType, requireObject);
+ } else if (mapInterface.isAssignableFrom(transportedClass)) {
+ JParameterizedType parameterized = transportedClass.isParameterized();
+ if (parameterized == null) {
+ poison("Requests that return Maps must be parameterized");
+ return false;
+ }
+ if (mapInterface.equals(parameterized.getBaseType())) {
+ methodBuilder.setCollectionType(CollectionType.MAP);
+ } else {
+ poison("Requests that return maps may be declared with" + " %s only",
+ mapInterface.getQualifiedSourceName());
+ return false;
+ }
+ // Also record the element type in the method builder
+ JClassType[] params = ModelUtils.findParameterizationOf(mapInterface,
+ transportedClass);
+ JClassType keyType = params[0];
+ JClassType valueType = params[1];
+ methodBuilder.setMapKeyType(keyType);
+ methodBuilder.setMapValueType(valueType);
+ validateTransportableType(methodBuilder, keyType, requireObject);
+ validateTransportableType(methodBuilder, valueType, requireObject);
} else {
// Unknown type, fail
poison("Invalid Request parameterization %s",
diff --git a/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java b/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
index f2dda1e..0761bcf 100644
--- a/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
+++ b/user/src/com/google/gwt/requestfactory/rebind/model/RequestMethod.java
@@ -17,6 +17,11 @@
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.requestfactory.shared.JsonRpcWireName;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* Represents a method declaration that causes data to be transported. This can
@@ -31,7 +36,19 @@
public static class Builder {
private RequestMethod toReturn = new RequestMethod();
+ public void addExtraSetter(JMethod method) {
+ if (toReturn.extraSetters == null) {
+ toReturn.extraSetters = new ArrayList<JMethod>();
+ }
+ toReturn.extraSetters.add(method);
+ }
+
public RequestMethod build() {
+ if (toReturn.extraSetters == null) {
+ toReturn.extraSetters = Collections.emptyList();
+ } else {
+ toReturn.extraSetters = Collections.unmodifiableList(toReturn.extraSetters);
+ }
try {
return toReturn;
} finally {
@@ -53,6 +70,17 @@
public void setDeclarationMethod(JMethod declarationMethod) {
toReturn.declarationMethod = declarationMethod;
+
+ JClassType returnClass = declarationMethod.getReturnType().isClassOrInterface();
+ JsonRpcWireName annotation = returnClass == null ? null
+ : returnClass.getAnnotation(JsonRpcWireName.class);
+ if (annotation == null) {
+ toReturn.operation = declarationMethod.getEnclosingType().getQualifiedBinaryName()
+ + "::" + declarationMethod.getName();
+ } else {
+ toReturn.operation = annotation.value();
+ toReturn.apiVersion = annotation.version();
+ }
}
public void setEntityType(EntityProxyModel entityType) {
@@ -63,6 +91,14 @@
toReturn.instanceType = instanceType;
}
+ public void setMapKeyType(JClassType elementType) {
+ toReturn.mapKeyType = elementType;
+ }
+
+ public void setMapValueType(JClassType elementType) {
+ toReturn.mapValueType = elementType;
+ }
+
public void setValueType(boolean valueType) {
toReturn.valueType = valueType;
}
@@ -72,21 +108,29 @@
* Indicates the type of collection that a Request will return.
*/
public enum CollectionType {
- // NB: Intended to be extended with a MAP value
- LIST, SET
+ LIST, SET, MAP
}
+ private String apiVersion;
private JClassType collectionElementType;
private CollectionType collectionType;
+ private JClassType dataType;
private JMethod declarationMethod;
private EntityProxyModel entityType;
+ private List<JMethod> extraSetters = new ArrayList<JMethod>();
private EntityProxyModel instanceType;
- private JClassType dataType;
+ private String operation;
+ private JClassType mapValueType;
+ private JClassType mapKeyType;
private boolean valueType;
private RequestMethod() {
}
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
/**
* If the method returns a collection, this method will return the element
* type.
@@ -118,6 +162,10 @@
return entityType;
}
+ public List<JMethod> getExtraSetters() {
+ return extraSetters;
+ }
+
/**
* If the method is intended to be invoked on an instance of an EntityProxy,
* returns the EntityProxyModel describing that type.
@@ -126,6 +174,18 @@
return instanceType;
}
+ public JClassType getMapKeyType() {
+ return mapKeyType;
+ }
+
+ public JClassType getMapValueType() {
+ return mapValueType;
+ }
+
+ public String getOperation() {
+ return operation;
+ }
+
public boolean isCollectionType() {
return collectionType != null;
}
diff --git a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
index a04226b..9445f54 100644
--- a/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
+++ b/user/src/com/google/gwt/requestfactory/server/ReflectiveServiceLayer.java
@@ -158,7 +158,11 @@
@Override
public Method getSetter(Class<?> domainType, String property) {
- return getBeanMethod(BeanMethod.SET, domainType, property);
+ Method setter = getBeanMethod(BeanMethod.SET, domainType, property);
+ if (setter == null) {
+ setter = getBeanMethod(BeanMethod.SET_BUILDER, domainType, property);
+ }
+ return setter;
}
@Override
diff --git a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
index 2f02c01..0691717 100644
--- a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestContext.java
@@ -15,8 +15,12 @@
*/
package com.google.gwt.requestfactory.server.testing;
+import com.google.gwt.autobean.server.impl.BeanMethod;
import com.google.gwt.autobean.server.impl.TypeUtils;
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
import com.google.gwt.requestfactory.shared.InstanceRequest;
+import com.google.gwt.requestfactory.shared.JsonRpcContent;
+import com.google.gwt.requestfactory.shared.JsonRpcWireName;
import com.google.gwt.requestfactory.shared.Request;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.impl.AbstractRequest;
@@ -24,9 +28,11 @@
import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
import com.google.gwt.requestfactory.shared.impl.RequestData;
+import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Collection;
@@ -34,10 +40,8 @@
* An in-process implementation of RequestContext
*/
class InProcessRequestContext extends AbstractRequestContext {
- static final Object[] NO_ARGS = new Object[0];
-
class RequestContextHandler implements InvocationHandler {
- public Object invoke(Object proxy, Method method, Object[] args)
+ public Object invoke(Object proxy, Method method, final Object[] args)
throws Throwable {
// Maybe delegate to superclass
Class<?> owner = method.getDeclaringClass();
@@ -78,21 +82,63 @@
}
}
- // Calculate request metadata
- final String operation = method.getDeclaringClass().getName() + "::"
- + method.getName();
- final Class<?> returnType = TypeUtils.ensureBaseType(returnGenericType);
- final Class<?> elementType = Collection.class.isAssignableFrom(returnType)
+ Class<?> returnType = TypeUtils.ensureBaseType(returnGenericType);
+ Class<?> elementType = Collection.class.isAssignableFrom(returnType)
? TypeUtils.ensureBaseType(TypeUtils.getSingleParameterization(
Collection.class, returnGenericType)) : null;
+ final RequestData data;
+ if (dialect.equals(Dialect.STANDARD)) {
+ String operation = method.getDeclaringClass().getName() + "::"
+ + method.getName();
+
+ data = new RequestData(operation, actualArgs, returnType, elementType);
+ } else {
+ // Calculate request metadata
+ JsonRpcWireName wireInfo = method.getReturnType().getAnnotation(
+ JsonRpcWireName.class);
+ String apiVersion = wireInfo.version();
+ String operation = wireInfo.value();
+
+ int foundContent = -1;
+ final String[] parameterNames = args == null ? new String[0]
+ : new String[args.length];
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ parameter : for (int i = 0, j = parameterAnnotations.length; i < j; i++) {
+ for (Annotation annotation : parameterAnnotations[i]) {
+ if (PropertyName.class.equals(annotation.annotationType())) {
+ parameterNames[i] = ((PropertyName) annotation).value();
+ continue parameter;
+ } else if (JsonRpcContent.class.equals(annotation.annotationType())) {
+ foundContent = i;
+ continue parameter;
+ }
+ }
+ throw new UnsupportedOperationException("No "
+ + PropertyName.class.getCanonicalName()
+ + " annotation on parameter " + i + " of method "
+ + method.toString());
+ }
+ final int contentIdx = foundContent;
+
+ data = new RequestData(operation, actualArgs, returnType, elementType);
+ for (int i = 0, j = args.length; i < j; i++) {
+ if (i != contentIdx) {
+ data.setNamedParameter(parameterNames[i], args[i]);
+ } else {
+ data.setRequestContent(args[i]);
+ }
+ data.setApiVersion(apiVersion);
+ }
+ }
+
// Create the request, just filling in the RequestData details
- AbstractRequest<Object> req = new AbstractRequest<Object>(
+ final AbstractRequest<Object> req = new AbstractRequest<Object>(
InProcessRequestContext.this) {
@Override
protected RequestData makeRequestData() {
- return new RequestData(operation, actualArgs, propertyRefs,
- returnType, elementType);
+ data.setPropertyRefs(propertyRefs);
+ return data;
}
};
@@ -100,11 +146,42 @@
// Instance invocations are enqueued when using() is called
addInvocation(req);
}
- return req;
- }
- };
- protected InProcessRequestContext(AbstractRequestFactory factory) {
- super(factory);
+ if (dialect.equals(Dialect.STANDARD)) {
+ return req;
+ } else if (dialect.equals(Dialect.JSON_RPC)) {
+ // Support optional parameters for JSON-RPC payloads
+ Class<?> requestType = method.getReturnType().asSubclass(Request.class);
+ return Proxy.newProxyInstance(requestType.getClassLoader(),
+ new Class<?>[] {requestType}, new InvocationHandler() {
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args)
+ throws Throwable {
+ if (Object.class.equals(method.getDeclaringClass())
+ || Request.class.equals(method.getDeclaringClass())) {
+ return method.invoke(req, args);
+ } else if (BeanMethod.SET.matches(method)
+ || BeanMethod.SET_BUILDER.matches(method)) {
+ req.getRequestData().setNamedParameter(
+ BeanMethod.SET.inferName(method), args[0]);
+ return Void.TYPE.equals(method.getReturnType()) ? null
+ : proxy;
+ }
+ throw new UnsupportedOperationException(method.toString());
+ }
+ });
+ } else {
+ throw new RuntimeException("Should not reach here");
+ }
+ }
+ }
+
+ static final Object[] NO_ARGS = new Object[0];
+ private final Dialect dialect;
+
+ protected InProcessRequestContext(AbstractRequestFactory factory,
+ Dialect dialect) {
+ super(factory, dialect);
+ this.dialect = dialect;
}
}
diff --git a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
index af61538..836fb1a 100644
--- a/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/server/testing/InProcessRequestFactory.java
@@ -24,9 +24,11 @@
import com.google.gwt.requestfactory.shared.BaseProxy;
import com.google.gwt.requestfactory.shared.EntityProxy;
import com.google.gwt.requestfactory.shared.EntityProxyId;
+import com.google.gwt.requestfactory.shared.JsonRpcService;
import com.google.gwt.requestfactory.shared.RequestContext;
import com.google.gwt.requestfactory.shared.RequestFactory;
import com.google.gwt.requestfactory.shared.ValueProxy;
+import com.google.gwt.requestfactory.shared.impl.AbstractRequestContext.Dialect;
import com.google.gwt.requestfactory.shared.impl.AbstractRequestFactory;
import com.google.gwt.requestfactory.shared.impl.BaseProxyCategory;
import com.google.gwt.requestfactory.shared.impl.EntityProxyCategory;
@@ -62,8 +64,10 @@
Class<? extends RequestContext> context = method.getReturnType().asSubclass(
RequestContext.class);
+ Dialect dialect = method.getReturnType().isAnnotationPresent(
+ JsonRpcService.class) ? Dialect.JSON_RPC : Dialect.STANDARD;
RequestContextHandler handler = new InProcessRequestContext(
- InProcessRequestFactory.this).new RequestContextHandler();
+ InProcessRequestFactory.this, dialect).new RequestContextHandler();
return context.cast(Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] {context}, handler));
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java
new file mode 100644
index 0000000..7fe081e
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcContent.java
@@ -0,0 +1,32 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <b>Experimental API, subject to change.</b> Applied to a Request method
+ * declaration to indicate that a particular parameter is used as the
+ * {@code request} portion of the JSON-RPC request. This is analogous to the
+ * payload body in a REST-style request.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface JsonRpcContent {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java
new file mode 100644
index 0000000..5cb7300
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcProxy.java
@@ -0,0 +1,23 @@
+/*
+ * 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.requestfactory.shared;
+
+/**
+ * <b>Experimental API, subject to change</b> Used instead of the
+ * {@link ProxyFor} annotation.
+ */
+public @interface JsonRpcProxy {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java
new file mode 100644
index 0000000..e147b72
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcService.java
@@ -0,0 +1,30 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <b>Experimental API, subject to change</b> Indicates that a RequestContext
+ * should be encoded as a JSON-RPC request.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JsonRpcService {
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java b/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java
new file mode 100644
index 0000000..36449c4
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/JsonRpcWireName.java
@@ -0,0 +1,30 @@
+/*
+ * 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.requestfactory.shared;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * <b>Experimental API, subject to change</b> Provides the method name for a
+ * JSON-RPC invocation.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface JsonRpcWireName {
+ String value();
+
+ String version() default "";
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
index 2a20f06..8524524 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequest.java
@@ -83,7 +83,7 @@
* methods place the instance in the first parameter slot.
*/
public Request<T> using(BaseProxy instanceObject) {
- getRequestData().getParameters()[0] = instanceObject;
+ getRequestData().getOrderedParameters()[0] = instanceObject;
/*
* Instance methods enqueue themselves when their using() method is called.
* This ensures that the instance parameter will have been set when
@@ -100,6 +100,10 @@
protected abstract RequestData makeRequestData();
+ Receiver<? super T> getReceiver() {
+ return receiver;
+ }
+
boolean hasReceiver() {
return receiver != null;
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
index ea8dff2..7e89967 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestContext.java
@@ -24,6 +24,9 @@
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.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.gwt.requestfactory.shared.BaseProxy;
import com.google.gwt.requestfactory.shared.EntityProxy;
@@ -40,6 +43,7 @@
import com.google.gwt.requestfactory.shared.messages.IdMessage;
import com.google.gwt.requestfactory.shared.messages.IdMessage.Strength;
import com.google.gwt.requestfactory.shared.messages.InvocationMessage;
+import com.google.gwt.requestfactory.shared.messages.JsonRpcRequest;
import com.google.gwt.requestfactory.shared.messages.MessageFactory;
import com.google.gwt.requestfactory.shared.messages.OperationMessage;
import com.google.gwt.requestfactory.shared.messages.RequestMessage;
@@ -63,6 +67,238 @@
*/
public 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
+ * object is instantiated.
+ */
+ public enum Dialect {
+ STANDARD {
+ @Override
+ DialectImpl create(AbstractRequestContext context) {
+ return context.new StandardPayloadDialect();
+ }
+ },
+ JSON_RPC {
+ @Override
+ DialectImpl create(AbstractRequestContext context) {
+ return context.new JsonRpcPayloadDialect();
+ }
+ };
+ abstract DialectImpl create(AbstractRequestContext context);
+ }
+
+ interface DialectImpl {
+
+ void addInvocation(AbstractRequest<?> request);
+
+ String makePayload();
+
+ void processPayload(Receiver<Void> receiver, String payload);
+ }
+
+ class JsonRpcPayloadDialect implements DialectImpl {
+ /**
+ * Called by generated subclasses to enqueue a method invocation.
+ */
+ public void addInvocation(AbstractRequest<?> request) {
+ /*
+ * TODO(bobv): Support for multiple invocations per request needs to be
+ * ironed out. Once this is done, addInvocation() can be removed from the
+ * DialectImpl interface and restored to to AbstractRequestContext.
+ */
+ if (!invocations.isEmpty()) {
+ throw new RuntimeException(
+ "Only one invocation per request, pending backend support");
+ }
+ invocations.add(request);
+ for (Object arg : request.getRequestData().getOrderedParameters()) {
+ retainArg(arg);
+ }
+ }
+
+ public String makePayload() {
+ RequestData data = invocations.get(0).getRequestData();
+
+ AutoBean<JsonRpcRequest> bean = MessageFactoryHolder.FACTORY.jsonRpcRequest();
+ JsonRpcRequest request = bean.as();
+
+ request.setVersion("2.0");
+ request.setApiVersion(data.getApiVersion());
+ request.setId(payloadId++);
+
+ Map<String, Splittable> params = new HashMap<String, Splittable>();
+ for (Map.Entry<String, Object> entry : data.getNamedParameters().entrySet()) {
+ Object obj = entry.getValue();
+ Splittable value = encode(obj);
+ params.put(entry.getKey(), value);
+ }
+ if (data.getRequestResource() != null) {
+ params.put("resource", encode(data.getRequestResource()));
+ }
+ request.setParams(params);
+ request.setMethod(data.getOperation());
+
+ return AutoBeanCodex.encode(bean).getPayload();
+ }
+
+ public void processPayload(Receiver<Void> receiver, String payload) {
+ Splittable raw = StringQuoter.split(payload);
+
+ @SuppressWarnings("unchecked")
+ Receiver<Object> callback = (Receiver<Object>) invocations.get(0).getReceiver();
+
+ if (!raw.isNull("error")) {
+ Splittable error = raw.get("error");
+ ServerFailure failure = new ServerFailure(
+ error.get("message").asString());
+ fail(receiver, failure);
+ return;
+ }
+
+ Splittable result = raw.get("result");
+ @SuppressWarnings("unchecked")
+ Class<BaseProxy> target = (Class<BaseProxy>) invocations.get(0).getRequestData().getReturnType();
+
+ SimpleProxyId<BaseProxy> id = getRequestFactory().allocateId(target);
+ AutoBean<BaseProxy> bean = getRequestFactory().createProxy(target, id);
+ AutoBeanCodex.decodeInto(result, bean);
+
+ if (callback != null) {
+ callback.onSuccess(bean.as());
+ }
+ if (receiver != null) {
+ receiver.onSuccess(null);
+ }
+ }
+
+ Splittable encode(Object obj) {
+ Splittable value;
+ if (obj == null) {
+ return LazySplittable.NULL;
+ } else if (obj.getClass().isEnum()
+ && getRequestFactory() instanceof EnumMap) {
+ value = ValueCodex.encode(((EnumMap) getRequestFactory()).getToken((Enum<?>) obj));
+ } else if (ValueCodex.canDecode(obj.getClass())) {
+ value = ValueCodex.encode(obj);
+ } else {
+ // XXX user-provided implementation of interface?
+ value = AutoBeanCodex.encode(AutoBeanUtils.getAutoBean(obj));
+ }
+ return value;
+ }
+ }
+
+ class StandardPayloadDialect implements DialectImpl {
+
+ /**
+ * Called by generated subclasses to enqueue a method invocation.
+ */
+ public void addInvocation(AbstractRequest<?> request) {
+ invocations.add(request);
+ for (Object arg : request.getRequestData().getOrderedParameters()) {
+ retainArg(arg);
+ }
+ }
+
+ /**
+ * Assemble all of the state that has been accumulated in this context. This
+ * includes:
+ * <ul>
+ * <li>Diffs accumulated on objects passed to {@link #edit}.
+ * <li>Invocations accumulated as Request subtypes passed to
+ * {@link #addInvocation}.
+ * </ul>
+ */
+ public String makePayload() {
+ // Get the factory from the runtime-specific holder.
+ MessageFactory f = MessageFactoryHolder.FACTORY;
+
+ List<OperationMessage> operations = makePayloadOperations();
+ List<InvocationMessage> invocationMessages = makePayloadInvocations();
+
+ // Create the outer envelope message
+ AutoBean<RequestMessage> bean = f.request();
+ RequestMessage requestMessage = bean.as();
+ if (!invocationMessages.isEmpty()) {
+ requestMessage.setInvocations(invocationMessages);
+ }
+ if (!operations.isEmpty()) {
+ requestMessage.setOperations(operations);
+ }
+ return AutoBeanCodex.encode(bean).getPayload();
+ }
+
+ public void processPayload(final Receiver<Void> receiver, String payload) {
+ 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());
+
+ fail(receiver, fail);
+ return;
+ }
+
+ // Process violations and then stop
+ if (response.getViolations() != null) {
+ Set<Violation> errors = new HashSet<Violation>();
+ for (ViolationMessage message : response.getViolations()) {
+ errors.add(new MyViolation(message));
+ }
+
+ violation(receiver, errors);
+ return;
+ }
+
+ // Process operations
+ processReturnOperations(response);
+
+ // Send return values
+ Set<Throwable> causes = null;
+ for (int i = 0, j = invocations.size(); i < j; i++) {
+ try {
+ 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();
+ invocations.get(i).onFail(
+ new ServerFailure(failure.getMessage(),
+ failure.getExceptionType(), failure.getStackTrace(),
+ failure.isFatal()));
+ }
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+
+ if (receiver != null) {
+ try {
+ receiver.onSuccess(null);
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+ // After success, shut down the context
+ editedProxies.clear();
+ invocations.clear();
+ returnedProxies.clear();
+
+ if (causes != null) {
+ throw new UmbrellaException(causes);
+ }
+ }
+ }
private class MyViolation implements Violation {
private final BaseProxy currentProxy;
@@ -116,18 +352,21 @@
private static final WriteOperation[] DELETE_ONLY = {WriteOperation.DELETE};
private static final WriteOperation[] PERSIST_AND_UPDATE = {
- WriteOperation.PERSIST, WriteOperation.UPDATE};
+ WriteOperation.PERSIST, WriteOperation.UPDATE};
private static final WriteOperation[] UPDATE_ONLY = {WriteOperation.UPDATE};
+ private static int payloadId = 100;
- private final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
+ protected final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
private boolean locked;
private final AbstractRequestFactory requestFactory;
+
/**
* A map of all EntityProxies that the RequestContext has interacted with.
* 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>>();
+
/**
* A map that contains the canonical instance of an entity to return in the
* return graph, since this is built from scratch.
@@ -142,8 +381,12 @@
*/
private final Map<Integer, SimpleProxyId<?>> syntheticIds = new HashMap<Integer, SimpleProxyId<?>>();
- protected AbstractRequestContext(AbstractRequestFactory factory) {
+ private final DialectImpl dialect;
+
+ protected AbstractRequestContext(AbstractRequestFactory factory,
+ Dialect dialect) {
this.requestFactory = factory;
+ this.dialect = dialect.create(this);
}
/**
@@ -291,15 +534,80 @@
*/
public boolean isValueType(Class<?> clazz) {
return requestFactory.isValueType(clazz);
- }
+ };
/**
* Called by generated subclasses to enqueue a method invocation.
*/
protected void addInvocation(AbstractRequest<?> request) {
- invocations.add(request);
- for (Object arg : request.getRequestData().getParameters()) {
- retainArg(arg);
+ dialect.addInvocation(request);
+ }
+
+ /**
+ * Invoke the appropriate {@code onFailure} callbacks, possibly throwing an
+ * {@link UmbrellaException} if one or more callbacks fails.
+ */
+ protected void fail(Receiver<Void> receiver, ServerFailure failure) {
+ reuse();
+ Set<Throwable> causes = null;
+ for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
+ invocations)) {
+ try {
+ request.onFail(failure);
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+ if (receiver != null) {
+ try {
+ receiver.onFailure(failure);
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+
+ if (causes != null) {
+ throw new UmbrellaException(causes);
+ }
+ }
+
+ /**
+ * Invoke the appropriate {@code onViolation} callbacks, possibly throwing an
+ * {@link UmbrellaException} if one or more callbacks fails.
+ */
+ protected void violation(final Receiver<Void> receiver, Set<Violation> errors) {
+ reuse();
+ Set<Throwable> causes = null;
+ for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
+ invocations)) {
+ try {
+ request.onViolation(errors);
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+ if (receiver != null) {
+ try {
+ receiver.onViolation(errors);
+ } catch (Throwable t) {
+ if (causes == null) {
+ causes = new HashSet<Throwable>();
+ }
+ causes.add(t);
+ }
+ }
+
+ if (causes != null) {
+ throw new UmbrellaException(causes);
}
}
@@ -603,150 +911,14 @@
freezeEntities(true);
- String payload = makePayload();
+ String payload = dialect.makePayload();
requestFactory.getRequestTransport().send(payload, new TransportReceiver() {
public void onTransportFailure(ServerFailure failure) {
fail(receiver, failure);
}
public void onTransportSuccess(String payload) {
- 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());
-
- fail(receiver, fail);
- return;
- }
-
- // Process violations and then stop
- if (response.getViolations() != null) {
- Set<Violation> errors = new HashSet<Violation>();
- for (ViolationMessage message : response.getViolations()) {
- errors.add(new MyViolation(message));
- }
-
- violation(receiver, errors);
- return;
- }
-
- // Process operations
- processReturnOperations(response);
-
- // Send return values
- Set<Throwable> causes = null;
- for (int i = 0, j = invocations.size(); i < j; i++) {
- try {
- 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();
- invocations.get(i).onFail(
- new ServerFailure(failure.getMessage(),
- failure.getExceptionType(), failure.getStackTrace(),
- failure.isFatal()));
- }
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
-
- if (receiver != null) {
- try {
- receiver.onSuccess(null);
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
- // After success, shut down the context
- editedProxies.clear();
- invocations.clear();
- returnedProxies.clear();
-
- if (causes != null) {
- throw new UmbrellaException(causes);
- }
- }
-
- /**
- * Invoke the appropriate {@code onFailure} callbacks, possibly throwing
- * an {@link UmbrellaException} if one or more callbacks fails.
- */
- private void fail(Receiver<Void> receiver, ServerFailure failure) {
- reuse();
- Set<Throwable> causes = null;
- for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
- invocations)) {
- try {
- request.onFail(failure);
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
- if (receiver != null) {
- try {
- receiver.onFailure(failure);
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
-
- if (causes != null) {
- throw new UmbrellaException(causes);
- }
- }
-
- /**
- * Invoke the appropriate {@code onViolation} callbacks, possibly throwing
- * an {@link UmbrellaException} if one or more callbacks fails.
- */
- private void violation(final Receiver<Void> receiver,
- Set<Violation> errors) {
- reuse();
- Set<Throwable> causes = null;
- for (AbstractRequest<?> request : new ArrayList<AbstractRequest<?>>(
- invocations)) {
- try {
- request.onViolation(errors);
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
- if (receiver != null) {
- try {
- receiver.onViolation(errors);
- } catch (Throwable t) {
- if (causes == null) {
- causes = new HashSet<Throwable>();
- }
- causes.add(t);
- }
- }
-
- if (causes != null) {
- throw new UmbrellaException(causes);
- }
+ dialect.processPayload(receiver, payload);
}
});
}
@@ -772,34 +944,6 @@
}
/**
- * Assemble all of the state that has been accumulated in this context. This
- * includes:
- * <ul>
- * <li>Diffs accumulated on objects passed to {@link #edit}.
- * <li>Invocations accumulated as Request subtypes passed to
- * {@link #addInvocation}.
- * </ul>
- */
- private String makePayload() {
- // Get the factory from the runtime-specific holder.
- MessageFactory f = MessageFactoryHolder.FACTORY;
-
- List<OperationMessage> operations = makePayloadOperations();
- List<InvocationMessage> invocationMessages = makePayloadInvocations();
-
- // Create the outer envelope message
- AutoBean<RequestMessage> bean = f.request();
- RequestMessage requestMessage = bean.as();
- if (!invocationMessages.isEmpty()) {
- requestMessage.setInvocations(invocationMessages);
- }
- if (!operations.isEmpty()) {
- requestMessage.setOperations(operations);
- }
- return AutoBeanCodex.encode(bean).getPayload();
- }
-
- /**
* Create an InvocationMessage for each remote method call being made by the
* context.
*/
@@ -823,8 +967,8 @@
// Parameter values or references
List<Splittable> parameters = new ArrayList<Splittable>(
- data.getParameters().length);
- for (Object param : data.getParameters()) {
+ data.getOrderedParameters().length);
+ for (Object param : data.getOrderedParameters()) {
parameters.add(EntityCodex.encode(this, param));
}
if (!parameters.isEmpty()) {
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
index 9616776..25c202b 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/AbstractRequestFactory.java
@@ -72,7 +72,7 @@
}
AbstractRequestContext context = new AbstractRequestContext(
- AbstractRequestFactory.this);
+ AbstractRequestFactory.this, AbstractRequestContext.Dialect.STANDARD);
return new AbstractRequest<P>(context) {
{
requestContext.addInvocation(this);
@@ -82,7 +82,7 @@
protected RequestData makeRequestData() {
return new RequestData(
"com.google.gwt.requestfactory.shared.impl.FindRequest::find",
- new Object[]{proxyId}, propertyRefs, proxyId.getProxyClass(), null);
+ new Object[] {proxyId}, propertyRefs, proxyId.getProxyClass(), null);
}
};
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
index 73b9c55..7e175c2 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/ProxySerializerImpl.java
@@ -63,7 +63,7 @@
private final Map<SimpleProxyId<?>, AutoBean<?>> serialized = new HashMap<SimpleProxyId<?>, AutoBean<?>>();
public ProxySerializerImpl(AbstractRequestFactory factory, ProxyStore store) {
- super(factory);
+ super(factory, Dialect.STANDARD);
this.store = store;
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
index cb9c659..db66e2c 100644
--- a/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
+++ b/user/src/com/google/gwt/requestfactory/shared/impl/RequestData.java
@@ -15,6 +15,9 @@
*/
package com.google.gwt.requestfactory.shared.impl;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
/**
@@ -25,34 +28,54 @@
private final Class<?> elementType;
private final String operation;
private final Object[] parameters;
- private final Set<String> propertyRefs;
+ private Set<String> propertyRefs;
private final Class<?> returnType;
+ private Map<String, Object> requestParameters;
+ private Object requestContent;
+ private String apiVersion;
public RequestData(String operation, Object[] parameters,
- Set<String> propertyRefs, Class<?> returnType, Class<?> elementType) {
+ Class<?> returnType, Class<?> elementType) {
this.operation = operation;
this.parameters = parameters;
- this.propertyRefs = propertyRefs;
this.returnType = returnType;
this.elementType = elementType;
}
/**
+ * Used by generated code.
+ */
+ public RequestData(String operation, Object[] parameters,
+ Set<String> propertyRefs, Class<?> returnType, Class<?> elementType) {
+ this(operation, parameters, returnType, elementType);
+ setPropertyRefs(propertyRefs);
+ }
+
+ public String getApiVersion() {
+ return apiVersion;
+ }
+
+ /**
* Used to interpret the returned payload.
*/
public Class<?> getElementType() {
return elementType;
}
+ public Map<String, Object> getNamedParameters() {
+ return requestParameters == null ? Collections.<String, Object> emptyMap()
+ : requestParameters;
+ }
+
public String getOperation() {
return operation;
}
/**
- * Used by InstanceRequest subtypes to reset the instance object in the
- * <code>using</code> method.
+ * Used by standard-mode payloads and InstanceRequest subtypes to reset the
+ * instance object in the <code>using</code> method.
*/
- public Object[] getParameters() {
+ public Object[] getOrderedParameters() {
return parameters;
}
@@ -60,10 +83,38 @@
return propertyRefs;
}
+ public Object getRequestResource() {
+ return requestContent;
+ }
+
/**
* Used to interpret the returned payload.
*/
public Class<?> getReturnType() {
return returnType;
}
+
+ public void setApiVersion(String apiVersion) {
+ this.apiVersion = apiVersion;
+ }
+
+ public void setNamedParameter(String key, Object value) {
+ if (requestParameters == null) {
+ requestParameters = new HashMap<String, Object>();
+ }
+ requestParameters.put(key, value);
+ }
+
+ public void setPropertyRefs(Set<String> propertyRefs) {
+ this.propertyRefs = propertyRefs;
+ }
+
+ /**
+ * Represents the {@code request} object in a JSON-RPC request.
+ *
+ * @see com.google.gwt.requestfactory.shared.JsonRpcContent
+ */
+ public void setRequestContent(Object requestContent) {
+ this.requestContent = requestContent;
+ }
}
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java b/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java
new file mode 100644
index 0000000..063f315
--- /dev/null
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/JsonRpcRequest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.requestfactory.shared.messages;
+
+import com.google.gwt.autobean.shared.AutoBean.PropertyName;
+import com.google.gwt.autobean.shared.Splittable;
+
+import java.util.Map;
+
+/**
+ * A JSON-RPC request payload.
+ */
+public interface JsonRpcRequest {
+ String getApiVersion();
+
+ int getId();
+
+ String getMethod();
+
+ Map<String, Splittable> getParams();
+
+ @PropertyName("jsonrpc")
+ String getVersion();
+
+ void setApiVersion(String version);
+
+ void setId(int id);
+
+ void setMethod(String method);
+
+ void setParams(Map<String, Splittable> params);
+
+ @PropertyName("jsonrpc")
+ void setVersion(String version);
+}
diff --git a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
index a6201f5..c38216e 100644
--- a/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/messages/MessageFactory.java
@@ -27,6 +27,8 @@
AutoBean<IdMessage> id();
AutoBean<InvocationMessage> invocation();
+
+ AutoBean<JsonRpcRequest> jsonRpcRequest();
AutoBean<OperationMessage> operation();
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 3f1c489..cb331b6 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
@@ -15,11 +15,15 @@
*/
package com.google.gwt.autobean.shared.impl;
-import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.autobean.client.impl.JsoSplittable;
import com.google.gwt.autobean.shared.Splittable;
+import com.google.gwt.core.client.JavaScriptException;
+import com.google.gwt.core.client.JsDate;
+import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamWriter;
+import java.util.Date;
+
/**
* This a super-source version with a client-only implementation.
*/
@@ -39,4 +43,17 @@
}
return toReturn;
}
+
+ public static Date tryParseDate(String date) {
+ try {
+ return new Date(Long.parseLong(date));
+ } catch (NumberFormatException ignored) {
+ }
+ try {
+ JsDate js = JsDate.create(date);
+ return new Date((long) js.getTime());
+ } catch (JavaScriptException ignored) {
+ }
+ return null;
+ }
}
diff --git a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
index 14f14fb..2cb275b 100644
--- a/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
+++ b/user/test/com/google/gwt/requestfactory/rebind/model/RequestFactoryModelTest.java
@@ -15,6 +15,7 @@
*/
package com.google.gwt.requestfactory.rebind.model;
+import com.google.gwt.autobean.shared.Splittable;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.javac.CompilationState;
@@ -148,13 +149,15 @@
public void testMissingProxyFor() {
testModelWithMethodDeclArgs("Request<TestProxy> okMethodProxy();",
TestContextImpl.class.getName(), null,
- "The t.TestProxy type does not have a @ProxyFor or @ProxyForName annotation");
+ "The t.TestProxy type does not have a @ProxyFor, "
+ + "@ProxyForName, or @JsonRpcProxy annotation");
}
public void testMissingService() {
testModelWithMethodDeclArgs("Request<String> okMethod();", null,
TestContextImpl.class.getName(),
- "RequestContext subtype t.TestContext is missing a @Service annotation");
+ "RequestContext subtype t.TestContext is missing a "
+ + "@Service or @JsonRpcService annotation");
}
public void testModelWithMethodDecl(final String clientMethodDecls,
@@ -296,6 +299,7 @@
new EmptyMockJavaResource(RequestFactory.class),
new EmptyMockJavaResource(Receiver.class),
new EmptyMockJavaResource(ServiceLocator.class),
+ new EmptyMockJavaResource(Splittable.class),
new EmptyMockJavaResource(ValueProxy.class),
new RealJavaResource(Request.class),