com.googlecode.gwt.test.internal.SerializableModifier Maven / Gradle / Ivy
package com.googlecode.gwt.test.internal;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.googlecode.gwt.test.exceptions.GwtTestRpcException;
import com.googlecode.gwt.test.utils.GwtReflectionUtils;
import javassist.*;
import javassist.bytecode.Descriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
*
* A {@link JavaClassModifier} which enable the simulation of GWT-RPC serialization. For
* internal use only.
*
*
*
* GWT-RPC serialization means translation from JavaScript to Java and to Java back to JavaScript.
* It's not just like Java Serialization : transient fields are not set to null after the GWT-RPC
* serialization, they are set to theirs initial value, after the default constructor has been
* called.
*
*
*
* To simulate this behaviour, a lot of work is done behind the scene by this class. While it is
* loading a class, the {@link GwtClassLoader} knows if the class is attempt to be compiled in
* JavaScript. If it is and if it can be serialized (by implementing {@link IsSerializable} or
* {@link Serializable} interface), it does a lot of bytecode manipulation (through the internal
* {@link SerializableModifier} class) :
*
* - it replaces the potential {@link IsSerializable} reference to {@link Serializable} to make
* the class Java-serializable.
* - if the class does not provide a default constructor (which is required by GWT-RPC
* serializer), it means the class will never be RPC-Serialized. The class is not modified.
* - if the class extends {@link Externalizable} instead of {@link Serializable} : it means the
* class will never be RPC-Serialized. The class is not modified.
* - Otherwise, it copies the default constructor instructions in a new member method.
* - then, it override the private Object.readObject(ObjectInputStream), a callback which is
* called during a Java-Serialization : it call the default serialization method and then the new
* method, so transient field are set with theirs initial value.
*
* After those bytecode modification on a class, its Java-serialization will have the same behavior
* as the GWT-RPC one.
*
*
*
* The java serialization is then done by a set of optimized object from the internal API.
* (com.googlecode.gwt.test.rpc.DeepCopy).
*
*
* @author Gael Lazzari
*/
// TODO: make package-private
public class SerializableModifier implements JavaClassModifier {
private static final String DEFAULT_CONS_METHOD_NAME = "DEFAULT_CONS_METHOD";
private static final Logger LOGGER = LoggerFactory.getLogger(SerializableModifier.class);
public static void readObject(Serializable ex, ObjectInputStream ois) {
try {
// call the default read method
ois.defaultReadObject();
// keep non transient/static/final value somhere
Map buffer = getFieldValues(ex);
// call the exported default constructor to reinitialise triansient
// field
// values
// which are not expected to be null
GwtReflectionUtils.callPrivateMethod(ex, DEFAULT_CONS_METHOD_NAME);
// set the kept field values
for (Map.Entry entry : buffer.entrySet()) {
entry.getKey().set(ex, entry.getValue());
}
} catch (Exception e) {
throw new GwtTestRpcException("Error during deserialization of object "
+ ex.getClass().getName(), e);
}
}
private static Map getFieldValues(Serializable o)
throws IllegalArgumentException, IllegalAccessException {
Map result = new HashMap();
for (Field field : GwtReflectionUtils.getFields(o.getClass())) {
int fieldModifier = field.getModifiers();
if (!Modifier.isStatic(fieldModifier) && !Modifier.isTransient(fieldModifier)) {
result.put(field, field.get(o));
}
}
return result;
}
private final CtClass charSequenceCtClass;
private final CtClass externalizableCtClass;
private final CtClass isSerializableCtClass;
private final CtClass serializableCtClass;
public SerializableModifier() {
serializableCtClass = GwtClassPool.getCtClass(Serializable.class);
isSerializableCtClass = GwtClassPool.getCtClass(IsSerializable.class);
externalizableCtClass = GwtClassPool.getCtClass(Externalizable.class);
charSequenceCtClass = GwtClassPool.getCtClass(CharSequence.class);
}
public void modify(CtClass classToModify) throws Exception {
if (classToModify.isInterface() || classToModify.isPrimitive() || classToModify.isEnum()
|| classToModify.isArray() || classToModify.isAnnotation()
|| Modifier.isAbstract(classToModify.getModifiers())) {
return;
}
if (classToModify.subtypeOf(charSequenceCtClass)) {
return;
}
// Externalizable object which is not serialized by GWT RPC
if (classToModify.subtypeOf(externalizableCtClass)) {
return;
}
// substitute isSerializable with Serializable
if (classToModify.subtypeOf(isSerializableCtClass)
&& !classToModify.subtypeOf(serializableCtClass)) {
CtClass[] interfaces = classToModify.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
if (isSerializableCtClass.equals(interfaces[i])) {
interfaces[i] = serializableCtClass;
}
}
classToModify.setInterfaces(interfaces);
}
if (!classToModify.subtypeOf(serializableCtClass)) {
return;
}
if (getReadObjectMethod(classToModify) != null) {
// this class should never be serialized by GWT RPC
return;
}
// GWT RPC Serializable objects should have an empty constructor
CtConstructor defaultCons = getDefaultConstructor(classToModify);
if (defaultCons == null) {
return;
}
LOGGER.debug("Apply serializable bytecode modifier");
CtMethod defaultConstMethod = defaultCons.toMethod(DEFAULT_CONS_METHOD_NAME, classToModify);
defaultConstMethod.setModifiers(Modifier.PUBLIC);
if (hasDefaultConstMethod(classToModify.getSuperclass())) {
defaultConstMethod.insertBefore("super." + DEFAULT_CONS_METHOD_NAME + "();");
}
classToModify.addMethod(defaultConstMethod);
overrideReadObject(classToModify);
}
private String callStaticReadObject() {
StringBuilder buffer = new StringBuilder();
buffer.append("{");
buffer.append(this.getClass().getName() + ".readObject(");
buffer.append("(").append(Serializable.class.getName()).append(")");
buffer.append(" this, $1);}");
return buffer.toString();
}
private CtConstructor getDefaultConstructor(CtClass ctClass) {
try {
return ctClass.getConstructor(Descriptor.ofConstructor(new CtClass[0]));
} catch (NotFoundException e) {
return null;
}
}
private CtMethod getReadObjectMethod(CtClass ctClass) {
CtClass[] paramTypes = new CtClass[]{GwtClassPool.getCtClass(ObjectInputStream.class)};
try {
return ctClass.getDeclaredMethod("readObject", paramTypes);
} catch (NotFoundException e) {
return null;
}
}
private boolean hasDefaultConstMethod(CtClass ctClass) {
try {
CtMethod m = ctClass.getMethod(DEFAULT_CONS_METHOD_NAME,
Descriptor.ofMethod(CtClass.voidType, new CtClass[0]));
return m != null;
} catch (NotFoundException e) {
return false;
}
}
private void overrideReadObject(CtClass classToModify) throws NotFoundException,
CannotCompileException {
CtClass[] paramTypes = new CtClass[]{GwtClassPool.getCtClass(ObjectInputStream.class)};
CtMethod readObjectMethod = new CtMethod(CtClass.voidType, "readObject", paramTypes,
classToModify);
readObjectMethod.setModifiers(Modifier.PRIVATE);
// add exception types
CtClass classNotFoundException = GwtClassPool.getCtClass(ClassNotFoundException.class);
CtClass ioException = GwtClassPool.getCtClass(IOException.class);
readObjectMethod.setExceptionTypes(new CtClass[]{classNotFoundException, ioException});
// set body (call static readObject(Serializable, ObjectInputStream)
readObjectMethod.setBody(callStaticReadObject());
classToModify.addMethod(readObjectMethod);
}
}