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];
+ }
+ }
+}