blob: e905589687c7d860ecc9125985ef31e37a99d143 [file] [log] [blame]
/*
* 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;
}
}
}