Implement RPC for JDO persistent objects.
The JDO object must contain the annotation:

@PersistenceCapable(identityType = IdentityType.APPLICATION, detachable = "true")

and be detached at the time it is sent to the client as part of an RPC request.

Review by: bobv



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5662 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index bdb514f..cb9b665 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -1949,6 +1949,7 @@
     specialObfuscatedIdents.put("finalize", "fZ");
 
     // Object fields
+    specialObfuscatedIdents.put("expando", "eX");
     specialObfuscatedIdents.put("typeId", "tI");
     specialObfuscatedIdents.put("typeMarker", "tM");
 
diff --git a/user/src/com/google/gwt/core/client/WeakMapping.java b/user/src/com/google/gwt/core/client/WeakMapping.java
new file mode 100644
index 0000000..327721d
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/WeakMapping.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2009 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.core.client;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class associating a (String, Object) map with arbitrary source objects
+ * (except for Strings). This implementation is used in hosted mode.
+ */
+public class WeakMapping {
+
+  /*
+   * This implementation is used in hosted mode only. It uses a HashMap to
+   * associate the (key, value) maps with source object instances. The object
+   * instances are wrapped in IdentityWeakReference objects in order to both
+   * allow the underlying objects to be garbage-collected and to apply
+   * IdentityHashMap semantics so that distinct objects that happen to compare
+   * as equals() still get to have distinct maps associated with them.
+   */
+
+  /**
+   * A WeakReference implementing equals() and hashCode(). The hash code of the
+   * reference is permanently set to the identity hash code of the referent at
+   * construction time.
+   */
+  static class IdentityWeakReference extends WeakReference {
+
+    /**
+     * The identity hash code of the referent, cached during construction.
+     */
+    private final int hashCode;
+
+    public IdentityWeakReference(Object referent, ReferenceQueue queue) {
+      super(referent, queue);
+      hashCode = System.identityHashCode(referent);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      /*
+       * Identical objects are always equal.
+       */
+      if (this == other) {
+        return true;
+      }
+
+      /*
+       * We can only be equal to another IdentityWeakReference.
+       */
+      if (!(other instanceof IdentityWeakReference)) {
+        return false;
+      }
+
+      /*
+       * Check equality of the underlying referents. If either referent is no
+       * longer present, equals() will return false (note that the case of
+       * identical IdentityWeakReference objects has already been defined to
+       * return true above).
+       */
+      Object referent = get();
+      if (referent == null) {
+        return false;
+      }
+      return referent == ((IdentityWeakReference) other).get();
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+  }
+
+  /**
+   * A Map from Objects to <String,Object> maps. Hashing is based on object
+   * identity. Weak references are used to allow otherwise unreferenced Objects
+   * to be garbage collected.
+   */
+  private static Map<IdentityWeakReference, HashMap<String, Object>> map =
+    new HashMap<IdentityWeakReference, HashMap<String, Object>>();
+
+  /**
+   * A ReferenceQueue used to clean up the map as its keys are
+   * garbage-collected.
+   */
+  private static ReferenceQueue queue = new ReferenceQueue();
+
+  /**
+   * Returns the Object associated with the given key in the (key, value)
+   * mapping associated with the given Object instance.
+   * 
+   * @param instance the source Object.
+   * @param key a String key.
+   * @return an Object associated with that key on the given instance, or null.
+   */
+  public static Object get(Object instance, String key) {
+    cleanup();
+
+    IdentityWeakReference ref = new IdentityWeakReference(instance, queue);
+    HashMap<String, Object> m = map.get(ref);
+    if (m == null) {
+      return null;
+    }
+    return m.get(key);
+  }
+
+  /**
+   * Associates a value with a given key in the (key, value) mapping associated
+   * with the given Object instance. Note that the key space is module-wide, so
+   * some care should be taken to choose sufficiently unique identifiers.
+   * 
+   * <p>
+   * Due to restrictions of the web mode implementation, the instance argument
+   * must not be a String.
+   * 
+   * @param instance the source Object, which must not be a String.
+   * @param key a String key.
+   * @param value the Object to associate with the key on the given source
+   *          Object.
+   * @throws IllegalArgumentException if instance is a String.
+   */
+  public static void set(Object instance, String key, Object value) {
+    cleanup();
+
+    if (instance instanceof String) {
+      throw new IllegalArgumentException("Cannot use Strings with WeakMapping");
+    }
+
+    IdentityWeakReference ref = new IdentityWeakReference(instance, queue);
+    HashMap<String, Object> m = map.get(ref);
+    if (m == null) {
+      m = new HashMap<String, Object>();
+      map.put(ref, m);
+    }
+    m.put(key, value);
+  }
+
+  /**
+   * Remove garbage-collected keys from the map. The (key, value) maps
+   * associated with those keys will then become unreferenced themselves and
+   * will be eligible for future garbage collection.
+   */
+  private static void cleanup() {
+    Reference ref;
+    while ((ref = queue.poll()) != null) {
+      /**
+       * Note that we can still remove ref from the map even though its referent
+       * has been nulled out since we only need == equality to do so.
+       */
+      map.remove(ref);
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/user/rebind/rpc/ClientDataSerializer.java b/user/src/com/google/gwt/user/rebind/rpc/ClientDataSerializer.java
new file mode 100644
index 0000000..12424b5
--- /dev/null
+++ b/user/src/com/google/gwt/user/rebind/rpc/ClientDataSerializer.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2009 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.user.rebind.rpc;
+
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.TreeMap;
+
+/**
+ * An interface for serializing and deserializing portions of Object data that
+ * are present in the server implementation but are not present in client code.
+ * For example, some persistence frameworks make use of server-side bytecode
+ * enhancement; the fields added by such enhancement are unknown to the client,
+ * and therefore are not handled by normal GWT RPC mechanisms.
+ * 
+ * <p>
+ * This portion of the interface is called from the
+ * {@link FieldSerializerCreator} as part of the generation of client-side field
+ * serializers.
+ * 
+ * @see com.google.gwt.user.server.rpc.impl.ServerDataSerializer
+ */
+public abstract class ClientDataSerializer implements
+    Comparable<ClientDataSerializer> {
+
+  /**
+   * A mapping from ClientDataSerializer names to instances, sorted by name.
+   */
+  private static TreeMap<String, ClientDataSerializer> serializers =
+    new TreeMap<String, ClientDataSerializer>();
+
+  /**
+   * All active ServerDataSerializers must be initialized here and placed into
+   * the serializers map.
+   * 
+   * <p>
+   * The map must be kept in sync with the one in
+   * {@link com.google.gwt.user.server.rpc.impl.ServerDataSerializer}.
+   */
+  static {
+    // Load and register a JdoDetachedStateSerializer
+    ClientDataSerializer serializer = JdoDetachedStateClientDataSerializer.getInstance();
+    serializers.put(serializer.getName(), serializer);
+  }
+
+  /**
+   * Returns a Collection of all ClientDataSerializer instances, ordered by name.
+   * The returned collection is unmodifiable.
+   */
+  public static Collection<ClientDataSerializer> getSerializers() {
+    return Collections.unmodifiableCollection(serializers.values());
+  }
+
+  /**
+   * Allow ServerDataSerialzer instances to be sorted by class name.
+   */
+  public int compareTo(ClientDataSerializer other) {
+    return getName().compareTo(other.getName());
+  }
+
+  /**
+   * Returns the name of this {@link ServerDataSerializer} instance, used to
+   * determine the sorting order when multiple serializers apply to a given
+   * class type.  The name will be used as a key to store the serialized data
+   * on the client.
+   * 
+   * <p>
+   * The name must be identical to that of the corresponding
+   * {@link ServerDataSerializer}.
+   */
+  public abstract String getName();
+
+  /**
+   * Returns true if the given classType should be processed by a
+   * ServerClientSerializer.
+   * 
+   * @param classType the class type to be queried.
+   */
+  public abstract boolean shouldSerialize(JClassType classType);
+}
diff --git a/user/src/com/google/gwt/user/rebind/rpc/FieldSerializerCreator.java b/user/src/com/google/gwt/user/rebind/rpc/FieldSerializerCreator.java
index e21bf52..ec0c37f 100644
--- a/user/src/com/google/gwt/user/rebind/rpc/FieldSerializerCreator.java
+++ b/user/src/com/google/gwt/user/rebind/rpc/FieldSerializerCreator.java
@@ -16,6 +16,7 @@
 package com.google.gwt.user.rebind.rpc;
 
 import com.google.gwt.core.client.UnsafeNativeLong;
+import com.google.gwt.core.client.WeakMapping;
 import com.google.gwt.core.ext.GeneratorContext;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.typeinfo.JArrayType;
@@ -46,6 +47,8 @@
  * fully qualified type names everywhere
  */
 public class FieldSerializerCreator {
+  
+  private final static String WEAK_MAPPING_CLASS_NAME = WeakMapping.class.getName();
 
   private final JClassType serializableClass;
 
@@ -348,6 +351,13 @@
       writeEnumDeserializationStatements(serializableClass.isEnum());
     } else {
       writeClassDeserializationStatements();
+      
+      for (ClientDataSerializer serializer : ClientDataSerializer.getSerializers()) {
+        if (serializer.shouldSerialize(serializableClass)) {
+          sourceWriter.println(WEAK_MAPPING_CLASS_NAME + ".set(instance, "
+              + "\"" + serializer.getName() + "\", streamReader.readString());");
+        }
+      }
     }
     sourceWriter.outdent();
     sourceWriter.println("}");
@@ -455,6 +465,14 @@
       writeEnumSerializationStatements(serializableClass.isEnum());
     } else {
       writeClassSerializationStatements();
+
+      for (ClientDataSerializer serializer : ClientDataSerializer.getSerializers()) {
+        if (serializer.shouldSerialize(serializableClass)) {
+          sourceWriter.println("streamWriter.writeString((String) "
+              + WEAK_MAPPING_CLASS_NAME + ".get(instance, \""
+              + serializer.getName() + "\"));");
+        }
+      }
     }
 
     sourceWriter.outdent();
diff --git a/user/src/com/google/gwt/user/rebind/rpc/JdoDetachedStateClientDataSerializer.java b/user/src/com/google/gwt/user/rebind/rpc/JdoDetachedStateClientDataSerializer.java
new file mode 100644
index 0000000..5f08742
--- /dev/null
+++ b/user/src/com/google/gwt/user/rebind/rpc/JdoDetachedStateClientDataSerializer.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2009 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.user.rebind.rpc;
+
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * An implementation of ClientFieldSerializer that handles the jdoDetachedState
+ * field in the JDO API, version 2.2.
+ */
+final class JdoDetachedStateClientDataSerializer extends
+    ClientDataSerializer {
+
+  private static Class<? extends Annotation> annotationClass;
+  private static Method detachableMethod;
+  
+  /**
+   * The singleton instance.
+   */
+  private static final JdoDetachedStateClientDataSerializer theInstance =
+    new JdoDetachedStateClientDataSerializer();
+  
+  static {
+    try {
+      annotationClass = Class.forName(
+          "javax.jdo.annotations.PersistenceCapable").asSubclass(
+          Annotation.class);
+      detachableMethod = annotationClass.getDeclaredMethod("detachable",
+          (Class[]) null);
+    } catch (ClassNotFoundException e) {
+      // Ignore, annotationClass will be null
+    } catch (NoSuchMethodException e) {
+      // Set annotationClass to null, don't do serialization
+      annotationClass = null;
+    }
+  }
+
+  /**
+   * Return the unique instance of this class.
+   */
+  public static JdoDetachedStateClientDataSerializer getInstance() {
+    return theInstance;
+  }
+  
+  /**
+   * Ensure this class has a singleton instance only.
+   */
+  private JdoDetachedStateClientDataSerializer() {
+  }
+
+  @Override
+  public String getName() {
+    return "gwt-jdo-jdoDetachedState";
+  }
+  
+  /**
+   * Returns true if the given classType should be processed by a
+   * ClientDataSerializer.
+   * 
+   * @param classType the class type to be queried.
+   */
+  @Override
+  public boolean shouldSerialize(JClassType classType) {
+    try {
+      if (annotationClass == null) {
+        return false;
+      }
+      Annotation annotation = classType.getAnnotation(annotationClass);
+      if (annotation == null) {
+        return false;
+      }
+      Object value = detachableMethod.invoke(annotation, (Object[]) null);
+      if (value instanceof String) {
+        return "true".equalsIgnoreCase((String) value);
+      } else {
+        return false;
+      }
+    } catch (IllegalAccessException e) {
+      // will return false
+    } catch (InvocationTargetException e) {
+      // will return false
+    }
+
+    return false;
+  }
+}
diff --git a/user/src/com/google/gwt/user/server/Base64Utils.java b/user/src/com/google/gwt/user/server/Base64Utils.java
new file mode 100644
index 0000000..14accb2
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/Base64Utils.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2009 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.user.server;
+
+/**
+ * A utility to decode and encode byte arrays as Strings, using only "safe" characters.
+ */
+public class Base64Utils {
+
+  /**
+   * An array mapping size but values to the characters that will be used to represent them.
+   */
+  private static final char[] base64Chars = new char[] {
+    'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
+    'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
+    'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3',
+    '4', '5', '6', '7', '8', '9', '$', '_'
+  };
+
+  /**
+   * An array mapping legal base 64 characters [a-zA-Z0-9$_] to their associated 6-bit values.
+   */
+  private static final byte[] base64Values = new byte[123];
+
+  /**
+   * Initialize the base 64 encoder values.
+   */
+  static {
+    for (int i = 'a'; i <= 'z'; i++) {
+      base64Values[i] = (byte) (i - 'a');
+    }
+    for (int i = 'A'; i <= 'Z'; i++) {
+      base64Values[i] = (byte) (i - 'A' + 26);
+    }
+    for (int i = '0'; i <= '9'; i++) {
+      base64Values[i] = (byte) (i - '0' + 52);
+    }
+    base64Values['$'] = 62;
+    base64Values['_'] = 63;
+  }
+
+  /**
+   * Decode a base64 string into a byte array.
+   * 
+   * @param data the encoded data.
+   * @return a byte array.
+   * @see #fromBase64(String)
+   */
+  public static byte[] fromBase64(String data) {
+    if (data == null) {
+      return null;
+    }
+    
+    int len = data.length();
+    assert (len % 4) == 0;
+    
+    if (len == 0) {
+      return new byte[0];
+    }
+    
+    char[] chars = new char[len];
+    data.getChars(0, len, chars, 0);
+    
+    int olen = 3 * (len / 4);
+    if (chars[len - 2] == '=') {
+      --olen;
+    }
+    if (chars[len - 1] == '=') {
+      --olen;
+    }
+    
+    byte[] bytes = new byte[olen];
+    
+    int iidx = 0;
+    int oidx = 0;
+    while (iidx < len) {
+      int c0 = base64Values[chars[iidx++] & 0xff];
+      int c1 = base64Values[chars[iidx++] & 0xff];
+      int c2 = base64Values[chars[iidx++] & 0xff];
+      int c3 = base64Values[chars[iidx++] & 0xff];
+      int c24 = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
+      
+      bytes[oidx++] = (byte) (c24 >> 16);
+      if (oidx == olen) {
+        break;
+      }
+      bytes[oidx++] = (byte) (c24 >>  8);
+      if (oidx == olen) {
+        break;
+      }
+      bytes[oidx++] = (byte)  c24;
+    }
+    
+    return bytes;
+  }
+
+  /**
+   * Converts a byte array into a base 64 encoded string. Null is encoded as
+   * null, and an empty array is encoded as an empty string. Otherwise, the byte
+   * data is read 3 bytes at a time, with bytes off the end of the array padded
+   * with zeros. Each 24-bit chunk is encoded as 4 characters from the sequence
+   * [a-zA-Z0-9$_]. If one of the size-bit source positions consists entirely of
+   * padding zeros, an '=' character is used instead.
+   * 
+   * @param data a byte array, which may be null or empty
+   * @return a String
+   */
+  public static String toBase64(byte[] data) {
+    if (data == null) {
+      return null;
+    }
+
+    int len = data.length;
+    if (len == 0) {
+      return "";
+    }
+    
+    int olen = 4 * ((len + 2) / 3);
+    char[] chars = new char[olen];
+
+    int iidx = 0;
+    int oidx = 0;
+    int charsLeft = len;
+    while (charsLeft > 0) {
+      int b0 = data[iidx++] & 0xff;
+      int b1 = (charsLeft > 1) ? data[iidx++] & 0xff : 0;
+      int b2 = (charsLeft > 2) ? data[iidx++] & 0xff : 0;
+      int b24 = (b0 << 16) | (b1 << 8) | b2;
+
+      int c0 = (b24 >> 18) & 0x3f;
+      int c1 = (b24 >> 12) & 0x3f;
+      int c2 = (b24 >> 6) & 0x3f;
+      int c3 = b24 & 0x3f;
+
+      chars[oidx++] = base64Chars[c0];
+      chars[oidx++] = base64Chars[c1];
+      chars[oidx++] = (charsLeft > 1) ? base64Chars[c2] : '=';
+      chars[oidx++] = (charsLeft > 2) ? base64Chars[c3] : '=';
+
+      charsLeft -= 3;
+    }
+
+    return new String(chars);
+  }
+}
+
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/JdoDetachedStateServerDataSerializer.java b/user/src/com/google/gwt/user/server/rpc/impl/JdoDetachedStateServerDataSerializer.java
new file mode 100644
index 0000000..12fccdd
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/impl/JdoDetachedStateServerDataSerializer.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2009 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.user.server.rpc.impl;
+
+import com.google.gwt.user.client.rpc.SerializationException;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.util.BitSet;
+
+/**
+ * An implementation of ServerFieldSerializer that handles the jdoDetachedState
+ * field in the JDO API, version 2.2.
+ */
+final class JdoDetachedStateServerDataSerializer extends
+    ServerDataSerializer {
+
+  /**
+   * A Class object for the javax.jdo.spi.Detachable interface, or null if it is
+   * not present in the runtime environment.
+   */
+  private static Class<?> JAVAX_JDO_SPI_DETACHABLE_CLASS;
+
+  /**
+   * A constant indicating an Externalizable entry in the jdoDetachedState
+   * Object array.
+   */
+  private static final int JDO_DETACHED_STATE_ENTRY_EXTERNALIZABLE = 0;
+
+  /**
+   * A constant indicating a null entry in the jdoDetachedState Object array.
+   */
+  private static final int JDO_DETACHED_STATE_ENTRY_NULL = 1;
+
+  /**
+   * A constant indicating a Serializable entry in the jdoDetachedState Object
+   * array.
+   */
+  private static final int JDO_DETACHED_STATE_ENTRY_SERIALIZABLE = 2;
+
+  /**
+   * A constant indicating the name of the jdoDetachedState field.
+   */
+  private static final String JDO_DETACHED_STATE_FIELD_NAME = "jdoDetachedState";
+
+  /**
+   * A version number for the serialized form of the jdoDetachedState field.
+   * Version 1 corresponds to JDO API version 2.2.
+   */
+  private static final int JDO_DETACHED_STATE_SERIALIZATION_VERSION = 1;
+
+  /**
+   * A constant indicating the name of the jdoFlags field.
+   */
+  private static final String JDO_FLAGS_FIELD_NAME = "jdoFlags";
+
+  /**
+   * A constant indicating the "LOAD_REQUIRED" value for the jdoFlags field.
+   */
+  private static final int JDO_FLAGS_LOAD_REQUIRED = 1;
+  
+  /**
+   * The singleton instance.
+   */
+  private static final JdoDetachedStateServerDataSerializer theInstance =
+    new JdoDetachedStateServerDataSerializer();
+
+  static {
+    try {
+      JAVAX_JDO_SPI_DETACHABLE_CLASS = Class.forName("javax.jdo.spi.Detachable");
+    } catch (ClassNotFoundException e) {
+      // Ignore, if JDO is not present in our enviroment the variable will be
+      // initialized to null.
+    }
+  }
+
+  /**
+   * Return the unique instance of this class.
+   */
+  public static JdoDetachedStateServerDataSerializer getInstance() {
+    return theInstance;
+  }
+  
+  /**
+   * Ensure this class has a singleton instance only.
+   */
+  private JdoDetachedStateServerDataSerializer() {
+  }
+
+  /**
+   * Custom deserialize the contents of the jdoDetachedState field.
+   * 
+   * @param serializedData the serialized data, as an array of bytes, possibly
+   *          null.
+   * @param instance the Object instance to be modified.
+   * @throws SerializationException if the field contents cannot be
+   *           reconstructed.
+   */
+  @Override
+  public void deserializeServerData(byte[] serializedData, Object instance)
+      throws SerializationException {
+    try {
+      Class<?> instanceClass = instance.getClass();
+      Field jdoDetachedStateField = instanceClass.getDeclaredField(JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_FIELD_NAME);
+      jdoDetachedStateField.setAccessible(true);
+
+      if (serializedData == null) {
+        throw new SerializationException("JDO persistent object serialized data is null");
+      }
+
+      ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
+      ObjectInputStream in = new ObjectInputStream(bais);
+
+      // We only understand version 1 (JDO version 2.2) at this time.
+      int version = in.readInt();
+      if (version != JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_SERIALIZATION_VERSION) {
+        throw new SerializationException(
+            "Got JDO detached state serialization version "
+                + version
+                + ", expected version "
+                + JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_SERIALIZATION_VERSION
+                + ".");
+      }
+
+      Object[] jdoDetachedState = new Object[4];
+      for (int i = 0; i < 3; i++) {
+        byte type = in.readByte();
+        switch (type) {
+          case JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_ENTRY_NULL:
+            jdoDetachedState[i] = null;
+            break;
+
+          case JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_ENTRY_EXTERNALIZABLE:
+            try {
+              String className = (String) in.readObject();
+              Class<? extends Externalizable> c = Class.forName(className).asSubclass(
+                  java.io.Externalizable.class);
+              Externalizable e = c.newInstance();
+              e.readExternal(in);
+              jdoDetachedState[i] = e;
+            } catch (ClassCastException e) {
+              throw new SerializationException(e);
+            } catch (ClassNotFoundException e) {
+              throw new SerializationException(e);
+            } catch (IllegalAccessException e) {
+              throw new SerializationException(e);
+            } catch (InstantiationException e) {
+              throw new SerializationException(e);
+            }
+            break;
+
+          case JdoDetachedStateServerDataSerializer.JDO_DETACHED_STATE_ENTRY_SERIALIZABLE:
+            try {
+              jdoDetachedState[i] = in.readObject();
+            } catch (ClassNotFoundException e) {
+              throw new SerializationException(e);
+            }
+            break;
+        }
+      }
+
+      // Mark all loaded fields as modified
+      jdoDetachedState[3] = new BitSet();
+      ((BitSet) jdoDetachedState[3]).or((BitSet) jdoDetachedState[2]);
+
+      // Set the field
+      jdoDetachedStateField.set(instance, jdoDetachedState);
+    } catch (IllegalAccessException e) {
+      throw new SerializationException(e);
+    } catch (IOException e) {
+      throw new SerializationException(
+          "An unexpected IOException occured while deserializing jdoDetachedState",
+          e);
+    } catch (NoSuchFieldException e) {
+      throw new SerializationException(e);
+    }
+  }
+
+  @Override
+  public String getName() {
+    return "gwt-jdo-jdoDetachedState";
+  }
+
+  /**
+   * Custom serialize the contents of the jdoDetachedState field. If the field
+   * is null, a null array is returned. Otherwise, a byte array is returned with
+   * the server-only contents of the instance in a custom serialized form. The
+   * current implementation of this method assumes JDO API version 2.2.
+   * 
+   * @param instance an Object containing server-only data.
+   * @return a byte array containing a representation of the field.
+   * @throws SerializationException if the instance cannot be serialized by this
+   *           serializer.
+   */
+  @Override
+  public byte[] serializeServerData(Object instance)
+      throws SerializationException {
+    try {
+      Class<?> instanceClass = instance.getClass();
+
+      // Ensure the jdoFlags field is not set to LOAD_REQUIRED
+      Field jdoFlagsField = instanceClass.getDeclaredField(JDO_FLAGS_FIELD_NAME);
+      jdoFlagsField.setAccessible(true);
+      byte jdoFlags = ((Byte) jdoFlagsField.get(instance)).byteValue();
+      if (jdoFlags == JDO_FLAGS_LOAD_REQUIRED) {
+        throw new SerializationException("JDO persistent object data not loaded");
+      }
+
+      // Retrieve the jdoDetachedStateField and ensure it is non-null
+      Field jdoDetachedStateField = instanceClass.getDeclaredField(JDO_DETACHED_STATE_FIELD_NAME);
+      jdoDetachedStateField.setAccessible(true);
+      Object[] jdoDetachedState = (Object[]) jdoDetachedStateField.get(instance);
+      if (jdoDetachedState == null) {
+        throw new SerializationException("JDO persistent object has null jdoDetachedState");
+      }
+
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      ObjectOutputStream out = new ObjectOutputStream(baos);
+      
+      // Version 1 == JDO API version 2.2
+      out.writeInt(JDO_DETACHED_STATE_SERIALIZATION_VERSION);
+
+      // Write only the first 3 fields since the last field will be clobbered
+      // on return to the
+      // server.
+      for (int i = 0; i < 3; i++) {
+        Object entry = jdoDetachedState[i];
+        if (entry == null) {
+          // Null value
+          out.writeByte(JDO_DETACHED_STATE_ENTRY_NULL);
+        } else if (entry instanceof Externalizable) {
+          // Externalizable value
+          out.writeByte(JDO_DETACHED_STATE_ENTRY_EXTERNALIZABLE);
+          out.writeObject(entry.getClass().getCanonicalName());
+          ((Externalizable) entry).writeExternal(out);
+        } else if (entry instanceof Serializable) {
+          // Serializable value
+          out.writeByte(JDO_DETACHED_STATE_ENTRY_SERIALIZABLE);
+          out.writeObject(entry);
+        } else {
+          throw new SerializationException(
+              "Entry "
+              + i
+              + " of jdoDetachedState is neither null, Externalizable nor serializable");
+        }
+      }
+
+      out.close();
+      return baos.toByteArray();
+    } catch (IllegalAccessException e) {
+      throw new SerializationException(e);
+    } catch (IOException e) {
+      throw new SerializationException(
+          "An unexpected IOException occured while serializing jdoDetachedState",
+          e);
+    } catch (NoSuchFieldException e) {
+      throw new SerializationException(e);
+    }
+  }
+
+  /**
+   * Returns true if the instanceClass implements the javax.jdo.spi.Detachable
+   * interface.
+   * 
+   * @param instanceClass the class to be queried.
+   */
+  @Override
+  public boolean shouldSerialize(Class<?> instanceClass) {
+    return JAVAX_JDO_SPI_DETACHABLE_CLASS != null
+        && JAVAX_JDO_SPI_DETACHABLE_CLASS.isAssignableFrom(instanceClass);
+  }
+
+  @Override
+  public boolean shouldSkipField(Field field) {
+    return ("jdoDetachedState".equals(field.getName()))
+        && (JAVAX_JDO_SPI_DETACHABLE_CLASS != null)
+        && (JAVAX_JDO_SPI_DETACHABLE_CLASS.isAssignableFrom(field.getDeclaringClass()));
+  }
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
index 01e2bce..1664bd1 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
@@ -250,6 +250,13 @@
   }
 
   private static boolean fieldQualifiesForSerialization(Field field) {
+    // Check if the field will be handled by a ServerDataSerializer; if so, skip it here.
+    for (ServerDataSerializer serializer : ServerDataSerializer.getSerializers()) {
+      if (serializer.shouldSkipField(field)) {
+        return false;
+      }
+    }
+    
     if (Throwable.class == field.getDeclaringClass()) {
       /**
        * Only serialize Throwable's detailMessage field; all others are ignored.
@@ -311,7 +318,7 @@
 
   private static boolean isNotStaticTransientOrFinal(Field field) {
     /*
-     * Only serialize fields that are not static, transient and final.
+     * Only serialize fields that are not static, transient or final.
      */
     int fieldModifiers = field.getModifiers();
     return !Modifier.isStatic(fieldModifiers)
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerDataSerializer.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerDataSerializer.java
new file mode 100644
index 0000000..607c44d
--- /dev/null
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerDataSerializer.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2009 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.user.server.rpc.impl;
+
+import com.google.gwt.user.client.rpc.SerializationException;
+
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.TreeMap;
+
+/**
+ * An interface for serializing and deserializing portions of Object data that
+ * are present in the server implementation but are not present in client code.
+ * For example, some persistence frameworks make use of server-side bytecode
+ * enhancement; the fields added by such enhancement are unknown to the client,
+ * and therefore are not handled by normal GWT RPC mechanisms.
+ * 
+ * <p>
+ * This portion of the interface is called from the
+ * {@link ServerSerializationStreamReader} and {@link ServerSerializationStreamWriter} classes
+ * as part of the server-side marshalling of data for RPC calls.
+ * 
+ * @see com.google.gwt.user.rebind.rpc.ClientDataSerializer
+ */
+public abstract class ServerDataSerializer implements
+    Comparable<ServerDataSerializer> {
+
+  /**
+   * A mapping from ServerDataSerializer names to instances, sorted by name.
+   */
+  private static TreeMap<String, ServerDataSerializer> serializers =
+    new TreeMap<String, ServerDataSerializer>();
+
+  /**
+   * All active ServerDataSerializers must be initialized here and placed into
+   * the serializers map.
+   * 
+   * <p>
+   * The map must be kept in sync with the one in
+   * {@link com.google.gwt.user.rebind.rpc.ClientDataSerializer}.
+   */
+  static {
+    // Load and register a JdoDetachedStateSerializer
+    ServerDataSerializer serializer = JdoDetachedStateServerDataSerializer.getInstance();
+    serializers.put(serializer.getName(), serializer);
+  }
+
+  /**
+   * Returns a Collection of all ServerDataSerializer instances, ordered by name.
+   * The returned collection is unmodifiable.
+   */
+  public static Collection<ServerDataSerializer> getSerializers() {
+    return Collections.unmodifiableCollection(serializers.values());
+  }
+
+  /**
+   * Allow ServerDataSerialzer instances to be sorted by class name.
+   */
+  public int compareTo(ServerDataSerializer other) {
+    return getName().compareTo(other.getName());
+  }
+
+  /**
+   * Custom deserialize server-only data.
+   * 
+   * @param serializedData the serialized data, as an array of bytes, possible
+   *          null.
+   * @param instance the Object instance to be modified.
+   * @throws SerializationException if the field contents cannot be
+   *           reconstructed.
+   */
+  public abstract void deserializeServerData(byte[] serializedData,
+      Object instance) throws SerializationException;
+
+  /**
+   * Returns the name of this {@link ServerDataSerializer} instance, used to
+   * determine the sorting order when multiple serializers apply to a given
+   * class type.
+   * 
+   * <p>
+   * The name must be identical to that of the corresponding
+   * {@link ClientDataSerializer}.
+   */
+  public abstract String getName();
+
+  /**
+   * Custom serialize the contents of a server-only field.
+   * 
+   * @param instance an Object containing server-only data.
+   * @return a byte array containing a representation of the field.
+   * @throws SerializationException if the instance cannot be serialized by this serializer.
+   */
+  public abstract byte[] serializeServerData(Object instance)
+      throws SerializationException;
+
+  /**
+   * Returns true if the instanceClass should be processed by a ServerDataSerializer.
+   * 
+   * @param instanceClass the class to be queried.
+   */
+  public abstract boolean shouldSerialize(Class<?> instanceClass);
+  
+  /**
+   * Returns true if the given field should be skipped by the normal RPC mechanism.
+   * 
+   * @param field the field to be queried.
+   */
+  public abstract boolean shouldSkipField(Field field);
+}
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
index 7dbef17..dfd8f4a 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
@@ -18,6 +18,7 @@
 import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
 import com.google.gwt.user.client.rpc.SerializationException;
 import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamReader;
+import com.google.gwt.user.server.Base64Utils;
 import com.google.gwt.user.server.rpc.RPC;
 import com.google.gwt.user.server.rpc.SerializationPolicy;
 import com.google.gwt.user.server.rpc.SerializationPolicyProvider;
@@ -602,6 +603,18 @@
       deserializeImpl(SerializabilityUtil.hasCustomFieldSerializer(superClass),
           superClass, instance);
     }
+
+    /*
+     * Iterate through all ServerDataSerializers, in name order, allowing each
+     * to perform custom deserialization.
+     */
+    for (ServerDataSerializer serializer : ServerDataSerializer.getSerializers()) {
+      if (serializer.shouldSerialize(instanceClass)) {
+        String encodedData = readString();
+        byte[] serializedData = Base64Utils.fromBase64(encodedData);
+        serializer.deserializeServerData(serializedData, instance);
+      }
+    }
   }
 
   private Object deserializeImpl(Class<?> customSerializer,
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
index bb9d203..fd15349 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamWriter.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.user.client.rpc.SerializationException;
 import com.google.gwt.user.client.rpc.impl.AbstractSerializationStreamWriter;
+import com.google.gwt.user.server.Base64Utils;
 import com.google.gwt.user.server.rpc.SerializationPolicy;
 
 import java.lang.reflect.Field;
@@ -657,6 +658,18 @@
     if (serializationPolicy.shouldSerializeFields(superClass)) {
       serializeImpl(instance, superClass);
     }
+
+    /*
+     * Iterate through all ServerDataSerializers, in name order, allowing each
+     * to perform custom serialization.
+     */
+    for (ServerDataSerializer serializer : ServerDataSerializer.getSerializers()) {
+      if (serializer.shouldSerialize(instanceClass)) {
+        byte[] serializedData = serializer.serializeServerData(instance);
+        String encodedData = Base64Utils.toBase64(serializedData);
+        writeString(encodedData);
+      }
+    }
   }
 
   private void serializeImpl(Object instance, Class<?> instanceClass)
diff --git a/user/super/com/google/gwt/emul/java/lang/Object.java b/user/super/com/google/gwt/emul/java/lang/Object.java
index e6fce34..cdc598c 100644
--- a/user/super/com/google/gwt/emul/java/lang/Object.java
+++ b/user/super/com/google/gwt/emul/java/lang/Object.java
@@ -15,6 +15,7 @@
  */
 package java.lang;
 
+import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.core.client.impl.Impl;
 
 /**
@@ -25,6 +26,15 @@
 public class Object {
 
   /**
+   * Used by {@link com.google.gwt.core.client.WeakMapping} in web mode
+   * to store an expando containing a String -> Object mapping.
+   * 
+   * @skip
+   */
+  @SuppressWarnings("unused")
+  private transient JavaScriptObject expando;
+
+  /**
    * magic magic magic.
    * 
    * @skip
diff --git a/user/super/com/google/gwt/user/translatable/com/google/gwt/core/client/WeakMapping.java b/user/super/com/google/gwt/user/translatable/com/google/gwt/core/client/WeakMapping.java
new file mode 100644
index 0000000..6359537
--- /dev/null
+++ b/user/super/com/google/gwt/user/translatable/com/google/gwt/core/client/WeakMapping.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2009 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.core.client;
+
+import com.google.gwt.core.client.GwtScriptOnly;
+
+/**
+ * A class associating a (String, Object) map with arbitrary source objects
+ * (except for Strings). This implementation is used in web mode.
+ */
+@GwtScriptOnly
+public class WeakMapping {
+
+  /*
+   * This implementation is used in web mode only. It stores the (key, value)
+   * maps in an expando field on their source objects.
+   */
+  
+  /**
+   * Returns the Object associated with the given key in the (key, value)
+   * mapping associated with the given Object instance.
+   * 
+   * @param instance the source Object.
+   * @param key a String key.
+   * @return an Object associated with that key on the given instance, or null.
+   * @throws IllegalArgumentException if instance is a String.
+   */
+  public static native Object get(Object instance, String key) /*-{
+    return instance.@java.lang.Object::expando[':' + key];
+  }-*/;
+
+  /**
+   * Associates a value with a given key in the (key, value) mapping associated
+   * with the given Object instance. Note that the key space is module-wide, so
+   * some care should be taken to choose sufficiently unique identifiers.
+   * 
+   * @param instance the source Object.
+   * @param key a String key.
+   * @param value the Object to associate with the key on the given source
+   *          Object.
+   * @throws IllegalArgumentException if instance is a String.
+   */
+  public static void set(Object instance, String key, Object value) {
+    if (instance instanceof String) {
+      throw new IllegalArgumentException("Cannot use Strings with WeakMapping");
+    }
+    setNative(instance, key, value);
+  }
+  
+  private static native void setNative(Object instance, String key, Object value) /*-{
+    if (!instance.@java.lang.Object::expando) {
+      instance.@java.lang.Object::expando = {};
+    }
+    instance.@java.lang.Object::expando[':' + key] = value;
+  }-*/;
+}
diff --git a/user/test/com/google/gwt/core/CoreSuite.java b/user/test/com/google/gwt/core/CoreSuite.java
index 18e67ad..0b3e8e3 100644
--- a/user/test/com/google/gwt/core/CoreSuite.java
+++ b/user/test/com/google/gwt/core/CoreSuite.java
@@ -19,6 +19,7 @@
 import com.google.gwt.core.client.HttpThrowableReporterTest;
 import com.google.gwt.core.client.JavaScriptExceptionTest;
 import com.google.gwt.core.client.JsArrayTest;
+import com.google.gwt.core.client.WeakMappingTest;
 import com.google.gwt.core.client.impl.StackTraceCreatorTest;
 import com.google.gwt.junit.tools.GWTTestSuite;
 
@@ -37,6 +38,7 @@
     suite.addTestSuite(JsArrayTest.class);
     suite.addTestSuite(GWTTest.class);
     suite.addTestSuite(StackTraceCreatorTest.class);
+    suite.addTestSuite(WeakMappingTest.class);
     // $JUnit-END$
 
     return suite;
diff --git a/user/test/com/google/gwt/core/client/WeakMappingTest.java b/user/test/com/google/gwt/core/client/WeakMappingTest.java
new file mode 100644
index 0000000..89b1f15
--- /dev/null
+++ b/user/test/com/google/gwt/core/client/WeakMappingTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2009 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.core.client;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Tests for the WeakMapping class.
+ */
+public class WeakMappingTest extends GWTTestCase {
+
+  public WeakMappingTest() {
+  }
+  
+  public void testSetAndGet() {
+    Set<Integer> stronglyReferencedObjects = new HashSet<Integer>();
+    for (int i = 0; i < 1000; i++) {
+      Integer instance = new Integer(i);
+      if ((i % 5) == 0) {
+        stronglyReferencedObjects.add(instance);
+      }
+      WeakMapping.set(instance, "key", new Float(i));
+    }
+    
+    System.gc();
+    
+    for (Integer instance : stronglyReferencedObjects) {
+      Object value = WeakMapping.get(instance, "key");
+      assertNotNull(value);
+      assertTrue(value instanceof Float);
+      assert(((Float) value).floatValue() == instance.intValue());
+    }
+  }
+  
+  public void testNoStringsAllowed() {
+    boolean gotException = false;
+    try {
+      WeakMapping.set("A String", "key", "value");
+    } catch (IllegalArgumentException e) {
+      gotException = true;
+    }
+    
+    assertTrue(gotException);
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.core.Core";
+  }
+}
diff --git a/user/test/com/google/gwt/user/RPCSuite.java b/user/test/com/google/gwt/user/RPCSuite.java
index 4ded336..a1db671 100644
--- a/user/test/com/google/gwt/user/RPCSuite.java
+++ b/user/test/com/google/gwt/user/RPCSuite.java
@@ -36,6 +36,7 @@
 import com.google.gwt.user.client.rpc.ValueTypesTestWithTypeObfuscation;
 import com.google.gwt.user.rebind.rpc.SerializableTypeOracleBuilderTest;
 import com.google.gwt.user.rebind.rpc.TypeHierarchyUtilsTest;
+import com.google.gwt.user.server.Base64Test;
 import com.google.gwt.user.server.rpc.RPCRequestTest;
 import com.google.gwt.user.server.rpc.RPCServletUtilsTest;
 import com.google.gwt.user.server.rpc.RPCTest;
@@ -75,6 +76,7 @@
     suite.addTestSuite(RPCRequestTest.class);
     suite.addTestSuite(FailedRequestTest.class);
     suite.addTestSuite(FailingRequestBuilderTest.class);
+    suite.addTestSuite(Base64Test.class);
 
     // GWTTestCases
     suite.addTestSuite(ValueTypesTest.class);
diff --git a/user/test/com/google/gwt/user/server/Base64Test.java b/user/test/com/google/gwt/user/server/Base64Test.java
new file mode 100644
index 0000000..e0142af
--- /dev/null
+++ b/user/test/com/google/gwt/user/server/Base64Test.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 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.user.server;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests for the {@link com.google.gwt.user.server.Base64Utils Base64Utils} class.
+ */
+public class Base64Test extends TestCase {
+
+  /**
+   * Tests that base 64 encoding/decoding round trips are lossless. 
+   */
+  public static void testBase64Utils() {
+    base64RoundTrip((byte[]) null);
+    base64RoundTrip(new byte[0]);
+
+    java.util.Random r = new java.util.Random(100);
+    for (int i = 0; i < 10000; i++) {
+      base64RoundTrip(r);
+    }
+  }
+
+  private static void base64RoundTrip(java.util.Random r) {
+    int len = r.nextInt(10);
+    byte[] b1 = new byte[len];
+    r.nextBytes(b1);
+
+    base64RoundTrip(b1);
+  }
+
+  private static void base64RoundTrip(byte[] b1) {
+    String s = Base64Utils.toBase64(b1);
+    if (b1 == null) {
+      assert s == null;
+    } else {
+      assert s != null;
+      if (b1.length == 0) {
+        assert s.length() == 0;
+      } else {
+        assert s.length() != 0;
+      }
+    }
+
+    byte[] b2 = Base64Utils.fromBase64(s);
+    if (b1 == null) {
+      assert b2 == null;
+      return;
+    }
+    assert b2 != null;
+    assert (b1.length == b2.length);
+
+    for (int i = 0; i < b1.length; i++) {
+      assert b1[i] == b2[i];
+    }
+  }
+}