/* * 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; /** * */ 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; } 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; } 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(6), FLOAT(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 Float) { type = ValueType.FLOAT; } else if (obj instanceof Integer) { type = ValueType.INT; } else if (obj instanceof Long) { type = ValueType.LONG; } 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 RuntimeException( "Unexpected Java type in convertFromJavaValue: " + obj); } value = obj; } /** * Convert a value to the requested Java type. * * @param reqType type to convert to * @return value as that type. */ public Object convertToJavaType(Class<?> reqType) { if (reqType.isArray()) { // TODO(jat): handle arrays? } if (reqType.equals(Boolean.class)) { assert type == ValueType.BOOLEAN; return value; } else if (reqType.equals(Byte.class) || reqType.equals(byte.class)) { assert isNumber(); return Byte.valueOf(((Number) value).byteValue()); } else if (reqType.equals(Character.class) || reqType.equals(char.class)) { if (type == ValueType.CHAR) { return value; } else { assert isNumber(); return Character.valueOf((char) ((Number) value).shortValue()); } } else if (reqType.equals(Double.class) || reqType.equals(double.class)) { assert isNumber(); return Double.valueOf(((Number) value).doubleValue()); } else if (reqType.equals(Float.class) || reqType.equals(float.class)) { assert isNumber(); return Float.valueOf(((Number) value).floatValue()); } else if (reqType.equals(Integer.class) || reqType.equals(int.class)) { assert isNumber(); return Integer.valueOf(((Number) value).intValue()); } else if (reqType.equals(Long.class) || reqType.equals(long.class)) { assert isNumber(); return Long.valueOf(((Number) value).longValue()); } else if (reqType.equals(Short.class) || reqType.equals(short.class)) { assert isNumber(); return Short.valueOf(((Number) value).shortValue()); } else if (reqType.equals(String.class)) { assert type == ValueType.STRING; return value; } else { // Wants an object, caller must deal with object references. return value; } } 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 float getFloat() { assert type == ValueType.FLOAT; return ((Float) value).floatValue(); } 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 long getLong() { assert type == ValueType.LONG; return ((Long) value).longValue(); } 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 isFloat() { return type == ValueType.FLOAT; } 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 isLong() { return type == ValueType.LONG; } public boolean isNull() { return type == ValueType.NULL; } public boolean isNumber() { switch (type) { case BYTE: case CHAR: case DOUBLE: case FLOAT: case INT: case LONG: case SHORT: return true; default: return false; } } public boolean isPrimitive() { switch (type) { case BOOLEAN: case BYTE: case CHAR: case DOUBLE: case FLOAT: case INT: case LONG: 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 setFloat(float val) { type = ValueType.FLOAT; value = Float.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 writeTaggedFloat(DataOutputStream stream, float value) throws IOException { stream.writeByte(ValueType.FLOAT.getTag()); stream.writeFloat(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 FLOAT: value.setFloat(stream.readFloat()); break; case INT: value.setInt(stream.readInt()); break; case LONG: value.setLong(stream.readLong()); 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; } 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.isFloat()) { writeTaggedFloat(stream, value.getFloat()); } else if (value.isInt()) { writeTaggedInt(stream, value.getInt()); } else if (value.isString()) { writeTaggedString(stream, value.getString()); } else { assert false; } } }