| /* |
| * Copyright 2008 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.dev.shell; |
| |
| import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue; |
| import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.SpecialDispatchId; |
| import com.google.gwt.dev.shell.BrowserChannel.Value.ValueType; |
| import com.google.gwt.util.tools.Utility; |
| |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.Socket; |
| import java.util.Set; |
| |
| /** |
| * A base class for communication between Dev-mode client and server. |
| */ |
| public abstract class BrowserChannel { |
| |
| /** |
| * An error indicating that the remote side died and we should unroll the |
| * call stack as painlessly as possible to allow cleanup. |
| */ |
| public static class RemoteDeathError extends Error { |
| |
| public RemoteDeathError(Throwable cause) { |
| super("Remote connection lost", cause); |
| } |
| } |
| |
| /** |
| * Class representing a reference to a Java object. |
| */ |
| public static class JavaObjectRef implements RemoteObjectRef { |
| private int refId; |
| |
| public JavaObjectRef(int refId) { |
| this.refId = refId; |
| } |
| |
| @Override |
| public int getRefid() { |
| return Math.abs(refId); |
| } |
| |
| @Override |
| public int hashCode() { |
| return refId; |
| } |
| |
| public boolean isException() { |
| return refId < 0; |
| } |
| |
| @Override |
| public String toString() { |
| return "JavaObjectRef(ref=" + refId + ")"; |
| } |
| } |
| |
| /** |
| * Class representing a reference to a JS object. |
| */ |
| public static class JsObjectRef implements RemoteObjectRef { |
| |
| private int refId; |
| |
| public JsObjectRef(int refId) { |
| this.refId = refId; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| return (o instanceof JsObjectRef) && ((JsObjectRef) o).refId == refId; |
| } |
| |
| @Override |
| public int getRefid() { |
| // exceptions are negative, so we get the absolute value |
| return Math.abs(refId); |
| } |
| |
| @Override |
| public int hashCode() { |
| return refId; |
| } |
| |
| public boolean isException() { |
| return refId < 0; |
| } |
| |
| @Override |
| public String toString() { |
| return "JsObjectRef(" + refId + ")"; |
| } |
| } |
| |
| /** |
| * Enumeration of message type ids. |
| * |
| * <p>Ids are used instead of relying on the ordinal to avoid sychronization |
| * problems with the client. |
| */ |
| public enum MessageType { |
| /** |
| * A message to invoke a method on the other side of the wire. Note that |
| * the messages are asymmetric -- see {@link InvokeOnClientMessage} and |
| * {@link InvokeOnServerMessage}. |
| */ |
| INVOKE(0), |
| |
| /** |
| * Returns the result of an INVOKE, INVOKE_SPECIAL, or LOAD_MODULE message. |
| */ |
| RETURN(1), |
| |
| /** |
| * v1 LOAD_MODULE message. |
| */ |
| OLD_LOAD_MODULE(2), |
| |
| /** |
| * Normal closure of the connection. |
| */ |
| QUIT(3), |
| |
| /** |
| * A request by the server to load JSNI source into the client's JS engine. |
| */ |
| LOAD_JSNI(4), |
| |
| INVOKE_SPECIAL(5), |
| |
| FREE_VALUE(6), |
| |
| /** |
| * Abnormal termination of the connection. |
| */ |
| FATAL_ERROR(7), |
| |
| CHECK_VERSIONS(8), |
| |
| PROTOCOL_VERSION(9), |
| |
| CHOOSE_TRANSPORT(10), |
| |
| SWITCH_TRANSPORT(11), |
| |
| LOAD_MODULE(12), |
| |
| REQUEST_ICON(13), |
| |
| USER_AGENT_ICON(14), |
| |
| REQUEST_PLUGIN(15); |
| |
| private final int id; |
| |
| private MessageType(int id) { |
| this.id = id; |
| } |
| |
| public int getId() { |
| return id; |
| } |
| } |
| |
| /** |
| * Represents an object on the other side of the channel, known to this side |
| * by an reference ID. |
| */ |
| public interface RemoteObjectRef { |
| |
| /** |
| * @return the reference ID for this object. |
| */ |
| int getRefid(); |
| } |
| |
| /** |
| * Hook interface for responding to messages. |
| */ |
| public abstract static class SessionHandler<T extends BrowserChannel> { |
| |
| /** |
| * Wrapper to return both a return value/exception and a flag as to whether |
| * an exception was thrown or not. |
| */ |
| public static class ExceptionOrReturnValue { |
| private final boolean isException; |
| private final Value returnValue; |
| |
| public ExceptionOrReturnValue(boolean isException, Value returnValue) { |
| this.isException = isException; |
| this.returnValue = returnValue; |
| } |
| |
| public Value getReturnValue() { |
| return returnValue; |
| } |
| |
| public boolean isException() { |
| return isException; |
| } |
| } |
| |
| /** |
| * Enumeration of dispatch IDs on object 0 (the ServerMethods object). |
| * |
| * <p>Ids are set specifically rather than relying on the ordinal to avoid |
| * synchronization problems with the client. |
| * |
| * TODO: hasMethod/hasProperty no longer used, remove them! |
| */ |
| public enum SpecialDispatchId { |
| HasMethod(0), HasProperty(1), GetProperty(2), SetProperty(3); |
| |
| private final int id; |
| |
| private SpecialDispatchId(int id) { |
| this.id = id; |
| } |
| |
| public int getId() { |
| return id; |
| } |
| } |
| |
| public abstract void freeValue(T channel, int[] ids); |
| } |
| |
| /** |
| * Represents a value for BrowserChannel. |
| */ |
| public static class Value { |
| /** |
| * Enum of type tags sent across the wire. |
| */ |
| public enum ValueType { |
| /** |
| * Primitive values. |
| */ |
| NULL(0), BOOLEAN(1), BYTE(2), CHAR(3), SHORT(4), INT(5), LONG_UNUSED(6), |
| FLOAT_UNUSED(7), DOUBLE(8), STRING(9), |
| |
| /** |
| * Representations of Java or JS objects, sent as an index into a table |
| * kept on the side holding the actual object. |
| */ |
| JAVA_OBJECT(10), JS_OBJECT(11), |
| |
| /** |
| * A Javascript undef value, also used for void returns. |
| */ |
| UNDEFINED(12); |
| |
| private final int id; |
| |
| private ValueType(int id) { |
| this.id = id; |
| } |
| |
| byte getTag() { |
| return (byte) id; |
| } |
| } |
| |
| /** |
| * Type tag value. |
| */ |
| private ValueType type = ValueType.UNDEFINED; |
| |
| /** |
| * Represents a value sent/received across the wire. |
| */ |
| private Object value = null; |
| |
| public Value() { |
| } |
| |
| public Value(Object obj) { |
| convertFromJavaValue(obj); |
| } |
| |
| /** |
| * Convert a Java object to a value. Objects must be primitive wrappers, |
| * Strings, or JsObjectRef/JavaObjectRef instances. |
| * |
| * @param obj value to convert. |
| */ |
| public void convertFromJavaValue(Object obj) { |
| if (obj == null) { |
| type = ValueType.NULL; |
| } else if (obj instanceof Boolean) { |
| type = ValueType.BOOLEAN; |
| } else if (obj instanceof Byte) { |
| type = ValueType.BYTE; |
| } else if (obj instanceof Character) { |
| type = ValueType.CHAR; |
| } else if (obj instanceof Double) { |
| type = ValueType.DOUBLE; |
| } else if (obj instanceof Integer) { |
| type = ValueType.INT; |
| } else if (obj instanceof Short) { |
| type = ValueType.SHORT; |
| } else if (obj instanceof String) { |
| type = ValueType.STRING; |
| } else if (obj instanceof JsObjectRef) { |
| // TODO: exception handling? |
| type = ValueType.JS_OBJECT; |
| } else if (obj instanceof JavaObjectRef) { |
| // TODO: exception handling? |
| type = ValueType.JAVA_OBJECT; |
| } else { |
| throw new IllegalArgumentException("Unexpected type: " + obj.getClass()); |
| } |
| value = obj; |
| } |
| |
| public boolean getBoolean() { |
| assert type == ValueType.BOOLEAN; |
| return ((Boolean) value).booleanValue(); |
| } |
| |
| public byte getByte() { |
| assert type == ValueType.BYTE; |
| return ((Byte) value).byteValue(); |
| } |
| |
| public char getChar() { |
| assert type == ValueType.CHAR; |
| return ((Character) value).charValue(); |
| } |
| |
| public double getDouble() { |
| assert type == ValueType.DOUBLE; |
| return ((Double) value).doubleValue(); |
| } |
| |
| public int getInt() { |
| assert type == ValueType.INT; |
| return ((Integer) value).intValue(); |
| } |
| |
| public JavaObjectRef getJavaObject() { |
| assert type == ValueType.JAVA_OBJECT; |
| return (JavaObjectRef) value; |
| } |
| |
| public JsObjectRef getJsObject() { |
| assert type == ValueType.JS_OBJECT; |
| return (JsObjectRef) value; |
| } |
| |
| public short getShort() { |
| assert type == ValueType.SHORT; |
| return ((Short) value).shortValue(); |
| } |
| |
| public String getString() { |
| assert type == ValueType.STRING; |
| return (String) value; |
| } |
| |
| public ValueType getType() { |
| return type; |
| } |
| |
| public Object getValue() { |
| return value; |
| } |
| |
| public boolean isBoolean() { |
| return type == ValueType.BOOLEAN; |
| } |
| |
| public boolean isByte() { |
| return type == ValueType.BYTE; |
| } |
| |
| public boolean isChar() { |
| return type == ValueType.CHAR; |
| } |
| |
| public boolean isDouble() { |
| return type == ValueType.DOUBLE; |
| } |
| |
| public boolean isInt() { |
| return type == ValueType.INT; |
| } |
| |
| public boolean isJavaObject() { |
| return type == ValueType.JAVA_OBJECT; |
| } |
| |
| public boolean isJsObject() { |
| return type == ValueType.JS_OBJECT; |
| } |
| |
| public boolean isNull() { |
| return type == ValueType.NULL; |
| } |
| |
| public boolean isNumber() { |
| switch (type) { |
| case BYTE: |
| case CHAR: |
| case DOUBLE: |
| case INT: |
| case SHORT: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| public boolean isPrimitive() { |
| switch (type) { |
| case BOOLEAN: |
| case BYTE: |
| case CHAR: |
| case DOUBLE: |
| case INT: |
| case SHORT: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| public boolean isShort() { |
| return type == ValueType.SHORT; |
| } |
| |
| public boolean isString() { |
| return type == ValueType.STRING; |
| } |
| |
| public boolean isUndefined() { |
| return type == ValueType.UNDEFINED; |
| } |
| |
| public void setBoolean(boolean val) { |
| type = ValueType.BOOLEAN; |
| value = Boolean.valueOf(val); |
| } |
| |
| public void setByte(byte val) { |
| type = ValueType.BYTE; |
| value = Byte.valueOf(val); |
| } |
| |
| public void setChar(char val) { |
| type = ValueType.CHAR; |
| value = Character.valueOf(val); |
| } |
| |
| public void setDouble(double val) { |
| type = ValueType.DOUBLE; |
| value = Double.valueOf(val); |
| } |
| |
| public void setInt(int val) { |
| type = ValueType.INT; |
| value = Integer.valueOf(val); |
| } |
| |
| public void setJavaObject(JavaObjectRef val) { |
| type = ValueType.JAVA_OBJECT; |
| value = val; |
| } |
| |
| public void setJsObject(JsObjectRef val) { |
| type = ValueType.JS_OBJECT; |
| value = val; |
| } |
| |
| public void setLong(long val) { |
| type = ValueType.BOOLEAN; |
| value = Long.valueOf(val); |
| } |
| |
| public void setNull() { |
| type = ValueType.NULL; |
| value = null; |
| } |
| |
| public void setShort(short val) { |
| type = ValueType.SHORT; |
| value = Short.valueOf(val); |
| } |
| |
| public void setString(String val) { |
| type = ValueType.STRING; |
| value = val; |
| } |
| |
| public void setUndefined() { |
| type = ValueType.UNDEFINED; |
| value = null; |
| } |
| |
| @Override |
| public String toString() { |
| return type + ": " + value; |
| } |
| } |
| |
| /** |
| * The initial request from the client, supplies a range of supported versions |
| * and the version from hosted.html (so stale copies on an external server |
| * can be detected). |
| */ |
| protected static class CheckVersionsMessage extends Message { |
| |
| public static CheckVersionsMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int minVersion = stream.readInt(); |
| int maxVersion = stream.readInt(); |
| String hostedHtmlVersion = readUtf8String(stream); |
| return new CheckVersionsMessage(channel, minVersion, maxVersion, |
| hostedHtmlVersion); |
| } |
| |
| private final String hostedHtmlVersion; |
| |
| private final int maxVersion; |
| |
| private final int minVersion; |
| |
| public CheckVersionsMessage(BrowserChannel channel, int minVersion, |
| int maxVersion, String hostedHtmlVersion) { |
| super(channel); |
| this.minVersion = minVersion; |
| this.maxVersion = maxVersion; |
| this.hostedHtmlVersion = hostedHtmlVersion; |
| } |
| |
| public String getHostedHtmlVersion() { |
| return hostedHtmlVersion; |
| } |
| |
| public int getMaxVersion() { |
| return maxVersion; |
| } |
| |
| public int getMinVersion() { |
| return minVersion; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.CHECK_VERSIONS.getId()); |
| stream.writeInt(minVersion); |
| stream.writeInt(maxVersion); |
| writeUtf8String(stream, hostedHtmlVersion); |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A message from the client giving a list of supported connection methods |
| * and requesting the server choose one of them to switch protocol traffic to. |
| */ |
| protected static class ChooseTransportMessage extends Message { |
| |
| public static ChooseTransportMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int n = stream.readInt(); |
| String[] transports = new String[n]; |
| for (int i = 0; i < n; ++i) { |
| transports[i] = readUtf8String(stream); |
| } |
| return new ChooseTransportMessage(channel, transports); |
| } |
| |
| private final String[] transports; |
| |
| public ChooseTransportMessage(BrowserChannel channel, |
| String[] transports) { |
| super(channel); |
| this.transports = transports; |
| } |
| |
| public String[] getTransports() { |
| return transports; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.CHOOSE_TRANSPORT.getId()); |
| stream.writeInt(transports.length); |
| for (String transport : transports) { |
| writeUtf8String(stream, transport); |
| } |
| } |
| } |
| |
| /** |
| * A message reporting a connection error to the client. |
| */ |
| protected static class FatalErrorMessage extends Message { |
| |
| public static FatalErrorMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| // NOTE: Tag has already been read. |
| String error = readUtf8String(stream); |
| return new FatalErrorMessage(channel, error); |
| } |
| |
| private final String error; |
| |
| public FatalErrorMessage(BrowserChannel channel, String error) { |
| super(channel); |
| this.error = error; |
| } |
| |
| public String getError() { |
| return error; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.FATAL_ERROR.getId()); |
| writeUtf8String(stream, error); |
| } |
| } |
| |
| /** |
| * A message asking the other side to free object references. Note that there |
| * is no response to this message, and this must only be sent immediately |
| * before an Invoke or Return message. |
| */ |
| protected static class FreeMessage extends Message { |
| public static FreeMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int numIds = stream.readInt(); |
| // TODO: sanity check id count |
| int ids[] = new int[numIds]; |
| for (int i = 0; i < numIds; ++i) { |
| ids[i] = stream.readInt(); |
| } |
| return new FreeMessage(channel, ids); |
| } |
| |
| public static void send(BrowserChannel channel, int[] ids) |
| throws IOException { |
| DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.writeByte(MessageType.FREE_VALUE.getId()); |
| stream.writeInt(ids.length); |
| for (int id : ids) { |
| stream.writeInt(id); |
| } |
| stream.flush(); |
| } |
| |
| private final int ids[]; |
| |
| public FreeMessage(BrowserChannel channel, int[] ids) { |
| super(channel); |
| this.ids = ids; |
| } |
| |
| public int[] getIds() { |
| return ids; |
| } |
| |
| @Override |
| public boolean isAsynchronous() { |
| return true; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel(), ids); |
| } |
| } |
| |
| /** |
| * A request from the server to invoke a function on the client. |
| * |
| * Note that MessageType.INVOKE can refer to either this class |
| * or {@link InvokeOnServerMessage} depending on the direction, as the |
| * protocol is asymmetric (Java needs a dispatch ID, Javascript needs a |
| * name). |
| */ |
| protected static class InvokeOnClientMessage extends Message { |
| public static InvokeOnClientMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| // NOTE: Tag has already been read. |
| String methodName = readUtf8String(stream); |
| Value thisRef = channel.readValue(stream); |
| int argLen = stream.readInt(); |
| Value[] args = new Value[argLen]; |
| for (int i = 0; i < argLen; i++) { |
| args[i] = channel.readValue(stream); |
| } |
| return new InvokeOnClientMessage(channel, methodName, thisRef, args); |
| } |
| |
| private final Value[] args; |
| private final String methodName; |
| private final Value thisRef; |
| |
| public InvokeOnClientMessage(BrowserChannel channel, String methodName, |
| Value thisRef, Value[] args) { |
| super(channel); |
| this.thisRef = thisRef; |
| this.methodName = methodName; |
| this.args = args; |
| } |
| |
| public Value[] getArgs() { |
| return args; |
| } |
| |
| public String getMethodName() { |
| return methodName; |
| } |
| |
| public Value getThis() { |
| return thisRef; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| |
| stream.writeByte(MessageType.INVOKE.getId()); |
| writeUtf8String(stream, methodName); |
| getBrowserChannel().writeValue(stream, thisRef); |
| stream.writeInt(args.length); |
| for (int i = 0; i < args.length; i++) { |
| getBrowserChannel().writeValue(stream, args[i]); |
| } |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A request from the client to invoke a function on the server. |
| * |
| * Note that MessageType.INVOKE can refer to either this class |
| * or {@link InvokeOnClientMessage} depending on the direction, as the |
| * protocol is asymmetric (Java needs a dispatch ID, Javascript needs a |
| * name). |
| */ |
| protected static class InvokeOnServerMessage extends Message { |
| public static InvokeOnServerMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| // NOTE: Tag has already been read. |
| int methodDispatchId = stream.readInt(); |
| Value thisRef = channel.readValue(stream); |
| int argLen = stream.readInt(); |
| Value[] args = new Value[argLen]; |
| for (int i = 0; i < argLen; i++) { |
| args[i] = channel.readValue(stream); |
| } |
| return new InvokeOnServerMessage(channel, methodDispatchId, thisRef, |
| args); |
| } |
| |
| private final Value[] args; |
| private final int methodDispatchId; |
| private final Value thisRef; |
| |
| public InvokeOnServerMessage(BrowserChannel channel, int methodDispatchId, |
| Value thisRef, Value[] args) { |
| super(channel); |
| this.thisRef = thisRef; |
| this.methodDispatchId = methodDispatchId; |
| this.args = args; |
| } |
| |
| public Value[] getArgs() { |
| return args; |
| } |
| |
| public int getMethodDispatchId() { |
| return methodDispatchId; |
| } |
| |
| public Value getThis() { |
| return thisRef; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| |
| stream.writeByte(MessageType.INVOKE.getId()); |
| stream.writeInt(methodDispatchId); |
| getBrowserChannel().writeValue(stream, thisRef); |
| stream.writeInt(args.length); |
| for (int i = 0; i < args.length; i++) { |
| getBrowserChannel().writeValue(stream, args[i]); |
| } |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A request from the to invoke a function on the other side. |
| */ |
| protected static class InvokeSpecialMessage extends Message { |
| public static InvokeSpecialMessage receive(BrowserChannel channel) |
| throws IOException, BrowserChannelException { |
| final DataInputStream stream = channel.getStreamFromOtherSide(); |
| // NOTE: Tag has already been read. |
| final int specialMethodInt = stream.readByte(); |
| SpecialDispatchId[] ids = SpecialDispatchId.values(); |
| if (specialMethodInt < 0 || specialMethodInt >= ids.length) { |
| throw new BrowserChannelException("Invalid dispatch id " |
| + specialMethodInt); |
| } |
| final SpecialDispatchId dispatchId = ids[specialMethodInt]; |
| final int argLen = stream.readInt(); |
| final Value[] args = new Value[argLen]; |
| for (int i = 0; i < argLen; i++) { |
| args[i] = channel.readValue(stream); |
| } |
| return new InvokeSpecialMessage(channel, dispatchId, args); |
| } |
| |
| private final Value[] args; |
| private final SpecialDispatchId dispatchId; |
| |
| public InvokeSpecialMessage(BrowserChannel channel, |
| SpecialDispatchId dispatchId, Value[] args) { |
| super(channel); |
| this.dispatchId = dispatchId; |
| this.args = args; |
| } |
| |
| public Value[] getArgs() { |
| return args; |
| } |
| |
| public SpecialDispatchId getDispatchId() { |
| return dispatchId; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| |
| stream.writeByte(MessageType.INVOKE_SPECIAL.getId()); |
| stream.writeByte(dispatchId.getId()); |
| stream.writeInt(args.length); |
| for (int i = 0; i < args.length; i++) { |
| getBrowserChannel().writeValue(stream, args[i]); |
| } |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A message sending JSNI code to be evaluated. Note that there is no response |
| * to this message, and this must only be sent immediately before an Invoke or |
| * Return message. |
| */ |
| protected static class LoadJsniMessage extends Message { |
| public static LoadJsniMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| String js = readUtf8String(stream); |
| return new LoadJsniMessage(channel, js); |
| } |
| |
| public static void send(BrowserChannel channel, String js) |
| throws IOException { |
| DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.write(MessageType.LOAD_JSNI.getId()); |
| writeUtf8String(stream, js); |
| stream.flush(); |
| } |
| |
| private final String js; |
| |
| public LoadJsniMessage(BrowserChannel channel, String js) { |
| super(channel); |
| this.js = js; |
| } |
| |
| public String getJsni() { |
| return js; |
| } |
| |
| @Override |
| public boolean isAsynchronous() { |
| return true; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel(), js); |
| } |
| } |
| |
| /** |
| * A request from the client that the server load and initialize a given |
| * module. |
| */ |
| protected static class LoadModuleMessage extends Message { |
| public static LoadModuleMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| String url = readUtf8String(stream); |
| String tabKey = readUtf8String(stream); |
| String sessionKey = readUtf8String(stream); |
| String moduleName = readUtf8String(stream); |
| String userAgent = readUtf8String(stream); |
| return new LoadModuleMessage(channel, url, tabKey, sessionKey, moduleName, |
| userAgent); |
| } |
| |
| private final String moduleName; |
| |
| private final String sessionKey; |
| |
| private final String tabKey; |
| |
| private final String url; |
| |
| private final String userAgent; |
| |
| /** |
| * Creates a LoadModule message to be sent to the server. |
| * |
| * @param channel BrowserChannel instance |
| * @param url URL of main top-level window - may not be null |
| * @param tabKey opaque key identifying the tab in the browser, or an |
| * empty string if it cannot be determined - may not be null |
| * @param sessionKey opaque key identifying a particular session (ie, |
| * group of modules) - may not be null |
| * @param moduleName name of GWT module to load - may not be null |
| * @param userAgent user agent identifier of the browser - may not be null |
| */ |
| public LoadModuleMessage(BrowserChannel channel, String url, |
| String tabKey, String sessionKey, String moduleName, String userAgent) { |
| super(channel); |
| assert url != null; |
| assert tabKey != null; |
| assert sessionKey != null; |
| assert moduleName != null; |
| assert userAgent != null; |
| this.url = url; |
| this.tabKey = tabKey; |
| this.sessionKey = sessionKey; |
| this.moduleName = moduleName; |
| this.userAgent = userAgent; |
| } |
| |
| public String getModuleName() { |
| return moduleName; |
| } |
| |
| public String getSessionKey() { |
| return sessionKey; |
| } |
| |
| public String getTabKey() { |
| return tabKey; |
| } |
| |
| public String getUrl() { |
| return url; |
| } |
| |
| public String getUserAgent() { |
| return userAgent; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.LOAD_MODULE.getId()); |
| writeUtf8String(stream, url); |
| writeUtf8String(stream, tabKey); |
| writeUtf8String(stream, sessionKey); |
| writeUtf8String(stream, moduleName); |
| writeUtf8String(stream, userAgent); |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * Abstract base class of OOPHM messages. |
| */ |
| protected abstract static class Message { |
| public static MessageType readMessageType(DataInputStream stream) |
| throws IOException, BrowserChannelException { |
| stream.mark(1); |
| int type = stream.readByte(); |
| MessageType[] types = MessageType.values(); |
| if (type < 0 || type >= types.length) { |
| stream.reset(); |
| throw new BrowserChannelException("Invalid message type " + type); |
| } |
| return types[type]; |
| } |
| |
| private final BrowserChannel channel; |
| |
| public Message(BrowserChannel channel) { |
| this.channel = channel; |
| } |
| |
| public final BrowserChannel getBrowserChannel() { |
| return channel; |
| } |
| |
| /** |
| * @return true if this message type is asynchronous and does not expect a |
| * return message. |
| */ |
| public boolean isAsynchronous() { |
| return false; |
| } |
| |
| /** |
| * @throws IOException if a subclass encounters an I/O error |
| */ |
| public void send() throws IOException { |
| throw new UnsupportedOperationException(getClass().getName() |
| + " is a message format that can only be received."); |
| } |
| } |
| |
| /** |
| * Provides a way of allocating JS and Java object ids without knowing |
| * which one is the remote type, so code can be shared between client and |
| * server. |
| */ |
| protected interface ObjectRefFactory { |
| |
| JavaObjectRef getJavaObjectRef(int refId); |
| |
| JsObjectRef getJsObjectRef(int refId); |
| |
| Set<Integer> getRefIdsForCleanup(); |
| } |
| |
| /** |
| * A request from the client that the server load and initialize a given |
| * module (original v1 version). |
| */ |
| protected static class OldLoadModuleMessage extends Message { |
| public static OldLoadModuleMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int protoVersion = stream.readInt(); |
| String moduleName = readUtf8String(stream); |
| String userAgent = readUtf8String(stream); |
| return new OldLoadModuleMessage(channel, protoVersion, moduleName, |
| userAgent); |
| } |
| |
| private final String moduleName; |
| |
| private final int protoVersion; |
| |
| private final String userAgent; |
| |
| public OldLoadModuleMessage(BrowserChannel channel, int protoVersion, |
| String moduleName, String userAgent) { |
| super(channel); |
| this.protoVersion = protoVersion; |
| this.moduleName = moduleName; |
| this.userAgent = userAgent; |
| } |
| |
| public String getModuleName() { |
| return moduleName; |
| } |
| |
| public int getProtoVersion() { |
| return protoVersion; |
| } |
| |
| public String getUserAgent() { |
| return userAgent; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.OLD_LOAD_MODULE.getId()); |
| stream.writeInt(protoVersion); |
| writeUtf8String(stream, moduleName); |
| writeUtf8String(stream, userAgent); |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * Reports the selected protocol version. |
| */ |
| protected static class ProtocolVersionMessage extends Message { |
| |
| public static ProtocolVersionMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int protocolVersion = stream.readInt(); |
| return new ProtocolVersionMessage(channel, protocolVersion); |
| } |
| |
| private final int protocolVersion; |
| |
| public ProtocolVersionMessage(BrowserChannel channel, int protocolVersion) { |
| super(channel); |
| this.protocolVersion = protocolVersion; |
| } |
| |
| public int getProtocolVersion() { |
| return protocolVersion; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.PROTOCOL_VERSION.getId()); |
| stream.writeInt(protocolVersion); |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A message signifying a soft close of the communications channel. |
| */ |
| protected static class QuitMessage extends Message { |
| public static QuitMessage receive(BrowserChannel channel) { |
| return new QuitMessage(channel); |
| } |
| |
| public static void send(BrowserChannel channel) throws IOException { |
| final DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.writeByte(MessageType.QUIT.getId()); |
| stream.flush(); |
| } |
| |
| public QuitMessage(BrowserChannel channel) { |
| super(channel); |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel()); |
| } |
| } |
| |
| /** |
| * A message asking the client to send an icon suitable for use in the UI. |
| * <p>See {@link UserAgentIconMessage}. |
| */ |
| protected static class RequestIconMessage extends Message { |
| |
| /** |
| * Receive a RequestIconMessage, assuming the message tag has already been |
| * read. |
| * |
| * @throws IOException |
| */ |
| public static RequestIconMessage receive(BrowserChannel channel) |
| throws IOException { |
| return new RequestIconMessage(channel); |
| } |
| |
| public static void send(BrowserChannel channel) |
| throws IOException { |
| DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.writeByte(MessageType.REQUEST_ICON.getId()); |
| stream.flush(); |
| } |
| |
| public RequestIconMessage(BrowserChannel channel) { |
| super(channel); |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel()); |
| } |
| } |
| |
| /** |
| * Signifies a return from a previous invoke. |
| */ |
| protected static class ReturnMessage extends Message { |
| public static ReturnMessage receive(BrowserChannel channel) |
| throws IOException { |
| final DataInputStream stream = channel.getStreamFromOtherSide(); |
| final boolean isException = stream.readBoolean(); |
| final Value returnValue = channel.readValue(stream); |
| return new ReturnMessage(channel, isException, returnValue); |
| } |
| |
| public static void send(BrowserChannel channel, boolean isException, |
| Value returnValue) throws IOException { |
| final DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.writeByte(MessageType.RETURN.getId()); |
| stream.writeBoolean(isException); |
| channel.writeValue(stream, returnValue); |
| stream.flush(); |
| } |
| |
| public static void send(BrowserChannel channel, |
| ExceptionOrReturnValue returnOrException) throws IOException { |
| send(channel, returnOrException.isException(), |
| returnOrException.getReturnValue()); |
| } |
| |
| private final boolean isException; |
| private final Value returnValue; |
| |
| public ReturnMessage(BrowserChannel channel, boolean isException, |
| Value returnValue) { |
| super(channel); |
| this.returnValue = returnValue; |
| this.isException = isException; |
| } |
| |
| public Value getReturnValue() { |
| return returnValue; |
| } |
| |
| public boolean isException() { |
| return isException; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel(), isException, returnValue); |
| } |
| } |
| |
| /** |
| * A response to ChooseTransport telling the client which transport should |
| * be used for the remainder of the protocol. |
| */ |
| protected static class SwitchTransportMessage extends Message { |
| |
| public static SwitchTransportMessage receive(BrowserChannel channel) |
| throws IOException { |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| String transport = readUtf8String(stream); |
| String transportArgs = readUtf8String(stream); |
| return new SwitchTransportMessage(channel, transport, transportArgs); |
| } |
| |
| private final String transport; |
| |
| private final String transportArgs; |
| |
| public SwitchTransportMessage(BrowserChannel channel, |
| String transport, String transportArgs) { |
| super(channel); |
| // Change nulls to empty strings |
| if (transport == null) { |
| transport = ""; |
| } |
| if (transportArgs == null) { |
| transportArgs = ""; |
| } |
| this.transport = transport; |
| this.transportArgs = transportArgs; |
| } |
| |
| public String getTransport() { |
| return transport; |
| } |
| |
| public String getTransportArgs() { |
| return transportArgs; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); |
| stream.writeByte(MessageType.SWITCH_TRANSPORT.getId()); |
| writeUtf8String(stream, transport); |
| writeUtf8String(stream, transportArgs); |
| stream.flush(); |
| } |
| } |
| |
| /** |
| * A message supplying an icon, which fits in 24x24 and in a standard image |
| * format such as PNG or GIF, suitable for use in the UI. |
| * <p>See {@link RequestIconMessage}. |
| */ |
| protected static class UserAgentIconMessage extends Message { |
| public static UserAgentIconMessage receive(BrowserChannel channel) |
| throws IOException { |
| byte[] iconBytes = null; |
| DataInputStream stream = channel.getStreamFromOtherSide(); |
| int len = stream.readInt(); |
| if (len > 0) { |
| iconBytes = new byte[len]; |
| for (int i = 0; i < len; ++i) { |
| iconBytes[i] = stream.readByte(); |
| } |
| } |
| return new UserAgentIconMessage(channel, iconBytes); |
| } |
| |
| public static void send(BrowserChannel channel, byte[] iconBytes) |
| throws IOException { |
| DataOutputStream stream = channel.getStreamToOtherSide(); |
| stream.writeByte(MessageType.USER_AGENT_ICON.getId()); |
| if (iconBytes == null) { |
| stream.writeInt(0); |
| } else { |
| stream.writeInt(iconBytes.length); |
| for (byte b : iconBytes) { |
| stream.writeByte(b); |
| } |
| } |
| stream.flush(); |
| } |
| |
| private byte[] iconBytes; |
| |
| public UserAgentIconMessage(BrowserChannel channel, byte[] iconBytes) { |
| super(channel); |
| this.iconBytes = iconBytes; |
| } |
| |
| public byte[] getIconBytes() { |
| return iconBytes; |
| } |
| |
| @Override |
| public void send() throws IOException { |
| send(getBrowserChannel(), iconBytes); |
| } |
| } |
| |
| /** |
| * The current version of the protocol. |
| */ |
| public static final int PROTOCOL_VERSION_CURRENT = 3; |
| |
| /** |
| * The oldest protocol version supported by this code. |
| */ |
| public static final int PROTOCOL_VERSION_OLDEST = 2; |
| |
| /** |
| * The protocol version that added the GetIcon message. |
| */ |
| public static final int PROTOCOL_VERSION_GET_ICON = 3; |
| |
| public static final int SPECIAL_CLIENTMETHODS_OBJECT = 0; |
| |
| public static final int SPECIAL_SERVERMETHODS_OBJECT = 0; |
| |
| protected static JavaObjectRef getJavaObjectRef(int refId) { |
| return new JavaObjectRef(refId); |
| } |
| |
| protected static String readUtf8String(DataInputStream stream) |
| throws IOException { |
| final int len = stream.readInt(); |
| final byte[] data = new byte[len]; |
| stream.readFully(data); |
| return new String(data, "UTF8"); |
| } |
| |
| protected static ValueType readValueType(DataInputStream stream) |
| throws IOException, BrowserChannelException { |
| int type = stream.readByte(); |
| ValueType[] types = ValueType.values(); |
| if (type < 0 || type >= types.length) { |
| throw new BrowserChannelException("Invalid value type " + type); |
| } |
| return types[type]; |
| } |
| |
| protected static void writeJavaObject(DataOutputStream stream, |
| JavaObjectRef value) throws IOException { |
| stream.writeByte(ValueType.JAVA_OBJECT.getTag()); |
| stream.writeInt(value.getRefid()); |
| } |
| |
| protected static void writeJsObject(DataOutputStream stream, |
| JsObjectRef value) throws IOException { |
| stream.writeByte(ValueType.JS_OBJECT.getTag()); |
| stream.writeInt(value.getRefid()); |
| } |
| |
| protected static void writeNull(DataOutputStream stream) throws IOException { |
| stream.writeByte(ValueType.NULL.getTag()); |
| } |
| |
| protected static void writeTaggedBoolean(DataOutputStream stream, |
| boolean value) throws IOException { |
| stream.writeByte(ValueType.BOOLEAN.getTag()); |
| stream.writeBoolean(value); |
| } |
| |
| protected static void writeTaggedByte(DataOutputStream stream, byte value) |
| throws IOException { |
| stream.writeByte(ValueType.BYTE.getTag()); |
| stream.writeByte(value); |
| } |
| |
| protected static void writeTaggedChar(DataOutputStream stream, char value) |
| throws IOException { |
| stream.writeByte(ValueType.CHAR.getTag()); |
| stream.writeChar(value); |
| } |
| |
| protected static void writeTaggedDouble(DataOutputStream stream, double value) |
| throws IOException { |
| stream.writeByte(ValueType.DOUBLE.getTag()); |
| stream.writeDouble(value); |
| } |
| |
| protected static void writeTaggedInt(DataOutputStream stream, int value) |
| throws IOException { |
| stream.writeByte(ValueType.INT.getTag()); |
| stream.writeInt(value); |
| } |
| |
| protected static void writeTaggedShort(DataOutputStream stream, short value) |
| throws IOException { |
| stream.writeByte(ValueType.SHORT.getTag()); |
| stream.writeShort(value); |
| } |
| |
| protected static void writeTaggedString(DataOutputStream stream, String data) |
| throws IOException { |
| stream.writeByte(ValueType.STRING.getTag()); |
| writeUtf8String(stream, data); |
| } |
| |
| protected static void writeUtf8String(DataOutputStream stream, String data) |
| throws IOException { |
| try { |
| final byte[] bytes = data.getBytes("UTF8"); |
| stream.writeInt(bytes.length); |
| stream.write(bytes); |
| } catch (UnsupportedEncodingException e) { |
| // TODO: Add description. |
| throw new RuntimeException(); |
| } |
| } |
| |
| private static void writeUndefined(DataOutputStream stream) |
| throws IOException { |
| stream.writeByte(ValueType.UNDEFINED.getTag()); |
| } |
| |
| private final ObjectRefFactory objectRefFactory; |
| |
| private Socket socket; |
| |
| private final DataInputStream streamFromOtherSide; |
| |
| private final DataOutputStream streamToOtherSide; |
| |
| public BrowserChannel(Socket socket, ObjectRefFactory objectRefFactory) |
| throws IOException { |
| this(new BufferedInputStream(socket.getInputStream()), |
| new BufferedOutputStream(socket.getOutputStream()), |
| objectRefFactory); |
| this.socket = socket; |
| } |
| |
| protected BrowserChannel(InputStream inputStream, OutputStream outputStream, |
| ObjectRefFactory objectRefFactory) { |
| streamFromOtherSide = new DataInputStream(inputStream); |
| streamToOtherSide = new DataOutputStream(outputStream); |
| socket = null; |
| this.objectRefFactory = objectRefFactory; |
| } |
| |
| public void endSession() { |
| Utility.close(streamFromOtherSide); |
| Utility.close(streamToOtherSide); |
| Utility.close(socket); |
| } |
| |
| /** |
| * @return a set of remote object reference IDs to be freed. |
| */ |
| public Set<Integer> getRefIdsForCleanup() { |
| return objectRefFactory.getRefIdsForCleanup(); |
| } |
| |
| public String getRemoteEndpoint() { |
| if (socket == null) { |
| return ""; |
| } |
| return socket.getInetAddress().getCanonicalHostName() + ":" |
| + socket.getPort(); |
| } |
| |
| protected DataInputStream getStreamFromOtherSide() { |
| return streamFromOtherSide; |
| } |
| |
| protected DataOutputStream getStreamToOtherSide() { |
| return streamToOtherSide; |
| } |
| |
| protected Value readValue(DataInputStream stream) throws IOException { |
| ValueType tag; |
| try { |
| tag = readValueType(stream); |
| } catch (BrowserChannelException e) { |
| IOException ee = new IOException(); |
| ee.initCause(e); |
| throw ee; |
| } |
| Value value = new Value(); |
| switch (tag) { |
| case NULL: |
| value.setNull(); |
| break; |
| case UNDEFINED: |
| value.setUndefined(); |
| break; |
| case BOOLEAN: |
| value.setBoolean(stream.readByte() != 0); |
| break; |
| case BYTE: |
| value.setByte(stream.readByte()); |
| break; |
| case CHAR: |
| value.setChar(stream.readChar()); |
| break; |
| case INT: |
| value.setInt(stream.readInt()); |
| break; |
| case DOUBLE: |
| value.setDouble(stream.readDouble()); |
| break; |
| case SHORT: |
| value.setShort(stream.readShort()); |
| break; |
| case STRING: |
| value.setString(readUtf8String(stream)); |
| break; |
| case JS_OBJECT: |
| value.setJsObject(objectRefFactory.getJsObjectRef(stream.readInt())); |
| break; |
| case JAVA_OBJECT: |
| value.setJavaObject(objectRefFactory.getJavaObjectRef( |
| stream.readInt())); |
| break; |
| default: |
| throw new IllegalArgumentException("Unexpected type: " + tag); |
| } |
| return value; |
| } |
| |
| protected void sendFreedValues() throws IOException { |
| Set<Integer> freed = objectRefFactory.getRefIdsForCleanup(); |
| int n = freed.size(); |
| if (n > 0) { |
| int[] ids = new int[n]; |
| int i = 0; |
| for (Integer id : freed) { |
| ids[i++] = id; |
| } |
| FreeMessage.send(this, ids); |
| } |
| } |
| |
| protected void writeValue(DataOutputStream stream, Value value) |
| throws IOException { |
| if (value.isNull()) { |
| writeNull(stream); |
| } else if (value.isUndefined()) { |
| writeUndefined(stream); |
| } else if (value.isJsObject()) { |
| writeJsObject(stream, value.getJsObject()); |
| } else if (value.isJavaObject()) { |
| writeJavaObject(stream, value.getJavaObject()); |
| } else if (value.isBoolean()) { |
| writeTaggedBoolean(stream, value.getBoolean()); |
| } else if (value.isByte()) { |
| writeTaggedByte(stream, value.getByte()); |
| } else if (value.isChar()) { |
| writeTaggedChar(stream, value.getChar()); |
| } else if (value.isShort()) { |
| writeTaggedShort(stream, value.getShort()); |
| } else if (value.isDouble()) { |
| writeTaggedDouble(stream, value.getDouble()); |
| } else if (value.isInt()) { |
| writeTaggedInt(stream, value.getInt()); |
| } else if (value.isString()) { |
| writeTaggedString(stream, value.getString()); |
| } else { |
| throw new IllegalArgumentException("Unexpected type: " + value.getType()); |
| } |
| } |
| } |