/* * 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("rawtypes") 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 Production 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 Development 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(); } }