Update the OOPHM wire protocol for the following:
 - support future changes such as clients supporting multiple versions of
   the wire protocol simultaneously, switching to alternate transport
   mechanisms such as shared memory, and being able to detect an out-of-date
   hosted.html file for developers running an external server (-noserver)
 - collect additional data for upcoming OOPHM UI changes
   (in particular, the top-level URL and a session key)
 - split plugin initialization from connection to differentiate between
   failures in those steps
 - add tests for wire protocol messages
 - refactor for upcoming HTMLUnit hosted mode integration

Backwards compatibility with v1 plugins is retained by keeping thd old
LoadModule message, defining a new tag for the new LoadModule message,
and checking for v1 clients during the initialization of the protocol.

Patch by: jat
Review by: amitmanjhi


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5911 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
index 3037377..975ca75 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/hosted.html
@@ -11,6 +11,7 @@
   var moduleName = moduleFunc ? moduleFunc.moduleName : "unknown";
   $stats({moduleName:moduleName,subSystem:'startup',evtGroup:'moduleStartup',millis:(new Date()).getTime(),type:'moduleEvalStart'});
 }
+var $hostedHtmlVersion="2.0";
 
 var gwtOnLoad;
 var $hosted = "localhost:9997";
@@ -23,11 +24,30 @@
   }
 } catch(e) {
 }
