| /* |
| * 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.CommandClientSerializationStreamReader.BACKREF_IDENT; |
| |
| 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.CommandSink; |
| 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.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.RpcCommandVisitor; |
| import com.google.gwt.rpc.client.ast.SetCommand; |
| import com.google.gwt.rpc.client.ast.ShortValueCommand; |
| import com.google.gwt.rpc.client.ast.StringValueCommand; |
| import com.google.gwt.rpc.client.ast.ThrowCommand; |
| import com.google.gwt.rpc.client.ast.ValueCommand; |
| import com.google.gwt.rpc.client.impl.CommandClientSerializationStreamReader; |
| import com.google.gwt.rpc.client.impl.EscapeUtil; |
| import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; |
| import com.google.gwt.user.client.rpc.SerializationException; |
| import com.google.gwt.user.client.rpc.SerializationStreamReader; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.lang.reflect.Array; |
| import java.nio.BufferOverflowException; |
| import java.nio.ByteBuffer; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| /** |
| * A CommandSink that will generate a web-mode payload. |
| * |
| * ONE-SHOT EVAL (no incremental evaluation, must call finish()) |
| */ |
| public class WebModePayloadSink extends CommandSink { |
| |
| private class BackRefAssigner extends RpcCommandVisitor { |
| private final Set<ValueCommand> seenOnce = new HashSet<ValueCommand>(); |
| |
| @Override |
| public void endVisit(InvokeCustomFieldSerializerCommand x, Context ctx) { |
| // We always need a backref for custom serializers |
| makeBackRef(x); |
| } |
| |
| @Override |
| public void endVisit(LongValueCommand x, Context ctx) { |
| process(x); |
| } |
| |
| @Override |
| public void endVisit(StringValueCommand x, Context ctx) { |
| process(x); |
| } |
| |
| @Override |
| public boolean visit(ArrayValueCommand x, Context ctx) { |
| return process(x); |
| } |
| |
| @Override |
| public boolean visit(InstantiateCommand x, Context ctx) { |
| return process(x); |
| } |
| |
| private boolean process(ValueCommand x) { |
| if (!seenOnce.add(x)) { |
| makeBackRef(x); |
| return false; |
| } |
| return true; |
| } |
| } |
| private class PayloadVisitor extends RpcCommandVisitor { |
| private final Map<Class<?>, byte[]> constructorFunctions = new IdentityHashMap<Class<?>, byte[]>(); |
| private final Map<RpcCommand, ByteBuffer> commandBuffers = new IdentityHashMap<RpcCommand, ByteBuffer>(); |
| private ByteBuffer currentBuffer; |
| private final Stack<RpcCommand> stack = new Stack<RpcCommand>(); |
| private final Set<RpcCommand> started = new HashSet<RpcCommand>(); |
| |
| @Override |
| public void endVisit(BooleanValueCommand x, Context ctx) { |
| if (x.getValue()) { |
| one(); |
| } else { |
| zero(); |
| } |
| } |
| |
| @Override |
| public void endVisit(ByteValueCommand x, Context ctx) { |
| push(String.valueOf(x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(CharValueCommand x, Context ctx) { |
| push(String.valueOf((int) x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(DoubleValueCommand x, Context ctx) { |
| push(String.valueOf(x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(EnumValueCommand x, Context ctx) { |
| String fieldName = clientOracle.getFieldId(x.getValue()); |
| if (fieldName == null) { |
| throw new IncompatibleRemoteServiceException( |
| "The client cannot accept " + x.getValue().name()); |
| } |
| String clinitName = clientOracle.getMethodId( |
| x.getValue().getDeclaringClass(), "$clinit"); |
| assert clinitName != null; |
| |
| // (clinit(), A) |
| lparen(); |
| push(clinitName); |
| lparen(); |
| rparen(); |
| comma(); |
| push(fieldName); |
| rparen(); |
| } |
| |
| @Override |
| public void endVisit(FloatValueCommand x, Context ctx) { |
| push(String.valueOf((double) x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(IntValueCommand x, Context ctx) { |
| push(String.valueOf(x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(LongValueCommand x, Context ctx) { |
| // TODO (rice): use backwards-compatible wire format? |
| long fieldValue = x.getValue(); |
| |
| /* |
| * Client code represents longs internally as an Object with numeric |
| * properties l, m, and h. In order to make serialization of longs faster, |
| * we'll send the component parts so that the value can be directly |
| * reconstituted on the client. |
| */ |
| int l = (int) (fieldValue & 0x3fffff); |
| int m = (int) ((fieldValue >> 22) & 0x3fffff); |
| int h = (int) ((fieldValue >> 44) & 0xfffff); |
| // CHECKSTYLE_OFF |
| push("{l:" + l + ",m:" + m + ",h:" + h + "}"); |
| // CHECKSTYLE_ON |
| } |
| |
| @Override |
| public void endVisit(NullValueCommand x, Context ctx) { |
| _null(); |
| } |
| |
| @Override |
| public void endVisit(ShortValueCommand x, Context ctx) { |
| push(String.valueOf(x.getValue())); |
| } |
| |
| @Override |
| public void endVisit(StringValueCommand x, Context ctx) { |
| if (hasBackRef(x)) { |
| if (!isStarted(x)) { |
| String escaped = EscapeUtil.escape(x.getValue()); |
| push(begin(x)); |
| eq(); |
| quote(); |
| push(escaped); |
| quote(); |
| commit(x, false); |
| } else { |
| push(makeBackRef(x)); |
| } |
| } else { |
| String escaped = EscapeUtil.escape(x.getValue()); |
| quote(); |
| push(escaped); |
| quote(); |
| } |
| } |
| |
| @Override |
| public boolean visit(ArrayValueCommand x, Context ctx) { |
| boolean hasBackRef = hasBackRef(x); |
| if (hasBackRef && isStarted(x)) { |
| push(makeBackRef(x)); |
| return false; |
| } |
| |
| // constructorFunction(x = [value,value,value]) |
| byte[] currentBackRef = begin(x); |
| push(constructorFunction(x)); |
| lparen(); |
| if (hasBackRef) { |
| push(currentBackRef); |
| eq(); |
| } |
| lbracket(); |
| for (Iterator<ValueCommand> it = x.getComponentValues().iterator(); it.hasNext();) { |
| accept(it.next()); |
| if (it.hasNext()) { |
| comma(); |
| } |
| } |
| rbracket(); |
| rparen(); |
| commit(x, false); |
| if (!hasBackRef) { |
| forget(x); |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean visit(InstantiateCommand x, Context ctx) { |
| boolean hasBackRef = hasBackRef(x); |
| if (hasBackRef && isStarted(x)) { |
| push(makeBackRef(x)); |
| return false; |
| } |
| |
| byte[] currentBackRef = begin(x); |
| byte[] constructorFunction = constructorFunction(x); |
| |
| String getSeedFunc = clientOracle.getMethodId("java.lang.Class", |
| "getSeedFunction", "Ljava/lang/Class;"); |
| String classLitId = clientOracle.getFieldId( |
| "com.google.gwt.lang.ClassLiteralHolder", |
| getJavahSignatureName(x.getTargetClass()) + "_classLit"); |
| assert classLitId != null : "No class literal for " |
| + x.getTargetClass().getName(); |
| |
| /* |
| * If we need to maintain a backreference to the object, it's established |
| * in the first argument instead of using the return value of the |
| * constructorFunction. This is done in case one of the fields should |
| * require a reference to the object that is currently being constructed. |
| */ |
| // constructorFunctionFoo(x = new (classLit.getSeedFunction()), field1, field2) |
| push(constructorFunction); |
| lparen(); |
| if (hasBackRef) { |
| push(currentBackRef); |
| eq(); |
| } |
| _new(); |
| lparen(); |
| push(getSeedFunc); |
| lparen(); |
| push(classLitId); |
| rparen(); |
| rparen(); |
| for (SetCommand setter : x.getSetters()) { |
| comma(); |
| accept(setter.getValue()); |
| } |
| rparen(); |
| |
| commit(x, false); |
| if (!hasBackRef) { |
| forget(x); |
| } |
| return false; |
| } |
| |
| @Override |
| public boolean visit(InvokeCustomFieldSerializerCommand x, Context ctx) { |
| if (isStarted(x)) { |
| push(makeBackRef(x)); |
| return false; |
| } |
| |
| // ( backref = instantiate(), deserialize(), setter, ..., backref ) |
| byte[] currentBackRef = begin(x); |
| |
| lparen(); |
| |
| InstantiateCommand makeReader = new InstantiateCommand( |
| CommandClientSerializationStreamReader.class); |
| /* |
| * Ensure that the reader will stick around for both instantiate and |
| * deserialize calls. |
| */ |
| makeBackRef(makeReader); |
| |
| ArrayValueCommand payload = new ArrayValueCommand(Object.class); |
| for (ValueCommand value : x.getValues()) { |
| payload.add(value); |
| } |
| makeReader.set(CommandClientSerializationStreamReader.class, "payload", |
| payload); |
| |
| String instantiateIdent = clientOracle.getMethodId( |
| x.getSerializerClass(), "instantiate", |
| SerializationStreamReader.class); |
| |
| // x = new Foo, |
| // x = instantiate(reader), |
| push(currentBackRef); |
| eq(); |
| if (instantiateIdent == null) { |
| // No instantiate method, we'll have to invoke the constructor |
| |
| // new Foo() |
| String constructorMethodName; |
| if (x.getTargetClass().getEnclosingClass() == null) { |
| constructorMethodName = x.getTargetClass().getSimpleName(); |
| } else { |
| String name = x.getTargetClass().getName(); |
| constructorMethodName = name.substring(name.lastIndexOf('.') + 1); |
| } |
| |
| String constructorIdent = clientOracle.getMethodId(x.getTargetClass(), |
| constructorMethodName); |
| assert constructorIdent != null : "constructorIdent " |
| + constructorMethodName; |
| |
| // new constructor, |
| _new(); |
| push(constructorIdent); |
| comma(); |
| } else { |
| // instantiate(reader), |
| push(instantiateIdent); |
| lparen(); |
| accept(makeReader); |
| rparen(); |
| comma(); |
| } |
| |
| // Call the deserialize method if it exists |
| String deserializeIdent = clientOracle.getMethodId( |
| x.getSerializerClass(), "deserialize", |
| SerializationStreamReader.class, x.getManuallySerializedType()); |
| if (deserializeIdent != null) { |
| // deserialize(reader, obj), |
| push(deserializeIdent); |
| lparen(); |
| accept(makeReader); |
| comma(); |
| push(currentBackRef); |
| rparen(); |
| comma(); |
| } |
| |
| // If there are extra fields, set them |
| for (SetCommand setter : x.getSetters()) { |
| accept(setter); |
| comma(); |
| } |
| |
| push(currentBackRef); |
| rparen(); |
| commit(x, false); |
| forget(makeReader); |
| |
| return false; |
| } |
| |
| @Override |
| public boolean visit(ReturnCommand x, Context ctx) { |
| int size = x.getValues().size(); |
| |
| begin(x); |
| _return(); |
| |
| // return [a,b,c]; |
| lbracket(); |
| for (int i = 0; i < size; i++) { |
| accept(x.getValues().get(i)); |
| if (i < size - 1) { |
| comma(); |
| } |
| } |
| rbracket(); |
| |
| semi(); |
| commit(x); |
| |
| return false; |
| } |
| |
| @Override |
| public boolean visit(SetCommand x, Context ctx) { |
| String fieldName = clientOracle.getFieldId(x.getFieldDeclClass(), |
| x.getField()); |
| |
| if (fieldName == null) { |
| // TODO: What does it mean if the client doesn't have a field? |
| throw new IncompatibleRemoteServiceException( |
| "The client does not have field " + x.getField() + " in type " |
| + x.getFieldDeclClass().getName()); |
| } |
| |
| // i[3].foo = bar |
| push(makeBackRef((ValueCommand) stack.peek())); |
| dot(); |
| push(fieldName); |
| eq(); |
| accept(x.getValue()); |
| |
| return false; |
| } |
| |
| /** |
| * In order to improve robustness of the payload, we perform the throw from |
| * within a function. |
| */ |
| @Override |
| public boolean visit(ThrowCommand x, Context ctx) { |
| // throw foo; |
| begin(x); |
| _throw(); |
| |
| assert x.getValues().size() == 1; |
| accept(x.getValues()); |
| |
| semi(); |
| commit(x); |
| |
| return false; |
| } |
| |
| // CHECKSTYLE_OFF |
| |
| private void _new() { |
| push(NEW_BYTES); |
| } |
| |
| private void _null() { |
| push(NULL_BYTES); |
| } |
| |
| private void _return() { |
| push(RETURN_BYTES); |
| } |
| |
| private void _throw() { |
| push(THROW_BYTES); |
| } |
| |
| // CHECKSTYLE_ON |
| |
| private void begin(RpcCommand x) { |
| assert !commandBuffers.containsKey(x) : "ValueCommand already active"; |
| |
| started.add(x); |
| stack.push(x); |
| currentBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); |
| commandBuffers.put(x, currentBuffer); |
| } |
| |
| private byte[] begin(ValueCommand x) { |
| begin((RpcCommand) x); |
| return makeBackRef(x); |
| } |
| |
| private void comma() { |
| push(COMMA_BYTES); |
| spaceOpt(); |
| } |
| |
| private void commit(RpcCommand x) { |
| commit(x, true); |
| } |
| |
| private void commit(RpcCommand x, boolean send) { |
| if (stack.pop() != x) { |
| throw new IllegalStateException("Did not pop expected command"); |
| } |
| |
| // Don't need to retain any internal data |
| x.clear(); |
| |
| ByteBuffer sb = commandBuffers.remove(x); |
| assert sb != null : "No ByteBuffer for " + x; |
| |
| if (!stack.isEmpty()) { |
| currentBuffer = commandBuffers.get(stack.peek()); |
| assert currentBuffer != null : "Could not restore currentBuilder"; |
| } else { |
| currentBuffer = null; |
| } |
| |
| sb.limit(sb.position()).rewind(); |
| |
| if (send) { |
| try { |
| send(sb); |
| } catch (SerializationException e) { |
| halt(e); |
| } |
| } else { |
| push(sb); |
| } |
| } |
| |
| private byte[] constructorFunction(ArrayValueCommand x) { |
| Class<?> targetClass = Array.newInstance(x.getComponentType(), 0).getClass(); |
| byte[] functionName = constructorFunctions.get(targetClass); |
| if (functionName != null) { |
| return functionName; |
| } |
| |
| String initValuesId = clientOracle.getMethodId( |
| "com.google.gwt.lang.Array", "initValues", "Ljava/lang/Class;", |
| "Lcom/google/gwt/core/client/JavaScriptObject;", "I", |
| "Lcom/google/gwt/lang/Array;"); |
| assert initValuesId != null : "Could not find initValues"; |
| |
| String classLitId = clientOracle.getFieldId( |
| "com.google.gwt.lang.ClassLiteralHolder", |
| getJavahSignatureName(x.getComponentType()) + "_classLit"); |
| assert classLitId != null : "No class literal for " |
| + x.getComponentType().getName(); |
| |
| functionName = getBytes(clientOracle.createUnusedIdent(classLitId)); |
| constructorFunctions.put(targetClass, functionName); |
| |
| /* |
| * Set the castableTypeData and queryIds to exact values, |
| * or fall back to acting like a plain Object[] array. |
| */ |
| CastableTypeData castableTypeData = clientOracle.getCastableTypeData(targetClass); |
| if (castableTypeData == null) { |
| castableTypeData = clientOracle.getCastableTypeData(Object[].class); |
| } |
| |
| int queryId = clientOracle.getQueryId(x.getComponentType()); |
| if (queryId == 0) { |
| queryId = clientOracle.getQueryId(Object.class); |
| } |
| |
| byte[] ident = getBytes("_0"); |
| |
| // function foo(_0) {return initValues(classLit, castableTypeData, queryId, _0)} |
| function(); |
| push(functionName); |
| lparen(); |
| push(ident); |
| rparen(); |
| lbrace(); |
| _return(); |
| push(initValuesId); |
| lparen(); |
| push(classLitId); |
| comma(); |
| push(castableTypeData.toJs()); |
| comma(); |
| push(String.valueOf(queryId)); |
| comma(); |
| push(ident); |
| rparen(); |
| rbrace(); |
| |
| flush(x); |
| |
| return functionName; |
| } |
| |
| private byte[] constructorFunction(InstantiateCommand x) { |
| Class<?> targetClass = x.getTargetClass(); |
| byte[] functionName = constructorFunctions.get(targetClass); |
| if (functionName != null) { |
| return functionName; |
| } |
| |
| String seedName = clientOracle.getSeedName(targetClass); |
| assert seedName != null : "TypeOverride failed to rescue " |
| + targetClass.getName(); |
| functionName = getBytes(clientOracle.createUnusedIdent(seedName)); |
| constructorFunctions.put(targetClass, functionName); |
| byte[][] idents = new byte[x.getSetters().size() + 1][]; |
| for (int i = 0, j = idents.length; i < j; i++) { |
| idents[i] = getBytes("_" + i); |
| } |
| |
| // function foo(_0, _1, _2) {_0.a = _1; _0.b=_2; return _0} |
| function(); |
| push(functionName); |
| lparen(); |
| for (int i = 0, j = idents.length; i < j; i++) { |
| push(idents[i]); |
| if (i < j - 1) { |
| comma(); |
| } |
| } |
| rparen(); |
| lbrace(); |
| newlineOpt(); |
| for (int i = 1, j = idents.length; i < j; i++) { |
| SetCommand setter = x.getSetters().get(i - 1); |
| String fieldIdent = clientOracle.getFieldId(setter.getFieldDeclClass(), |
| setter.getField()); |
| |
| // _0.foo = bar; |
| spaceOpt(); |
| push(idents[0]); |
| dot(); |
| push(fieldIdent); |
| eq(); |
| push(idents[i]); |
| semi(); |
| } |
| spaceOpt(); |
| _return(); |
| push(idents[0]); |
| rbrace(); |
| newlineOpt(); |
| |
| flush(x); |
| |
| return functionName; |
| } |
| |
| private void dot() { |
| push(DOT_BYTES); |
| } |
| |
| private void eq() { |
| spaceOpt(); |
| push(EQ_BYTES); |
| spaceOpt(); |
| } |
| |
| /** |
| * Cause an immediate write of accumulated output for a command. This is |
| * used primarily for writing object allocations |
| */ |
| private void flush(RpcCommand x) { |
| ByteBuffer sb = commandBuffers.get(x); |
| if (sb == null || sb.position() == 0) { |
| return; |
| } |
| |
| sb.limit(sb.position()).rewind(); |
| try { |
| send(sb); |
| } catch (SerializationException e) { |
| halt(e); |
| } |
| sb.clear(); |
| } |
| |
| private void function() { |
| newlineOpt(); |
| push(FUNCTION_BYTES); |
| } |
| |
| /** |
| * Keep in sync with JReferenceType implementations. |
| */ |
| private String getJavahSignatureName(Class<?> clazz) { |
| if (clazz.isArray()) { |
| Class<?> leafType = clazz; |
| int dims = 0; |
| do { |
| dims++; |
| leafType = leafType.getComponentType(); |
| } while (leafType.getComponentType() != null); |
| assert dims > 0; |
| // leafType cannot be null here |
| |
| String s = getJavahSignatureName(leafType); |
| for (int i = 0; i < dims; ++i) { |
| s = "_3" + s; |
| } |
| return s; |
| } else if (clazz.isPrimitive()) { |
| return WebModeClientOracle.jsniName(clazz); |
| } else { |
| String name = clazz.getName(); |
| return "L" + name.replaceAll("_", "_1").replace('.', '_') + "_2"; |
| } |
| } |
| |
| private boolean isStarted(RpcCommand x) { |
| return started.contains(x); |
| } |
| |
| private void lbrace() { |
| push(LBRACE_BYTES); |
| } |
| |
| private void lbracket() { |
| push(LBRACKET_BYTES); |
| } |
| |
| private void lparen() { |
| push(LPAREN_BYTES); |
| } |
| |
| private void newlineOpt() { |
| pushOpt(NEWLINE_BYTES); |
| } |
| |
| private void one() { |
| push(ONE_BYTES); |
| } |
| |
| /** |
| * Add data to the current command's serialization output. |
| */ |
| private void push(byte[] bytes) { |
| assert currentBuffer != null : "Must call begin(RpcCommand) first"; |
| try { |
| currentBuffer.put(bytes); |
| } catch (BufferOverflowException e) { |
| reallocateCurrentBuffer(bytes.length); |
| currentBuffer.put(bytes); |
| } |
| } |
| |
| /** |
| * Add data to the current command's serialization output. |
| */ |
| private void push(ByteBuffer buffer) { |
| assert currentBuffer != null : "Must call begin(RpcCommand) first"; |
| try { |
| currentBuffer.put(buffer); |
| } catch (BufferOverflowException e) { |
| reallocateCurrentBuffer(buffer.remaining()); |
| currentBuffer.put(buffer); |
| } |
| } |
| |
| /** |
| * Add data to the current command's serialization output. |
| */ |
| private void push(String s) { |
| push(getBytes(s)); |
| } |
| |
| /** |
| * Optionally add data to the current command's serialization output. |
| */ |
| private void pushOpt(byte[] x) { |
| if (PRETTY) { |
| push(x); |
| } |
| } |
| |
| private void quote() { |
| push(QUOTE_BYTES); |
| } |
| |
| private void rbrace() { |
| push(RBRACE_BYTES); |
| } |
| |
| private void rbracket() { |
| push(RBRACKET_BYTES); |
| } |
| |
| private void reallocateCurrentBuffer(int bytesNeeded) { |
| // Allocate a new buffer of sufficient size |
| int newSize = currentBuffer.capacity() |
| + Math.max(2 * bytesNeeded, currentBuffer.capacity()); |
| ByteBuffer newBuffer = ByteBuffer.allocate(newSize); |
| |
| // Copy the old buffer over |
| currentBuffer.limit(currentBuffer.position()).rewind(); |
| newBuffer.put(currentBuffer); |
| |
| // Reassign the current buffer |
| assert commandBuffers.get(stack.peek()) == currentBuffer; |
| commandBuffers.put(stack.peek(), newBuffer); |
| currentBuffer = newBuffer; |
| } |
| |
| private void rparen() { |
| push(RPAREN_BYTES); |
| } |
| |
| private void semi() { |
| push(SEMI_BYTES); |
| newlineOpt(); |
| } |
| |
| private void spaceOpt() { |
| pushOpt(SPACE_BYTES); |
| } |
| |
| private void zero() { |
| push(ZERO_BYTES); |
| } |
| } |
| |
| /* |
| * Instead of converting these commonly-used strings to bytes every time we |
| * want to write them to the output, we'll simply create a fixed pool. |
| */ |
| static final byte[] COMMA_BYTES = getBytes(","); |
| static final byte[] DOT_BYTES = getBytes("."); |
| static final byte[] EQ_BYTES = getBytes("="); |
| static final byte[] FUNCTION_BYTES = getBytes("function "); |
| static final byte[] LBRACE_BYTES = getBytes("{"); |
| static final byte[] LBRACKET_BYTES = getBytes("["); |
| static final byte[] LPAREN_BYTES = getBytes("("); |
| static final byte[] NEW_BYTES = getBytes("new "); |
| static final byte[] NEWLINE_BYTES = getBytes("\n"); |
| static final byte[] NULL_BYTES = getBytes("null"); |
| static final byte[] ONE_BYTES = getBytes("1"); |
| static final byte[] QUOTE_BYTES = getBytes("\""); |
| static final byte[] RBRACE_BYTES = getBytes("}"); |
| static final byte[] RBRACKET_BYTES = getBytes("]"); |
| static final byte[] RETURN_BYTES = getBytes("return "); |
| static final byte[] RPAREN_BYTES = getBytes(")"); |
| static final byte[] SPACE_BYTES = getBytes(" "); |
| static final byte[] SEMI_BYTES = getBytes(";"); |
| static final byte[] THROW_BYTES = getBytes("throw "); |
| static final byte[] ZERO_BYTES = getBytes("0"); |
| |
| /** |
| * A runtime flag to indicate that the generated output should be made to be |
| * human-readable. |
| */ |
| static final boolean PRETTY = Boolean.getBoolean("gwt.rpc.pretty"); |
| |
| private static final int DEFAULT_BUFFER_SIZE = 256; |
| |
| static byte[] getBytes(String x) { |
| try { |
| return x.getBytes("UTF-8"); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("UTF-8 is unsupported", e); |
| } |
| } |
| |
| private final ClientOracle clientOracle; |
| private boolean finished = false; |
| private final OutputStream out; |
| private final Map<ValueCommand, byte[]> valueBackRefs = new HashMap<ValueCommand, byte[]>(); |
| private final PayloadVisitor visitor = new PayloadVisitor(); |
| |
| private Stack<byte[]> freeBackRefs = new Stack<byte[]>(); |
| |
| public WebModePayloadSink(ClientOracle clientOracle, OutputStream out) { |
| this.clientOracle = clientOracle; |
| this.out = out; |
| } |
| |
| @Override |
| public void accept(RpcCommand command) throws SerializationException { |
| if (finished) { |
| throw new IllegalStateException("finish() has already been called"); |
| } |
| |
| new BackRefAssigner().accept(command); |
| |
| if (command instanceof ValueCommand) { |
| makeBackRef((ValueCommand) command); |
| } |
| visitor.accept(command); |
| } |
| |
| /** |
| * The caller must close the stream. |
| */ |
| @Override |
| public void finish() throws SerializationException { |
| finished = true; |
| } |
| |
| void forget(ValueCommand x) { |
| assert valueBackRefs.containsKey(x); |
| freeBackRefs.push(valueBackRefs.remove(x)); |
| } |
| |
| boolean hasBackRef(ValueCommand x) { |
| return valueBackRefs.containsKey(x); |
| } |
| |
| byte[] makeBackRef(ValueCommand x) { |
| byte[] toReturn = valueBackRefs.get(x); |
| if (toReturn == null) { |
| if (freeBackRefs.isEmpty()) { |
| int idx = valueBackRefs.size(); |
| toReturn = getBytes(BACKREF_IDENT + "._" |
| + Integer.toString(idx, Character.MAX_RADIX)); |
| } else { |
| toReturn = freeBackRefs.pop(); |
| } |
| valueBackRefs.put(x, toReturn); |
| } |
| return toReturn; |
| } |
| |
| void send(ByteBuffer x) throws SerializationException { |
| try { |
| assert x.hasArray(); |
| out.write(x.array(), x.position(), x.limit()); |
| } catch (IOException e) { |
| throw new SerializationException("Could not send data", e); |
| } |
| } |
| |
| void send(String x) throws SerializationException { |
| try { |
| out.write(getBytes(x)); |
| } catch (IOException e) { |
| throw new SerializationException("Could not send data", e); |
| } |
| } |
| } |