blob: dc83ff673db1e28bc536657ddacea531e1cd41b9 [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 static com.google.gwt.rpc.client.impl.SimplePayloadSink.ARRAY_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.BACKREF_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.BOOLEAN_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.BYTE_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.CHAR_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.DOUBLE_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.ENUM_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.FLOAT_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.INT_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.INVOKE_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.LONG_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.NL_CHAR;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.OBJECT_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.RETURN_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.RPC_SEPARATOR_CHAR;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.SHORT_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.STRING_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.THROW_TYPE;
import static com.google.gwt.rpc.client.impl.SimplePayloadSink.VOID_TYPE;
import com.google.gwt.rpc.client.ast.ArrayValueCommand;
import com.google.gwt.rpc.client.ast.BooleanValueCommand;
import com.google.gwt.rpc.client.ast.ByteValueCommand;
import com.google.gwt.rpc.client.ast.CharValueCommand;
import com.google.gwt.rpc.client.ast.DoubleValueCommand;
import com.google.gwt.rpc.client.ast.EnumValueCommand;
import com.google.gwt.rpc.client.ast.FloatValueCommand;
import com.google.gwt.rpc.client.ast.HasSetters;
import com.google.gwt.rpc.client.ast.IdentityValueCommand;
import com.google.gwt.rpc.client.ast.InstantiateCommand;
import com.google.gwt.rpc.client.ast.IntValueCommand;
import com.google.gwt.rpc.client.ast.InvokeCustomFieldSerializerCommand;
import com.google.gwt.rpc.client.ast.LongValueCommand;
import com.google.gwt.rpc.client.ast.NullValueCommand;
import com.google.gwt.rpc.client.ast.ReturnCommand;
import com.google.gwt.rpc.client.ast.RpcCommand;
import com.google.gwt.rpc.client.ast.ScalarValueCommand;
import com.google.gwt.rpc.client.ast.ShortValueCommand;
import com.google.gwt.rpc.client.ast.StringValueCommand;
import com.google.gwt.rpc.client.ast.ValueCommand;
import com.google.gwt.user.server.rpc.impl.SerializabilityUtil;
import java.lang.reflect.Array;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
/**
* Decodes the simple payload.
*/
public class SimplePayloadDecoder {
private static final String OBFUSCATED_CLASS_PREFIX = "Class$ ";
private static final Map<String, Class<?>> PRIMITIVE_TYPES = new HashMap<String, Class<?>>();
static {
// Obfuscated when class metadata is disabled
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + BOOLEAN_TYPE, boolean.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + BYTE_TYPE, byte.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + CHAR_TYPE, char.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + DOUBLE_TYPE, double.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + FLOAT_TYPE, float.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + INT_TYPE, int.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + LONG_TYPE, long.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + SHORT_TYPE, short.class);
PRIMITIVE_TYPES.put(OBFUSCATED_CLASS_PREFIX + VOID_TYPE, void.class);
// Regular
PRIMITIVE_TYPES.put(boolean.class.getName(), boolean.class);
PRIMITIVE_TYPES.put(byte.class.getName(), byte.class);
PRIMITIVE_TYPES.put(char.class.getName(), char.class);
PRIMITIVE_TYPES.put(double.class.getName(), double.class);
PRIMITIVE_TYPES.put(float.class.getName(), float.class);
PRIMITIVE_TYPES.put(int.class.getName(), int.class);
PRIMITIVE_TYPES.put(long.class.getName(), long.class);
PRIMITIVE_TYPES.put(short.class.getName(), short.class);
PRIMITIVE_TYPES.put(void.class.getName(), void.class);
}
private final Map<Integer, ValueCommand> backRefs = new HashMap<Integer, ValueCommand>();
private final Map<String, Class<?>> classCache = new HashMap<String, Class<?>>(
PRIMITIVE_TYPES);
private final ClientOracle clientOracle;
private final Stack<RpcCommand> commands = new Stack<RpcCommand>();
private int idx;
private final CharSequence payload;
private ReturnCommand toReturn;
private ValueCommand toThrow;
/**
* Construct a new SimplePayloadDecoder. This will consume the entire payload
* which will be made available through {@link #getValues}. If the payload
* stream contains an embedded exception, processing will end early and the
* Throwable will be available via {@link #getThrownValue()}.
*
* @throws ClassNotFoundException
*/
public SimplePayloadDecoder(ClientOracle clientOracle, CharSequence payload)
throws ClassNotFoundException {
this.clientOracle = clientOracle;
this.payload = payload;
while (toReturn == null && idx < payload.length()) {
decodeCommand();
// We hit an error in the stream; stop now
if (toThrow != null) {
return;
}
}
}
/**
* Returns the thrown value, if any.
*/
public ValueCommand getThrownValue() {
return toThrow;
}
/**
* Returns the values encoded in the payload.
*/
public List<ValueCommand> getValues() {
return toReturn == null ? Collections.<ValueCommand> emptyList()
: toReturn.getValues();
}
private void decodeCommand() throws ClassNotFoundException {
char command = next();
if (command == NL_CHAR) {
// Pretty mode payload
command = next();
}
String token = token();
switch (command) {
case BOOLEAN_TYPE: {
push(new BooleanValueCommand(token.equals("1")));
break;
}
case BYTE_TYPE: {
push(new ByteValueCommand(Byte.valueOf(token)));
break;
}
case CHAR_TYPE: {
push(new CharValueCommand(Character.valueOf((char) Integer.valueOf(
token).intValue())));
break;
}
case DOUBLE_TYPE: {
push(new DoubleValueCommand(Double.valueOf(token)));
break;
}
case FLOAT_TYPE: {
push(new FloatValueCommand(Float.valueOf(token)));
break;
}
case INT_TYPE: {
push(new IntValueCommand(Integer.valueOf(token)));
break;
}
case LONG_TYPE: {
push(new LongValueCommand(Long.valueOf(token)));
break;
}
case VOID_TYPE: {
push(NullValueCommand.INSTANCE);
break;
}
case SHORT_TYPE: {
push(new ShortValueCommand(Short.valueOf(token)));
break;
}
case STRING_TYPE: {
// "4~abcd
int length = Integer.valueOf(token);
String value = next(length);
if (next() != RPC_SEPARATOR_CHAR) {
throw new RuntimeException("Overran string");
}
push(new StringValueCommand(value));
break;
}
case ENUM_TYPE: {
// ETypeSeedName~IOrdinal~
EnumValueCommand x = new EnumValueCommand();
push(x);
// use ordinal (and not name), since name might have been obfuscated
int ordinal = readCommand(IntValueCommand.class).getValue();
@SuppressWarnings("unchecked")
Class<? extends Enum> clazz = findClass(token).asSubclass(Enum.class);
/*
* TODO: Note this approach could be prone to subtle corruption or
* an ArrayOutOfBoundsException if the client and server have drifted.
*/
Enum<?> enumConstants[] = clazz.getEnumConstants();
x.setValue(enumConstants[ordinal]);
break;
}
case ARRAY_TYPE: {
// Encoded as (leafType, dimensions, length, .... )
Class<?> leaf = findClass(token);
Integer numDims = readCommand(IntValueCommand.class).getValue();
Class<?> clazz;
if (numDims > 1) {
int[] dims = new int[numDims - 1];
clazz = Array.newInstance(leaf, dims).getClass();
} else {
clazz = leaf;
}
ArrayValueCommand x = new ArrayValueCommand(clazz);
push(x);
int length = readCommand(IntValueCommand.class).getValue();
for (int i = 0; i < length; i++) {
x.add(readCommand(ValueCommand.class));
}
break;
}
case OBJECT_TYPE: {
// @TypeSeedName~3~... N-many setters ...
Class<?> clazz = findClass(token);
InstantiateCommand x = new InstantiateCommand(clazz);
push(x);
readSetters(clazz, x);
break;
}
case INVOKE_TYPE: {
// !TypeSeedName~Number of objects written by CFS~...CFS objects...~
// Number of extra fields~...N-many setters...
Class<?> clazz = findClass(token);
Class<?> serializerClass = null;
// The custom serializer type might be for a supertype
Class<?> manualType = clazz;
while (manualType != null) {
serializerClass = SerializabilityUtil.hasCustomFieldSerializer(manualType);
if (serializerClass != null) {
break;
}
manualType = manualType.getSuperclass();
}
InvokeCustomFieldSerializerCommand x = new InvokeCustomFieldSerializerCommand(
clazz, serializerClass, manualType);
push(x);
readFields(x);
readSetters(clazz, x);
break;
}
case RETURN_TYPE: {
// R4~...values...
toReturn = new ReturnCommand();
int toRead = Integer.valueOf(token);
for (int i = 0; i < toRead; i++) {
toReturn.addValue(readCommand(ValueCommand.class));
}
break;
}
case THROW_TYPE: {
// T...value...
toThrow = readCommand(ValueCommand.class);
break;
}
case BACKREF_TYPE: {
// @backrefNumber~
ValueCommand x = backRefs.get(Integer.valueOf(token));
assert x != null : "Could not find backref";
commands.push(x);
break;
}
case RPC_SEPARATOR_CHAR: {
/*
* Not strictly necessary, but it makes an off-by-one easier to
* distinguish.
*/
throw new RuntimeException("Segmentation overrun at " + idx);
}
default:
throw new RuntimeException("Unknown command " + command);
}
}
/**
* Uses the ClientOracle to decode a type name.
*/
private Class<?> findClass(String token) throws ClassNotFoundException {
/*
* NB: This is the only method in SimplePayloadDecoder which would require
* any real adaptation to be made to run in web mode.
*/
Class<?> clazz = classCache.get(token);
if (clazz != null) {
return clazz;
}
String className = clientOracle.getTypeName(token);
if (className == null) {
// Probably a regular class name
className = token;
}
if (className.contains("[]")) {
// Array types are annoying to construct
int firstIndex = -1;
int j = -1;
int dims = 0;
while ((j = className.indexOf("[", j + 1)) != -1) {
if (dims++ == 0) {
firstIndex = j;
}
}
Class<?> componentType = findClass(className.substring(0, firstIndex));
assert componentType != null : "Could not determine component type with "
+ className.substring(0, firstIndex);
clazz = Array.newInstance(componentType, new int[dims]).getClass();
} else {
// Ensure that we use the bridge classloader in CCL
ClassLoader myCCL = getClass().getClassLoader();
clazz = Class.forName(className, false, myCCL);
}
classCache.put(token, clazz);
return clazz;
}
/**
* Reads the next character in the input, possibly evaluating escape
* sequences.
*/
private char next() {
char c = payload.charAt(idx++);
if (c == '\\') {
switch (payload.charAt(idx++)) {
case '0':
c = '\0';
break;
case '!':
// Compatibility since we're using the legacy escaping code
c = '|';
break;
case 'b':
c = '\b';
break;
case 't':
c = '\t';
break;
case 'n':
c = '\n';
break;
case 'f':
c = '\f';
break;
case 'r':
c = '\r';
break;
case '\\':
c = '\\';
break;
case '"':
c = '"';
break;
case 'u':
c = (char) Integer.parseInt(
payload.subSequence(idx, idx += 4).toString(), 16);
break;
case 'x':
c = (char) Integer.parseInt(
payload.subSequence(idx, idx += 2).toString(), 16);
break;
default:
throw new RuntimeException("Unhandled escape " + payload.charAt(idx));
}
}
return c;
}
/**
* Reads <code>count</code> many characters and returns them as a string.
*/
private String next(int count) {
StringBuilder sb = new StringBuilder();
while (count-- > 0) {
sb.append(next());
}
return sb.toString();
}
/**
* Retains the object value and establishes a backreference.
*/
private void push(IdentityValueCommand x) {
commands.push(x);
backRefs.put(backRefs.size(), x);
}
/**
* Retains the scalar value, but does not establish a backreference.
*/
private void push(ScalarValueCommand x) {
commands.push(x);
}
/**
* Retains the string value and establishes a backreference.
*/
private void push(StringValueCommand x) {
commands.push(x);
backRefs.put(backRefs.size(), x);
}
/**
* Read one command from the stream.
*
* @param <T> the expected type of RpcCommand to read
* @param clazz the expected type of RpcCommand to read
* @throws ClassCastException if a command was successfully read, but could
* not be assigned to <code>clazz</code>
*/
private <T extends RpcCommand> T readCommand(Class<T> clazz)
throws ClassNotFoundException {
decodeCommand();
RpcCommand value = commands.pop();
assert clazz.isInstance(value) : "Cannot assign a "
+ value.getClass().getName() + " to " + clazz.getName();
return clazz.cast(value);
}
/**
* Format is (int, value...).
*/
private void readFields(InvokeCustomFieldSerializerCommand x)
throws ClassNotFoundException {
int length = readCommand(IntValueCommand.class).getValue();
for (int i = 0; i < length; i++) {
x.addValue(readCommand(ValueCommand.class));
}
}
/**
* Format is (fieldDeclClassName, fieldId, value). fieldDeclClassName may be
* null.
*/
private void readSetter(Class<?> clazz, HasSetters x)
throws ClassNotFoundException {
// Only used by hosted mode to handle shadowing
if (!clientOracle.isScript()) {
String fieldDeclClassName = readCommand(StringValueCommand.class).getValue();
if (fieldDeclClassName != null) {
clazz = findClass(fieldDeclClassName);
}
}
String fieldId = readCommand(StringValueCommand.class).getValue();
Pair<Class<?>, String> data = clientOracle.getFieldName(clazz, fieldId);
Class<?> fieldDeclClass = data.getA();
String fieldName = data.getB();
ValueCommand value = readCommand(ValueCommand.class);
x.set(fieldDeclClass, fieldName, value);
}
/**
* Format is (int, setter...).
*/
private void readSetters(Class<?> clazz, HasSetters x)
throws ClassNotFoundException {
int length = readCommand(IntValueCommand.class).getValue();
for (int i = 0; i < length; i++) {
readSetter(clazz, x);
}
}
/**
* Read through the next separator character.
*/
private String token() {
StringBuilder sb = new StringBuilder();
char n = next();
while (n != RPC_SEPARATOR_CHAR) {
sb.append(n);
n = next();
}
return sb.toString();
}
}