Commiting the initial version of the HtmlUnit plugin. Many tests, including all
of EmulSuite, pass. Remaining work:
(i) Test with other GwtTests and fix.
(ii) Re-factor the code.
(iii) Garbage collection of javaObject references.
(iv) Minor todos like implementation of toString.
Patch by: amitmanjhi
Review (and some pair-programming) by: jat (TBR)
git-svn-id: https://google-web-toolkit.googlecode.com/svn/branches/farewellSwt@6119 8db76d5a-ed1c-0410-87a9-c151d255dfc7
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 e76ea89..99629c7 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java
@@ -69,11 +69,16 @@
* Class representing a reference to a JS object.
*/
public static class JsObjectRef {
- private int refId;
-
- public JsObjectRef(int refId) {
+
+ // TODO: refactor and remove this method.
+ public static void checkIdMap(int refId) {
assert !JSOBJECT_ID_MAP.get().containsKey(refId)
- || (JSOBJECT_ID_MAP.get().get(refId).get() == null);
+ || (JSOBJECT_ID_MAP.get().get(refId).get() == null);
+ }
+
+ private int refId;
+
+ public JsObjectRef(int refId) {
this.refId = refId;
}
@@ -1325,6 +1330,7 @@
}
}
+ JsObjectRef.checkIdMap(refId);
JsObjectRef toReturn = new JsObjectRef(refId);
Reference<JsObjectRef> ref = new WeakReference<JsObjectRef>(toReturn,
JSOBJECT_REF_QUEUE.get());
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java
new file mode 100644
index 0000000..4713e95
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java
@@ -0,0 +1,201 @@
+/*
+ * 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.core.ext.TreeLogger;
+import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+
+import java.io.IOException;
+import java.net.Socket;
+
+/**
+ * Implementation of the BrowserChannel for the client side.
+ *
+ */
+public class BrowserChannelClient extends BrowserChannel {
+
+ private static final int PROTOCOL_VERSION = 2;
+ private final HtmlUnitSessionHandler htmlUnitSessionHandler;
+ private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
+ private final String moduleName;
+ private final String tabKey;
+ private final String sessionKey;
+ private final String url;
+ private final String versionString;
+ private boolean connected = false;
+
+ public BrowserChannelClient(String addressParts[], String url,
+ String sessionKey, String moduleName, String versionString,
+ HtmlUnitSessionHandler htmlUnitSessionHandler) throws IOException {
+ super(new Socket(addressParts[0], Integer.parseInt(addressParts[1])));
+ connected = true;
+ this.url = url;
+ this.sessionKey = sessionKey;
+ this.moduleName = moduleName;
+ this.tabKey = ""; // TODO(jat): update when tab support is added.
+ this.versionString = versionString;
+ logger.setMaxDetail(TreeLogger.WARN);
+ logger.log(TreeLogger.SPAM, "BrowserChannelClient, versionString: "
+ + versionString);
+ this.htmlUnitSessionHandler = htmlUnitSessionHandler;
+ }
+
+ public boolean disconnectFromHost() throws IOException {
+ logger.log(TreeLogger.DEBUG, "disconnecting channel " + this);
+ if (!isConnected()) {
+ logger.log(TreeLogger.DEBUG,
+ "Disconnecting already disconnected channel " + this);
+ return false;
+ }
+ new QuitMessage(this).send();
+ endSession();
+ connected = false;
+ return true;
+ }
+
+ public boolean isConnected() {
+ return connected;
+ }
+
+ // TODO (amitmanjhi): refer the state (message?) transition diagram
+ /**
+ * returns true iff execution completes normally.
+ */
+ public boolean process() throws IOException, BrowserChannelException {
+ if (!init()) {
+ disconnectFromHost();
+ return false;
+ }
+ logger.log(TreeLogger.DEBUG, "sending " + MessageType.LOAD_MODULE
+ + " message, userAgent: " + htmlUnitSessionHandler.getUserAgent());
+ ReturnMessage returnMessage = null;
+ synchronized (htmlUnitSessionHandler.getHtmlPage()) {
+ new LoadModuleMessage(this, url, tabKey, sessionKey, moduleName,
+ htmlUnitSessionHandler.getUserAgent()).send();
+ returnMessage = reactToMessages(htmlUnitSessionHandler, true);
+ }
+ logger.log(TreeLogger.DEBUG, "loaded module, returnValue: "
+ + returnMessage.getReturnValue() + ", isException: "
+ + returnMessage.isException());
+ return !returnMessage.isException();
+ }
+
+ public ReturnMessage reactToMessagesWhileWaitingForReturn(
+ HtmlUnitSessionHandler handler) throws IOException,
+ BrowserChannelException {
+ return reactToMessages(handler, true);
+ }
+
+ /*
+ * Perform the initial interaction. Return true if interaction succeeds, false
+ * if it fails. Do a check protocol versions, expected with 2.0+ oophm
+ * protocol.
+ */
+ private boolean init() throws IOException, BrowserChannelException {
+ logger.log(TreeLogger.DEBUG, "sending " + MessageType.CHECK_VERSIONS
+ + " message");
+ new CheckVersionsMessage(this, PROTOCOL_VERSION, PROTOCOL_VERSION,
+ versionString).send();
+ MessageType type = Message.readMessageType(getStreamFromOtherSide());
+ switch (type) {
+ case PROTOCOL_VERSION:
+ ProtocolVersionMessage protocolMessage = ProtocolVersionMessage.receive(this);
+ logger.log(TreeLogger.DEBUG, MessageType.PROTOCOL_VERSION
+ + ": protocol version = " + protocolMessage.getProtocolVersion());
+ // TODO(jat) : save selected protocol version when a range is supported.
+ break;
+ case FATAL_ERROR:
+ FatalErrorMessage errorMessage = FatalErrorMessage.receive(this);
+ logger.log(TreeLogger.ERROR, "Received FATAL_ERROR message "
+ + errorMessage.getError());
+ return false;
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ private ReturnMessage reactToMessages(
+ HtmlUnitSessionHandler htmlUnitSessionHandler, boolean expectReturn)
+ throws IOException, BrowserChannelException {
+ while (true) {
+ ExceptionOrReturnValue returnValue;
+ MessageType type = Message.readMessageType(getStreamFromOtherSide());
+ logger.log(TreeLogger.INFO, "client: received " + type + ", thread: "
+ + Thread.currentThread().getName());
+ try {
+ switch (type) {
+ case INVOKE:
+ InvokeOnClientMessage invokeMessage = InvokeOnClientMessage.receive(this);
+ returnValue = htmlUnitSessionHandler.invoke(this,
+ invokeMessage.getThis(), invokeMessage.getMethodName(),
+ invokeMessage.getArgs());
+ htmlUnitSessionHandler.sendFreeValues(this);
+ new ReturnMessage(this, returnValue.isException(),
+ returnValue.getReturnValue()).send();
+ break;
+ case INVOKE_SPECIAL:
+ InvokeSpecialMessage invokeSpecialMessage = InvokeSpecialMessage.receive(this);
+ logger.log(TreeLogger.DEBUG, type + " message " + ", thisRef: "
+ + invokeSpecialMessage.getArgs());
+ returnValue = htmlUnitSessionHandler.invokeSpecial(this,
+ invokeSpecialMessage.getDispatchId(),
+ invokeSpecialMessage.getArgs());
+ htmlUnitSessionHandler.sendFreeValues(this);
+ new ReturnMessage(this, returnValue.isException(),
+ returnValue.getReturnValue()).send();
+ break;
+ case FREE_VALUE:
+ FreeMessage freeMessage = FreeMessage.receive(this);
+ logger.log(TreeLogger.DEBUG, type + " message "
+ + freeMessage.getIds());
+ htmlUnitSessionHandler.freeValue(this, freeMessage.getIds());
+ // no response
+ break;
+ case LOAD_JSNI:
+ LoadJsniMessage loadJsniMessage = LoadJsniMessage.receive(this);
+ String jsniString = loadJsniMessage.getJsni();
+ htmlUnitSessionHandler.loadJsni(this, jsniString);
+ // no response
+ break;
+ case RETURN:
+ if (!expectReturn) {
+ logger.log(TreeLogger.ERROR, "Received unexpected "
+ + MessageType.RETURN);
+ }
+ return ReturnMessage.receive(this);
+ case QUIT:
+ if (expectReturn) {
+ logger.log(TreeLogger.ERROR, "Received " + MessageType.QUIT
+ + " while waiting for return");
+ }
+ disconnectFromHost();
+ return null;
+ default:
+ logger.log(TreeLogger.ERROR, "Unkown messageType: " + type
+ + ", expectReturn: " + expectReturn);
+ disconnectFromHost();
+ return null;
+ }
+ } catch (Exception ex) {
+ logger.log(TreeLogger.ERROR, "Unknown exception" + ex);
+ ex.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java b/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java
new file mode 100644
index 0000000..702d408
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java
@@ -0,0 +1,188 @@
+/*
+ * 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.gargoylesoftware.htmlunit.javascript.host.Window;
+
+import net.sourceforge.htmlunit.corejs.javascript.Context;
+import net.sourceforge.htmlunit.corejs.javascript.Function;
+import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
+import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+
+import java.io.IOException;
+
+/**
+ * HTMLUnit object that represents the hosted-mode plugin.
+ */
+public class HostedModePluginObject extends ScriptableObject {
+
+ /**
+ * Function object which implements the connect method on the hosted-mode
+ * plugin.
+ */
+ private class ConnectMethod extends ScriptableObject implements Function {
+
+ private static final long serialVersionUID = -8799481412144205779L;
+ private static final int EXPECTED_NUM_ARGS = 5;
+
+ public Object call(Context context, Scriptable scope, Scriptable thisObj,
+ Object[] args) {
+ // Allow extra arguments for forward compatibility
+ if (args.length < EXPECTED_NUM_ARGS) {
+ throw Context.reportRuntimeError("Bad number of parameters for function"
+ + " connect: expected "
+ + EXPECTED_NUM_ARGS
+ + ", got "
+ + args.length);
+ }
+ try {
+ /*
+ * connect arguments: url, sessionKey, ipAddress:port, moduleName,
+ * hostedHtmlVersion
+ */
+ return connect((String) args[0], (String) args[1], (String) args[2],
+ (String) args[3], (String) args[4]);
+ } catch (ClassCastException e) {
+ throw Context.reportRuntimeError("Incorrect parameter types for "
+ + " connect: expected String/String/String/String/String");
+ }
+ }
+
+ public Scriptable construct(Context context, Scriptable scope, Object[] args) {
+ throw Context.reportRuntimeError("Function connect can't be used as a "
+ + "constructor");
+ }
+
+ @Override
+ public String getClassName() {
+ return "function HostedModePluginObject.connect";
+ }
+ }
+
+ /**
+ * Function object which implements the init method on the hosted-mode plugin.
+ */
+ private class InitMethod extends ScriptableObject implements Function {
+
+ private static final long serialVersionUID = -8799481412144205779L;
+ private static final String VERSION = "2.0";
+
+ public Object call(Context context, Scriptable scope, Scriptable thisObj,
+ Object[] args) {
+ // Allow extra arguments for forward compatibility
+ if (args.length < 1) {
+ throw Context.reportRuntimeError("Bad number of parameters for function"
+ + " init: expected 1, got " + args.length);
+ }
+ try {
+ window = (Window) args[0];
+ // TODO (amitmanjhi): what checking needs to be done here for window?
+ return init(VERSION);
+ } catch (ClassCastException e) {
+ throw Context.reportRuntimeError("Incorrect parameter types for "
+ + " initt: expected String");
+ }
+ }
+
+ public Scriptable construct(Context context, Scriptable scope, Object[] args) {
+ throw Context.reportRuntimeError("Function init can't be used as a "
+ + "constructor");
+ }
+
+ @Override
+ public String getClassName() {
+ return "function HostedModePluginObject.init";
+ }
+ }
+
+ private static final long serialVersionUID = -1815031145376726799L;
+
+ private Scriptable connectMethod;
+ private Scriptable initMethod;
+ private Window window;
+
+ /**
+ * Initiate a hosted mode connection to the requested port and load the
+ * requested module.
+ *
+ * @param url the complete url
+ * @param sessionKey a length 16 string to identify a "session"
+ * @param address "host:port" or "ipAddress:port" to use for the OOPHM server
+ * @param module module name to load
+ * @param version version string
+ * @return true if the connection succeeds
+ */
+ public boolean connect(String url, String sessionKey, String address,
+ String module, String version) {
+ String addressParts[] = address.split(":");
+ if (addressParts.length < 2) {
+ return false;
+ }
+ // TODO: add whitelist and default-port support?
+ System.out.println("connect(url=" + url + ", sessionKey=" + sessionKey
+ + ", address=" + address + ", module=" + module + ", version="
+ + version + "), window=" + System.identityHashCode(window) + ")");
+
+ try {
+ HtmlUnitSessionHandler htmlUnitSessionHandler = new HtmlUnitSessionHandler(window);
+ BrowserChannelClient browserChannelClient = new BrowserChannelClient(
+ addressParts, url, sessionKey, module, version,
+ htmlUnitSessionHandler);
+ htmlUnitSessionHandler.setSessionData(new SessionData(
+ htmlUnitSessionHandler, browserChannelClient));
+ return browserChannelClient.process();
+ } catch (BrowserChannelException e) {
+ e.printStackTrace();
+ return false;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public Object get(String name, Scriptable start) {
+ if ("connect".equals(name)) {
+ if (connectMethod == null) {
+ connectMethod = new ConnectMethod();
+ }
+ return connectMethod;
+ } else if ("init".equals(name)) {
+ if (initMethod == null) {
+ initMethod = new InitMethod();
+ }
+ return initMethod;
+ }
+ return NOT_FOUND;
+ }
+
+ @Override
+ public String getClassName() {
+ return "HostedModePluginObject";
+ }
+
+ /**
+ * Verify that the plugin can be initialized properly and supports the
+ * requested version.
+ *
+ * @param version hosted mode protocol version
+ * @return true if initialization succeeds, otherwise false
+ */
+ public boolean init(String version) {
+ // TODO: what needs to be done here?
+ return true;
+ }
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java b/dev/oophm/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
new file mode 100644
index 0000000..201dafb
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java
@@ -0,0 +1,290 @@
+/*
+ * 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.core.ext.TreeLogger;
+import com.google.gwt.dev.shell.BrowserChannel.JavaObjectRef;
+import com.google.gwt.dev.shell.BrowserChannel.JsObjectRef;
+import com.google.gwt.dev.shell.BrowserChannel.SessionHandler;
+import com.google.gwt.dev.shell.BrowserChannel.Value;
+import com.google.gwt.dev.shell.BrowserChannel.Value.ValueType;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+
+import com.gargoylesoftware.htmlunit.ScriptResult;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
+import com.gargoylesoftware.htmlunit.javascript.host.Window;
+
+import net.sourceforge.htmlunit.corejs.javascript.Context;
+import net.sourceforge.htmlunit.corejs.javascript.Function;
+import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
+import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+import net.sourceforge.htmlunit.corejs.javascript.Undefined;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handle session tasks for HtmlUnit. TODO (amitmanjhi): refactor
+ * SessionHandler.
+ */
+public class HtmlUnitSessionHandler extends SessionHandler {
+
+ Map<Integer, JavaObject> javaObjectCache;
+ int nextId;
+
+ /**
+ * The htmlPage is also used to synchronize calls to Java code.
+ */
+ private HtmlPage htmlPage;
+
+ private Set<Integer> javaObjectsToFree;
+ private JavaScriptEngine jsEngine;
+ private IdentityHashMap<Scriptable, Integer> jsObjectToRef;
+
+ private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
+
+ private int nextRefId = 1;
+ private Map<Integer, Scriptable> refToJsObject;
+
+ private SessionData sessionData;
+ private final Window window;
+
+ HtmlUnitSessionHandler(Window window) {
+ this.window = window;
+ logger.setMaxDetail(TreeLogger.ERROR);
+ jsEngine = this.window.getJavaScriptEngine();
+ htmlPage = (HtmlPage) this.window.getWebWindow().getEnclosedPage();
+ logger.log(TreeLogger.INFO, "jsEngine = " + jsEngine + ", HtmlPage = "
+ + htmlPage);
+
+ jsObjectToRef = new IdentityHashMap<Scriptable, Integer>();
+ javaObjectsToFree = new HashSet<Integer>();
+ nextRefId = 1;
+ refToJsObject = new HashMap<Integer, Scriptable>();
+
+ // related to JavaObject cache.
+ nextId = 1; // skipping zero, reserved.
+ javaObjectCache = new HashMap<Integer, JavaObject>();
+ }
+
+ @Override
+ public void freeValue(BrowserChannel channel, int[] ids) {
+ for (int id : ids) {
+ Scriptable scriptable = refToJsObject.remove(id);
+ if (scriptable != null) {
+ jsObjectToRef.remove(scriptable);
+ }
+ }
+ }
+
+ public HtmlPage getHtmlPage() {
+ return htmlPage;
+ }
+
+ public JavaObject getOrCreateJavaObject(int refId, Context context) {
+ JavaObject javaObject = javaObjectCache.get(refId);
+ if (javaObject == null) {
+ javaObject = new JavaObject(context, sessionData, nextId++);
+ javaObjectCache.put(refId, javaObject);
+ }
+ return javaObject;
+ }
+
+ @Override
+ public ExceptionOrReturnValue getProperty(BrowserChannel channel, int refId,
+ int dispId) {
+ throw new UnsupportedOperationException(
+ "getProperty should not be called on the client-side");
+ }
+
+ public String getUserAgent() {
+ return "HtmlUnit-"
+ + jsEngine.getWebClient().getBrowserVersion().getUserAgent();
+ }
+
+ @Override
+ public ExceptionOrReturnValue invoke(BrowserChannel channel, Value thisObj,
+ int dispId, Value[] args) {
+ throw new UnsupportedOperationException(
+ "should not be called on the client side");
+ }
+
+ public ExceptionOrReturnValue invoke(BrowserChannel channel, Value thisObj,
+ String methodName, Value[] args) {
+ logger.log(TreeLogger.DEBUG, "INVOKE: thisObj: " + thisObj
+ + ", methodName: " + methodName + ", args: " + args);
+ /*
+ * 1. lookup functions by name. 2. Find context and scope. 3. Convert
+ * thisObject to ScriptableObject 4. Convert args 5. Get return value
+ */
+ Context jsContext = Context.getCurrentContext();
+ ScriptableObject jsThis;
+ if (thisObj.getType() == ValueType.NULL) {
+ jsThis = (ScriptableObject) window;
+ } else {
+ jsThis = (ScriptableObject) makeJsvalFromValue(jsContext, thisObj);
+ }
+ Object functionObject = ScriptableObject.getProperty(
+ (ScriptableObject) window, methodName);
+ if (functionObject == ScriptableObject.NOT_FOUND) {
+ logger.log(TreeLogger.ERROR, "function " + methodName
+ + " NOT FOUND, thisObj: " + jsThis + ", methodName: " + methodName);
+ // TODO: see if this maps to QUIT
+ return new ExceptionOrReturnValue(true, new Value(null));
+ }
+ Function jsFunction = (Function) functionObject;
+ logger.log(TreeLogger.SPAM, "INVOKE: jsFunction: " + jsFunction);
+
+ Object jsArgs[] = new Object[args.length];
+ for (int i = 0; i < args.length; i++) {
+ jsArgs[i] = makeJsvalFromValue(jsContext, args[i]);
+ }
+ Object result = null;
+ try {
+ result = jsEngine.callFunction(htmlPage, jsFunction, jsContext, window,
+ jsThis, jsArgs);
+ } catch (Exception ex) {
+ logger.log(TreeLogger.ERROR, "INVOKE: exception " + ex + ", message: "
+ + ex.getMessage() + " when invoking " + methodName);
+ return new ExceptionOrReturnValue(true, makeValueFromJsval(jsContext,
+ Undefined.instance));
+ }
+ logger.log(TreeLogger.INFO, "INVOKE: result: " + result
+ + " of jsFunction: " + jsFunction);
+ return new ExceptionOrReturnValue(false, makeValueFromJsval(jsContext,
+ result));
+ }
+
+ public ExceptionOrReturnValue invokeSpecial(BrowserChannel channel,
+ SpecialDispatchId specialDispatchId, Value[] args) {
+ throw new UnsupportedOperationException(
+ "InvokeSpecial must not be called on the client side");
+ }
+
+ public void loadJsni(BrowserChannel channel, String jsniString) {
+ logger.log(TreeLogger.SPAM, "LOAD_JSNI: " + jsniString);
+ ScriptResult scriptResult = htmlPage.executeJavaScript(jsniString);
+ logger.log(TreeLogger.INFO, "LOAD_JSNI: scriptResult=" + scriptResult);
+ }
+
+ @Override
+ public TreeLogger loadModule(TreeLogger logger, BrowserChannel channel,
+ String moduleName, String userAgent, String url, String tabKey,
+ String sessionKey) {
+ throw new UnsupportedOperationException("loadModule must not be called");
+ }
+
+ public Value makeValueFromJsval(Context jsContext, Object value) {
+ if (value == Undefined.instance) {
+ return new Value();
+ }
+ if (value instanceof JavaObject) {
+ Value returnVal = new Value();
+ int refId = ((JavaObject) value).getRefId();
+ returnVal.setJavaObject(new JavaObjectRef(refId));
+ return returnVal;
+ }
+ if (value instanceof Scriptable) {
+ Integer refId = jsObjectToRef.get((Scriptable) value);
+ if (refId == null) {
+ refId = nextRefId++;
+ jsObjectToRef.put((Scriptable) value, refId);
+ refToJsObject.put(refId, (Scriptable) value);
+ }
+ Value returnVal = new Value();
+ returnVal.setJsObject(new JsObjectRef(refId));
+ return returnVal;
+ }
+ return new Value(value);
+ }
+
+ // TODO: check synchronization and multi-threading
+ public void sendFreeValues(BrowserChannel channel) {
+ int size = javaObjectsToFree.size();
+ if (size == 0) {
+ return;
+ }
+ int ids[] = new int[size];
+ int index = 0;
+ for (int id : javaObjectsToFree) {
+ ids[index++] = id;
+ }
+ if (ServerMethods.freeJava(channel, this, ids)) {
+ javaObjectsToFree.clear();
+ }
+ }
+
+ @Override
+ public ExceptionOrReturnValue setProperty(BrowserChannel channel, int refId,
+ int dispId, Value newValue) {
+ throw new UnsupportedOperationException(
+ "setProperty should not be called on the client-side");
+ }
+
+ public void setSessionData(SessionData sessionData) {
+ this.sessionData = sessionData;
+ }
+
+ @Override
+ public void unloadModule(BrowserChannel channel, String moduleName) {
+ throw new UnsupportedOperationException("unloadModule must not be called");
+ }
+
+ /*
+ * Returning java objects works. No need to return NativeNumber, NativeString,
+ * NativeBoolean, or Undefined.
+ */
+ Object makeJsvalFromValue(Context jsContext, Value value) {
+ switch (value.getType()) {
+ case NULL:
+ return null;
+ case BOOLEAN:
+ if (value.getBoolean()) {
+ return Boolean.TRUE;
+ }
+ return Boolean.FALSE;
+ case BYTE:
+ return new Byte(value.getByte());
+ case CHAR:
+ return new Character(value.getChar());
+ case SHORT:
+ return new Short(value.getShort());
+ case INT:
+ return new Integer(value.getInt());
+ case FLOAT:
+ return new Float(value.getFloat());
+ case DOUBLE:
+ return new Double(value.getDouble());
+ case STRING:
+ return value.getString();
+ case JAVA_OBJECT:
+ JavaObjectRef javaRef = value.getJavaObject();
+ return JavaObject.getOrCreateJavaObject(javaRef, sessionData, jsContext);
+ case JS_OBJECT:
+ Scriptable scriptable = refToJsObject.get(value.getJsObject().getRefid());
+ assert scriptable != null;
+ return scriptable;
+ case UNDEFINED:
+ return Undefined.instance;
+ }
+ return null;
+ }
+
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/JavaObject.java b/dev/oophm/src/com/google/gwt/dev/shell/JavaObject.java
new file mode 100644
index 0000000..c400c28
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/JavaObject.java
@@ -0,0 +1,170 @@
+/*
+ * 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.InvokeOnServerMessage;
+import com.google.gwt.dev.shell.BrowserChannel.JavaObjectRef;
+import com.google.gwt.dev.shell.BrowserChannel.ReturnMessage;
+import com.google.gwt.dev.shell.BrowserChannel.Value;
+
+import net.sourceforge.htmlunit.corejs.javascript.Context;
+import net.sourceforge.htmlunit.corejs.javascript.Function;
+import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
+import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+import net.sourceforge.htmlunit.corejs.javascript.Undefined;
+
+import java.io.IOException;
+
+/**
+ * Class to encapsulate JavaObject on the server side.
+ */
+public class JavaObject extends ScriptableObject implements Function {
+
+ private static final long serialVersionUID = -7923090130737830902L;
+
+ public static JavaObject getOrCreateJavaObject(JavaObjectRef javaRef,
+ SessionData sessionData, Context context) {
+ return sessionData.getSessionHandler().getOrCreateJavaObject(
+ javaRef.getRefid(), context);
+ }
+
+ static boolean isJavaObject(Context jsContext, ScriptableObject javaObject) {
+ return javaObject instanceof JavaObject;
+ }
+
+ private Context jsContext;
+
+ private final int objectRef;
+
+ private final SessionData sessionData;
+
+ public JavaObject(Context jsContext, SessionData sessionData, int objectRef) {
+ this.objectRef = objectRef;
+ this.sessionData = sessionData;
+ this.jsContext = jsContext;
+ }
+
+ /*
+ * If this function fails for any reason, we return Undefined instead of
+ * throwing an Exception in all cases except when Java throws an Exception.
+ */
+ public Object call(Context cx, Scriptable scope, Scriptable thisObj,
+ Object[] args) {
+
+ if (args.length < 2) {
+ return Undefined.instance;
+ }
+ Value valueArgs[] = new Value[args.length - 2];
+ for (int i = 0; i < valueArgs.length; i++) {
+ valueArgs[i] = sessionData.getSessionHandler().makeValueFromJsval(cx,
+ args[i + 2]);
+ }
+
+ /**
+ * Called when the JavaObject is invoked as a function. We ignore the
+ * thisObj argument, which is usually the window object.
+ *
+ * Returns a JS array, with the first element being a boolean indicating
+ * that an exception occured, and the second element is either the return
+ * value or the exception which was thrown. In this case, we always return
+ * false and raise the exception ourselves.
+ */
+
+ Value thisValue = sessionData.getSessionHandler().makeValueFromJsval(cx,
+ args[1]);
+ int dispatchId = ((Number) args[0]).intValue();
+
+ ReturnMessage returnMessage = null;
+ synchronized (sessionData.getSessionHandler().getHtmlPage()) {
+ try {
+ new InvokeOnServerMessage(sessionData.getChannel(), dispatchId,
+ thisValue, valueArgs).send();
+ } catch (IOException e) {
+ return Undefined.instance;
+ }
+ try {
+ returnMessage = ((BrowserChannelClient) sessionData.getChannel()).reactToMessagesWhileWaitingForReturn(sessionData.getSessionHandler());
+ } catch (IOException e) {
+ return Undefined.instance;
+ } catch (BrowserChannelException e) {
+ return Undefined.instance;
+ }
+ }
+ Value returnValue = returnMessage.getReturnValue();
+ if (returnMessage.isException()) {
+ throw new RuntimeException("JavaObject.call failed, returnMessage: "
+ + returnValue.toString());
+ }
+ /*
+ * Return a object array ret. ret[0] is a boolean indicating whether an
+ * exception was thrown or not. ret[1] is the exception or the return value.
+ */
+ Object ret[] = new Object[2];
+ ret[0] = Boolean.FALSE;
+ ret[1] = sessionData.getSessionHandler().makeJsvalFromValue(cx, returnValue);
+ return ret;
+ }
+
+ public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
+ throw Context.reportRuntimeError("JavaObject can't be used as a "
+ + "constructor");
+ }
+
+ // ignoring the 'start' argument.
+ @Override
+ public Object get(String name, Scriptable start) {
+ if ("toString".equals(name)) {
+ return sessionData.getToStringTearOff();
+ }
+ if ("id".equals(name)) {
+ return objectRef;
+ }
+ if ("__noSuchMethod__".equals(name)) {
+ return Undefined.instance;
+ }
+ System.err.println("Unknown property name in get " + name);
+ return Undefined.instance;
+ }
+
+ // ignoring the 'start' argument.
+ @Override
+ public Object get(int index, Scriptable start) {
+ Value value = ServerMethods.getProperty(sessionData.getChannel(),
+ sessionData.getSessionHandler(), objectRef, index);
+ return sessionData.getSessionHandler().makeJsvalFromValue(jsContext, value);
+ }
+
+ @Override
+ public String getClassName() {
+ return "Class JavaObject";
+ }
+
+ @Override
+ public void put(int dispatchId, Scriptable start, Object value) {
+ HtmlUnitSessionHandler sessionHandler = sessionData.getSessionHandler();
+ if (!ServerMethods.setProperty(sessionData.getChannel(), sessionHandler,
+ objectRef, dispatchId, sessionHandler.makeValueFromJsval(jsContext,
+ value))) {
+ // TODO: fix later.
+ throw new RuntimeException("setProperty failed");
+ }
+ }
+
+ int getRefId() {
+ return objectRef;
+ }
+
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java b/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
index 4ab0c77..0e3d7c5 100644
--- a/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
+++ b/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java
@@ -144,6 +144,7 @@
* @param jsRefId pointer to underlying JsRootedValue as an integer.
*/
public JsValueOOPHM(int jsRefId) {
+ JsObjectRef.checkIdMap(jsRefId);
this.value = new JsObjectRef(jsRefId);
}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/ServerMethods.java b/dev/oophm/src/com/google/gwt/dev/shell/ServerMethods.java
new file mode 100644
index 0000000..e1051d3
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/ServerMethods.java
@@ -0,0 +1,117 @@
+/*
+ * 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.shell.BrowserChannel.FreeMessage;
+import com.google.gwt.dev.shell.BrowserChannel.InvokeSpecialMessage;
+import com.google.gwt.dev.shell.BrowserChannel.ReturnMessage;
+import com.google.gwt.dev.shell.BrowserChannel.Value;
+import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.SpecialDispatchId;
+
+import java.io.IOException;
+
+/**
+ * A class to encapsulate function invocations of objects on the server side.
+ */
+public class ServerMethods {
+ /**
+ * Tell the server that the client no longer has any references to the
+ * specified Java object.
+ *
+ * @param ids ID of object to free
+ * @return false if an error occurred
+ */
+ static boolean freeJava(BrowserChannel channel,
+ HtmlUnitSessionHandler handler, int ids[]) {
+ if (!((BrowserChannelClient) channel).isConnected()) {
+ // ignoring freeJava after disconnect.
+ return true;
+ }
+ try {
+ new FreeMessage(channel, ids).send();
+ } catch (IOException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Get the value of a property on an object.
+ *
+ * @param objectRef ID of object to fetch field on
+ * @param dispatchId dispatch ID of field
+ * @return the value of the property, undef if none (or on error)
+ */
+ static Value getProperty(BrowserChannel channel,
+ HtmlUnitSessionHandler handler, int objectRef, int dispatchId) {
+ if (!((BrowserChannelClient) channel).isConnected()) {
+ // ignoring getProperty() after disconnect
+ return new Value();
+ }
+ Value args[] = new Value[2];
+ args[0] = new Value();
+ args[0].setInt(objectRef);
+ args[1] = new Value();
+ args[1].setInt(dispatchId);
+
+ synchronized (handler.getHtmlPage()) {
+ try {
+ new InvokeSpecialMessage(channel, SpecialDispatchId.GetProperty, args).send();
+ // TODO: refactor in order to remove cast.
+ ReturnMessage returnMessage = ((BrowserChannelClient) channel).reactToMessagesWhileWaitingForReturn(handler);
+ if (!returnMessage.isException()) {
+ return returnMessage.getReturnValue();
+ }
+ } catch (IOException e) {
+ } catch (BrowserChannelException e) {
+ }
+ }
+ return new Value();
+ }
+
+ /**
+ * Set the value of a property on an object.
+ *
+ * @param objectRef ID of object to fetch field on
+ * @param dispatchId dispatch ID of field
+ * @param value value to store in the property
+ * @return false if an error occurred
+ */
+ static boolean setProperty(BrowserChannel channel,
+ HtmlUnitSessionHandler handler, int objectRef, int dispatchId, Value value) {
+ Value args[] = new Value[3];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = new Value();
+ }
+ args[0].setInt(objectRef);
+ args[1].setInt(dispatchId);
+ args[2] = value;
+ synchronized (handler.getHtmlPage()) {
+ try {
+ new InvokeSpecialMessage(channel, SpecialDispatchId.SetProperty, args).send();
+ ReturnMessage returnMessage = ((BrowserChannelClient) channel).reactToMessagesWhileWaitingForReturn(handler);
+ if (!returnMessage.isException()) {
+ return true;
+ }
+ } catch (IOException e) {
+ } catch (BrowserChannelException e) {
+ }
+ }
+ // TODO: use the returned exception?
+ return false;
+ }
+
+}
diff --git a/dev/oophm/src/com/google/gwt/dev/shell/SessionData.java b/dev/oophm/src/com/google/gwt/dev/shell/SessionData.java
new file mode 100644
index 0000000..25428f2
--- /dev/null
+++ b/dev/oophm/src/com/google/gwt/dev/shell/SessionData.java
@@ -0,0 +1,42 @@
+/*
+ * 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;
+
+/**
+ * A class to encapsulate data needed by {@code JavaObject}.
+ */
+public class SessionData {
+ private final BrowserChannel browserChannel;
+ private final HtmlUnitSessionHandler sessionHandler;
+
+ public SessionData(HtmlUnitSessionHandler sessionHandler, BrowserChannel browserChannel) {
+ this.sessionHandler = sessionHandler;
+ this.browserChannel = browserChannel;
+ }
+
+ public BrowserChannel getChannel() {
+ return browserChannel;
+ }
+
+ public HtmlUnitSessionHandler getSessionHandler() {
+ return sessionHandler;
+ }
+
+ public Object getToStringTearOff() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/eclipse/dev/oophm/.classpath b/eclipse/dev/oophm/.classpath
index ab0e3f1..dc7e14f 100644
--- a/eclipse/dev/oophm/.classpath
+++ b/eclipse/dev/oophm/.classpath
@@ -4,6 +4,8 @@
<classpathentry kind="src" path="oophm/overlay"/>
<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/sun/swingworker/swing-worker-1.1.jar" sourcepath="/GWT_TOOLS/lib/sun/swingworker/swing-worker-1.1-src.zip"/>
+ <classpathentry kind="var" path="GWT_TOOLS/lib/htmlunit/htmlunit-core-js-2.5.jar" sourcepath="/GWT_TOOLS/lib/htmlunit/htmlunit-core-js-2.5-sources.jar"/>
+ <classpathentry kind="var" path="GWT_TOOLS/lib/htmlunit/htmlunit-2.5.jar" sourcepath="/GWT_TOOLS/lib/htmlunit/htmlunit-2.5-sources.jar"/>
<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-windows"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath
index 37be02a..35baf69 100644
--- a/eclipse/user/.classpath
+++ b/eclipse/user/.classpath
@@ -24,6 +24,7 @@
<classpathentry kind="var" path="GWT_TOOLS/lib/xerces/xerces-2_9_1/xml-apis.jar" />
<classpathentry kind="var" path="GWT_TOOLS/lib/w3c/sac/sac-1.3.jar"/>
<classpathentry kind="var" path="GWT_TOOLS/lib/w3c/flute/flute-1.3.jar"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-oophm"/>
<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-windows"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/user/build.xml b/user/build.xml
index 6a62d37..87bf4c8 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -49,6 +49,7 @@
<pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
<pathelement location="${gwt.tools.lib}/w3c/sac/sac-1.3.jar" />
<pathelement location="${gwt.tools.lib}/w3c/flute/flute-1.3.jar" />
+ <pathelement location="${gwt.build.lib}/gwt-dev-oophm.jar" />
<pathelement location="${gwt.dev.jar}" />
</classpath>
</gwt.javac>
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 54a6c58..244dc07 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -43,6 +43,7 @@
import junit.framework.TestCase;
import junit.framework.TestResult;
+import java.awt.GraphicsEnvironment;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -216,6 +217,32 @@
registerHandler(new ArgHandlerString() {
@Override
public String getPurpose() {
+ return "Runs hosted mode via HTMLUnit given a list of browsers; "
+ + "e.g. IE6,IE7,FF2,FF3...";
+ }
+
+ @Override
+ public String getTag() {
+ return "-htmlunithosted";
+ }
+
+ @Override
+ public String[] getTagArgs() {
+ return new String[] {"browserNames"};
+ }
+
+ @Override
+ public boolean setString(String str) {
+ String[] targets = str.split(",");
+ runStyle = new RunStyleHtmlUnitHosted(JUnitShell.this, targets);
+ numClients = ((RunStyleHtmlUnit) runStyle).numBrowsers();
+ return runStyle != null;
+ }
+ });
+
+ registerHandler(new ArgHandlerString() {
+ @Override
+ public String getPurpose() {
return "Runs web mode via HTMLUnit given a list of browsers; "
+ "e.g. " + RunStyleHtmlUnit.getBrowserList();
}
@@ -400,7 +427,7 @@
* begin running the test. "Contacted" does not necessarily mean "the test has
* begun," e.g. for linker errors stopping the test initialization.
*/
- private static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000;
+ private static final int TEST_BEGIN_TIMEOUT_MILLIS = 6000000;
/**
* The amount of time to wait for all clients to complete a single test
@@ -590,7 +617,8 @@
*/
private JUnitShell() {
setRunTomcat(true);
- setHeadless(true);
+ setHeadless(false);
+ setHeadless(GraphicsEnvironment.isHeadless());
// Legacy: -Dgwt.hybrid runs web mode
if (System.getProperty(PROP_JUNIT_HYBRID_MODE) != null) {
@@ -619,7 +647,8 @@
protected void initializeLogger() {
if (isHeadless()) {
consoleLogger = new PrintWriterTreeLogger();
- consoleLogger.setMaxDetail(getCompilerOptions().getLogLevel());
+ // TODO (amitmanjhi): GwtShell overlay fix.
+ consoleLogger.setMaxDetail(TreeLogger.INFO);
} else {
super.initializeLogger();
}
@@ -683,20 +712,11 @@
return !messageQueue.hasResult();
}
- @Override
+ // TODO (amitmanjhi): GwtShell overlay fix, removed Override.
protected boolean shouldAutoGenerateResources() {
return runStyle.shouldAutoGenerateResources();
}
- @Override
- protected void sleep() {
- if (runStyle.isLocal()) {
- super.sleep();
- } else {
- messageQueue.waitForResults(1000);
- }
- }
-
void compileForWebMode(String moduleName, String... userAgents)
throws UnableToCompleteException {
// Never fresh during JUnit.
@@ -881,7 +901,9 @@
testBeginTime = System.currentTimeMillis();
testBeginTimeout = testBeginTime + TEST_BEGIN_TIMEOUT_MILLIS;
testMethodTimeout = 0; // wait until test execution begins
- pumpEventLoop();
+ while (notDone()) {
+ messageQueue.waitForResults(1000);
+ }
} catch (TimeoutException e) {
lastLaunchFailed = true;
testResult.addError(testCase, e);
diff --git a/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java b/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
index e48ef8e..af5c022 100644
--- a/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
+++ b/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java
@@ -58,6 +58,7 @@
this.browser = browser;
this.url = url;
this.treeLogger = treeLogger;
+ this.setName("htmlUnit client thread");
}
public void handleAlert(Page page, String message) {
@@ -93,7 +94,7 @@
// TODO(jat): is this necessary?
webClient.waitForBackgroundJavaScriptStartingBefore(2000);
page.getEnclosingWindow().getJobManager().waitForJobs(60000);
- treeLogger.log(TreeLogger.DEBUG, "getPage returned "
+ treeLogger.log(TreeLogger.SPAM, "getPage returned "
+ ((HtmlPage) page).asXml());
// TODO(amitmanjhi): call webClient.closeAllWindows()
} catch (FailingHttpStatusCodeException e) {
@@ -182,7 +183,8 @@
protected HtmlUnitThread createHtmlUnitThread(BrowserVersion browser,
String url) {
- return new HtmlUnitThread(browser, url, shell.getTopLogger());
+ return new HtmlUnitThread(browser, url, shell.getTopLogger().branch(
+ TreeLogger.SPAM, "logging for HtmlUnit thread"));
}
private Set<BrowserVersion> getBrowserSet(String[] targetsIn) {
diff --git a/user/src/com/google/gwt/junit/RunStyleHtmlUnitHosted.java b/user/src/com/google/gwt/junit/RunStyleHtmlUnitHosted.java
new file mode 100644
index 0000000..4d14581
--- /dev/null
+++ b/user/src/com/google/gwt/junit/RunStyleHtmlUnitHosted.java
@@ -0,0 +1,110 @@
+/*
+ * 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.dev.util.log.PrintWriterTreeLogger;
+
+import com.gargoylesoftware.htmlunit.BrowserVersion;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.WebWindow;
+import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
+import com.gargoylesoftware.htmlunit.javascript.host.Window;
+
+import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
+
+/**
+ * Runstyle for HTMLUnit in hosted mode.
+ */
+public class RunStyleHtmlUnitHosted extends RunStyleHtmlUnit {
+
+ /**
+ * Run HMTLUnit in a separate thread, replacing the default JavaScriptEngine
+ * with one that has the necessary hosted mode hooks.
+ */
+ protected static class HtmlUnitHostedThread extends HtmlUnitThread {
+
+ public HtmlUnitHostedThread(BrowserVersion browser, String url,
+ TreeLogger treeLogger) {
+ super(browser, url, treeLogger);
+ }
+
+ @Override
+ protected void setupWebClient(WebClient webClient) {
+ JavaScriptEngine hostedEngine = new HostedJavaScriptEngine(webClient);
+ 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;
+
+ public HostedJavaScriptEngine(WebClient webClient) {
+ super(webClient);
+ }
+
+ @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(), ScriptableObject.READONLY);
+ }
+ }
+
+ public static HtmlUnitThread createHtmlUnitThread(BrowserVersion browser,
+ String url, TreeLogger treeLogger) {
+ return new HtmlUnitHostedThread(browser, url, treeLogger);
+ }
+
+ public static void startHtmlUnitThread(String url) {
+ PrintWriterTreeLogger pw = new PrintWriterTreeLogger();
+ // TODO(amitmanjhi): get the correct browser emulation
+ HtmlUnitThread thread = createHtmlUnitThread(
+ BrowserVersion.FIREFOX_3, url, pw);
+ thread.start();
+ }
+
+ public RunStyleHtmlUnitHosted(JUnitShell unitShell, String[] targets) {
+ super(unitShell, targets);
+ }
+
+ @Override
+ public void maybeCompileModule(String moduleName) {
+ // No compilation needed for hosted mode
+ }
+
+ @Override
+ protected HtmlUnitThread createHtmlUnitThread(BrowserVersion browser,
+ String url) {
+ return RunStyleHtmlUnitHosted.createHtmlUnitThread(browser, url,
+ shell.getTopLogger());
+ }
+
+ @Override
+ protected String getMyUrl(String moduleName) {
+ // TODO(jat): get the correct address/port
+ return super.getMyUrl(moduleName) + "?gwt.hosted=localhost:9997";
+ }
+}