Make RequestFactory.getHistoryToken() play nicely with RF.getClass() and RF.getProxyId().
Patch by: bobv
Review by: rjrjr, pdr

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8840 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/FavoritesManager.java b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/FavoritesManager.java
index 0268fea..b291835 100644
--- a/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/FavoritesManager.java
+++ b/samples/dynatablerf/src/com/google/gwt/sample/dynatablerf/client/FavoritesManager.java
@@ -47,9 +47,10 @@
           if (fragment.length() == 0) {
             continue;
           }
-          @SuppressWarnings("unchecked")
-          EntityProxyId<PersonProxy> id = (EntityProxyId<PersonProxy>) requestFactory.getProxyId(fragment);
-          favoriteIds.add(id);
+          EntityProxyId<PersonProxy> id = requestFactory.getProxyId(fragment);
+          if (id != null) {
+            favoriteIds.add(id);
+          }
         }
       } catch (NumberFormatException e) {
         // Not a big deal, start up without favorites
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/EntityProxyIdImpl.java b/user/src/com/google/gwt/requestfactory/client/impl/EntityProxyIdImpl.java
index fe27403..00f185e 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/EntityProxyIdImpl.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/EntityProxyIdImpl.java
@@ -35,7 +35,6 @@
  * created on this client.
  */
 final class EntityProxyIdImpl<P extends EntityProxy> implements EntityProxyId<P> {
-  static final String SEPARATOR = "---";
 
   private static int hashCode(ProxySchema<?> proxySchema, boolean hasFutureId, Object finalId) {
     final int prime = 31;
@@ -66,13 +65,6 @@
     this.futureId = futureId;
   }
 
-  public String asString() {
-    if (isFuture) {
-      throw new IllegalStateException("Need to persist this proxy first");
-    }
-    return encodedId + SEPARATOR + schema.getToken();
-  }
-
   @Override
   public boolean equals(Object obj) {
     if (this == obj) {
diff --git a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
index 4fa03b3..28f5d51 100644
--- a/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
+++ b/user/src/com/google/gwt/requestfactory/client/impl/RequestFactoryJsonImpl.java
@@ -67,6 +67,10 @@
 
   static final boolean IS_FUTURE = true;
   static final boolean NOT_FUTURE = false;
+  private static final String FUTURE_TOKEN = "F";
+  private static final String HISTORY_TOKEN_SEPARATOR = "--";
+  private static final int ID_INDEX = 0;
+  private static final int SCHEMA_INDEX = 1;
 
   private static Logger logger = Logger.getLogger(RequestFactory.class.getName());
 
@@ -113,8 +117,7 @@
     String payload = ClientRequestHelper.getRequestString(requestMap);
     transport.send(payload, new RequestTransport.TransportReceiver() {
       public void onTransportFailure(String message) {
-        abstractRequest.fail(new ServerFailure(message, null,
-            null));
+        abstractRequest.fail(new ServerFailure(message, null, null));
       }
 
       public void onTransportSuccess(String payload) {
@@ -155,7 +158,7 @@
   public void initialize(EventBus eventBus) {
     initialize(eventBus, new DefaultRequestTransport(eventBus));
   }
-  
+
   public void initialize(EventBus eventBus, RequestTransport transport) {
     this.valueStore = new ValueStoreJsonImpl();
     this.eventBus = eventBus;
@@ -165,47 +168,76 @@
 
   protected abstract FindRequest findRequest();
 
+  /**
+   * This implementation cannot be changed without breaking clients.
+   */
   protected Class<? extends EntityProxy> getClass(String token,
       ProxyToTypeMap recordToTypeMap) {
-    String[] bits = token.split("-");
-    ProxySchema<? extends EntityProxy> schema = recordToTypeMap.getType(bits[0]);
+    String[] bits = token.split(HISTORY_TOKEN_SEPARATOR);
+    String schemaToken;
+    if (bits.length == 1) {
+      schemaToken = token;
+    } else if (bits.length == 2 || bits.length == 3) {
+      schemaToken = bits[SCHEMA_INDEX];
+    } else {
+      return null;
+    }
+    ProxySchema<? extends EntityProxy> schema = recordToTypeMap.getType(schemaToken);
     if (schema == null) {
       return null;
     }
     return schema.getProxyClass();
   }
 
-  protected String getHistoryToken(EntityProxyId proxyId, ProxyToTypeMap recordToTypeMap) {
-    EntityProxyIdImpl entityProxyId = (EntityProxyIdImpl) proxyId;
-    Class<? extends EntityProxy> proxyClass = entityProxyId.schema.getProxyClass();
-    String rtn = recordToTypeMap.getClassToken(proxyClass) + "-";
-    Object datastoreId = entityProxyId.encodedId;
+  /**
+   * This implementation cannot be changed without breaking clients.
+   */
+  protected String getHistoryToken(EntityProxyId<?> proxyId,
+      ProxyToTypeMap recordToTypeMap) {
+    EntityProxyIdImpl<?> entityProxyId = (EntityProxyIdImpl<?>) proxyId;
+    StringBuilder toReturn = new StringBuilder();
+    boolean isFuture = false;
+    Object tokenId = entityProxyId.encodedId;
     if (entityProxyId.isFuture) {
-      datastoreId = futureToDatastoreMap.get(entityProxyId.encodedId);
+      // See if the associated entityproxy has been persisted in the meantime
+      Object persistedId = futureToDatastoreMap.get(entityProxyId.encodedId);
+      if (persistedId == null) {
+        // Return a future token
+        isFuture = true;
+      } else {
+        // Use the persisted id instead
+        tokenId = persistedId;
+      }
     }
-    if (datastoreId == null) {
-      rtn += "0-FUTURE";
-    } else {
-      rtn += datastoreId;
+    toReturn = new StringBuilder();
+    toReturn.append(tokenId);
+    toReturn.append(HISTORY_TOKEN_SEPARATOR).append(
+        entityProxyId.schema.getToken());
+    if (isFuture) {
+      toReturn.append(HISTORY_TOKEN_SEPARATOR).append(FUTURE_TOKEN);
     }
-    return rtn;
+    return toReturn.toString();
   }
 
-  protected EntityProxyId getProxyId(String token,
+  /**
+   * This implementation cannot be changed without breaking clients.
+   */
+  protected EntityProxyId<?> getProxyId(String historyToken,
       ProxyToTypeMap recordToTypeMap) {
-    String[] bits = token.split(EntityProxyIdImpl.SEPARATOR);
-    if (bits.length != 2) {
+    String[] bits = historyToken.split(HISTORY_TOKEN_SEPARATOR);
+    if (bits.length < 2 || bits.length > 3) {
       return null;
     }
+    boolean isFuture = bits.length == 3;
 
-    ProxySchema<? extends EntityProxy> schema = recordToTypeMap.getType(bits[1]);
+    ProxySchema<? extends EntityProxy> schema = recordToTypeMap.getType(bits[SCHEMA_INDEX]);
     if (schema == null) {
       return null;
     }
 
-    String id = bits[0];
+    String id = bits[ID_INDEX];
     Object futureId = datastoreToFutureMap.get(id, schema);
-    return new EntityProxyIdImpl(id, schema, false, futureId);
+    return new EntityProxyIdImpl<EntityProxy>(id, schema, isFuture, futureId);
   }
 
   ValueStoreJsonImpl getValueStore() {
@@ -230,8 +262,8 @@
 
   private <R extends ProxyImpl> R createFuture(ProxySchema<R> schema) {
     Long futureId = ++currentFutureId;
-    ProxyJsoImpl newRecord = ProxyJsoImpl.create(Long.toString(futureId), initialVersion,
-        schema, this);
+    ProxyJsoImpl newRecord = ProxyJsoImpl.create(Long.toString(futureId),
+        initialVersion, schema, this);
     return schema.create(newRecord, IS_FUTURE);
   }
 }
diff --git a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
index 0c4a04c..882645b 100644
--- a/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
+++ b/user/src/com/google/gwt/requestfactory/shared/RequestFactory.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -43,39 +43,49 @@
    */
   <P extends EntityProxy> Class<P> getClass(EntityProxyId<P> proxyId);
 
-  /**
+/**
    * Return the class object which may be used to create new instances of the
    * type of this token, via {@link #create}. The token may represent either a
-   * proxy instance (see {@link #getHistoryToken(Proxy)) or a proxy class (see
-   * @link #getToken(Class)}).
+   * proxy instance (see {@link #getHistoryToken(Proxy)}) or a proxy class (see
+   * {@link #getToken(Class)}).
    */
   Class<? extends EntityProxy> getClass(String token);
 
   /**
-   * @return the eventbus this factory's events are posted on, which was set via
-   *         {@link #initialize}
+   * Returns the eventbus this factory's events are posted on, which was set via
+   * {@link #initialize}.
    */
   EventBus getEventBus();
 
   /**
    * Get a {@link com.google.gwt.user.client.History} compatible token that
-   * represents the given proxy. It can be processed by
+   * represents the given proxy id. It can be processed by
    * {@link #getProxyId(String)} and {@link #getClass(String)}.
-   *
+   * <p>
+   * The history token returned for an EntityProxyId associated with a
+   * newly-created (future) EntityProxy will differ from the token returned by this
+   * method after the EntityProxy has been persisted. Once an EntityProxy has
+   * been persisted, the return value for this method will always be stable,
+   * regardless of when the EntityProxyId was retrieved relative to the persist
+   * operation. In other words, the "future" history token returned for an
+   * as-yet-unpersisted EntityProxy is only valid for the duration of the
+   * RequestFactory's lifespan.
+   * 
    * @return a {@link com.google.gwt.user.client.History} compatible token
    */
   String getHistoryToken(EntityProxyId<?> proxy);
 
   /**
-   * Return the appropriate {@link EntityProxyId}, a stable id for the Proxy.
+   * Return the appropriate {@link EntityProxyId} using a string returned from
+   * {@link #getHistoryToken(EntityProxyId)}.
    */
-  EntityProxyId<?> getProxyId(String token);
+  <T extends EntityProxy> EntityProxyId<T> getProxyId(String historyToken);
 
   /**
    * Get a {@link com.google.gwt.user.client.History} compatible token that
    * represents the given class. It can be processed by
    * {@link #getClass(String)}
-   *
+   * 
    * @return a {@link com.google.gwt.user.client.History} compatible token
    */
   String getToken(Class<? extends EntityProxy> clazz);
diff --git a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
index 1793b46..4fdc030 100644
--- a/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
+++ b/user/test/com/google/gwt/requestfactory/client/RequestFactoryTest.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.requestfactory.client.impl.ProxyImpl;
 import com.google.gwt.requestfactory.shared.EntityProxy;
+import com.google.gwt.requestfactory.shared.EntityProxyId;
 import com.google.gwt.requestfactory.shared.Receiver;
 import com.google.gwt.requestfactory.shared.Request;
 import com.google.gwt.requestfactory.shared.ServerFailure;
@@ -124,6 +125,14 @@
     return "com.google.gwt.requestfactory.RequestFactorySuite";
   }
 
+  public void testClassToken() {
+    String token = req.getToken(SimpleFooProxy.class);
+    assertEquals(SimpleFooProxy.class, req.getClass(token));
+
+    SimpleFooProxy foo = req.create(SimpleFooProxy.class);
+    assertEquals(SimpleFooProxy.class, req.getClass(foo.stableId()));
+  }
+
   public void  testDummyCreate() {
     delayTestFinish(5000);
 
@@ -170,6 +179,44 @@
     });
   }
 
+  public void testHistoryToken() {
+    delayTestFinish(5000);
+    final SimpleBarProxy foo = req.create(SimpleBarProxy.class);
+    final EntityProxyId<SimpleBarProxy> futureId = foo.stableId();
+    final String futureToken = req.getHistoryToken(futureId);
+
+    // Check that a newly-created object's token can be found
+    assertEquals(futureId, req.getProxyId(futureToken));
+    assertEquals(req.getClass(futureId), req.getClass(futureToken));
+
+    RequestObject<SimpleBarProxy> fooReq = req.simpleBarRequest().persistAndReturnSelf(
+        foo);
+    fooReq.fire(new Receiver<SimpleBarProxy>() {
+      @Override
+      public void onSuccess(final SimpleBarProxy returned) {
+        EntityProxyId<SimpleBarProxy> persistedId = returned.stableId();
+        String persistedToken = req.getHistoryToken(returned.stableId());
+
+        // Expect variations after persist
+        assertFalse(futureToken.equals(persistedToken));
+        
+        // Make sure the token is stable after persist using the future id
+        assertEquals(persistedToken, req.getHistoryToken(futureId));
+
+        // Check that the persisted object can be found with future token
+        assertEquals(futureId, req.getProxyId(futureToken));
+        assertEquals(futureId, req.getProxyId(persistedToken));
+        assertEquals(req.getClass(futureId), req.getClass(persistedToken));
+
+        assertEquals(persistedId, req.getProxyId(futureToken));
+        assertEquals(persistedId, req.getProxyId(persistedToken));
+        assertEquals(req.getClass(persistedId), req.getClass(futureToken));
+
+        finishTestAndReset();
+      }
+    });
+  }
+
   public void testFetchEntity() {
     delayTestFinish(5000);
     req.simpleFooRequest().findSimpleFooById(999L).fire(