blob: d91c0e3c54a71f36452ac5022a1eae301af9e2f0 [file] [log] [blame]
/*
* 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.rpc.server;
import com.google.gwt.rpc.client.ast.CommandSink;
import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* Encapsulates data about the structure of the client code.
*/
public final class WebModeClientOracle extends ClientOracle implements
Serializable {
/*
* TODO: Don't use Java serialization.
*/
/**
* A Builder object to create ClientOracles.
*/
public static class Builder {
private WebModeClientOracle oracle = new WebModeClientOracle();
public void add(String jsIdent, String jsniIdent, String className,
String memberName, int queryId, CastableTypeData castableTypeData) {
oracle.idents.add(jsIdent);
ClassData data = oracle.getClassData(className);
/*
* Don't overwrite castableTypeData and queryId if already set.
* There are many versions of symbols for a given className,
* corresponding to the type of member fields, etc.,
* which don't have the queryId or castableTypeData initialized. Only
* the symbol data for the class itself has this info.
*/
if (data.castableTypeData == null) {
data.queryId = queryId;
data.castableTypeData = castableTypeData;
}
if (jsniIdent == null || jsniIdent.length() == 0) {
data.typeName = className;
data.seedName = jsIdent;
oracle.seedNamesToClassData.put(jsIdent, data);
} else {
if (jsniIdent.contains("(")) {
jsniIdent = jsniIdent.substring(jsniIdent.indexOf("::") + 2,
jsniIdent.indexOf(')') + 1);
data.methodJsniNamesToIdents.put(jsniIdent, jsIdent);
} else {
data.fieldIdentsToNames.put(jsIdent, memberName);
data.fieldNamesToIdents.put(memberName, jsIdent);
}
}
}
public WebModeClientOracle getOracle() {
WebModeClientOracle toReturn = oracle;
oracle = null;
return toReturn;
}
public void setSerializableFields(String className, List<String> fieldNames) {
ClassData data = oracle.getClassData(className);
assert data.serializableFields == null
|| fieldNames.containsAll(data.serializableFields);
if (fieldNames.size() == 1) {
data.serializableFields = Collections.singletonList(fieldNames.get(0));
} else {
data.serializableFields = new ArrayList<String>(fieldNames);
Collections.sort(data.serializableFields);
}
}
}
/**
* A pair with extra data.
*/
public static class Triple<A, B, C> extends Pair<A, B> {
private final C[] c;
public Triple(A a, B b, C... c) {
super(a, b);
this.c = c;
}
public C[] getC() {
return c;
}
}
private static class ClassData implements Serializable {
private static final long serialVersionUID = 5L;
public CastableTypeData castableTypeData;
public final Map<String, String> fieldIdentsToNames = new HashMap<String, String>();
public final Map<String, String> fieldNamesToIdents = new HashMap<String, String>();
public final Map<String, String> methodJsniNamesToIdents = new HashMap<String, String>();
public int queryId;
public String seedName;
public List<String> serializableFields = Collections.emptyList();
public String typeName;
}
/**
* Defined to prevent simple changes from invalidating stored data.
*
* TODO: Use something other than Java serialization to store this type's
* data.
*/
private static final long serialVersionUID = 1L;
/**
* Recreate a WebModeClientOracle based on the contents previously emitted by
* {@link #store}. The underlying format should be considered opaque.
*/
public static WebModeClientOracle load(InputStream stream) throws IOException {
try {
stream = new GZIPInputStream(stream);
return readStreamAsObject(stream, WebModeClientOracle.class);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Should never reach this", e);
}
}
static String jsniName(Class<?> clazz) {
if (clazz.isPrimitive()) {
if (clazz.equals(boolean.class)) {
return "Z";
} else if (clazz.equals(byte.class)) {
return "B";
} else if (clazz.equals(char.class)) {
return "C";
} else if (clazz.equals(short.class)) {
return "S";
} else if (clazz.equals(int.class)) {
return "I";
} else if (clazz.equals(long.class)) {
return "J";
} else if (clazz.equals(float.class)) {
return "F";
} else if (clazz.equals(double.class)) {
return "D";
}
throw new RuntimeException("Unhandled primitive type " + clazz.getName());
} else if (clazz.isArray()) {
return "[" + jsniName(clazz.getComponentType());
} else {
return "L" + clazz.getName().replace('.', '/') + ";";
}
}
/**
* Copied from dev.Utility class which is not part of servlet.jar.
*/
private static <T> T readStreamAsObject(InputStream inputStream, Class<T> type)
throws ClassNotFoundException {
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(inputStream);
return type.cast(objectInputStream.readObject());
} catch (IOException e) {
return null;
} finally {
try {
objectInputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
/**
* Serializes an object and writes it to a stream. Copied from Util to avoid
* dependecy on gwt-dev.
*/
private static void writeObjectToStream(OutputStream stream,
Object... objects) throws IOException {
ObjectOutputStream objectStream = new ObjectOutputStream(stream);
for (Object object : objects) {
objectStream.writeObject(object);
}
objectStream.flush();
}
/**
* A map of class names to ClassData elements.
*/
private final Map<String, ClassData> classData = new HashMap<String, ClassData>();
private final Set<String> idents = new HashSet<String>();
private final Map<String, ClassData> seedNamesToClassData = new HashMap<String, ClassData>();
private transient Map<Class<?>, Field[]> operableFieldMap = new IdentityHashMap<Class<?>, Field[]>();
/**
* Instances of WebModeClientOracle are created either through the
* {@link Builder} class or via the {@link #load} method.
*/
protected WebModeClientOracle() {
}
@Override
public CommandSink createCommandSink(OutputStream out) throws IOException {
return new WebModePayloadSink(this, out);
}
@Override
public String createUnusedIdent(String ident) {
while (idents.contains(ident)) {
ident += "$";
}
return ident;
}
@Override
public CastableTypeData getCastableTypeData(Class<?> clazz) {
while (clazz != null) {
CastableTypeData toReturn = getCastableTypeData(canonicalName(clazz));
if (toReturn != null) {
return toReturn;
}
clazz = clazz.getSuperclass();
}
return null;
}
@Override
public String getFieldId(Class<?> clazz, String fieldName) {
while (clazz != null) {
String className = clazz.getName();
ClassData data = getClassData(className);
if (data.fieldNamesToIdents.containsKey(fieldName)) {
return data.fieldNamesToIdents.get(fieldName);
}
clazz = clazz.getSuperclass();
}
return null;
}
@Override
public String getFieldId(Enum<?> value) {
return getFieldId(value.getDeclaringClass(), value.name());
}
@Override
public String getFieldId(String className, String fieldName) {
ClassData data = getClassData(className);
return data.fieldNamesToIdents.get(fieldName);
}
@Override
public Pair<Class<?>, String> getFieldName(Class<?> clazz, String fieldId) {
while (clazz != null) {
ClassData data = getClassData(clazz.getName());
String fieldName = data.fieldIdentsToNames.get(fieldId);
if (fieldName == null) {
clazz = clazz.getSuperclass();
} else {
return new Pair<Class<?>, String>(clazz, fieldName);
}
}
return null;
}
/**
* This will search superclasses.
*/
@Override
public String getMethodId(Class<?> clazz, String methodName, Class<?>... args) {
while (clazz != null) {
String toReturn = getMethodId(clazz.getName(), methodName, args);
if (toReturn != null) {
return toReturn;
}
clazz = clazz.getSuperclass();
}
return null;
}
@Override
public String getMethodId(String className, String methodName,
String... jsniArgTypes) {
StringBuilder sb = new StringBuilder();
sb.append(methodName);
sb.append("(");
for (String jsniArg : jsniArgTypes) {
sb.append(jsniArg);
}
sb.append(")");
ClassData data = getClassData(className);
String jsIdent = data.methodJsniNamesToIdents.get(sb.toString());
return jsIdent;
}
@Override
public Field[] getOperableFields(Class<?> clazz) {
Field[] toReturn;
synchronized (operableFieldMap) {
toReturn = operableFieldMap.get(clazz);
}
if (toReturn != null) {
return toReturn;
}
ClassData data = getClassData(clazz.getName());
toReturn = new Field[data.serializableFields.size()];
for (int i = 0; i < toReturn.length; i++) {
String fieldName = data.serializableFields.get(i);
try {
toReturn[i] = clazz.getDeclaredField(fieldName);
} catch (SecurityException e) {
throw new IncompatibleRemoteServiceException("Cannot access field "
+ fieldName, e);
} catch (NoSuchFieldException e) {
throw new IncompatibleRemoteServiceException("No field " + fieldName, e);
}
}
synchronized (operableFieldMap) {
operableFieldMap.put(clazz, toReturn);
}
return toReturn;
}
@Override
public int getQueryId(Class<?> clazz) {
while (clazz != null) {
int toReturn = getQueryId(canonicalName(clazz));
if (toReturn != 0) {
return toReturn;
}
clazz = clazz.getSuperclass();
}
return 0;
}
@Override
public String getSeedName(Class<?> clazz) {
ClassData data = getClassData(clazz.getName());
return data.seedName;
}
@Override
public String getTypeName(String seedName) {
// TODO: Decide how to handle the no-metadata case
if (seedName.startsWith("Class$")) {
seedName = seedName.substring(6);
}
ClassData data = seedNamesToClassData.get(seedName);
return data == null ? null : data.typeName;
}
@Override
public boolean isScript() {
return true;
}
/**
* Write the state of the WebModeClientOracle into an OutputStream. The
* underlying format should be considered opaque.
*/
public void store(OutputStream stream) throws IOException {
stream = new GZIPOutputStream(stream);
writeObjectToStream(stream, this);
stream.close();
}
private String canonicalName(Class<?> clazz) {
if (clazz.isArray()) {
Class<?> leafType = clazz;
do {
leafType = leafType.getComponentType();
} while (leafType.isArray());
Class<?> enclosing = leafType.getEnclosingClass();
if (enclosing != null) {
// com.foo.Enclosing$Name[]
return canonicalName(enclosing) + "$" + clazz.getSimpleName();
} else if (leafType.getPackage() == null) {
// Name0[
return clazz.getSimpleName();
} else {
// com.foo.Name[]
return leafType.getPackage().getName() + "." + clazz.getSimpleName();
}
} else {
return clazz.getName();
}
}
private CastableTypeData getCastableTypeData(String className) {
ClassData data = getClassData(className);
return data.castableTypeData;
}
private ClassData getClassData(String className) {
ClassData toReturn = classData.get(className);
if (toReturn == null) {
toReturn = new ClassData();
classData.put(className, toReturn);
}
return toReturn;
}
/**
* This will not search superclasses and is used to access magic GWT types
* like Array.
*/
private String getMethodId(String className, String methodName,
Class<?>... args) {
String[] jsniArgTypes = new String[args.length];
for (int i = 0, j = args.length; i < j; i++) {
jsniArgTypes[i] = jsniName(args[i]);
}
return getMethodId(className, methodName, jsniArgTypes);
}
private int getQueryId(String className) {
ClassData data = getClassData(className);
return data.queryId;
}
/**
* Reinitialize the <code>operableFieldMap</code> field when the
* WebModeClientOracle is reloaded.
*/
private Object readResolve() {
operableFieldMap = new HashMap<Class<?>, Field[]>();
return this;
}
}