--- /dev/null
+/*
+ * Copyright 2009 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.junit;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.shell.HostedModePluginObject;
+import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet;
+
+import com.gargoylesoftware.htmlunit.AlertHandler;
+import com.gargoylesoftware.htmlunit.BrowserVersion;
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
+import com.gargoylesoftware.htmlunit.IncorrectnessListener;
+import com.gargoylesoftware.htmlunit.OnbeforeunloadHandler;
+import com.gargoylesoftware.htmlunit.Page;
+import com.gargoylesoftware.htmlunit.ScriptException;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebWindow;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
+import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener;
+import com.gargoylesoftware.htmlunit.javascript.host.Window;
+
+import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+
+import org.w3c.css.sac.CSSParseException;
+import org.w3c.css.sac.ErrorHandler;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Launches a web-mode test via HTMLUnit.
+ */
+public class RunStyleHtmlUnit extends RunStyle {
+
+ /**
+ * Runs HTMLUnit in a separate thread.
+ */
+ protected static class HtmlUnitThread extends Thread implements AlertHandler,
+ IncorrectnessListener, OnbeforeunloadHandler {
+
+ private final BrowserVersion browser;
+ private final boolean developmentMode;
+ private final TreeLogger treeLogger;
+ private final String url;
+ private Object waitForUnload = new Object();
+
+ public HtmlUnitThread(BrowserVersion browser, String url,
+ TreeLogger treeLogger, boolean developmentMode) {
+ this.browser = browser;
+ this.url = url;
+ this.treeLogger = treeLogger;
+ this.setName("htmlUnit client thread");
+ this.developmentMode = developmentMode;
+ }
+
+ public void handleAlert(Page page, String message) {
+ treeLogger.log(TreeLogger.ERROR, "Alert: " + message);
+ }
+
+ public boolean handleEvent(Page page, String returnValue) {
+ synchronized (waitForUnload) {
+ waitForUnload.notifyAll();
+ }
+ return true;
+ }
+
+ public void notify(String message, Object origin) {
+ if ("Obsolete content type encountered: 'text/javascript'.".equals(message)) {
+ // silently eat warning about text/javascript MIME type
+ return;
+ }
+ treeLogger.log(TreeLogger.WARN, message);
+ }
+
+ @Override
+ public void run() {
+ WebClient webClient = new WebClient(browser);
+ webClient.setAlertHandler(this);
+ // Adding a handler that ignores errors to work-around
+ // https://sourceforge.net/tracker/?func=detail&aid=3090806&group_id=47038&atid=448266
+ webClient.setCssErrorHandler(new ErrorHandler() {
+
+ public void error(CSSParseException exception) {
+ // ignore
+ }
+
+ public void fatalError(CSSParseException exception) {
+ treeLogger.log(TreeLogger.WARN,
+ "CSS fatal error: " + exception.getURI() + " ["
+ + exception.getLineNumber() + ":"
+ + exception.getColumnNumber() + "] " + exception.getMessage());
+ }
+
+ public void warning(CSSParseException exception) {
+ // ignore
+ }
+ });
+ webClient.setIncorrectnessListener(this);
+ webClient.setOnbeforeunloadHandler(this);
+ webClient.setJavaScriptErrorListener(new JavaScriptErrorListener() {
+
+ @Override
+ public void loadScriptError(HtmlPage htmlPage, URL scriptUrl,
+ Exception exception) {
+ treeLogger.log(TreeLogger.ERROR,
+ "Load Script Error: " + exception, exception);
+ }
+
+ @Override
+ public void malformedScriptURL(HtmlPage htmlPage, String url,
+ MalformedURLException malformedURLException) {
+ treeLogger.log(TreeLogger.ERROR,
+ "Malformed Script URL: " + malformedURLException.getLocalizedMessage());
+ }
+
+ @Override
+ public void scriptException(HtmlPage htmlPage,
+ ScriptException scriptException) {
+ treeLogger.log(TreeLogger.DEBUG,
+ "Script Exception: " + scriptException.getLocalizedMessage() +
+ ", line " + scriptException.getFailingLine());
+ }
+
+ @Override
+ public void timeoutError(HtmlPage htmlPage, long allowedTime,
+ long executionTime) {
+ treeLogger.log(TreeLogger.ERROR,
+ "Script Timeout Error " + executionTime + " > " + allowedTime);
+ }
+ });
+ setupWebClient(webClient);
+ try {
+ Page page = webClient.getPage(url);
+ webClient.waitForBackgroundJavaScriptStartingBefore(2000);
+ if (treeLogger.isLoggable(TreeLogger.SPAM)) {
+ treeLogger.log(TreeLogger.SPAM, "getPage returned "
+ + ((HtmlPage) page).asXml());
+ }
+ // TODO(amitmanjhi): call webClient.closeAllWindows()
+ } catch (FailingHttpStatusCodeException e) {
+ treeLogger.log(TreeLogger.ERROR, "HTTP request failed", e);
+ return;
+ } catch (MalformedURLException e) {
+ treeLogger.log(TreeLogger.ERROR, "Bad URL", e);
+ return;
+ } catch (IOException e) {
+ treeLogger.log(TreeLogger.ERROR, "I/O error on HTTP request", e);
+ return;
+ }
+ }
+
+ protected void setupWebClient(WebClient webClient) {
+ if (developmentMode) {
+ JavaScriptEngine hostedEngine = new HostedJavaScriptEngine(webClient,
+ treeLogger);
+ webClient.setJavaScriptEngine(hostedEngine);
+ }
+ }
+ }
+
+ /**
+ * JavaScriptEngine subclass that provides a hook of initializing the
+ * __gwt_HostedModePlugin property on any new window, so it acts just like
+ * Firefox with the XPCOM plugin installed.
+ */
+ private static class HostedJavaScriptEngine extends JavaScriptEngine {
+
+ private static final long serialVersionUID = 3594816610842448691L;
+ private final TreeLogger logger;
+
+ public HostedJavaScriptEngine(WebClient webClient, TreeLogger logger) {
+ super(webClient);
+ this.logger = logger;
+ }
+
+ @Override
+ public void initialize(WebWindow webWindow) {
+ // Hook in the hosted-mode plugin after initializing the JS engine.
+ super.initialize(webWindow);
+ Window window = (Window) webWindow.getScriptObject();
+ window.defineProperty("__gwt_HostedModePlugin",
+ new HostedModePluginObject(this, logger), ScriptableObject.READONLY);
+ }
+ }
+
+ private static final Map<String, BrowserVersion> BROWSER_MAP = createBrowserMap();
+
+ /*
+ * as long as this number is greater than 1, GWTTestCaseTest::testRetry will
+ * pass
+ */
+ private static final int DEFAULT_TRIES = 1;
+
+ private static final Set<Platform> PLATFORMS = ImmutableSet.of(Platform.HtmlUnitBug,
+ Platform.HtmlUnitLayout, Platform.HtmlUnitUnknown);
+
+ /**
+ * Returns the list of browsers Htmlunit emulates as a comma separated string.
+ */
+ static String getBrowserList() {
+ StringBuffer sb = new StringBuffer();
+ for (String str : BROWSER_MAP.keySet()) {
+ sb.append(str);
+ sb.append(",");
+ }
+ if (sb.length() > 1) {
+ return sb.substring(0, sb.length() - 1);
+ }
+ return sb.toString();
+ }
+
+ private static Map<String, BrowserVersion> createBrowserMap() {
+ Map<String, BrowserVersion> browserMap = new HashMap<String, BrowserVersion>();
+ for (BrowserVersion browser : new BrowserVersion[] {
+ BrowserVersion.FIREFOX_3_6, BrowserVersion.INTERNET_EXPLORER_6,
+ BrowserVersion.INTERNET_EXPLORER_7}) {
+ browserMap.put(browser.getNickname(), browser);
+ }
+ return Collections.unmodifiableMap(browserMap);
+ }
+
+ private Set<BrowserVersion> browsers = new HashSet<BrowserVersion>();
+ private boolean developmentMode;
+ private final List<Thread> threads = new ArrayList<Thread>();
+
+ /**
+ * Create a RunStyle instance with the passed-in browser targets.
+ */
+ public RunStyleHtmlUnit(JUnitShell shell) {
+ super(shell);
+ }
+
+ @Override
+ public Set<Platform> getPlatforms() {
+ return PLATFORMS;
+ }
+
+ @Override
+ public int initialize(String args) {
+ if (args == null || args.length() == 0) {
+ // If no browsers specified, default to Firefox 3.
+ args = "FF3.6";
+ }
+ Set<BrowserVersion> browserSet = new HashSet<BrowserVersion>();
+ for (String browserName : args.split(",")) {
+ BrowserVersion browser = BROWSER_MAP.get(browserName);
+ if (browser == null) {
+ getLogger().log(
+ TreeLogger.ERROR,
+ "RunStyleHtmlUnit: Unknown browser " + "name " + browserName
+ + ", expected browser name: one of " + BROWSER_MAP.keySet());
+ return -1;
+ }
+ browserSet.add(browser);
+ }
+ browsers = Collections.unmodifiableSet(browserSet);
+
+ setTries(DEFAULT_TRIES); // set to the default value for this RunStyle
+ return browsers.size();
+ }
+
+ @Override
+ public void launchModule(String moduleName) {
+ for (BrowserVersion browser : browsers) {
+ String url = shell.getModuleUrl(moduleName);
+ HtmlUnitThread hut = createHtmlUnitThread(browser, url);
+ TreeLogger logger = shell.getTopLogger();
+ if (logger.isLoggable(TreeLogger.INFO)) {
+ logger.log(TreeLogger.INFO,
+ "Starting " + url + " on browser " + browser.getNickname());
+ }
+ /*
+ * TODO (amitmanjhi): Is it worth pausing here and waiting for the main
+ * test thread to get to an "okay" state.
+ */
+ hut.start();
+ threads.add(hut);
+ }
+ }
+
+ public int numBrowsers() {
+ return browsers.size();
+ }
+
+ @Override
+ public boolean setupMode(TreeLogger logger, boolean developmentMode) {
+ this.developmentMode = developmentMode;
+ return true;
+ }
+
+ protected HtmlUnitThread createHtmlUnitThread(BrowserVersion browser,
+ String url) {
+ return new HtmlUnitThread(browser, url, shell.getTopLogger().branch(
+ TreeLogger.SPAM, "logging for HtmlUnit thread"), developmentMode);
+ }
+}
--- /dev/null
+/*
+ * 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 {
+ type = ValueType.STRING;
+ obj = String.valueOf(obj);
+// throw new RuntimeException(
+// "Unexpected Java type in convertFromJavaValue: " + obj.getClass() + " " + 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;
+ }
+ }
+}