blob: 4b6942ba824634864720ec0bce576984fd10079b [file] [log] [blame]
/*
* 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.Value;
import com.google.gwt.dev.shell.BrowserChannel.Value.ValueType;
import com.google.gwt.dev.shell.BrowserChannelClient.SessionHandlerClient;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.gargoylesoftware.htmlunit.ScriptException;
import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.WebWindow;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
import com.gargoylesoftware.htmlunit.javascript.SimpleScriptableProxy;
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.JavaScriptException;
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.IdentityHashMap;
import java.util.Map;
/**
* Handle session tasks for HtmlUnit.
*/
public class HtmlUnitSessionHandler extends SessionHandlerClient {
private class ToStringMethod extends ScriptableObject implements Function {
private static final int EXPECTED_NUM_ARGS = 0;
private static final long serialVersionUID = 1592865718416163348L;
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"
+ " toString: expected "
+ EXPECTED_NUM_ARGS
+ ", got "
+ args.length);
}
// thisObj is the javaObject.
Value thisValue = makeValueFromJsval(context, thisObj);
ExceptionOrReturnValue returnValue = JavaObject.getReturnFromJavaMethod(
context, HtmlUnitSessionHandler.this, sessionData.getChannel(),
TO_STRING_DISPATCH_ID, thisValue, EMPTY_VALUES);
return HtmlUnitSessionHandler.this.makeJsvalFromValue(context,
returnValue.getReturnValue());
}
public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
throw Context.reportRuntimeError("Function connect can't be used as a "
+ "constructor");
}
@Override
public String getClassName() {
return "function toString";
}
}
private static final Value EMPTY_VALUES[] = new Value[0];
private static final String REPLACE_METHOD_SIGNATURE = "@com.google.gwt.user.client.Window$Location::replace(Ljava/lang/String;)";
private static final int TO_STRING_DISPATCH_ID = 0;
Map<Integer, JavaObject> javaObjectCache;
/**
* The htmlPage is also used to synchronize calls to Java code.
*/
private HtmlPage htmlPage;
private JavaScriptEngine jsEngine;
private IdentityHashMap<Scriptable, Integer> jsObjectToRef;
private int nextRefId;
private Map<Integer, Scriptable> refToJsObject;
private SessionData sessionData;
private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger();
private final ToStringMethod toStringMethod = new ToStringMethod();
private final Window window;
HtmlUnitSessionHandler(Window window, JavaScriptEngine jsEngine) {
this.window = window;
logger.setMaxDetail(TreeLogger.ERROR);
this.jsEngine = jsEngine;
htmlPage = (HtmlPage) this.window.getWebWindow().getEnclosedPage();
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "jsEngine = " + jsEngine + ", HtmlPage = "
+ htmlPage);
}
jsObjectToRef = new IdentityHashMap<Scriptable, Integer>();
nextRefId = 1;
refToJsObject = new HashMap<Integer, Scriptable>();
// related to JavaObject cache.
javaObjectCache = new HashMap<Integer, JavaObject>();
}
@Override
public void freeValue(BrowserChannelClient channel, int[] ids) {
for (int id : ids) {
Scriptable scriptable = refToJsObject.remove(id);
if (scriptable != null) {
jsObjectToRef.remove(scriptable);
}
}
}
public JavaObject getOrCreateJavaObject(int refId, Context context) {
JavaObject javaObject = javaObjectCache.get(refId);
if (javaObject == null) {
javaObject = new JavaObject(context, sessionData, refId);
javaObjectCache.put(refId, javaObject);
}
return javaObject;
}
@Override
public HtmlPage getSynchronizationObject() {
return htmlPage;
}
/**
* @param jsContext the Context
*/
public Object getToStringTearOff(Context jsContext) {
return toStringMethod;
}
@Override
public String getUserAgent() {
return "HtmlUnit-"
+ jsEngine.getWebClient().getBrowserVersion().getUserAgent();
}
@SuppressWarnings("unchecked")
@Override
public ExceptionOrReturnValue invoke(BrowserChannelClient channel, Value thisObj,
String methodName, Value[] args) {
if (logger.isLoggable(TreeLogger.DEBUG)) {
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 = null;
if (thisObj.getType() == ValueType.NULL) {
jsThis = window;
} else {
Object obj = makeJsvalFromValue(jsContext, thisObj);
if (obj instanceof ScriptableObject) {
jsThis = (ScriptableObject) obj;
} else if (obj instanceof SimpleScriptableProxy<?>) {
jsThis = ((SimpleScriptableProxy<SimpleScriptable>) obj).getDelegee();
} else {
logger.log(TreeLogger.ERROR, "Unable to convert " + obj + " to either "
+ " ScriptableObject or SimpleScriptableProxy");
return new ExceptionOrReturnValue(true, new Value(null));
}
}
Object functionObject = ScriptableObject.getProperty(
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;
if (logger.isLoggable(TreeLogger.SPAM)) {
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 {
if (args.length == 1
&& methodName.indexOf(REPLACE_METHOD_SIGNATURE) != -1) {
// getUrl() is not visible
String currentUrl = window.jsxGet_location().toString();
currentUrl = getUrlBeforeHash(currentUrl);
String newUrl = getUrlBeforeHash((String) args[0].getValue());
if (!newUrl.equals(currentUrl)) {
WebWindow webWindow = window.getWebWindow();
do {
webWindow.getJobManager().removeAllJobs();
webWindow = webWindow.getParentWindow();
} while (webWindow != webWindow.getTopWindow());
}
}
result = jsEngine.callFunction(htmlPage, jsFunction, window,
jsThis, jsArgs);
} catch (ScriptException se) {
if (se.getCause() instanceof JavaScriptException) {
JavaScriptException ex = (JavaScriptException) se.getCause();
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "INVOKE: JavaScriptException " + ex
+ ", message: " + ex.getMessage() + " when invoking "
+ methodName);
}
return new ExceptionOrReturnValue(true, makeValueFromJsval(jsContext,
ex.getValue()));
} else {
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "INVOKE: exception " + se + ", message: "
+ se.getMessage() + " when invoking " + methodName);
}
return new ExceptionOrReturnValue(true, makeValueFromJsval(jsContext,
Undefined.instance));
}
} catch (Exception ex) {
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "INVOKE: exception " + ex + ", message: "
+ ex.getMessage() + " when invoking " + methodName);
}
return new ExceptionOrReturnValue(true, makeValueFromJsval(jsContext,
Undefined.instance));
}
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "INVOKE: result: " + result
+ " of jsFunction: " + jsFunction);
}
return new ExceptionOrReturnValue(false, makeValueFromJsval(jsContext,
result));
}
@Override
public void loadJsni(BrowserChannelClient channel, String jsniString) {
if (logger.isLoggable(TreeLogger.SPAM)) {
logger.log(TreeLogger.SPAM, "LOAD_JSNI: " + jsniString);
}
ScriptResult scriptResult = htmlPage.executeJavaScript(jsniString);
if (logger.isLoggable(TreeLogger.INFO)) {
logger.log(TreeLogger.INFO, "LOAD_JSNI: scriptResult=" + scriptResult);
}
}
/**
* @param jsContext the Context
*/
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) {
if (value instanceof ScriptableObject) {
/*
* HACK: check for native types like NativeString. NativeString is
* package-protected. What other types do we need to check?
*/
ScriptableObject scriptableValue = (ScriptableObject) value;
String className = scriptableValue.getClassName();
if (className.equals("String")) {
return new Value(scriptableValue.toString());
}
}
Integer refId = jsObjectToRef.get(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;
}
if (value instanceof Number) {
return new Value(convertNumberFromJsval(((Number) value)));
}
return new Value(value);
}
/**
* HtmlUnit can return non-js types for numerical values e.g. long for TypedArrays. This method
* mimics the type conversion for numeric values in other devmode plugins so that we will have
* similar behavior in HtmlUnit as well.
*/
private Object convertNumberFromJsval(Number number) {
if (number instanceof Byte || number instanceof Short || number instanceof Integer) {
return number.intValue();
}
return number.doubleValue();
}
public void setSessionData(SessionData sessionData) {
this.sessionData = sessionData;
}
/*
* 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 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;
}
private String getUrlBeforeHash(String currentUrl) {
int hashIndex = -1;
if ((hashIndex = currentUrl.indexOf("#")) != -1) {
currentUrl = currentUrl.substring(0, hashIndex);
}
return currentUrl;
}
}