+
+function getTopWindow() {
+  var topWin = window.top;
+  while (topWin.opener) {
+    topWin = topWin.opener.top;
+  }
+  return topWin;
+}
+
+function loadIframe(url) {
+  var iframe = $doc.createElement('iframe');
+  iframe.src = "javascript:''";
+  iframe.style.width = "100%";
+  iframe.style.height = "100%";
+  iframe.style.borderWidth = "0px";
+  $doc.body.insertBefore(iframe, $doc.body.firstChild);
+  iframe.contentWindow.location.replace(url);
+}
+
 if ($legacyHosted) {
   gwtOnLoad = function(errFn, modName, modBase) {
     $moduleName = modName;
     $moduleBase = modBase;
-    if (!external.gwtOnLoad(window, modName, "1.6")) {
+    if (!external.gwtOnLoad(window, modName, $hostedHtmlVersion)) {
       if (errFn) {
         errFn(modName);
       }
@@ -35,7 +55,7 @@
   }
 
   window.onunload = function() {
-    external.gwtOnLoad(window, null, "1.6");
+    external.gwtOnLoad(window, null, $hostedHtmlVersion);
   };
 } else {
   // install eval wrapper on FF to avoid EvalError problem
@@ -175,29 +195,51 @@
       findPluginObject,
       findPluginEmbed,
     ];
-    var found = false;
+    var topWin = getTopWindow();
+    var url = topWin.location.href;
+    if (!topWin.__gwt_SessionID) {
+      var ASCII_EXCLAMATION = 33;
+      var ASCII_TILDE = 126;
+      var chars = [];
+      for (var i = 0; i < 16; ++i) {
+        chars.push(Math.floor(ASCII_EXCLAMATION
+            + Math.random() * (ASCII_TILDE - ASCII_EXCLAMATION + 1)));
+      }
+      topWin.__gwt_SessionID = String.fromCharCode.apply(null, chars);
+    }
+    var plugin = null;
     for (var i = 0; i < pluginFinders.length; ++i) {
       try {
-        var plugin = pluginFinders[i]();
-        if (plugin != null) {
-          // TODO: split connect into init/connect so we can tell plugin
-          // failures from connection failures.
-          if (plugin.connect($hosted, $moduleName, window)) {
-            found = true;
-            break;
-          } else {
-            if (errFn) {
-              errFn(modName);
-            } else {
-              alert("failed to connect to hosted mode server at " + $hosted);
-            }
-          }
+        plugin = pluginFinders[i]();
+        if (plugin != null && plugin.init(window)) {
+          break;
         }
       } catch (e) {
       }
     }
-    if (!found) {
-      alert("No GWT plugin found or hosted-mode connection failed");
+    if (!plugin) {
+      // try searching for a v1 plugin for backwards compatibility
+      var found = false;
+      for (var i = 0; i < pluginFinders.length; ++i) {
+        try {
+          plugin = pluginFinders[i]();
+          if (plugin != null && plugin.connect($hosted, $moduleName, window)) {
+            return;
+          }
+        } catch (e) {
+        }
+      }      
+      loadIframe("http://google-web-toolkit.googlecode.com/svn/trunk/plugins/MissingBrowserPlugin.html");
+    } else {
+      if (!plugin.connect(url, topWin.__gwt_SessionID, $hosted, $moduleName,
+          $hostedHtmlVersion)) {
+        if (errFn) {
+          errFn(modName);
+        } else {
+          alert("Plugin failed to connect to hosted mode server at " + $hosted);
+          loadIframe("http://code.google.com/p/google-web-toolkit/wiki/TroubleshootingOOPHM");
+        }
+      }
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
index ea06563..7542206 100644
--- a/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
+++ b/dev/core/src/com/google/gwt/dev/SwtHostedModeBase.java
@@ -76,8 +76,8 @@
     }
 
     public ModuleSpaceHost createModuleSpaceHost(TreeLogger logger,
-        String moduleName, String userAgent, String remoteEndpoint)
-        throws UnableToCompleteException {
+        String moduleName, String userAgent, String url, String sessionKey,
+        String remoteEndpoint) throws UnableToCompleteException {
       throw new UnsupportedOperationException();
     }
 
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
index 9253b34..019f268 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidgetHost.java
@@ -49,9 +49,17 @@
 
   /**
    * For OOPHM.
+   * 
+   * @param logger
+   * @param moduleName
+   * @param userAgent
+   * @param url URL of top-level window (may be null for old browser plugins) 
+   * @param sessionKey unique session key (may be null for old browser plugins)
+   * @param remoteEndpoint
    */
   ModuleSpaceHost createModuleSpaceHost(TreeLogger logger, String moduleName,
-      String userAgent, String remoteEndpoint) throws UnableToCompleteException;
+      String userAgent, String url, String sessionKey,
+      String remoteEndpoint) throws UnableToCompleteException;
 
   TreeLogger getLogger();
 
diff --git a/dev/core/src/com/google/gwt/dev/util/TemporaryBufferStream.java b/dev/core/src/com/google/gwt/dev/util/TemporaryBufferStream.java
new file mode 100644
index 0000000..608cb55
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/TemporaryBufferStream.java
@@ -0,0 +1,56 @@
+/*
+ * 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.dev.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+/**
+ * A utility for tests that allow writing to a temporary buffer and reading
+ * from the same buffer to verify that serialization/deserialization works.
+ */
+public class TemporaryBufferStream {
+
+  private ArrayList<Byte> buf = new ArrayList<Byte>();
+
+  private InputStream inputStream = new InputStream() {
+    @Override
+    public int read() throws IOException {
+      try {
+        return buf.remove(0) & 255;
+      } catch (IndexOutOfBoundsException e) {
+        return -1;
+      }
+    }
+  };
+  
+  private OutputStream outputStream = new OutputStream() {
+    @Override
+    public void write(int b) throws IOException {
+      buf.add(Byte.valueOf((byte) b));
+    }
+  };
+  
+  public InputStream getInputStream() {
+    return inputStream;
+  }
+  
+  public OutputStream getOutputStream() {
+    return outputStream;
+  }
+}
diff --git a/dev/oophm/build.xml b/dev/oophm/build.xml
index 947f655..2d4f957 100755
--- a/dev/oophm/build.xml
+++ b/dev/oophm/build.xml
@@ -6,6 +6,23 @@
   <property.ensure name="gwt.core.root" location="../core" />
   <property.ensure name="gwt.core.build" location="${project.build}/../core" />
 
+  <fileset id="default.tests" dir="${javac.junit.out}">
+    <include name="**/com/google/**/*Test.class" />
+  </fileset>
+
+  <target name="compile.tests" depends="build" description="Compiles the test code for this project">
+    <mkdir dir="${javac.junit.out}" />
+    <gwt.javac srcdir="test" destdir="${javac.junit.out}">
+      <classpath>
+        <pathelement location="${javac.out}" />
+        <pathelement location="${gwt.core.build}/bin" />
+        <pathelement location="${gwt.core.build}/bin-tests" />
+        <pathelement location="${alldeps.jar}" />
+        <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+      </classpath>
+    </gwt.javac>
+  </target>
+
   <target name="compile" description="Compile all java files">
     <mkdir dir="${javac.out}" />
     <gwt.javac>
@@ -35,4 +52,41 @@
     <delete file="${project.lib}" failonerror="false" />
   </target>
 
+  <target name="test" depends="build, compile.tests" description="Run unit tests for this project.">
+    <!-- TODO: refactor gwt.junit so it can be reused here -->
+    <taskdef name="junit" classname="org.apache.tools.ant.taskdefs.optional.junit.JUnitTask">
+      <classpath>
+        <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+        <pathelement location="${gwt.tools.antlib}/ant-junit-1.6.5.jar" />
+      </classpath>
+    </taskdef>
+  
+    <echo message="Writing test results to ${junit.out}/reports for ${test.cases}" />
+    <mkdir dir="${junit.out}/reports" />
+  
+    <echo message="${javac.out} ${javac.junit.out}" />
+    <junit dir="${junit.out}" fork="yes" printsummary="yes" haltonfailure="true">
+      <classpath>
+        <pathelement location="test" />
+        <pathelement location="${javac.junit.out}" />
+        <pathelement location="${javac.out}" />
+        <pathelement location="${gwt.core.build}/bin" />
+        <pathelement location="${gwt.core.build}/bin-tests" />
+        <pathelement location="${alldeps.jar}" />
+        <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+        <!-- Pull in gwt-dev and gwt-user sources for .gwt.xml files -->
+        <pathelement location="${gwt.root}/user/src/" />
+        <pathelement location="${gwt.root}/user/super/" />
+        <pathelement location="${gwt.root}/dev/core/super" />
+      </classpath>
+  
+      <formatter type="plain" />
+      <formatter type="xml" />
+  
+      <batchtest todir="${junit.out}/reports">
+        <fileset refid="default.tests" />
+      </batchtest>
+    </junit>
+  </target>
+
 </project>
diff --git a/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
index 9d1705c..b8fe956 100644
--- a/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
+++ b/dev/oophm/src/com/google/gwt/dev/OophmHostedModeBase.java
@@ -142,14 +142,15 @@
     }
 
     public ModuleSpaceHost createModuleSpaceHost(TreeLogger mainLogger,
-        String moduleName, String userAgent, String remoteSocket)
-        throws UnableToCompleteException {
+        String moduleName, String userAgent, String url, String sessionKey,
+        String remoteSocket) throws UnableToCompleteException {
       TreeLogger logger = mainLogger;
       TreeLogger.Type maxLevel = TreeLogger.INFO;
       if (mainLogger instanceof AbstractTreeLogger) {
         maxLevel = ((AbstractTreeLogger) mainLogger).getMaxDetail();
       }
-
+      // TODO(jat): collect different sessions into the same tab, with a
+      //    dropdown to select individual module's logs
       ModulePanel tab;
       if (!isHeadless()) {
         tab = new ModulePanel(maxLevel, moduleName, userAgent, remoteSocket,
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
index bdf36f9..85bafa4 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
@@ -26,6 +26,8 @@
 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.lang.ref.Reference;
 import java.lang.ref.ReferenceQueue;
@@ -102,9 +104,14 @@
 
   /**
    * Enumeration of message type ids.
+   * 
+   * NOTE: order is important as this defines the ordinals used in the wire
+   * protocol.
    */
   public enum MessageType {
-    Invoke, Return, LoadModule, Quit, LoadJsni, InvokeSpecial, FreeValue;
+    INVOKE, RETURN, OLD_LOAD_MODULE, QUIT, LOAD_JSNI, INVOKE_SPECIAL, FREE_VALUE,
+    FATAL_ERROR, CHECK_VERSIONS, PROTOCOL_VERSION, CHOOSE_TRANSPORT,
+    SWITCH_TRANSPORT, LOAD_MODULE;
   }
 
   /**
@@ -152,7 +159,8 @@
         Value thisObj, int dispId, Value[] args);
 
     public abstract TreeLogger loadModule(TreeLogger logger,
-        BrowserChannel channel, String moduleName, String userAgent);
+        BrowserChannel channel, String moduleName, String userAgent, String url,
+        String sessionKey);
 
     public abstract ExceptionOrReturnValue setProperty(BrowserChannel channel,
         int refId, int dispId, Value newValue);
@@ -514,6 +522,132 @@
   }
 
   /**
+   * 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 int minVersion;
+
+    private final int maxVersion;
+
+    private final String hostedHtmlVersion;
+
+    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.ordinal());
+      stream.writeInt(minVersion);
+      stream.writeInt(maxVersion);
+      writeUntaggedString(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.ordinal());
+      stream.writeInt(transports.length);
+      for (String transport : transports) {
+        writeUntaggedString(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.ordinal());
+      writeUntaggedString(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.
@@ -534,7 +668,7 @@
     public static void send(BrowserChannel channel, int[] ids)
         throws IOException {
       DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.FreeValue.ordinal());
+      stream.writeByte(MessageType.FREE_VALUE.ordinal());
       stream.writeInt(ids.length);
       for (int id : ids) {
         stream.writeInt(id);
@@ -565,44 +699,38 @@
   }
 
   /**
-   * A request from the to invoke a function on the other side.
+   * 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 InvokeMessage extends Message {
-    public static InvokeMessage receive(BrowserChannel channel)
+  protected static class InvokeOnClientMessage extends Message {
+    public static InvokeOnClientMessage receive(BrowserChannel channel)
         throws IOException {
-      final DataInputStream stream = channel.getStreamFromOtherSide();
+      DataInputStream stream = channel.getStreamFromOtherSide();
       // NOTE: Tag has already been read.
-      final int methodDispatchId = stream.readInt();
-      final Value thisRef = readValue(stream);
-      final int argLen = stream.readInt();
-      final Value[] args = new Value[argLen];
+      String methodName = readUtf8String(stream);
+      Value thisRef = readValue(stream);
+      int argLen = stream.readInt();
+      Value[] args = new Value[argLen];
       for (int i = 0; i < argLen; i++) {
         args[i] = readValue(stream);
       }
-      return new InvokeMessage(channel, methodDispatchId, thisRef, args);
+      return new InvokeOnClientMessage(channel, methodName, thisRef, args);
     }
 
-    private final int methodDispatchId;
     private final String methodName;
 
     private final Value thisRef;
     private final Value[] args;
 
-    public InvokeMessage(BrowserChannel channel, int methodDispatchId,
-        Value thisRef, Value[] args) {
-      super(channel);
-      this.thisRef = thisRef;
-      this.methodName = null;
-      this.methodDispatchId = methodDispatchId;
-      this.args = args;
-    }
-
-    public InvokeMessage(BrowserChannel channel, String methodName,
+    public InvokeOnClientMessage(BrowserChannel channel, String methodName,
         Value thisRef, Value[] args) {
       super(channel);
       this.thisRef = thisRef;
       this.methodName = methodName;
-      this.methodDispatchId = -1;
       this.args = args;
     }
 
@@ -610,10 +738,6 @@
       return args;
     }
 
-    public int getMethodDispatchId() {
-      return methodDispatchId;
-    }
-
     public String getMethodName() {
       return methodName;
     }
@@ -626,7 +750,7 @@
     public void send() throws IOException {
       final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
 
-      stream.writeByte(MessageType.Invoke.ordinal());
+      stream.writeByte(MessageType.INVOKE.ordinal());
       writeUntaggedString(stream, methodName);
       writeValue(stream, thisRef);
       stream.writeInt(args.length);
@@ -638,6 +762,69 @@
   }
 
   /**
+   * 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 = readValue(stream);
+      int argLen = stream.readInt();
+      Value[] args = new Value[argLen];
+      for (int i = 0; i < argLen; i++) {
+        args[i] = readValue(stream);
+      }
+      return new InvokeOnServerMessage(channel, methodDispatchId, thisRef,
+          args);
+    }
+
+    private final int methodDispatchId;
+    private final Value thisRef;
+    private final Value[] args;
+
+    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.ordinal());
+      stream.writeInt(methodDispatchId);
+      writeValue(stream, thisRef);
+      stream.writeInt(args.length);
+      for (int i = 0; i < args.length; i++) {
+        writeValue(stream, args[i]);
+      }
+      stream.flush();
+    }
+  }
+
+  /**
    * A request from the to invoke a function on the other side.
    */
   protected static class InvokeSpecialMessage extends Message {
@@ -682,7 +869,7 @@
     public void send() throws IOException {
       final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
 
-      stream.writeByte(MessageType.InvokeSpecial.ordinal());
+      stream.writeByte(MessageType.INVOKE_SPECIAL.ordinal());
       stream.writeByte(dispatchId.ordinal());
       stream.writeInt(args.length);
       for (int i = 0; i < args.length; i++) {
@@ -701,14 +888,14 @@
     public static LoadJsniMessage receive(BrowserChannel channel)
         throws IOException {
       DataInputStream stream = channel.getStreamFromOtherSide();
-      String js = stream.readUTF();
+      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.LoadJsni.ordinal());
+      stream.write(MessageType.LOAD_JSNI.ordinal());
       writeUntaggedString(stream, js);
       stream.flush();
     }
@@ -720,6 +907,10 @@
       this.js = js;
     }
 
+    public String getJsni() {
+      return js;
+    }
+
     @Override
     public boolean isAsynchronous() {
       return true;
@@ -737,44 +928,43 @@
    */
   protected static class LoadModuleMessage extends Message {
     public static LoadModuleMessage receive(BrowserChannel channel)
-        throws IOException, BrowserChannelException {
-      final DataInputStream stream = channel.getStreamFromOtherSide();
-      final int version = stream.readInt();
-      checkProtocolVersion(version);
-      final String moduleName = readUtf8String(stream);
-      final String userAgent = readUtf8String(stream);
-      return new LoadModuleMessage(channel, version, moduleName, userAgent);
-    }
-
-    private static void checkProtocolVersion(int version)
-        throws BrowserChannelException {
-      if (version != BROWSERCHANNEL_PROTOCOL_VERSION) {
-        throw new BrowserChannelException(
-            "Incompatible client version: server="
-                + BROWSERCHANNEL_PROTOCOL_VERSION + ", client=" + version);
-      }
+        throws IOException {
+      DataInputStream stream = channel.getStreamFromOtherSide();
+      String url = readUtf8String(stream);
+      String sessionKey = readUtf8String(stream);
+      String moduleName = readUtf8String(stream);
+      String userAgent = readUtf8String(stream);
+      return new LoadModuleMessage(channel, url, sessionKey, moduleName,
+          userAgent);
     }
 
     private final String moduleName;
 
     private final String userAgent;
 
-    private final int protocolVersion;
+    private final String url;
+    
+    private final String sessionKey;
 
-    public LoadModuleMessage(BrowserChannel channel, int protocolVersion,
-        String moduleName, String userAgent) {
+    public LoadModuleMessage(BrowserChannel channel, String url,
+        String sessionKey, String moduleName, String userAgent) {
       super(channel);
+      this.url = url;
+      this.sessionKey = sessionKey;
       this.moduleName = moduleName;
       this.userAgent = userAgent;
-      this.protocolVersion = protocolVersion;
     }
 
     public String getModuleName() {
       return moduleName;
     }
 
-    public int getProtocolVersion() {
-      return protocolVersion;
+    public String getSessionKey() {
+      return sessionKey;
+    }
+
+    public String getUrl() {
+      return url;
     }
 
     public String getUserAgent() {
@@ -783,9 +973,10 @@
 
     @Override
     public void send() throws IOException {
-      final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
-      stream.writeByte(MessageType.LoadModule.ordinal());
-      stream.writeInt(protocolVersion);
+      DataOutputStream stream = getBrowserChannel().getStreamToOtherSide();
+      stream.writeByte(MessageType.LOAD_MODULE.ordinal());
+      writeUntaggedString(stream, url);
+      writeUntaggedString(stream, sessionKey);
       writeUntaggedString(stream, moduleName);
       writeUntaggedString(stream, userAgent);
       stream.flush();
@@ -834,6 +1025,90 @@
   }
 
   /**
+   * 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 String userAgent;
+
+    private final int protoVersion;
+    
+    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.ordinal());
+      stream.writeInt(protoVersion);
+      writeUntaggedString(stream, moduleName);
+      writeUntaggedString(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.ordinal());
+      stream.writeInt(protocolVersion);
+      stream.flush();
+    }
+  }
+
+  /**
    * A message signifying a soft close of the communications channel.
    */
   protected static class QuitMessage extends Message {
@@ -843,7 +1118,7 @@
 
     public static void send(BrowserChannel channel) throws IOException {
       final DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.Quit.ordinal());
+      stream.writeByte(MessageType.QUIT.ordinal());
       stream.flush();
     }
 
@@ -872,7 +1147,7 @@
     public static void send(BrowserChannel channel, boolean isException,
         Value returnValue) throws IOException {
       final DataOutputStream stream = channel.getStreamToOtherSide();
-      stream.writeByte(MessageType.Return.ordinal());
+      stream.writeByte(MessageType.RETURN.ordinal());
       stream.writeBoolean(isException);
       writeValue(stream, returnValue);
       stream.flush();
@@ -908,7 +1183,56 @@
     }
   }
 
-  public static final int BROWSERCHANNEL_PROTOCOL_VERSION = 1;
+  /**
+   * 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.ordinal());
+      writeUntaggedString(stream, transport);
+      writeUntaggedString(stream, transportArgs);
+    }
+  }
+
+  public static final int BROWSERCHANNEL_PROTOCOL_VERSION = 2;
 
   public static final int SPECIAL_CLIENTMETHODS_OBJECT = 0;
 
@@ -1161,13 +1485,18 @@
   private Socket socket;
 
   public BrowserChannel(Socket socket) throws IOException {
-    streamFromOtherSide = new DataInputStream(new BufferedInputStream(
-        socket.getInputStream()));
-    streamToOtherSide = new DataOutputStream(new BufferedOutputStream(
-        socket.getOutputStream()));
+    this(new BufferedInputStream(socket.getInputStream()),
+        new BufferedOutputStream(socket.getOutputStream()));
     this.socket = socket;
   }
 
+  protected BrowserChannel(InputStream inputStream, OutputStream outputStream)
+      throws IOException {
+    streamFromOtherSide = new DataInputStream(inputStream);
+    streamToOtherSide = new DataOutputStream(outputStream);
+    socket = null;
+  }
+
   public void endSession() {
     Utility.close(streamFromOtherSide);
     Utility.close(streamToOtherSide);
@@ -1209,13 +1538,16 @@
   }
 
   public String getRemoteEndpoint() {
+    if (socket == null) {
+      return "";
+    }
     return socket.getInetAddress().getCanonicalHostName() + ":"
         + socket.getPort();
   }
 
   public Value invoke(String methodName, Value vthis, Value[] vargs,
       SessionHandler handler) throws IOException, BrowserChannelException {
-    new InvokeMessage(this, methodName, vthis, vargs).send();
+    new InvokeOnClientMessage(this, methodName, vthis, vargs).send();
     final ReturnMessage msg = reactToMessagesWhileWaitingForReturn(handler);
     return msg.returnValue;
   }
@@ -1226,19 +1558,19 @@
       getStreamToOtherSide().flush();
       MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
       switch (messageType) {
-        case FreeValue:
+        case FREE_VALUE:
           final FreeMessage freeMsg = FreeMessage.receive(this);
           handler.freeValue(this, freeMsg.getIds());
           break;
-        case Invoke:
-          final InvokeMessage imsg = InvokeMessage.receive(this);
+        case INVOKE:
+          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
           ReturnMessage.send(this, handler.invoke(this, imsg.getThis(),
               imsg.getMethodDispatchId(), imsg.getArgs()));
           break;
-        case InvokeSpecial:
+        case INVOKE_SPECIAL:
           handleInvokeSpecial(handler);
           break;
-        case Quit:
+        case QUIT:
           return;
         default:
           throw new BrowserChannelException("Invalid message type "
@@ -1253,18 +1585,18 @@
       getStreamToOtherSide().flush();
       MessageType messageType = Message.readMessageType(getStreamFromOtherSide());
       switch (messageType) {
-        case FreeValue:
+        case FREE_VALUE:
           final FreeMessage freeMsg = FreeMessage.receive(this);
           handler.freeValue(this, freeMsg.getIds());
           break;
-        case Return:
+        case RETURN:
           return ReturnMessage.receive(this);
-        case Invoke:
-          final InvokeMessage imsg = InvokeMessage.receive(this);
+        case INVOKE:
+          final InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
           ReturnMessage.send(this, handler.invoke(this, imsg.getThis(),
               imsg.getMethodDispatchId(), imsg.getArgs()));
           break;
-        case InvokeSpecial:
+        case INVOKE_SPECIAL:
           handleInvokeSpecial(handler);
           break;
         default:
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
index ab285e0..4b98529 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelServer.java
@@ -82,8 +82,8 @@
       vargs[i] = convertFromJsValue(remoteObjects, args[i]);
     }
     try {
-      InvokeMessage invokeMessage = new InvokeMessage(this, methodName, vthis,
-          vargs);
+      InvokeOnClientMessage invokeMessage = new InvokeOnClientMessage(this,
+          methodName, vthis, vargs);
       invokeMessage.send();
       final ReturnMessage msg = reactToMessagesWhileWaitingForReturn(handler);
       Value returnValue = msg.getReturnValue();
@@ -139,21 +139,7 @@
 
   public void run() {
     try {
-      MessageType type = Message.readMessageType(getStreamFromOtherSide());
-      assert type == MessageType.LoadModule;
-      LoadModuleMessage message = LoadModuleMessage.receive(this);
-      moduleName = message.getModuleName();
-      userAgent = message.getUserAgent();
-      Thread.currentThread().setName(
-          "Hosting " + moduleName + " for " + userAgent);
-      logger = handler.loadModule(logger, this, moduleName, userAgent);
-      try {
-        // send LoadModule response
-        ReturnMessage.send(this, false, new Value());
-        reactToMessages(handler);
-      } finally {
-        handler.unloadModule(this, moduleName);
-      }
+      processConnection();
     } catch (IOException e) {
       logger.log(TreeLogger.WARN, "Client connection lost", e);
     } catch (BrowserChannelException e) {
@@ -265,4 +251,113 @@
         break;
     }
   }
+
+  /**
+   * Create the requested transport and return the appropriate information so
+   * the client can connect to the same transport.
+   * 
+   * @param transport transport name to create
+   * @return transport-specific arguments for the client to use in attaching
+   *     to this transport
+   */
+  private String createTransport(String transport) {
+    // TODO(jat): implement support for additional transports
+    throw new UnsupportedOperationException(
+        "No alternate transports supported");
+  }
+
+  private void processConnection() throws IOException, BrowserChannelException {
+    MessageType type = Message.readMessageType(getStreamFromOtherSide());
+    // TODO(jat): add support for getting the a shim plugin downloading the
+    //    real plugin via a GetRealPlugin message before CheckVersions
+    String url = null;
+    String sessionKey = null;
+    switch (type) {
+      case OLD_LOAD_MODULE:
+        // v1 client
+        OldLoadModuleMessage oldLoadModule = OldLoadModuleMessage.receive(this);
+        if (oldLoadModule.getProtoVersion() != 1) {
+          // This message type was only used in v1, so something is really
+          // broken here.
+          throw new BrowserChannelException(
+              "Old LoadModule message used, but not v1 protocol");
+        }
+        moduleName = oldLoadModule.getModuleName();
+        userAgent = oldLoadModule.getUserAgent();
+        break;
+      case CHECK_VERSIONS:
+        String connectError = null;
+        CheckVersionsMessage hello = CheckVersionsMessage.receive(this);
+        int minVersion = hello.getMinVersion();
+        int maxVersion = hello.getMaxVersion();
+        String hostedHtmlVersion = hello.getHostedHtmlVersion();
+        if (minVersion > BROWSERCHANNEL_PROTOCOL_VERSION
+            || maxVersion < BROWSERCHANNEL_PROTOCOL_VERSION) {
+          connectError = "No supported protocol version in range " + minVersion
+          + " - " + maxVersion;
+        }
+        // TODO(jat): verify hosted.html version
+        if (connectError != null) {
+          logger.log(TreeLogger.ERROR, "Connection error " + connectError, null);
+          new FatalErrorMessage(this, connectError).send();
+          return;
+        }
+        new ProtocolVersionMessage(this, BROWSERCHANNEL_PROTOCOL_VERSION).send();
+        type = Message.readMessageType(getStreamFromOtherSide());
+        
+        // Optionally allow client to request switch of transports.  Inband is
+        // always supported, so a return of an empty transport string requires
+        // the client to stay in this channel.
+        if (type == MessageType.CHOOSE_TRANSPORT) {
+          ChooseTransportMessage chooseTransport = ChooseTransportMessage.receive(this);
+          String transport = selectTransport(chooseTransport.getTransports());
+          String transportArgs = null;
+          if (transport != null) {
+            transportArgs = createTransport(transport);
+          }
+          new SwitchTransportMessage(this, transport, transportArgs).send();
+          type = Message.readMessageType(getStreamFromOtherSide());
+        }
+        
+        // Now we expect a LoadModule message to load a GWT module.
+        if (type != MessageType.LOAD_MODULE) {
+          logger.log(TreeLogger.ERROR, "Unexpected message type " + type
+              + "; expecting LoadModule");
+          return;
+        }
+        LoadModuleMessage loadModule = LoadModuleMessage.receive(this);
+        url = loadModule.getUrl();
+        sessionKey = loadModule.getSessionKey();
+        moduleName = loadModule.getModuleName();
+        userAgent = loadModule.getUserAgent();
+        break;
+      default:
+        logger.log(TreeLogger.ERROR, "Unexpected message type " + type
+            + "; expecting CheckVersions");
+        return;
+    }
+    Thread.currentThread().setName(
+        "Hosting " + moduleName + " for " + userAgent + " on " + url + " @ "
+        + sessionKey);
+    logger = handler.loadModule(logger, this, moduleName, userAgent, url,
+        sessionKey);
+    try {
+      // send LoadModule response
+      ReturnMessage.send(this, false, new Value());
+      reactToMessages(handler);
+    } finally {
+      handler.unloadModule(this, moduleName);
+    }
+  }
+
+  /**
+   * Select a transport from those provided by the client.
+   * 
+   * @param transports array of supported transports
+   * @return null to continue in-band, or a transport type
+   */
+  private String selectTransport(String[] transports) {
+    // TODO(jat): add support for shared memory, others
+    return null;
+  }
 }
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java b/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
index 45ef3bc..1eff0dc 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/OophmSessionHandler.java
@@ -155,13 +155,13 @@
 
   @Override
   public TreeLogger loadModule(TreeLogger logger, BrowserChannel channel,
-      String moduleName, String userAgent) {
+      String moduleName, String userAgent, String url, String sessionKey) {
     try {
       // Attach a new ModuleSpace to make it programmable.
       //
       BrowserChannelServer serverChannel = (BrowserChannelServer) channel;
       ModuleSpaceHost msh = host.createModuleSpaceHost(logger, moduleName,
-          userAgent, channel.getRemoteEndpoint());
+          userAgent, url, sessionKey, channel.getRemoteEndpoint());
       this.logger = logger = msh.getLogger();
       ModuleSpace moduleSpace = new ModuleSpaceOOPHM(msh, moduleName,
           serverChannel);
diff --git a/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java b/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java
new file mode 100644
index 0000000..9f472f2
--- /dev/null
+++ b/dev/oophm/test/com/google/gwt/dev/shell/BrowserChannelTest.java
@@ -0,0 +1,298 @@
+/*
+ * 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.dev.shell;
+
+import com.google.gwt.dev.util.TemporaryBufferStream;
+import com.google.gwt.dev.shell.BrowserChannel.MessageType;
+import com.google.gwt.dev.shell.BrowserChannel.Value;
+import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.SpecialDispatchId;
+import com.google.gwt.dev.shell.BrowserChannel.Value.ValueType;
+import com.google.gwt.dev.shell.BrowserChannel.CheckVersionsMessage;
+import com.google.gwt.dev.shell.BrowserChannel.ChooseTransportMessage;
+import com.google.gwt.dev.shell.BrowserChannel.FatalErrorMessage;
+import com.google.gwt.dev.shell.BrowserChannel.FreeMessage;
+import com.google.gwt.dev.shell.BrowserChannel.InvokeOnClientMessage;
+import com.google.gwt.dev.shell.BrowserChannel.InvokeOnServerMessage;
+import com.google.gwt.dev.shell.BrowserChannel.InvokeSpecialMessage;
+import com.google.gwt.dev.shell.BrowserChannel.JavaObjectRef;
+import com.google.gwt.dev.shell.BrowserChannel.LoadJsniMessage;
+import com.google.gwt.dev.shell.BrowserChannel.LoadModuleMessage;
+import com.google.gwt.dev.shell.BrowserChannel.OldLoadModuleMessage;
+import com.google.gwt.dev.shell.BrowserChannel.ProtocolVersionMessage;
+import com.google.gwt.dev.shell.BrowserChannel.QuitMessage;
+import com.google.gwt.dev.shell.BrowserChannel.ReturnMessage;
+import com.google.gwt.dev.shell.BrowserChannel.SwitchTransportMessage;
+
+import junit.framework.TestCase;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+/**
+ * Test for {@link BrowserChannel}.
+ */
+public class BrowserChannelTest extends TestCase {
+
+  private TemporaryBufferStream bufferStream = new TemporaryBufferStream();
+
+  private class TestBrowserChannel extends BrowserChannel {
+    public TestBrowserChannel(InputStream inputStream,
+        OutputStream outputStream) throws IOException {
+      super(inputStream, outputStream);
+    }
+    
+    public MessageType readMessageType() throws IOException,
+        BrowserChannelException {
+      getStreamToOtherSide().flush();
+      return Message.readMessageType(getStreamFromOtherSide());
+    }
+  }
+  
+  private DataInputStream iStr = new DataInputStream(
+      bufferStream.getInputStream());
+  private DataOutputStream oStr = new DataOutputStream(
+      bufferStream.getOutputStream());
+  private TestBrowserChannel channel;
+
+  @Override
+  protected void setUp() throws Exception {
+    channel = new TestBrowserChannel(bufferStream.getInputStream(),
+        bufferStream.getOutputStream()); 
+  }
+
+  public void testBooleanValue() throws IOException {
+    Value val = new Value();
+    val.setBoolean(true);
+    BrowserChannel.writeValue(oStr, val);
+    val = BrowserChannel.readValue(iStr);
+    assertEquals(ValueType.BOOLEAN, val.getType());
+    assertEquals(true, val.getBoolean());
+    val.setBoolean(false);
+    BrowserChannel.writeValue(oStr, val);
+    val = BrowserChannel.readValue(iStr);
+    assertEquals(ValueType.BOOLEAN, val.getType());
+    assertEquals(false, val.getBoolean());
+  }
+  
+  // TODO(jat): add more tests for Value types
+  
+  public void testCheckVersions() throws IOException, BrowserChannelException {
+    int minVersion = 1;
+    int maxVersion = 2;
+    String hostedHtmlVersion = "2.0";
+    new CheckVersionsMessage(channel, minVersion, maxVersion,
+        hostedHtmlVersion).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.CHECK_VERSIONS, type);
+    CheckVersionsMessage message = CheckVersionsMessage.receive(channel);
+    assertEquals(minVersion, message.getMinVersion());
+    assertEquals(maxVersion, message.getMaxVersion());
+    assertEquals(hostedHtmlVersion, message.getHostedHtmlVersion());
+  }
+  
+  public void testChooseTransport() throws IOException,
+      BrowserChannelException {
+    String[] transports = new String[] { "shm" };
+    new ChooseTransportMessage(channel, transports).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.CHOOSE_TRANSPORT, type);
+    ChooseTransportMessage message = ChooseTransportMessage.receive(channel);
+    String[] transportsRecv = message.getTransports();
+    assertTrue(Arrays.equals(transports, transportsRecv));
+  }
+  
+  public void testFatalErrorMessage() throws IOException,
+      BrowserChannelException {
+    String error = "Fatal error";
+    new FatalErrorMessage(channel, error).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.FATAL_ERROR, type);
+    FatalErrorMessage message = FatalErrorMessage.receive(channel);
+    assertEquals(error, message.getError());
+  }
+  
+  public void testFreeMessage() throws IOException, BrowserChannelException {
+    int[] ids = new int[] { 42, 1024 };
+    new FreeMessage(channel, ids).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.FREE_VALUE, type);
+    FreeMessage message = FreeMessage.receive(channel);
+    int[] idsRecv = message.getIds();
+    assertTrue(Arrays.equals(ids, idsRecv));
+  }
+  
+  public void testInvokeOnClientMessage() throws IOException,
+      BrowserChannelException {
+    String methodName = "fooMethod";
+    Value thisRef = new Value();
+    thisRef.setJavaObject(new JavaObjectRef(42));
+    Value[] args = new Value[] {
+      new Value(), new Value(),  
+    };
+    args[0].setInt(0);
+    args[1].setInt(1);
+    new InvokeOnClientMessage(channel, methodName, thisRef, args).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.INVOKE, type);
+    InvokeOnClientMessage message = InvokeOnClientMessage.receive(channel);
+    assertEquals(methodName, message.getMethodName());
+    Value thisRefRecv = message.getThis();
+    assertEquals(ValueType.JAVA_OBJECT, thisRefRecv.getType());
+    assertEquals(42, thisRefRecv.getJavaObject().getRefid());
+    Value[] argsRecv = message.getArgs();
+    assertEquals(2, argsRecv.length);
+    for (int i = 0; i < 2; ++i) {
+      assertEquals(ValueType.INT, argsRecv[i].getType());
+      assertEquals(i, argsRecv[i].getInt());
+    }
+  }
+  
+  public void testInvokeOnServerMessage() throws IOException,
+      BrowserChannelException {
+    int methodId = -1;
+    Value thisRef = new Value();
+    thisRef.setJavaObject(new JavaObjectRef(42));
+    Value[] args = new Value[] {
+      new Value(), new Value(),  
+    };
+    args[0].setInt(0);
+    args[1].setInt(1);
+    new InvokeOnServerMessage(channel, methodId, thisRef, args).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.INVOKE, type);
+    InvokeOnServerMessage message = InvokeOnServerMessage.receive(channel);
+    assertEquals(methodId, message.getMethodDispatchId());
+    Value thisRefRecv = message.getThis();
+    assertEquals(ValueType.JAVA_OBJECT, thisRefRecv.getType());
+    assertEquals(42, thisRefRecv.getJavaObject().getRefid());
+    Value[] argsRecv = message.getArgs();
+    assertEquals(2, argsRecv.length);
+    for (int i = 0; i < 2; ++i) {
+      assertEquals(ValueType.INT, argsRecv[i].getType());
+      assertEquals(i, argsRecv[i].getInt());
+    }
+  }
+  
+  public void testInvokeSpecialMessage() throws IOException,
+      BrowserChannelException {
+    Value[] args = new Value[] {
+      new Value(), new Value(),  
+    };
+    args[0].setInt(0);
+    args[1].setInt(1);
+    new InvokeSpecialMessage(channel, SpecialDispatchId.HasMethod, args).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.INVOKE_SPECIAL, type);
+    InvokeSpecialMessage message = InvokeSpecialMessage.receive(channel);
+    assertEquals(SpecialDispatchId.HasMethod, message.getDispatchId());
+    Value[] argsRecv = message.getArgs();
+    assertEquals(2, argsRecv.length);
+    for (int i = 0; i < 2; ++i) {
+      assertEquals(ValueType.INT, argsRecv[i].getType());
+      assertEquals(i, argsRecv[i].getInt());
+    }
+  }
+  
+  public void testLoadJsniMessage() throws IOException,
+      BrowserChannelException {
+    String jsni = "function foo() { }";
+    new LoadJsniMessage(channel, jsni).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.LOAD_JSNI, type);
+    LoadJsniMessage message = LoadJsniMessage.receive(channel);
+    assertEquals(jsni, message.getJsni());
+  }
+  
+  public void testLoadModuleMessage() throws IOException,
+      BrowserChannelException {
+    String url = "http://www.google.com";
+    String sessionKey = "asdkfjklAI*23ja";
+    String moduleName = "org.example.Hello";
+    String userAgent = "Firefox";
+    new LoadModuleMessage(channel, url, sessionKey, moduleName,
+        userAgent).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.LOAD_MODULE, type);
+    LoadModuleMessage message = LoadModuleMessage.receive(channel);
+    assertEquals(url, message.getUrl());
+    assertEquals(sessionKey, message.getSessionKey());
+    assertEquals(moduleName, message.getModuleName());
+    assertEquals(userAgent, message.getUserAgent());
+  }
+  
+  public void testOldLoadModuleMessage() throws IOException,
+      BrowserChannelException {
+    int protoVersion = 42;
+    String moduleName = "org.example.Hello";
+    String userAgent = "Firefox";
+    new OldLoadModuleMessage(channel, protoVersion, moduleName,
+        userAgent).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.OLD_LOAD_MODULE, type);
+    OldLoadModuleMessage message = OldLoadModuleMessage.receive(channel);
+    assertEquals(protoVersion, message.getProtoVersion());
+    assertEquals(moduleName, message.getModuleName());
+    assertEquals(userAgent, message.getUserAgent());
+  }
+  
+  public void testProtocolVersionMessage() throws IOException,
+      BrowserChannelException {
+    int protoVersion = 42;
+    new ProtocolVersionMessage(channel, protoVersion).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.PROTOCOL_VERSION, type);
+    ProtocolVersionMessage message = ProtocolVersionMessage.receive(channel);
+    assertEquals(protoVersion, message.getProtocolVersion());
+  }
+  
+  public void testQuitMessage() throws IOException,
+      BrowserChannelException {
+    new QuitMessage(channel).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.QUIT, type);
+    QuitMessage message = QuitMessage.receive(channel);
+  }
+  
+  public void testReturnMessage() throws IOException,
+      BrowserChannelException {
+    Value val = new Value();
+    val.setInt(42);
+    new ReturnMessage(channel, false, val).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.RETURN, type);
+    ReturnMessage message = ReturnMessage.receive(channel);
+    assertFalse(message.isException());
+    Value valRecv = message.getReturnValue();
+    assertEquals(ValueType.INT, valRecv.getType());
+    assertEquals(42, valRecv.getInt());
+  }
+  
+  public void testSwitchTransportMessage() throws IOException,
+      BrowserChannelException {
+    String transport = "shm";
+    String transportArgs = "17021";
+    new SwitchTransportMessage(channel, transport, transportArgs).send();
+    MessageType type = channel.readMessageType();
+    assertEquals(MessageType.SWITCH_TRANSPORT, type);
+    SwitchTransportMessage message = SwitchTransportMessage.receive(channel);
+    assertEquals(transport, message.getTransport());
+    assertEquals(transportArgs, message.getTransportArgs());
+  }
+}