Revision: 6119 Author: amitman...@google.com Date: Thu Sep 10 16:26:01 2009 Log: 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) http://code.google.com/p/google-web-toolkit/source/detail?r=6119 Added: /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/JavaObject.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/ServerMethods.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/SessionData.java /branches/farewellSwt/user/src/com/google/gwt/junit/RunStyleHtmlUnitHosted.java Modified: /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java /branches/farewellSwt/eclipse/dev/oophm/.classpath /branches/farewellSwt/eclipse/user/.classpath /branches/farewellSwt/user/build.xml /branches/farewellSwt/user/src/com/google/gwt/junit/JUnitShell.java /branches/farewellSwt/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannelClient.java Thu Sep 10 16:26:01 2009 @@ -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(); + } + } + } +} ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/HostedModePluginObject.java Thu Sep 10 16:26:01 2009 @@ -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; + } +} ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/HtmlUnitSessionHandler.java Thu Sep 10 16:26:01 2009 @@ -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; + } + +} ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/JavaObject.java Thu Sep 10 16:26:01 2009 @@ -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; + } + +} ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/ServerMethods.java Thu Sep 10 16:26:01 2009 @@ -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; + } + +} ======================================= --- /dev/null +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/SessionData.java Thu Sep 10 16:26:01 2009 @@ -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; + } +} ======================================= --- /dev/null +++ /branches/farewellSwt/user/src/com/google/gwt/junit/RunStyleHtmlUnitHosted.java Thu Sep 10 16:26:01 2009 @@ -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"; + } +} ======================================= --- /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java Mon Aug 10 16:15:14 2009 +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/BrowserChannel.java Thu Sep 10 16:26:01 2009 @@ -69,11 +69,16 @@ * Class representing a reference to a JS object. */ public static class JsObjectRef { - private 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); + } + + private int refId; + public JsObjectRef(int refId) { - assert !JSOBJECT_ID_MAP.get().containsKey(refId) - || (JSOBJECT_ID_MAP.get().get(refId).get() == null); 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()); ======================================= --- /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java Thu Jan 8 16:13:53 2009 +++ /branches/farewellSwt/dev/oophm/src/com/google/gwt/dev/shell/JsValueOOPHM.java Thu Sep 10 16:26:01 2009 @@ -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); } ======================================= --- /branches/farewellSwt/eclipse/dev/oophm/.classpath Wed Mar 11 12:33:27 2009 +++ /branches/farewellSwt/eclipse/dev/oophm/.classpath Thu Sep 10 16:26:01 2009 @@ -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> ======================================= --- /branches/farewellSwt/eclipse/user/.classpath Thu Aug 20 15:05:36 2009 +++ /branches/farewellSwt/eclipse/user/.classpath Thu Sep 10 16:26:01 2009 @@ -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> ======================================= --- /branches/farewellSwt/user/build.xml Thu Sep 3 10:59:12 2009 +++ /branches/farewellSwt/user/build.xml Thu Sep 10 16:26:01 2009 @@ -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> ======================================= --- /branches/farewellSwt/user/src/com/google/gwt/junit/JUnitShell.java Mon Aug 17 09:47:12 2009 +++ /branches/farewellSwt/user/src/com/google/gwt/junit/JUnitShell.java Thu Sep 10 16:26:01 2009 @@ -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; @@ -213,6 +214,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() { @@ -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,19 +712,10 @@ 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 { @@ -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); ======================================= --- /branches/farewellSwt/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java Wed Aug 12 17:20:08 2009 +++ /branches/farewellSwt/user/src/com/google/gwt/junit/RunStyleHtmlUnit.java Thu Sep 10 16:26:01 2009 @@ -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) { --~--~---------~--~----~------------~-------~--~----~ http://groups.google.com/group/Google-Web-Toolkit-Contributors -~----------~----~----~----~------~----~------~--~---