Properly encode request parameters that use Collections when in JSON-RPC mode.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10808 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 fa3d929..28bbb8f 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
@@ -1,16 +1,14 @@
 /*
  * 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
+ * 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
+ * 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.web.bindery.requestfactory.shared.impl;
@@ -75,9 +73,8 @@
  */
 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
-   * object is instantiated.
+   * 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 {
@@ -100,32 +97,30 @@
    */
   protected static class State {
     /**
-     * Supports the case where chained contexts are used and a response comes
-     * back from the server with a proxy type not reachable from the canonical
-     * context.
+     * Supports the case where chained contexts are used and a response comes back from the server
+     * with a proxy type not reachable from the canonical context.
      */
     public Set<AbstractRequestContext> appendedContexts;
     public final AbstractRequestContext canonical;
     public final DialectImpl dialect;
     public FanoutReceiver<Void> fanout;
     /**
-     * When {@code true} the {@link AbstractRequestContext#fire()} method will
-     * be a no-op.
+     * When {@code true} the {@link AbstractRequestContext#fire()} method will be a no-op.
      */
     public boolean fireDisabled;
     public final List<AbstractRequest<?>> invocations = new ArrayList<AbstractRequest<?>>();
 
     public boolean locked;
     /**
-     * A map of all EntityProxies that the RequestContext has interacted with.
-     * Objects are placed into this map by being returned from {@link #create},
-     * passed into {@link #edit}, or used as an invocation argument.
+     * A map of all EntityProxies that the RequestContext has interacted with. Objects are placed
+     * into this map by being returned from {@link #create}, passed into {@link #edit}, or used as
+     * an invocation argument.
      */
     public 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.
+     * A map that contains the canonical instance of an entity to return in the return graph, since
+     * this is built from scratch.
      */
     public final Map<SimpleProxyId<?>, AutoBean<?>> returnedProxies =
         new HashMap<SimpleProxyId<?>, AutoBean<?>>();
@@ -133,10 +128,9 @@
     public final AbstractRequestFactory requestFactory;
 
     /**
-     * A map that allows us to handle the case where the server has sent back an
-     * unpersisted entity. Because we assume that the server is stateless, the
-     * client will need to swap out the request-local ids with a regular
-     * client-allocated id.
+     * A map that allows us to handle the case where the server has sent back an unpersisted entity.
+     * Because we assume that the server is stateless, the client will need to swap out the
+     * request-local ids with a regular client-allocated id.
      */
     public final Map<Integer, SimpleProxyId<?>> syntheticIds =
         new HashMap<Integer, SimpleProxyId<?>>();
@@ -190,9 +184,9 @@
      */
     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.
+       * 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 (!state.invocations.isEmpty()) {
         throw new RuntimeException("Only one invocation per request, pending backend support");
@@ -263,10 +257,41 @@
     }
 
     Splittable encode(Object obj) {
-      Splittable value;
       if (obj == null) {
         return Splittable.NULL;
-      } else if (obj.getClass().isEnum() && getAutoBeanFactory() instanceof EnumMap) {
+      } else if (obj instanceof Collection) {
+        return collectionEncode((Collection<?>) obj);
+      }
+      return nonCollectionEncode(obj);
+    }
+
+    private Splittable collectionEncode(Collection<?> collection) {
+      StringBuffer sb = new StringBuffer();
+      Iterator<?> it = collection.iterator();
+      sb.append("[");
+      if (it.hasNext()) {
+        // TODO: Allow for the encoding of nested collections. See issue 5974.
+        sb.append(nonCollectionEncode(it.next()));
+        while (it.hasNext()) {
+          sb.append(",");
+          // TODO: Allow for the encoding of nested collections. See issue 5974.
+          sb.append(nonCollectionEncode(it.next()));
+        }
+      }
+      sb.append("]");
+
+      return StringQuoter.split(sb.toString());
+    }
+
+    private Splittable nonCollectionEncode(Object obj) {
+      if (obj instanceof Collection) {
+        // TODO: Allow for the encoding of nested collections. See issue 5974.
+        // Once we do this, this can turn into an assert.
+        throw new RuntimeException(
+            "Unable to encode request as JSON payload; Request methods must have parameters of the form List<T> or Set<T>, where T is a scalar (non-collection) type.");
+      }
+      Splittable value;
+      if (obj instanceof Enum && getAutoBeanFactory() instanceof EnumMap) {
         value = ValueCodex.encode(((EnumMap) getAutoBeanFactory()).getToken((Enum<?>) obj));
       } else if (ValueCodex.canDecode(obj.getClass())) {
         value = ValueCodex.encode(obj);
@@ -291,12 +316,10 @@
     }
 
     /**
-     * Assemble all of the state that has been accumulated in this context. This
-     * includes:
+     * 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}.
+     * <li>Invocations accumulated as Request subtypes passed to {@link #addInvocation}.
      * </ul>
      */
     public String makePayload() {
@@ -514,9 +537,9 @@
         (AutoBean<T>) state.editedProxies.get(BaseProxyCategory.stableId(bean));
     if (previouslySeen != null && !previouslySeen.isFrozen()) {
       /*
-       * If we've seen the object before, it might be because it was passed in
-       * as a method argument. This does not guarantee its mutability, so check
-       * that here before returning the cached object.
+       * If we've seen the object before, it might be because it was passed in as a method argument.
+       * This does not guarantee its mutability, so check that here before returning the cached
+       * object.
        */
       return previouslySeen.as();
     }
@@ -545,9 +568,9 @@
   }
 
   /**
-   * Make sure there's a default receiver so errors don't get dropped. This
-   * behavior should be revisited when chaining is supported, depending on
-   * whether or not chained invocations can fail independently.
+   * Make sure there's a default receiver so errors don't get dropped. This behavior should be
+   * revisited when chaining is supported, depending on whether or not chained invocations can fail
+   * independently.
    */
   public void fire() {
     boolean needsReceiver = true;
@@ -614,9 +637,8 @@
     /*
      * NB: Don't use the presence of ephemeral objects for this test.
      * 
-     * Diff the objects until one is found to be different. It's not just a
-     * simple flag-check because of the possibility of "unmaking" a change, per
-     * the JavaDoc.
+     * Diff the objects until one is found to be different. It's not just a simple flag-check
+     * because of the possibility of "unmaking" a change, per the JavaDoc.
      */
     for (AutoBean<? extends BaseProxy> bean : state.editedProxies.values()) {
       AutoBean<?> previous = bean.getTag(Constants.PARENT_OBJECT);
@@ -666,13 +688,11 @@
    * 
    * @param clazz The proxy type
    * @param id The id to be assigned to the new proxy
-   * @param useAppendedContexts if {@code true} use the AutoBeanFactory types
-   *          associated with any contexts that have been passed into
-   *          {@link #append(RequestContext)}. If {@code false}, this method
-   *          will only create proxy types reachable from the implemented
-   *          RequestContext interface.
-   * @throws IllegalArgumentException if the requested proxy type cannot be
-   *           created
+   * @param useAppendedContexts if {@code true} use the AutoBeanFactory types associated with any
+   *          contexts that have been passed into {@link #append(RequestContext)}. If {@code false},
+   *          this method will only create proxy types reachable from the implemented RequestContext
+   *          interface.
+   * @throws IllegalArgumentException if the requested proxy type cannot be created
    */
   protected <T extends BaseProxy> AutoBean<T> createProxy(Class<T> clazz, SimpleProxyId<T> id,
       boolean useAppendedContexts) {
@@ -729,8 +749,7 @@
   }
 
   /**
-   * Returns an AutoBeanFactory that can produce the types reachable only from
-   * this RequestContext.
+   * Returns an AutoBeanFactory that can produce the types reachable only from this RequestContext.
    */
   protected abstract AutoBeanFactory getAutoBeanFactory();
 
@@ -778,8 +797,8 @@
   }
 
   /**
-   * Creates or retrieves a new canonical AutoBean to represent the given id in
-   * the returned payload.
+   * 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) {
     @SuppressWarnings("unchecked")
@@ -794,8 +813,7 @@
   }
 
   /**
-   * Create a single OperationMessage that encapsulates the state of a proxy
-   * AutoBean.
+   * Create a single OperationMessage that encapsulates the state of a proxy AutoBean.
    */
   AutoBean<OperationMessage> makeOperationMessage(SimpleProxyId<BaseProxy> stableId,
       AutoBean<?> proxyBean, boolean useDelta) {
@@ -901,8 +919,7 @@
               Splittable raw = properties.get(propertyName);
               Object decoded = ValueCodex.decode(ctx.getType(), raw);
               /*
-               * Hack for Date subtypes, consider generalizing for
-               * "custom serializers"
+               * Hack for Date subtypes, consider generalizing for "custom serializers"
                */
               if (decoded != null && Date.class.equals(ctx.getType())) {
                 decoded = new DatePoser((Date) decoded);
@@ -920,8 +937,7 @@
     Q proxy = toMutate.as();
 
     /*
-     * Notify subscribers if the object differs from when it first came into the
-     * RequestContext.
+     * Notify subscribers if the object differs from when it first came into the RequestContext.
      */
     if (operations != null && state.requestFactory.isEntityType(id.getProxyClass())) {
       for (WriteOperation writeOperation : operations) {
@@ -962,8 +978,7 @@
   }
 
   /**
-   * This method checks that a proxy object is either immutable, or already
-   * edited by this context.
+   * This method checks that a proxy object is either immutable, or already edited by this context.
    */
   private <T> AutoBean<T> checkStreamsNotCrossed(T object) {
     AutoBean<T> bean = AutoBeanUtils.getAutoBean(object);
@@ -975,14 +990,13 @@
     State otherState = bean.getTag(REQUEST_CONTEXT_STATE);
     if (!bean.isFrozen() && otherState != this.state) {
       /*
-       * This means something is way off in the weeds. If a bean is editable,
-       * it's supposed to be associated with a RequestContext.
+       * This means something is way off in the weeds. If a bean is editable, it's supposed to be
+       * associated with a RequestContext.
        */
       assert otherState != null : "Unfrozen bean with null RequestContext";
 
       /*
-       * Already editing the object in another context or it would have been in
-       * the editing map.
+       * Already editing the object in another context or it would have been in the editing map.
        */
       throw new IllegalArgumentException("Attempting to edit an EntityProxy"
           + " previously edited by another RequestContext");
@@ -991,16 +1005,15 @@
   }
 
   /**
-   * Shallow-clones an autobean and makes duplicates of the collection types. A
-   * regular {@link AutoBean#clone} won't duplicate reference properties.
+   * 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(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.
+     * Take ownership here to prevent cycles in value objects from overflowing the stack.
      */
     takeOwnership(clone);
     clone.accept(new AutoBeanVisitor() {
@@ -1026,8 +1039,8 @@
 
           if (isValueType(ctx.getElementType()) || isEntityType(ctx.getElementType())) {
             /*
-             * Proxies must be edited up-front so that the elements in the
-             * collection have stable identity.
+             * Proxies must be edited up-front so that the elements in the collection have stable
+             * identity.
              */
             for (Object o : value.as()) {
               if (o == null) {
@@ -1053,8 +1066,7 @@
         if (value != null) {
           if (isValueType(ctx.getType()) || isEntityType(ctx.getType())) {
             /*
-             * Value proxies must be cloned upfront, since the value is replaced
-             * outright.
+             * Value proxies must be cloned upfront, since the value is replaced outright.
              */
             @SuppressWarnings("unchecked")
             AutoBean<BaseProxy> valueBean = (AutoBean<BaseProxy>) value;
@@ -1131,8 +1143,7 @@
   }
 
   /**
-   * Create an InvocationMessage for each remote method call being made by the
-   * context.
+   * Create an InvocationMessage for each remote method call being made by the context.
    */
   private List<InvocationMessage> makePayloadInvocations() {
     MessageFactory f = MessageFactoryHolder.FACTORY;
@@ -1217,8 +1228,7 @@
   }
 
   /**
-   * Ensures that any method arguments are retained in the context's sphere of
-   * influence.
+   * Ensures that any method arguments are retained in the context's sphere of influence.
    */
   private void retainArg(Object arg) {
     if (arg instanceof Iterable<?>) {