From a82893e83e0dd75aff77df6a71d32ecaf8140886 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Manuel=20Carrasco=20Mo=C3=B1ino?= Date: Sat, 4 Jan 2014 22:57:15 +0100 Subject: [PATCH] Implemented Ajax for JVM, and tests --- gwtquery-core/pom.xml | 7 +- .../google/gwt/junit/RunStyleHtmlUnit.java | 318 +++ .../java/com/google/gwt/query/client/GQ.java | 33 +- .../query/client/builders/JsonFactory.java | 3 + .../google/gwt/query/client/js/JsUtils.java | 2 +- .../gwt/query/client/plugins/Effects.java | 3 +- .../gwt/query/client/plugins/ajax/Ajax.java | 85 +- .../client/plugins/ajax/AjaxTransportJs.java | 12 +- .../client/plugins/effects/Transitions.java | 2 +- .../query/rebind/JsonBuilderGenerator.java | 11 +- .../google/gwt/query/vm/AjaxTransportJre.java | 103 +- .../google/gwt/query/vm/JsonFactoryJre.java | 21 +- .../com/google/gwt/query/vm/ResponseJre.java | 66 + .../super/com/google/gwt/query/client/GQ.java | 28 +- .../google/gwt/dev/shell/BrowserChannel.java | 1720 +++++++++++++++++ .../com/google/gwt/query/QueryTest.gwt.xml | 21 + .../gwt/query/client/ajax/AjaxCommon.java | 156 ++ .../gwt/query/client/ajax/AjaxTest.java | 90 +- .../gwt/query/client/ajax/AjaxTestGwt.java | 17 +- .../gwt/query/servlet/GQAjaxTestServlet.java | 74 + 20 files changed, 2676 insertions(+), 96 deletions(-) create mode 100644 gwtquery-core/src/main/java/com/google/gwt/junit/RunStyleHtmlUnit.java create mode 100644 gwtquery-core/src/main/java/com/google/gwt/query/vm/ResponseJre.java create mode 100644 gwtquery-core/src/test/java/com/google/gwt/dev/shell/BrowserChannel.java create mode 100644 gwtquery-core/src/test/java/com/google/gwt/query/QueryTest.gwt.xml create mode 100644 gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxCommon.java create mode 100644 gwtquery-core/src/test/java/com/google/gwt/query/servlet/GQAjaxTestServlet.java diff --git a/gwtquery-core/pom.xml b/gwtquery-core/pom.xml index c44b7f9b..7bfbbb9a 100644 --- a/gwtquery-core/pom.xml +++ b/gwtquery-core/pom.xml @@ -16,9 +16,14 @@ junit junit 4.7 - jar test + + net.sourceforge.htmlunit + htmlunit + 2.13 + test + com.google.gwt gwt-user diff --git a/gwtquery-core/src/main/java/com/google/gwt/junit/RunStyleHtmlUnit.java b/gwtquery-core/src/main/java/com/google/gwt/junit/RunStyleHtmlUnit.java new file mode 100644 index 00000000..0d8b5fe1 --- /dev/null +++ b/gwtquery-core/src/main/java/com/google/gwt/junit/RunStyleHtmlUnit.java @@ -0,0 +1,318 @@ +/* + * Copyright 2009 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.junit; + +import com.google.gwt.core.ext.TreeLogger; +import com.google.gwt.dev.shell.HostedModePluginObject; +import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet; + +import com.gargoylesoftware.htmlunit.AlertHandler; +import com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; +import com.gargoylesoftware.htmlunit.IncorrectnessListener; +import com.gargoylesoftware.htmlunit.OnbeforeunloadHandler; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.ScriptException; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebWindow; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine; +import com.gargoylesoftware.htmlunit.javascript.JavaScriptErrorListener; +import com.gargoylesoftware.htmlunit.javascript.host.Window; + +import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject; + +import org.w3c.css.sac.CSSParseException; +import org.w3c.css.sac.ErrorHandler; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Launches a web-mode test via HTMLUnit. + */ +public class RunStyleHtmlUnit extends RunStyle { + + /** + * Runs HTMLUnit in a separate thread. + */ + protected static class HtmlUnitThread extends Thread implements AlertHandler, + IncorrectnessListener, OnbeforeunloadHandler { + + private final BrowserVersion browser; + private final boolean developmentMode; + private final TreeLogger treeLogger; + private final String url; + private Object waitForUnload = new Object(); + + public HtmlUnitThread(BrowserVersion browser, String url, + TreeLogger treeLogger, boolean developmentMode) { + this.browser = browser; + this.url = url; + this.treeLogger = treeLogger; + this.setName("htmlUnit client thread"); + this.developmentMode = developmentMode; + } + + public void handleAlert(Page page, String message) { + treeLogger.log(TreeLogger.ERROR, "Alert: " + message); + } + + public boolean handleEvent(Page page, String returnValue) { + synchronized (waitForUnload) { + waitForUnload.notifyAll(); + } + return true; + } + + public void notify(String message, Object origin) { + if ("Obsolete content type encountered: 'text/javascript'.".equals(message)) { + // silently eat warning about text/javascript MIME type + return; + } + treeLogger.log(TreeLogger.WARN, message); + } + + @Override + public void run() { + WebClient webClient = new WebClient(browser); + webClient.setAlertHandler(this); + // Adding a handler that ignores errors to work-around + // https://sourceforge.net/tracker/?func=detail&aid=3090806&group_id=47038&atid=448266 + webClient.setCssErrorHandler(new ErrorHandler() { + + public void error(CSSParseException exception) { + // ignore + } + + public void fatalError(CSSParseException exception) { + treeLogger.log(TreeLogger.WARN, + "CSS fatal error: " + exception.getURI() + " [" + + exception.getLineNumber() + ":" + + exception.getColumnNumber() + "] " + exception.getMessage()); + } + + public void warning(CSSParseException exception) { + // ignore + } + }); + webClient.setIncorrectnessListener(this); + webClient.setOnbeforeunloadHandler(this); + webClient.setJavaScriptErrorListener(new JavaScriptErrorListener() { + + @Override + public void loadScriptError(HtmlPage htmlPage, URL scriptUrl, + Exception exception) { + treeLogger.log(TreeLogger.ERROR, + "Load Script Error: " + exception, exception); + } + + @Override + public void malformedScriptURL(HtmlPage htmlPage, String url, + MalformedURLException malformedURLException) { + treeLogger.log(TreeLogger.ERROR, + "Malformed Script URL: " + malformedURLException.getLocalizedMessage()); + } + + @Override + public void scriptException(HtmlPage htmlPage, + ScriptException scriptException) { + treeLogger.log(TreeLogger.DEBUG, + "Script Exception: " + scriptException.getLocalizedMessage() + + ", line " + scriptException.getFailingLine()); + } + + @Override + public void timeoutError(HtmlPage htmlPage, long allowedTime, + long executionTime) { + treeLogger.log(TreeLogger.ERROR, + "Script Timeout Error " + executionTime + " > " + allowedTime); + } + }); + setupWebClient(webClient); + try { + Page page = webClient.getPage(url); + webClient.waitForBackgroundJavaScriptStartingBefore(2000); + if (treeLogger.isLoggable(TreeLogger.SPAM)) { + treeLogger.log(TreeLogger.SPAM, "getPage returned " + + ((HtmlPage) page).asXml()); + } + // TODO(amitmanjhi): call webClient.closeAllWindows() + } catch (FailingHttpStatusCodeException e) { + treeLogger.log(TreeLogger.ERROR, "HTTP request failed", e); + return; + } catch (MalformedURLException e) { + treeLogger.log(TreeLogger.ERROR, "Bad URL", e); + return; + } catch (IOException e) { + treeLogger.log(TreeLogger.ERROR, "I/O error on HTTP request", e); + return; + } + } + + protected void setupWebClient(WebClient webClient) { + if (developmentMode) { + JavaScriptEngine hostedEngine = new HostedJavaScriptEngine(webClient, + treeLogger); + webClient.setJavaScriptEngine(hostedEngine); + } + } + } + + /** + * JavaScriptEngine subclass that provides a hook of initializing the + * __gwt_HostedModePlugin property on any new window, so it acts just like + * Firefox with the XPCOM plugin installed. + */ + private static class HostedJavaScriptEngine extends JavaScriptEngine { + + private static final long serialVersionUID = 3594816610842448691L; + private final TreeLogger logger; + + public HostedJavaScriptEngine(WebClient webClient, TreeLogger logger) { + super(webClient); + this.logger = logger; + } + + @Override + public void initialize(WebWindow webWindow) { + // Hook in the hosted-mode plugin after initializing the JS engine. + super.initialize(webWindow); + Window window = (Window) webWindow.getScriptObject(); + window.defineProperty("__gwt_HostedModePlugin", + new HostedModePluginObject(this, logger), ScriptableObject.READONLY); + } + } + + private static final Map BROWSER_MAP = createBrowserMap(); + + /* + * as long as this number is greater than 1, GWTTestCaseTest::testRetry will + * pass + */ + private static final int DEFAULT_TRIES = 1; + + private static final Set PLATFORMS = ImmutableSet.of(Platform.HtmlUnitBug, + Platform.HtmlUnitLayout, Platform.HtmlUnitUnknown); + + /** + * Returns the list of browsers Htmlunit emulates as a comma separated string. + */ + static String getBrowserList() { + StringBuffer sb = new StringBuffer(); + for (String str : BROWSER_MAP.keySet()) { + sb.append(str); + sb.append(","); + } + if (sb.length() > 1) { + return sb.substring(0, sb.length() - 1); + } + return sb.toString(); + } + + private static Map createBrowserMap() { + Map browserMap = new HashMap(); + for (BrowserVersion browser : new BrowserVersion[] { + BrowserVersion.FIREFOX_3_6, BrowserVersion.INTERNET_EXPLORER_6, + BrowserVersion.INTERNET_EXPLORER_7}) { + browserMap.put(browser.getNickname(), browser); + } + return Collections.unmodifiableMap(browserMap); + } + + private Set browsers = new HashSet(); + private boolean developmentMode; + private final List threads = new ArrayList(); + + /** + * Create a RunStyle instance with the passed-in browser targets. + */ + public RunStyleHtmlUnit(JUnitShell shell) { + super(shell); + } + + @Override + public Set getPlatforms() { + return PLATFORMS; + } + + @Override + public int initialize(String args) { + if (args == null || args.length() == 0) { + // If no browsers specified, default to Firefox 3. + args = "FF3.6"; + } + Set browserSet = new HashSet(); + for (String browserName : args.split(",")) { + BrowserVersion browser = BROWSER_MAP.get(browserName); + if (browser == null) { + getLogger().log( + TreeLogger.ERROR, + "RunStyleHtmlUnit: Unknown browser " + "name " + browserName + + ", expected browser name: one of " + BROWSER_MAP.keySet()); + return -1; + } + browserSet.add(browser); + } + browsers = Collections.unmodifiableSet(browserSet); + + setTries(DEFAULT_TRIES); // set to the default value for this RunStyle + return browsers.size(); + } + + @Override + public void launchModule(String moduleName) { + for (BrowserVersion browser : browsers) { + String url = shell.getModuleUrl(moduleName); + HtmlUnitThread hut = createHtmlUnitThread(browser, url); + TreeLogger logger = shell.getTopLogger(); + if (logger.isLoggable(TreeLogger.INFO)) { + logger.log(TreeLogger.INFO, + "Starting " + url + " on browser " + browser.getNickname()); + } + /* + * TODO (amitmanjhi): Is it worth pausing here and waiting for the main + * test thread to get to an "okay" state. + */ + hut.start(); + threads.add(hut); + } + } + + public int numBrowsers() { + return browsers.size(); + } + + @Override + public boolean setupMode(TreeLogger logger, boolean developmentMode) { + this.developmentMode = developmentMode; + return true; + } + + protected HtmlUnitThread createHtmlUnitThread(BrowserVersion browser, + String url) { + return new HtmlUnitThread(browser, url, shell.getTopLogger().branch( + TreeLogger.SPAM, "logging for HtmlUnit thread"), developmentMode); + } +} diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/GQ.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/GQ.java index 0791feaf..1e6aa304 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/GQ.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/GQ.java @@ -18,19 +18,18 @@ package com.google.gwt.query.client; import com.google.gwt.core.shared.GWT; import com.google.gwt.query.client.builders.JsonBuilder; import com.google.gwt.query.client.builders.JsonFactory; +import com.google.gwt.query.client.plugins.ajax.Ajax.AjaxTransport; +import com.google.gwt.query.client.plugins.ajax.AjaxTransportJs; +import com.google.gwt.query.vm.AjaxTransportJre; import com.google.gwt.query.vm.JsonFactoryJre; public class GQ { private static JsonFactory jsonFactory; + private static AjaxTransport ajaxTransport; public static T create(Class clz) { - if (jsonFactory == null) { - jsonFactory = GWT.isClient() ? - GWT.create(JsonFactory.class) : - new JsonFactoryJre(); - } - return jsonFactory.create(clz); + return getFactory().create(clz); } public static T create(Class clz, String payload) { @@ -38,4 +37,26 @@ public class GQ { ret.load(payload); return ret; } + + public static T create(String s) { + return getFactory().create(s); + } + + public static AjaxTransport getAjaxTransport() { + if (ajaxTransport == null) { + ajaxTransport = GWT.isClient() ? + new AjaxTransportJs() : + new AjaxTransportJre(); + } + return ajaxTransport; + } + + private static JsonFactory getFactory() { + if (jsonFactory == null) { + jsonFactory = GWT.isClient() ? + GWT.create(JsonFactory.class) : + new JsonFactoryJre(); + } + return jsonFactory; + } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/builders/JsonFactory.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/builders/JsonFactory.java index 30f08bb9..adcd5212 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/builders/JsonFactory.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/builders/JsonFactory.java @@ -1,5 +1,8 @@ package com.google.gwt.query.client.builders; +import com.google.gwt.query.client.Binder; + public interface JsonFactory { T create(Class clz); + T create(String s); } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/js/JsUtils.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/js/JsUtils.java index be06f79b..ac01f7ca 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/js/JsUtils.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/js/JsUtils.java @@ -531,7 +531,7 @@ public class JsUtils { } else { Properties p = prop.getJavaScriptObject(k); if (p != null) { - ret += p.toQueryString(); + ret += k + "=" + p.tostring(); } else { String v = prop.getStr(k); if (v != null && !v.isEmpty() && !"null".equalsIgnoreCase(v)) { diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/Effects.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/Effects.java index c7b66d5b..7f6ce927 100755 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/Effects.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/Effects.java @@ -166,7 +166,8 @@ public class Effects extends QueuePlugin { public Effects animate(Object stringOrProperties, int duration, Easing easing, Function... funcs) { final Properties p = (stringOrProperties instanceof String) - ? $$((String) stringOrProperties) : (Properties) stringOrProperties; + ? (Properties) $$((String) stringOrProperties) + : (Properties) stringOrProperties; if (p.getStr("duration") != null) { duration = p.getInt("duration"); diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/Ajax.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/Ajax.java index 9da8e7dd..ecf5b626 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/Ajax.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/Ajax.java @@ -1,25 +1,20 @@ package com.google.gwt.query.client.plugins.ajax; -import com.google.gwt.core.client.Callback; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.ScriptInjector; import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.ScriptElement; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.Response; +import com.google.gwt.query.client.Binder; import com.google.gwt.query.client.Function; import com.google.gwt.query.client.GQ; import com.google.gwt.query.client.GQuery; import com.google.gwt.query.client.Promise; import com.google.gwt.query.client.Properties; -import com.google.gwt.query.client.Binder; import com.google.gwt.query.client.builders.JsonBuilder; import com.google.gwt.query.client.js.JsUtils; import com.google.gwt.query.client.plugins.Plugin; -import com.google.gwt.query.client.plugins.deferred.PromiseFunction; -import com.google.gwt.query.client.plugins.deferred.PromiseReqBuilder; -import com.google.gwt.query.client.plugins.deferred.PromiseReqBuilderJSONP; /** * Ajax class for GQuery. @@ -37,6 +32,17 @@ import com.google.gwt.query.client.plugins.deferred.PromiseReqBuilderJSONP; */ public class Ajax extends GQuery { + public static interface AjaxTransport { + + Promise getJsonP(Settings settings); + + Promise getLoadScript(Settings settings); + + Promise getXhr(Settings settings); + } + + static AjaxTransport transport = GQ.getAjaxTransport(); + /** * Ajax Settings object */ @@ -109,7 +115,8 @@ public class Ajax extends GQuery { * @param settings a Properties object with the configuration of the Ajax request. */ public static Promise ajax(Settings settings) { - + resolveSettings(settings); + final Function onSuccess = settings.getSuccess(); if (onSuccess != null) { onSuccess.setElement(settings.getContext()); @@ -120,28 +127,26 @@ public class Ajax extends GQuery { onError.setElement(settings.getContext()); } - resolveSettings(settings); - final String dataType = settings.getDataType(); Promise ret = null; if ("jsonp".equalsIgnoreCase(dataType)) { - ret = new PromiseReqBuilderJSONP(settings.getUrl(), null, settings.getTimeout()); + ret = transport.getJsonP(settings); } else if ("loadscript".equalsIgnoreCase(dataType)){ - ret = createPromiseScriptInjector(settings.getUrl()); + ret = transport.getLoadScript(settings); } else { - ret = new PromiseReqBuilder(settings) + ret = transport.getXhr(settings) .then(new Function() { public Object f(Object...args) { Response response = arguments(0); Request request = arguments(1); - Object retData = null; + Object retData = response.getText(); try { if ("xml".equalsIgnoreCase(dataType)) { retData = JsUtils.parseXML(response.getText()); } else if ("json".equalsIgnoreCase(dataType)) { - retData = JsUtils.parseJSON(response.getText()); + retData = GQ.create(response.getText()); } else { retData = response.getText(); if ("script".equalsIgnoreCase(dataType)) { @@ -149,17 +154,20 @@ public class Ajax extends GQuery { } } } catch (Exception e) { - if (GWT.getUncaughtExceptionHandler() != null) { + if (GWT.isClient() && GWT.getUncaughtExceptionHandler() != null) { GWT.getUncaughtExceptionHandler().onUncaughtException(e); + } else { + e.printStackTrace(); } } return new Object[]{retData, "success", request, response}; } }, new Function() { public Object f(Object...args) { - Throwable exception = (Throwable)args[0]; - Request request = (Request)args[1]; - return new Object[]{null, exception.getMessage(), request, null, exception}; + Throwable exception = getArgument(0, Throwable.class); + Request request = getArgument(0, Request.class); + String msg = String.valueOf(exception); + return new Object[]{null, msg, request, null, exception}; } }); } @@ -175,9 +183,16 @@ public class Ajax extends GQuery { private static void resolveSettings(Settings settings) { String url = settings.getUrl(); assert settings != null && settings.getUrl() != null: "no url found in settings"; - - settings.setType(settings.getType() == null ? "POST" : settings.getType().toUpperCase()); + String type = "POST"; + if (settings.getType() != null) { + type = settings.getType().toUpperCase(); + } + if ("jsonp".equalsIgnoreCase(settings.getDataType())) { + type = "GET"; + } + settings.setType(type); + Binder data = settings.getData(); if (data != null) { if (data.getBound() instanceof JavaScriptObject && JsUtils.isFormData(data.getBound())) { @@ -195,27 +210,6 @@ public class Ajax extends GQuery { } } - private static Promise createPromiseScriptInjector(final String url) { - return new PromiseFunction() { - private ScriptElement scriptElement; - public void f(final Deferred dfd) { - scriptElement = ScriptInjector.fromUrl(url).setWindow(window) - .setCallback(new Callback() { - public void onSuccess(Void result) { - $(window).delay(0, new Function(){ - public void f() { - dfd.resolve(scriptElement); - } - }); - } - public void onFailure(Exception reason) { - dfd.reject(reason); - } - }).inject().cast(); - } - }; - } - public static Promise ajax(String url, Function onSuccess, Function onError) { return ajax(url, onSuccess, onError, (Settings) null); } @@ -240,11 +234,14 @@ public class Ajax extends GQuery { } public static Settings createSettings() { - return createSettings($$("")); + return createSettings(""); } public static Settings createSettings(String prop) { - return createSettings($$(prop)); + Settings s = GQ.create(Settings.class); + if (prop != null && !prop.isEmpty()) + s.parse(prop); + return s; } public static Settings createSettings(Binder p) { @@ -366,7 +363,7 @@ public class Ajax extends GQuery { // Note: using '\s\S' instead of '.' because gwt String emulation does // not support java embedded flag expressions (?s) and javascript does // not have multidot flag. - String s = getArguments()[0].toString().replaceAll("]+>\\s*", "") + String s = arguments(0).toString().replaceAll("]+>\\s*", "") .replaceAll("\\s*", "") .replaceAll("\\s*", "") .replaceAll("\\s*", "") diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/AjaxTransportJs.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/AjaxTransportJs.java index bc7a72ef..fe4e91b8 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/AjaxTransportJs.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/AjaxTransportJs.java @@ -6,20 +6,24 @@ import com.google.gwt.dom.client.ScriptElement; import com.google.gwt.query.client.Function; import com.google.gwt.query.client.GQuery; import com.google.gwt.query.client.Promise; +import com.google.gwt.query.client.plugins.ajax.Ajax.AjaxTransport; import com.google.gwt.query.client.plugins.ajax.Ajax.Settings; +import com.google.gwt.query.client.plugins.deferred.Deferred.DeferredPromiseImpl; import com.google.gwt.query.client.plugins.deferred.PromiseFunction; +import com.google.gwt.query.client.plugins.deferred.PromiseReqBuilder; import com.google.gwt.query.client.plugins.deferred.PromiseReqBuilderJSONP; /** * Ajax transport for Client side. */ -public class AjaxTransportJs { - +public class AjaxTransportJs implements AjaxTransport { + @Override public Promise getJsonP(Settings settings) { return new PromiseReqBuilderJSONP(settings.getUrl(), settings.getTimeout()); } + @Override public Promise getLoadScript(final Settings settings) { return new PromiseFunction() { private ScriptElement scriptElement; @@ -40,5 +44,9 @@ public class AjaxTransportJs { } }; } + + public Promise getXhr(Settings settings) { + return new PromiseReqBuilder(settings); + } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/effects/Transitions.java b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/effects/Transitions.java index 57403501..ac9581cd 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/effects/Transitions.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/effects/Transitions.java @@ -311,7 +311,7 @@ public class Transitions extends GQuery { } final Properties cssProps = (stringOrProperties instanceof String) - ? $$((String) stringOrProperties) + ? (Properties) $$((String) stringOrProperties) : (Properties) stringOrProperties; final String ease = easing == null ? "ease" : easing.toString(); diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/rebind/JsonBuilderGenerator.java b/gwtquery-core/src/main/java/com/google/gwt/query/rebind/JsonBuilderGenerator.java index 2e66bc3f..e1c905af 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/rebind/JsonBuilderGenerator.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/rebind/JsonBuilderGenerator.java @@ -56,7 +56,7 @@ public class JsonBuilderGenerator extends Generator { static JClassType jsType; static JClassType listType; static JClassType stringType; - static JClassType jsonCreatorType; + static JClassType jsonFactoryType; public static String capitalize(String s) { @@ -89,11 +89,11 @@ public class JsonBuilderGenerator extends Generator { jsType = oracle.findType(JavaScriptObject.class.getName()); listType = oracle.findType(List.class.getName()); functionType = oracle.findType(Function.class.getName()); - jsonCreatorType = oracle.findType(JsonFactory.class.getName()); + jsonFactoryType = oracle.findType(JsonFactory.class.getName()); String t[] = generateClassName(clazz); - boolean isFactory = clazz.isAssignableTo(jsonCreatorType); + boolean isFactory = clazz.isAssignableTo(jsonFactoryType); SourceWriter sw = getSourceWriter(treeLogger, generatorContext, t[0], t[1], isFactory, requestedClass); if (sw != null) { @@ -323,5 +323,10 @@ public class JsonBuilderGenerator extends Generator { sw.println("return null;"); sw.outdent(); sw.println("}"); + sw.println("public T create(String s) {"); + sw.indent(); + sw.println("return (T)" + Properties.class.getName() + ".create(s);"); + sw.outdent(); + sw.println("}"); } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/vm/AjaxTransportJre.java b/gwtquery-core/src/main/java/com/google/gwt/query/vm/AjaxTransportJre.java index 7e882d62..822da4c2 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/vm/AjaxTransportJre.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/vm/AjaxTransportJre.java @@ -1,8 +1,109 @@ package com.google.gwt.query.vm; +// import org.apache.http.impl.client.HttpClientBuilder; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +import com.google.gwt.http.client.RequestException; +import com.google.gwt.http.client.Response; +import com.google.gwt.query.client.Binder; +import com.google.gwt.query.client.Function; +import com.google.gwt.query.client.GQ; +import com.google.gwt.query.client.Promise; +import com.google.gwt.query.client.plugins.ajax.Ajax.AjaxTransport; +import com.google.gwt.query.client.plugins.ajax.Ajax.Settings; +import com.google.gwt.query.client.plugins.deferred.PromiseFunction; +import com.google.gwt.user.server.Base64Utils; /** + * */ -public class AjaxTransportJre { +public class AjaxTransportJre implements AjaxTransport { + + private final String USER_AGENT = "Mozilla/5.0"; + + public Promise getJsonP(final Settings settings) { + String url = settings.getUrl().replaceFirst("callback=[^&]*", ""); + url += (url.contains("?") ? "&" : "?") + "callback=jre_callback"; + settings.setUrl(url); + + return getXhr(settings) + .then(new Function() { + public Object f(Object... args) { + ResponseJre response = arguments(0); + return GQ.create(response.getText().replaceFirst("jre_callback\\((.*)\\)", "$1")); + } + }); + } + + public Promise getLoadScript(Settings settings) { + return getXhr(settings); + } + + public Promise getXhr(final Settings settings) { + return new PromiseFunction() { + public void f(Deferred dfd) { + try { + Response response = httpClient(settings); + int status = response.getStatusCode(); + if (status <= 0 || status >= 400) { + String statusText = status <= 0 ? "Bad CORS" : response.getStatusText(); + dfd.reject(null, new RequestException("HTTP ERROR: " + status + " " + statusText + "\n" + response.getText())); + } else { + dfd.resolve(response, null); + } + } catch (Exception e) { + dfd.reject(e, null); + } + } + }; + } + + private Response httpClient(Settings s) throws Exception { + + URL u = new URL(s.getUrl()); + + HttpURLConnection c = (HttpURLConnection) u.openConnection(); + c.setRequestMethod(s.getType()); + c.setRequestProperty("User-Agent", USER_AGENT); + if (s.getUsername() != null && s.getPassword() != null) { + c.setRequestProperty ("Authorization", "Basic " + Base64Utils.toBase64((s.getUsername() + ":" + s.getPassword()).getBytes())); + } + + Binder headers = s.getHeaders(); + if (headers != null) { + for (String h : headers.getFieldNames()) { + c.setRequestProperty(h, "" + headers.get(h)); + } + } + + if (!s.getType().equals("GET")) { + String ctype = s.getDataType(); + if (s.getDataType().toLowerCase().startsWith("json")) { + ctype = "application/json; charset=utf-8"; + } + c.setRequestProperty("Content-Type", ctype); + + c.setDoOutput(true); + DataOutputStream wr = new DataOutputStream(c.getOutputStream()); + wr.writeBytes(s.getDataString()); + wr.flush(); + wr.close(); + } + int code = c.getResponseCode(); + BufferedReader in = new BufferedReader(new InputStreamReader(c.getInputStream())); + String inputLine; + StringBuffer response = new StringBuffer(); + while ((inputLine = in.readLine()) != null) { + response.append(inputLine); + } + in.close(); + + return new ResponseJre(code, c.getResponseMessage(), response.toString(), c.getHeaderFields()); + } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/vm/JsonFactoryJre.java b/gwtquery-core/src/main/java/com/google/gwt/query/vm/JsonFactoryJre.java index ec115086..b81ca2d4 100644 --- a/gwtquery-core/src/main/java/com/google/gwt/query/vm/JsonFactoryJre.java +++ b/gwtquery-core/src/main/java/com/google/gwt/query/vm/JsonFactoryJre.java @@ -14,6 +14,7 @@ import org.json.JSONArray; import org.json.JSONObject; import com.google.gwt.query.client.Binder; +import com.google.gwt.query.client.Properties; import com.google.gwt.query.client.builders.JsonBuilder; import com.google.gwt.query.client.builders.JsonFactory; import com.google.gwt.query.client.builders.Name; @@ -94,7 +95,7 @@ public class JsonFactoryJre implements JsonFactory { } } else { ret = obj != null ? obj.get(attr): arr.get(idx); - if (ret instanceof JSONObject && JsonBuilder.class.isAssignableFrom(clz) && !clz.isAssignableFrom(ret.getClass())) { + if (ret instanceof JSONObject && Binder.class.isAssignableFrom(clz) && !clz.isAssignableFrom(ret.getClass())) { ret = jsonFactory.create(clz, (JSONObject)ret); } } @@ -125,8 +126,8 @@ public class JsonFactoryJre implements JsonFactory { return obj != null ? obj.put(attr, o) : arr.put(o); } else if (o instanceof Date) { return obj != null ? obj.put(attr, ((Date) o).getTime()) : arr.put(((Date) o).getTime()); - } else if (o instanceof JsonBuilder) { - return obj != null ? obj.put(attr, ((JsonBuilder) o).getBound()) : arr.put(((JsonBuilder) o).getBound()); + } else if (o instanceof Binder) { + return obj != null ? obj.put(attr, ((Binder) o).getBound()) : arr.put(((Binder) o).getBound()); } else if (o.getClass().isArray() || o instanceof List) { Object[] arg; if (o.getClass().isArray()) { @@ -166,8 +167,9 @@ public class JsonFactoryJre implements JsonFactory { } else if (mname.matches("toString")) { return jsonObject.toString(); } else if (mname.matches("toJson")) { - String jsonName = JsonBuilderGenerator.classNameToJsonName(getDataBindingClassName(proxy.getClass())); - return "{\"" + jsonName + "\":"+ jsonObject.toString() + "}"; +// String jsonName = JsonBuilderGenerator.classNameToJsonName(getDataBindingClassName(proxy.getClass())); +// return "{\"" + jsonName + "\":"+ jsonObject.toString() + "}"; + return jsonObject.toString() ; } else if ("toQueryString".equals(mname)) { return param(jsonObject); } else if (largs == 1 && mname.equals("get")) { @@ -237,7 +239,7 @@ public class JsonFactoryJre implements JsonFactory { } } if (p != null) { - ret += param(p); + ret += k + "=" + p.toString(); } else if (s != null) { if (!s.isEmpty() && !"null".equalsIgnoreCase(s)) { ret += k + "=" + s; @@ -265,4 +267,11 @@ public class JsonFactoryJre implements JsonFactory { InvocationHandler handler = new JsonBuilderHandler(); return (Binder)Proxy.newProxyInstance(Binder.class.getClassLoader(), new Class[] {Binder.class}, handler); } + + @SuppressWarnings("unchecked") + public T create(String s) { + Binder ret = createBinder(); + ret.parse(Properties.wrapPropertiesString(s)); + return (T)ret; + } } diff --git a/gwtquery-core/src/main/java/com/google/gwt/query/vm/ResponseJre.java b/gwtquery-core/src/main/java/com/google/gwt/query/vm/ResponseJre.java new file mode 100644 index 00000000..36aafd37 --- /dev/null +++ b/gwtquery-core/src/main/java/com/google/gwt/query/vm/ResponseJre.java @@ -0,0 +1,66 @@ +package com.google.gwt.query.vm; + +import java.util.List; +import java.util.Map; + +import com.google.gwt.http.client.Header; +import com.google.gwt.http.client.Response; + +/** + * Implementation of `Response` in the JVM + */ +public class ResponseJre extends Response { + private int status; + private Map> headers; + private String responseText; + private String statusText; + + public ResponseJre(int status, String statusText, String text, Map> headers) { + this.status = status; + this.headers = headers; + this.responseText = text; + this.statusText = statusText; + } + + public String getHeader(String header) { + List l = headers.get(header); + if (l != null && !l.isEmpty()) { + String ret = ""; + for (String s : l) { + ret += ret.isEmpty() ? s : (", " + s); + } + return ret; + } + return null; + } + + public Header[] getHeaders() { + Header[] ret = new Header[headers.size()]; + int i = 0; + for (final String s : headers.keySet()) { + final String v = getHeader(s); + ret[i] = new Header() { + public String getValue() { + return v; + } + public String getName() { + return s; + } + }; + i++; + } + return ret; + } + public String getHeadersAsString() { + return null; + } + public int getStatusCode() { + return status; + } + public String getStatusText() { + return statusText; + } + public String getText() { + return responseText; + } +} \ No newline at end of file diff --git a/gwtquery-core/src/main/super/com/google/gwt/query/super/com/google/gwt/query/client/GQ.java b/gwtquery-core/src/main/super/com/google/gwt/query/super/com/google/gwt/query/client/GQ.java index edad9154..d694afa5 100644 --- a/gwtquery-core/src/main/super/com/google/gwt/query/super/com/google/gwt/query/client/GQ.java +++ b/gwtquery-core/src/main/super/com/google/gwt/query/super/com/google/gwt/query/client/GQ.java @@ -16,19 +16,21 @@ package com.google.gwt.query.client; import com.google.gwt.core.shared.GWT; +import com.google.gwt.query.client.Binder; import com.google.gwt.query.client.builders.JsonBuilder; import com.google.gwt.query.client.builders.JsonFactory; +import com.google.gwt.query.client.plugins.ajax.AjaxTransportJs; +import com.google.gwt.query.client.plugins.ajax.Ajax.AjaxTransport; +import com.google.gwt.query.vm.AjaxTransportJre; import com.google.gwt.query.vm.JsonFactoryJre; public class GQ { private static JsonFactory jsonFactory; + private static AjaxTransport ajaxTransport; public static T create(Class clz) { - if (jsonFactory == null) { - jsonFactory = GWT.create(JsonFactory.class); - } - return jsonFactory.create(clz); + return getFactory().create(clz); } public static T create(Class clz, String payload) { @@ -36,4 +38,22 @@ public class GQ { ret.load(payload); return ret; } + + public static T create(String s) { + return getFactory().create(s); + } + + public static AjaxTransport getAjaxTransport() { + if (ajaxTransport == null) { + ajaxTransport = new AjaxTransportJs(); + } + return ajaxTransport; + } + + private static JsonFactory getFactory() { + if (jsonFactory == null) { + jsonFactory = GWT.create(JsonFactory.class); + } + return jsonFactory; + } } diff --git a/gwtquery-core/src/test/java/com/google/gwt/dev/shell/BrowserChannel.java b/gwtquery-core/src/test/java/com/google/gwt/dev/shell/BrowserChannel.java new file mode 100644 index 00000000..9a5a925e --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/dev/shell/BrowserChannel.java @@ -0,0 +1,1720 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.gwt.dev.shell; + +import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue; +import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.SpecialDispatchId; +import com.google.gwt.dev.shell.BrowserChannel.Value.ValueType; +import com.google.gwt.util.tools.Utility; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.Socket; +import java.util.Set; + +/** + * + */ +public abstract class BrowserChannel { + + /** + * An error indicating that the remote side died and we should unroll the + * call stack as painlessly as possible to allow cleanup. + */ + public static class RemoteDeathError extends Error { + + public RemoteDeathError(Throwable cause) { + super("Remote connection lost", cause); + } + } + + /** + * Class representing a reference to a Java object. + */ + public static class JavaObjectRef implements RemoteObjectRef { + private int refId; + + public JavaObjectRef(int refId) { + this.refId = refId; + } + + public int getRefid() { + return Math.abs(refId); + } + + @Override + public int hashCode() { + return refId; + } + + public boolean isException() { + return refId < 0; + } + + @Override + public String toString() { + return "JavaObjectRef(ref=" + refId + ")"; + } + } + + /** + * Class representing a reference to a JS object. + */ + public static class JsObjectRef implements RemoteObjectRef { + + private int refId; + + public JsObjectRef(int refId) { + this.refId = refId; + } + + @Override + public boolean equals(Object o) { + return (o instanceof JsObjectRef) && ((JsObjectRef) o).refId == refId; + } + + public int getRefid() { + // exceptions are negative, so we get the absolute value + return Math.abs(refId); + } + + @Override + public int hashCode() { + return refId; + } + + public boolean isException() { + return refId < 0; + } + + @Override + public String toString() { + return "JsObjectRef(" + refId + ")"; + } + } + + /** + * Enumeration of message type ids. + * + *

Ids are used instead of relying on the ordinal to avoid sychronization + * problems with the client. + */ + public enum MessageType { + /** + * A message to invoke a method on the other side of the wire. Note that + * the messages are asymmetric -- see {@link InvokeOnClientMessage} and + * {@link InvokeOnServerMessage}. + */ + INVOKE(0), + + /** + * Returns the result of an INVOKE, INVOKE_SPECIAL, or LOAD_MODULE message. + */ + RETURN(1), + + /** + * v1 LOAD_MODULE message. + */ + OLD_LOAD_MODULE(2), + + /** + * Normal closure of the connection. + */ + QUIT(3), + + /** + * A request by the server to load JSNI source into the client's JS engine. + */ + LOAD_JSNI(4), + + INVOKE_SPECIAL(5), + + FREE_VALUE(6), + + /** + * Abnormal termination of the connection. + */ + FATAL_ERROR(7), + + CHECK_VERSIONS(8), + + PROTOCOL_VERSION(9), + + CHOOSE_TRANSPORT(10), + + SWITCH_TRANSPORT(11), + + LOAD_MODULE(12), + + REQUEST_ICON(13), + + USER_AGENT_ICON(14), + + REQUEST_PLUGIN(15); + + private final int id; + + private MessageType(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + /** + * Represents an object on the other side of the channel, known to this side + * by an reference ID. + */ + public interface RemoteObjectRef { + + /** + * @return the reference ID for this object. + */ + int getRefid(); + } + + /** + * Hook interface for responding to messages. + */ + public abstract static class SessionHandler { + + /** + * Wrapper to return both a return value/exception and a flag as to whether + * an exception was thrown or not. + */ + public static class ExceptionOrReturnValue { + private final boolean isException; + private final Value returnValue; + + public ExceptionOrReturnValue(boolean isException, Value returnValue) { + this.isException = isException; + this.returnValue = returnValue; + } + + public Value getReturnValue() { + return returnValue; + } + + public boolean isException() { + return isException; + } + } + + /** + * Enumeration of dispatch IDs on object 0 (the ServerMethods object). + * + *

Ids are set specifically rather than relying on the ordinal to avoid + * synchronization problems with the client. + * + * TODO: hasMethod/hasProperty no longer used, remove them! + */ + public enum SpecialDispatchId { + HasMethod(0), HasProperty(1), GetProperty(2), SetProperty(3); + + private final int id; + + private SpecialDispatchId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + public abstract void freeValue(T channel, int[] ids); + } + + /** + * Represents a value for BrowserChannel. + */ + public static class Value { + /** + * Enum of type tags sent across the wire. + */ + public enum ValueType { + /** + * Primitive values. + */ + NULL(0), BOOLEAN(1), BYTE(2), CHAR(3), SHORT(4), INT(5), LONG(6), + FLOAT(7), DOUBLE(8), STRING(9), + + /** + * Representations of Java or JS objects, sent as an index into a table + * kept on the side holding the actual object. + */ + JAVA_OBJECT(10), JS_OBJECT(11), + + /** + * A Javascript undef value, also used for void returns. + */ + UNDEFINED(12); + + private final int id; + + private ValueType(int id) { + this.id = id; + } + + byte getTag() { + return (byte) id; + } + } + + /** + * Type tag value. + */ + private ValueType type = ValueType.UNDEFINED; + + /** + * Represents a value sent/received across the wire. + */ + private Object value = null; + + public Value() { + } + + public Value(Object obj) { + convertFromJavaValue(obj); + } + + /** + * Convert a Java object to a value. Objects must be primitive wrappers, + * Strings, or JsObjectRef/JavaObjectRef instances. + * + * @param obj value to convert. + */ + public void convertFromJavaValue(Object obj) { + if (obj == null) { + type = ValueType.NULL; + } else if (obj instanceof Boolean) { + type = ValueType.BOOLEAN; + } else if (obj instanceof Byte) { + type = ValueType.BYTE; + } else if (obj instanceof Character) { + type = ValueType.CHAR; + } else if (obj instanceof Double) { + type = ValueType.DOUBLE; + } else if (obj instanceof Float) { + type = ValueType.FLOAT; + } else if (obj instanceof Integer) { + type = ValueType.INT; + } else if (obj instanceof Long) { + type = ValueType.LONG; + } else if (obj instanceof Short) { + type = ValueType.SHORT; + } else if (obj instanceof String) { + type = ValueType.STRING; + } else if (obj instanceof JsObjectRef) { + // TODO: exception handling? + type = ValueType.JS_OBJECT; + } else if (obj instanceof JavaObjectRef) { + // TODO: exception handling? + type = ValueType.JAVA_OBJECT; + } else { + type = ValueType.STRING; + obj = String.valueOf(obj); +// throw new RuntimeException( +// "Unexpected Java type in convertFromJavaValue: " + obj.getClass() + " " + obj); + } + value = obj; + } + + /** + * Convert a value to the requested Java type. + * + * @param reqType type to convert to + * @return value as that type. + */ + public Object convertToJavaType(Class reqType) { + if (reqType.isArray()) { + // TODO(jat): handle arrays? + } + if (reqType.equals(Boolean.class)) { + assert type == ValueType.BOOLEAN; + return value; + } else if (reqType.equals(Byte.class) || reqType.equals(byte.class)) { + assert isNumber(); + return Byte.valueOf(((Number) value).byteValue()); + } else if (reqType.equals(Character.class) || reqType.equals(char.class)) { + if (type == ValueType.CHAR) { + return value; + } else { + assert isNumber(); + return Character.valueOf((char) ((Number) value).shortValue()); + } + } else if (reqType.equals(Double.class) || reqType.equals(double.class)) { + assert isNumber(); + return Double.valueOf(((Number) value).doubleValue()); + } else if (reqType.equals(Float.class) || reqType.equals(float.class)) { + assert isNumber(); + return Float.valueOf(((Number) value).floatValue()); + } else if (reqType.equals(Integer.class) || reqType.equals(int.class)) { + assert isNumber(); + return Integer.valueOf(((Number) value).intValue()); + } else if (reqType.equals(Long.class) || reqType.equals(long.class)) { + assert isNumber(); + return Long.valueOf(((Number) value).longValue()); + } else if (reqType.equals(Short.class) || reqType.equals(short.class)) { + assert isNumber(); + return Short.valueOf(((Number) value).shortValue()); + } else if (reqType.equals(String.class)) { + assert type == ValueType.STRING; + return value; + } else { + // Wants an object, caller must deal with object references. + return value; + } + } + + public boolean getBoolean() { + assert type == ValueType.BOOLEAN; + return ((Boolean) value).booleanValue(); + } + + public byte getByte() { + assert type == ValueType.BYTE; + return ((Byte) value).byteValue(); + } + + public char getChar() { + assert type == ValueType.CHAR; + return ((Character) value).charValue(); + } + + public double getDouble() { + assert type == ValueType.DOUBLE; + return ((Double) value).doubleValue(); + } + + public float getFloat() { + assert type == ValueType.FLOAT; + return ((Float) value).floatValue(); + } + + public int getInt() { + assert type == ValueType.INT; + return ((Integer) value).intValue(); + } + + public JavaObjectRef getJavaObject() { + assert type == ValueType.JAVA_OBJECT; + return (JavaObjectRef) value; + } + + public JsObjectRef getJsObject() { + assert type == ValueType.JS_OBJECT; + return (JsObjectRef) value; + } + + public long getLong() { + assert type == ValueType.LONG; + return ((Long) value).longValue(); + } + + public short getShort() { + assert type == ValueType.SHORT; + return ((Short) value).shortValue(); + } + + public String getString() { + assert type == ValueType.STRING; + return (String) value; + } + + public ValueType getType() { + return type; + } + + public Object getValue() { + return value; + } + + public boolean isBoolean() { + return type == ValueType.BOOLEAN; + } + + public boolean isByte() { + return type == ValueType.BYTE; + } + + public boolean isChar() { + return type == ValueType.CHAR; + } + + public boolean isDouble() { + return type == ValueType.DOUBLE; + } + + public boolean isFloat() { + return type == ValueType.FLOAT; + } + + public boolean isInt() { + return type == ValueType.INT; + } + + public boolean isJavaObject() { + return type == ValueType.JAVA_OBJECT; + } + + public boolean isJsObject() { + return type == ValueType.JS_OBJECT; + } + + public boolean isLong() { + return type == ValueType.LONG; + } + + public boolean isNull() { + return type == ValueType.NULL; + } + + public boolean isNumber() { + switch (type) { + case BYTE: + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + return true; + default: + return false; + } + } + + public boolean isPrimitive() { + switch (type) { + case BOOLEAN: + case BYTE: + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + return true; + default: + return false; + } + } + + public boolean isShort() { + return type == ValueType.SHORT; + } + + public boolean isString() { + return type == ValueType.STRING; + } + + public boolean isUndefined() { + return type == ValueType.UNDEFINED; + } + + public void setBoolean(boolean val) { + type = ValueType.BOOLEAN; + value = Boolean.valueOf(val); + } + + public void setByte(byte val) { + type = ValueType.BYTE; + value = Byte.valueOf(val); + } + + public void setChar(char val) { + type = ValueType.CHAR; + value = Character.valueOf(val); + } + + public void setDouble(double val) { + type = ValueType.DOUBLE; + value = Double.valueOf(val); + } + + public void setFloat(float val) { + type = ValueType.FLOAT; + value = Float.valueOf(val); + } + + public void setInt(int val) { + type = ValueType.INT; + value = Integer.valueOf(val); + } + + public void setJavaObject(JavaObjectRef val) { + type = ValueType.JAVA_OBJECT; + value = val; + } + + public void setJsObject(JsObjectRef val) { + type = ValueType.JS_OBJECT; + value = val; + } + + public void setLong(long val) { + type = ValueType.BOOLEAN; + value = Long.valueOf(val); + } + + public void setNull() { + type = ValueType.NULL; + value = null; + } + + public void setShort(short val) { + type = ValueType.SHORT; + value = Short.valueOf(val); + } + + public void setString(String val) { + type = ValueType.STRING; + value = val; + } + + public void setUndefined() { + type = ValueType.UNDEFINED; + value = null; + } + + @Override + public String toString() { + return type + ": " + value; + } + } + + /** + * The initial request from the client, supplies a range of supported versions + * and the version from hosted.html (so stale copies on an external server + * can be detected). + */ + protected static class CheckVersionsMessage extends Message { + + public static CheckVersionsMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + int minVersion = stream.readInt(); + int maxVersion = stream.readInt(); + String hostedHtmlVersion = readUtf8String(stream); + return new CheckVersionsMessage(channel, minVersion, maxVersion, + hostedHtmlVersion); + } + + private final String hostedHtmlVersion; + + private final int maxVersion; + + private final int minVersion; + + public CheckVersionsMessage(BrowserChannel channel, int minVersion, + int maxVersion, String hostedHtmlVersion) { + super(channel); + this.minVersion = minVersion; + this.maxVersion = maxVersion; + this.hostedHtmlVersion = hostedHtmlVersion; + } + + public String getHostedHtmlVersion() { + return hostedHtmlVersion; + } + + public int getMaxVersion() { + return maxVersion; + } + + public int getMinVersion() { + return minVersion; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.CHECK_VERSIONS.getId()); + stream.writeInt(minVersion); + stream.writeInt(maxVersion); + writeUtf8String(stream, hostedHtmlVersion); + stream.flush(); + } + } + + /** + * A message from the client giving a list of supported connection methods + * and requesting the server choose one of them to switch protocol traffic to. + */ + protected static class ChooseTransportMessage extends Message { + + public static ChooseTransportMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + int n = stream.readInt(); + String[] transports = new String[n]; + for (int i = 0; i < n; ++i) { + transports[i] = readUtf8String(stream); + } + return new ChooseTransportMessage(channel, transports); + } + + private final String[] transports; + + public ChooseTransportMessage(BrowserChannel channel, + String[] transports) { + super(channel); + this.transports = transports; + } + + public String[] getTransports() { + return transports; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.CHOOSE_TRANSPORT.getId()); + stream.writeInt(transports.length); + for (String transport : transports) { + writeUtf8String(stream, transport); + } + } + } + + /** + * A message reporting a connection error to the client. + */ + protected static class FatalErrorMessage extends Message { + + public static FatalErrorMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + // NOTE: Tag has already been read. + String error = readUtf8String(stream); + return new FatalErrorMessage(channel, error); + } + + private final String error; + + public FatalErrorMessage(BrowserChannel channel, String error) { + super(channel); + this.error = error; + } + + public String getError() { + return error; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.FATAL_ERROR.getId()); + writeUtf8String(stream, error); + } + } + + /** + * A message asking the other side to free object references. Note that there + * is no response to this message, and this must only be sent immediately + * before an Invoke or Return message. + */ + protected static class FreeMessage extends Message { + public static FreeMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + int numIds = stream.readInt(); + // TODO: sanity check id count + int ids[] = new int[numIds]; + for (int i = 0; i < numIds; ++i) { + ids[i] = stream.readInt(); + } + return new FreeMessage(channel, ids); + } + + public static void send(BrowserChannel channel, int[] ids) + throws IOException { + DataOutputStream stream = channel.getStreamToOtherSide(); + stream.writeByte(MessageType.FREE_VALUE.getId()); + stream.writeInt(ids.length); + for (int id : ids) { + stream.writeInt(id); + } + stream.flush(); + } + + private final int ids[]; + + public FreeMessage(BrowserChannel channel, int[] ids) { + super(channel); + this.ids = ids; + } + + public int[] getIds() { + return ids; + } + + @Override + public boolean isAsynchronous() { + return true; + } + + @Override + public void send() throws IOException { + send(getBrowserChannel(), ids); + } + } + + /** + * A request from the server to invoke a function on the client. + * + * Note that MessageType.INVOKE can refer to either this class + * or {@link InvokeOnServerMessage} depending on the direction, as the + * protocol is asymmetric (Java needs a dispatch ID, Javascript needs a + * name). + */ + protected static class InvokeOnClientMessage extends Message { + public static InvokeOnClientMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + // NOTE: Tag has already been read. + String methodName = readUtf8String(stream); + Value thisRef = channel.readValue(stream); + int argLen = stream.readInt(); + Value[] args = new Value[argLen]; + for (int i = 0; i < argLen; i++) { + args[i] = channel.readValue(stream); + } + return new InvokeOnClientMessage(channel, methodName, thisRef, args); + } + + private final Value[] args; + private final String methodName; + private final Value thisRef; + + public InvokeOnClientMessage(BrowserChannel channel, String methodName, + Value thisRef, Value[] args) { + super(channel); + this.thisRef = thisRef; + this.methodName = methodName; + this.args = args; + } + + public Value[] getArgs() { + return args; + } + + public String getMethodName() { + return methodName; + } + + public Value getThis() { + return thisRef; + } + + @Override + public void send() throws IOException { + final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + + stream.writeByte(MessageType.INVOKE.getId()); + writeUtf8String(stream, methodName); + getBrowserChannel().writeValue(stream, thisRef); + stream.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + getBrowserChannel().writeValue(stream, args[i]); + } + stream.flush(); + } + } + + /** + * A request from the client to invoke a function on the server. + * + * Note that MessageType.INVOKE can refer to either this class + * or {@link InvokeOnClientMessage} depending on the direction, as the + * protocol is asymmetric (Java needs a dispatch ID, Javascript needs a + * name). + */ + protected static class InvokeOnServerMessage extends Message { + public static InvokeOnServerMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + // NOTE: Tag has already been read. + int methodDispatchId = stream.readInt(); + Value thisRef = channel.readValue(stream); + int argLen = stream.readInt(); + Value[] args = new Value[argLen]; + for (int i = 0; i < argLen; i++) { + args[i] = channel.readValue(stream); + } + return new InvokeOnServerMessage(channel, methodDispatchId, thisRef, + args); + } + + private final Value[] args; + private final int methodDispatchId; + private final Value thisRef; + + public InvokeOnServerMessage(BrowserChannel channel, int methodDispatchId, + Value thisRef, Value[] args) { + super(channel); + this.thisRef = thisRef; + this.methodDispatchId = methodDispatchId; + this.args = args; + } + + public Value[] getArgs() { + return args; + } + + public int getMethodDispatchId() { + return methodDispatchId; + } + + public Value getThis() { + return thisRef; + } + + @Override + public void send() throws IOException { + final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + + stream.writeByte(MessageType.INVOKE.getId()); + stream.writeInt(methodDispatchId); + getBrowserChannel().writeValue(stream, thisRef); + stream.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + getBrowserChannel().writeValue(stream, args[i]); + } + stream.flush(); + } + } + + /** + * A request from the to invoke a function on the other side. + */ + protected static class InvokeSpecialMessage extends Message { + public static InvokeSpecialMessage receive(BrowserChannel channel) + throws IOException, BrowserChannelException { + final DataInputStream stream = channel.getStreamFromOtherSide(); + // NOTE: Tag has already been read. + final int specialMethodInt = stream.readByte(); + SpecialDispatchId[] ids = SpecialDispatchId.values(); + if (specialMethodInt < 0 || specialMethodInt >= ids.length) { + throw new BrowserChannelException("Invalid dispatch id " + + specialMethodInt); + } + final SpecialDispatchId dispatchId = ids[specialMethodInt]; + final int argLen = stream.readInt(); + final Value[] args = new Value[argLen]; + for (int i = 0; i < argLen; i++) { + args[i] = channel.readValue(stream); + } + return new InvokeSpecialMessage(channel, dispatchId, args); + } + + private final Value[] args; + private final SpecialDispatchId dispatchId; + + public InvokeSpecialMessage(BrowserChannel channel, + SpecialDispatchId dispatchId, Value[] args) { + super(channel); + this.dispatchId = dispatchId; + this.args = args; + } + + public Value[] getArgs() { + return args; + } + + public SpecialDispatchId getDispatchId() { + return dispatchId; + } + + @Override + public void send() throws IOException { + final DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + + stream.writeByte(MessageType.INVOKE_SPECIAL.getId()); + stream.writeByte(dispatchId.getId()); + stream.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + getBrowserChannel().writeValue(stream, args[i]); + } + stream.flush(); + } + } + + /** + * A message sending JSNI code to be evaluated. Note that there is no response + * to this message, and this must only be sent immediately before an Invoke or + * Return message. + */ + protected static class LoadJsniMessage extends Message { + public static LoadJsniMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + String js = readUtf8String(stream); + return new LoadJsniMessage(channel, js); + } + + public static void send(BrowserChannel channel, String js) + throws IOException { + DataOutputStream stream = channel.getStreamToOtherSide(); + stream.write(MessageType.LOAD_JSNI.getId()); + writeUtf8String(stream, js); + stream.flush(); + } + + private final String js; + + public LoadJsniMessage(BrowserChannel channel, String js) { + super(channel); + this.js = js; + } + + public String getJsni() { + return js; + } + + @Override + public boolean isAsynchronous() { + return true; + } + + @Override + public void send() throws IOException { + send(getBrowserChannel(), js); + } + } + + /** + * A request from the client that the server load and initialize a given + * module. + */ + protected static class LoadModuleMessage extends Message { + public static LoadModuleMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + String url = readUtf8String(stream); + String tabKey = readUtf8String(stream); + String sessionKey = readUtf8String(stream); + String moduleName = readUtf8String(stream); + String userAgent = readUtf8String(stream); + return new LoadModuleMessage(channel, url, tabKey, sessionKey, moduleName, + userAgent); + } + + private final String moduleName; + + private final String sessionKey; + + private final String tabKey; + + private final String url; + + private final String userAgent; + + /** + * Creates a LoadModule message to be sent to the server. + * + * @param channel BrowserChannel instance + * @param url URL of main top-level window - may not be null + * @param tabKey opaque key identifying the tab in the browser, or an + * empty string if it cannot be determined - may not be null + * @param sessionKey opaque key identifying a particular session (ie, + * group of modules) - may not be null + * @param moduleName name of GWT module to load - may not be null + * @param userAgent user agent identifier of the browser - may not be null + */ + public LoadModuleMessage(BrowserChannel channel, String url, + String tabKey, String sessionKey, String moduleName, String userAgent) { + super(channel); + assert url != null; + assert tabKey != null; + assert sessionKey != null; + assert moduleName != null; + assert userAgent != null; + this.url = url; + this.tabKey = tabKey; + this.sessionKey = sessionKey; + this.moduleName = moduleName; + this.userAgent = userAgent; + } + + public String getModuleName() { + return moduleName; + } + + public String getSessionKey() { + return sessionKey; + } + + public String getTabKey() { + return tabKey; + } + + public String getUrl() { + return url; + } + + public String getUserAgent() { + return userAgent; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.LOAD_MODULE.getId()); + writeUtf8String(stream, url); + writeUtf8String(stream, tabKey); + writeUtf8String(stream, sessionKey); + writeUtf8String(stream, moduleName); + writeUtf8String(stream, userAgent); + stream.flush(); + } + } + + /** + * Abstract base class of OOPHM messages. + */ + protected abstract static class Message { + public static MessageType readMessageType(DataInputStream stream) + throws IOException, BrowserChannelException { + stream.mark(1); + int type = stream.readByte(); + MessageType[] types = MessageType.values(); + if (type < 0 || type >= types.length) { + stream.reset(); + throw new BrowserChannelException("Invalid message type " + type); + } + return types[type]; + } + + private final BrowserChannel channel; + + public Message(BrowserChannel channel) { + this.channel = channel; + } + + public final BrowserChannel getBrowserChannel() { + return channel; + } + + /** + * @return true if this message type is asynchronous and does not expect a + * return message. + */ + public boolean isAsynchronous() { + return false; + } + + /** + * @throws IOException if a subclass encounters an I/O error + */ + public void send() throws IOException { + throw new UnsupportedOperationException(getClass().getName() + + " is a message format that can only be received."); + } + } + + /** + * Provides a way of allocating JS and Java object ids without knowing + * which one is the remote type, so code can be shared between client and + * server. + */ + protected interface ObjectRefFactory { + + JavaObjectRef getJavaObjectRef(int refId); + + JsObjectRef getJsObjectRef(int refId); + + Set getRefIdsForCleanup(); + } + + /** + * A request from the client that the server load and initialize a given + * module (original v1 version). + */ + protected static class OldLoadModuleMessage extends Message { + public static OldLoadModuleMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + int protoVersion = stream.readInt(); + String moduleName = readUtf8String(stream); + String userAgent = readUtf8String(stream); + return new OldLoadModuleMessage(channel, protoVersion, moduleName, + userAgent); + } + + private final String moduleName; + + private final int protoVersion; + + private final String userAgent; + + public OldLoadModuleMessage(BrowserChannel channel, int protoVersion, + String moduleName, String userAgent) { + super(channel); + this.protoVersion = protoVersion; + this.moduleName = moduleName; + this.userAgent = userAgent; + } + + public String getModuleName() { + return moduleName; + } + + public int getProtoVersion() { + return protoVersion; + } + + public String getUserAgent() { + return userAgent; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.OLD_LOAD_MODULE.getId()); + stream.writeInt(protoVersion); + writeUtf8String(stream, moduleName); + writeUtf8String(stream, userAgent); + stream.flush(); + } + } + + /** + * Reports the selected protocol version. + */ + protected static class ProtocolVersionMessage extends Message { + + public static ProtocolVersionMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + int protocolVersion = stream.readInt(); + return new ProtocolVersionMessage(channel, protocolVersion); + } + + private final int protocolVersion; + + public ProtocolVersionMessage(BrowserChannel channel, int protocolVersion) { + super(channel); + this.protocolVersion = protocolVersion; + } + + public int getProtocolVersion() { + return protocolVersion; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.PROTOCOL_VERSION.getId()); + stream.writeInt(protocolVersion); + stream.flush(); + } + } + + /** + * A message signifying a soft close of the communications channel. + */ + protected static class QuitMessage extends Message { + public static QuitMessage receive(BrowserChannel channel) { + return new QuitMessage(channel); + } + + public static void send(BrowserChannel channel) throws IOException { + final DataOutputStream stream = channel.getStreamToOtherSide(); + stream.writeByte(MessageType.QUIT.getId()); + stream.flush(); + } + + public QuitMessage(BrowserChannel channel) { + super(channel); + } + + @Override + public void send() throws IOException { + send(getBrowserChannel()); + } + } + + /** + * A message asking the client to send an icon suitable for use in the UI. + *

See {@link UserAgentIconMessage}. + */ + protected static class RequestIconMessage extends Message { + + /** + * Receive a RequestIconMessage, assuming the message tag has already been + * read. + * + * @throws IOException + */ + public static RequestIconMessage receive(BrowserChannel channel) + throws IOException { + return new RequestIconMessage(channel); + } + + public static void send(BrowserChannel channel) + throws IOException { + DataOutputStream stream = channel.getStreamToOtherSide(); + stream.writeByte(MessageType.REQUEST_ICON.getId()); + stream.flush(); + } + + public RequestIconMessage(BrowserChannel channel) { + super(channel); + } + + @Override + public void send() throws IOException { + send(getBrowserChannel()); + } + } + + /** + * Signifies a return from a previous invoke. + */ + protected static class ReturnMessage extends Message { + public static ReturnMessage receive(BrowserChannel channel) + throws IOException { + final DataInputStream stream = channel.getStreamFromOtherSide(); + final boolean isException = stream.readBoolean(); + final Value returnValue = channel.readValue(stream); + return new ReturnMessage(channel, isException, returnValue); + } + + public static void send(BrowserChannel channel, boolean isException, + Value returnValue) throws IOException { + final DataOutputStream stream = channel.getStreamToOtherSide(); + stream.writeByte(MessageType.RETURN.getId()); + stream.writeBoolean(isException); + channel.writeValue(stream, returnValue); + stream.flush(); + } + + public static void send(BrowserChannel channel, + ExceptionOrReturnValue returnOrException) throws IOException { + send(channel, returnOrException.isException(), + returnOrException.getReturnValue()); + } + + private final boolean isException; + private final Value returnValue; + + public ReturnMessage(BrowserChannel channel, boolean isException, + Value returnValue) { + super(channel); + this.returnValue = returnValue; + this.isException = isException; + } + + public Value getReturnValue() { + return returnValue; + } + + public boolean isException() { + return isException; + } + + @Override + public void send() throws IOException { + send(getBrowserChannel(), isException, returnValue); + } + } + + /** + * A response to ChooseTransport telling the client which transport should + * be used for the remainder of the protocol. + */ + protected static class SwitchTransportMessage extends Message { + + public static SwitchTransportMessage receive(BrowserChannel channel) + throws IOException { + DataInputStream stream = channel.getStreamFromOtherSide(); + String transport = readUtf8String(stream); + String transportArgs = readUtf8String(stream); + return new SwitchTransportMessage(channel, transport, transportArgs); + } + + private final String transport; + + private final String transportArgs; + + public SwitchTransportMessage(BrowserChannel channel, + String transport, String transportArgs) { + super(channel); + // Change nulls to empty strings + if (transport == null) { + transport = ""; + } + if (transportArgs == null) { + transportArgs = ""; + } + this.transport = transport; + this.transportArgs = transportArgs; + } + + public String getTransport() { + return transport; + } + + public String getTransportArgs() { + return transportArgs; + } + + @Override + public void send() throws IOException { + DataOutputStream stream = getBrowserChannel().getStreamToOtherSide(); + stream.writeByte(MessageType.SWITCH_TRANSPORT.getId()); + writeUtf8String(stream, transport); + writeUtf8String(stream, transportArgs); + stream.flush(); + } + } + + /** + * A message supplying an icon, which fits in 24x24 and in a standard image + * format such as PNG or GIF, suitable for use in the UI. + *

See {@link RequestIconMessage}. + */ + protected static class UserAgentIconMessage extends Message { + public static UserAgentIconMessage receive(BrowserChannel channel) + throws IOException { + byte[] iconBytes = null; + DataInputStream stream = channel.getStreamFromOtherSide(); + int len = stream.readInt(); + if (len > 0) { + iconBytes = new byte[len]; + for (int i = 0; i < len; ++i) { + iconBytes[i] = stream.readByte(); + } + } + return new UserAgentIconMessage(channel, iconBytes); + } + + public static void send(BrowserChannel channel, byte[] iconBytes) + throws IOException { + DataOutputStream stream = channel.getStreamToOtherSide(); + stream.writeByte(MessageType.USER_AGENT_ICON.getId()); + if (iconBytes == null) { + stream.writeInt(0); + } else { + stream.writeInt(iconBytes.length); + for (byte b : iconBytes) { + stream.writeByte(b); + } + } + stream.flush(); + } + + private byte[] iconBytes; + + public UserAgentIconMessage(BrowserChannel channel, byte[] iconBytes) { + super(channel); + this.iconBytes = iconBytes; + } + + public byte[] getIconBytes() { + return iconBytes; + } + + @Override + public void send() throws IOException { + send(getBrowserChannel(), iconBytes); + } + } + + /** + * The current version of the protocol. + */ + public static final int PROTOCOL_VERSION_CURRENT = 3; + + /** + * The oldest protocol version supported by this code. + */ + public static final int PROTOCOL_VERSION_OLDEST = 2; + + /** + * The protocol version that added the GetIcon message. + */ + public static final int PROTOCOL_VERSION_GET_ICON = 3; + + public static final int SPECIAL_CLIENTMETHODS_OBJECT = 0; + + public static final int SPECIAL_SERVERMETHODS_OBJECT = 0; + + protected static JavaObjectRef getJavaObjectRef(int refId) { + return new JavaObjectRef(refId); + } + + protected static String readUtf8String(DataInputStream stream) + throws IOException { + final int len = stream.readInt(); + final byte[] data = new byte[len]; + stream.readFully(data); + return new String(data, "UTF8"); + } + + protected static ValueType readValueType(DataInputStream stream) + throws IOException, BrowserChannelException { + int type = stream.readByte(); + ValueType[] types = ValueType.values(); + if (type < 0 || type >= types.length) { + throw new BrowserChannelException("Invalid value type " + type); + } + return types[type]; + } + + protected static void writeJavaObject(DataOutputStream stream, + JavaObjectRef value) throws IOException { + stream.writeByte(ValueType.JAVA_OBJECT.getTag()); + stream.writeInt(value.getRefid()); + } + + protected static void writeJsObject(DataOutputStream stream, + JsObjectRef value) throws IOException { + stream.writeByte(ValueType.JS_OBJECT.getTag()); + stream.writeInt(value.getRefid()); + } + + protected static void writeNull(DataOutputStream stream) throws IOException { + stream.writeByte(ValueType.NULL.getTag()); + } + + protected static void writeTaggedBoolean(DataOutputStream stream, + boolean value) throws IOException { + stream.writeByte(ValueType.BOOLEAN.getTag()); + stream.writeBoolean(value); + } + + protected static void writeTaggedByte(DataOutputStream stream, byte value) + throws IOException { + stream.writeByte(ValueType.BYTE.getTag()); + stream.writeByte(value); + } + + protected static void writeTaggedChar(DataOutputStream stream, char value) + throws IOException { + stream.writeByte(ValueType.CHAR.getTag()); + stream.writeChar(value); + } + + protected static void writeTaggedDouble(DataOutputStream stream, double value) + throws IOException { + stream.writeByte(ValueType.DOUBLE.getTag()); + stream.writeDouble(value); + } + + protected static void writeTaggedFloat(DataOutputStream stream, float value) + throws IOException { + stream.writeByte(ValueType.FLOAT.getTag()); + stream.writeFloat(value); + } + + protected static void writeTaggedInt(DataOutputStream stream, int value) + throws IOException { + stream.writeByte(ValueType.INT.getTag()); + stream.writeInt(value); + } + + protected static void writeTaggedShort(DataOutputStream stream, short value) + throws IOException { + stream.writeByte(ValueType.SHORT.getTag()); + stream.writeShort(value); + } + + protected static void writeTaggedString(DataOutputStream stream, String data) + throws IOException { + stream.writeByte(ValueType.STRING.getTag()); + writeUtf8String(stream, data); + } + + protected static void writeUtf8String(DataOutputStream stream, String data) + throws IOException { + try { + final byte[] bytes = data.getBytes("UTF8"); + stream.writeInt(bytes.length); + stream.write(bytes); + } catch (UnsupportedEncodingException e) { + // TODO: Add description. + throw new RuntimeException(); + } + } + + private static void writeUndefined(DataOutputStream stream) + throws IOException { + stream.writeByte(ValueType.UNDEFINED.getTag()); + } + + private final ObjectRefFactory objectRefFactory; + + private Socket socket; + + private final DataInputStream streamFromOtherSide; + + private final DataOutputStream streamToOtherSide; + + public BrowserChannel(Socket socket, ObjectRefFactory objectRefFactory) + throws IOException { + this(new BufferedInputStream(socket.getInputStream()), + new BufferedOutputStream(socket.getOutputStream()), + objectRefFactory); + this.socket = socket; + } + + protected BrowserChannel(InputStream inputStream, OutputStream outputStream, + ObjectRefFactory objectRefFactory) { + streamFromOtherSide = new DataInputStream(inputStream); + streamToOtherSide = new DataOutputStream(outputStream); + socket = null; + this.objectRefFactory = objectRefFactory; + } + + public void endSession() { + Utility.close(streamFromOtherSide); + Utility.close(streamToOtherSide); + Utility.close(socket); + } + + /** + * @return a set of remote object reference IDs to be freed. + */ + public Set getRefIdsForCleanup() { + return objectRefFactory.getRefIdsForCleanup(); + } + + public String getRemoteEndpoint() { + if (socket == null) { + return ""; + } + return socket.getInetAddress().getCanonicalHostName() + ":" + + socket.getPort(); + } + + protected DataInputStream getStreamFromOtherSide() { + return streamFromOtherSide; + } + + protected DataOutputStream getStreamToOtherSide() { + return streamToOtherSide; + } + + protected Value readValue(DataInputStream stream) throws IOException { + ValueType tag; + try { + tag = readValueType(stream); + } catch (BrowserChannelException e) { + IOException ee = new IOException(); + ee.initCause(e); + throw ee; + } + Value value = new Value(); + switch (tag) { + case NULL: + value.setNull(); + break; + case UNDEFINED: + value.setUndefined(); + break; + case BOOLEAN: + value.setBoolean(stream.readByte() != 0); + break; + case BYTE: + value.setByte(stream.readByte()); + break; + case CHAR: + value.setChar(stream.readChar()); + break; + case FLOAT: + value.setFloat(stream.readFloat()); + break; + case INT: + value.setInt(stream.readInt()); + break; + case LONG: + value.setLong(stream.readLong()); + break; + case DOUBLE: + value.setDouble(stream.readDouble()); + break; + case SHORT: + value.setShort(stream.readShort()); + break; + case STRING: + value.setString(readUtf8String(stream)); + break; + case JS_OBJECT: + value.setJsObject(objectRefFactory.getJsObjectRef(stream.readInt())); + break; + case JAVA_OBJECT: + value.setJavaObject(objectRefFactory.getJavaObjectRef( + stream.readInt())); + break; + } + return value; + } + + protected void sendFreedValues() throws IOException { + Set freed = objectRefFactory.getRefIdsForCleanup(); + int n = freed.size(); + if (n > 0) { + int[] ids = new int[n]; + int i = 0; + for (Integer id : freed) { + ids[i++] = id; + } + FreeMessage.send(this, ids); + } + } + + protected void writeValue(DataOutputStream stream, Value value) + throws IOException { + if (value.isNull()) { + writeNull(stream); + } else if (value.isUndefined()) { + writeUndefined(stream); + } else if (value.isJsObject()) { + writeJsObject(stream, value.getJsObject()); + } else if (value.isJavaObject()) { + writeJavaObject(stream, value.getJavaObject()); + } else if (value.isBoolean()) { + writeTaggedBoolean(stream, value.getBoolean()); + } else if (value.isByte()) { + writeTaggedByte(stream, value.getByte()); + } else if (value.isChar()) { + writeTaggedChar(stream, value.getChar()); + } else if (value.isShort()) { + writeTaggedShort(stream, value.getShort()); + } else if (value.isDouble()) { + writeTaggedDouble(stream, value.getDouble()); + } else if (value.isFloat()) { + writeTaggedFloat(stream, value.getFloat()); + } else if (value.isInt()) { + writeTaggedInt(stream, value.getInt()); + } else if (value.isString()) { + writeTaggedString(stream, value.getString()); + } else { + assert false; + } + } +} diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/QueryTest.gwt.xml b/gwtquery-core/src/test/java/com/google/gwt/query/QueryTest.gwt.xml new file mode 100644 index 00000000..5b2b1d35 --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/query/QueryTest.gwt.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxCommon.java b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxCommon.java new file mode 100644 index 00000000..2ff30953 --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxCommon.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013, The gwtquery team. + * + * 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.query.client.ajax; + +import static com.google.gwt.query.client.GQuery.*; + +import org.mortbay.jetty.Server; + +import net.sourceforge.htmlunit.corejs.javascript.Context; +import net.sourceforge.htmlunit.corejs.javascript.Scriptable; + +import com.gargoylesoftware.htmlunit.javascript.host.xml.XMLHttpRequest; +import com.google.gwt.core.client.GWT; +import com.google.gwt.junit.client.GWTTestCase; +import com.google.gwt.query.client.Binder; +import com.google.gwt.query.client.Function; +import com.google.gwt.query.client.GQ; +import com.google.gwt.query.client.Properties; +import com.google.gwt.query.client.plugins.ajax.Ajax; +import com.google.gwt.query.client.plugins.ajax.Ajax.Settings; + +/** + * Tests for Deferred which can run either in JVM and GWT + */ +public abstract class AjaxCommon extends GWTTestCase { + + public String getModuleName() { + return null; + } + + protected String echoUrl, corsUrl; + protected Binder json, jsonData; + protected String servletPath = "test.json"; + + private void performAjaxJsonTest(Settings s) { + delayTestFinish(5000); + Ajax.ajax(s).done(new Function(){public void f() { + Binder p = arguments(0); + assertEquals("abc", p.get("a")); + finishTest(); + }}).fail(new Function(){public void f() { + fail(); + }}); + } + + public void testAjaxJsonPost() { + delayTestFinish(5000); + Settings s = Ajax.createSettings() + .setUrl(echoUrl) + .setData(json) + .setDataType("json") + .setUsername("testuser") + .setPassword("testpassword") + ; + performAjaxJsonTest(s); + } + + public void testAjaxJsonPost_CORS() { + delayTestFinish(5000); + Settings s = Ajax.createSettings() + .setUrl(corsUrl) + .setData(json) + .setDataType("json"); + + performAjaxJsonTest(s); + } + + public void testAjaxJsonGet() { + Settings s = Ajax.createSettings() + .setType("get") + .setUrl(echoUrl) + .setData(jsonData) + .setDataType("json"); + + performAjaxJsonTest(s); + } + + public void testAjaxJsonGet_CORS() { + Settings s = Ajax.createSettings() + .setType("get") + .setUrl(corsUrl) + .setData(jsonData) + .setDataType("json"); + + performAjaxJsonTest(s); + } + + public void testAjaxGetJsonP() { + delayTestFinish(5000); + Settings s = Ajax.createSettings() + .setType("post") + .setUrl(echoUrl) + .setData(jsonData) + .setDataType("jsonp"); + + performAjaxJsonTest(s); + } + + public void testAjaxGetJsonP_CORS() { + delayTestFinish(5000); + Settings s = Ajax.createSettings() + .setType("post") + .setUrl(corsUrl) + .setData(jsonData) + .setDataType("jsonp"); + + performAjaxJsonTest(s); + } + + + +// public void testAjaxJson() { +// delayTestFinish(5000); +// Settings s = Ajax.createSettings() +// .setType("get") +// .setUrl(GWT.getModuleBaseURL() + "test.json") +// .setData($$("data: {a: abc, d: ddd}")) +// .setDataType("json"); +// +// Ajax.ajax(s).done(new Function(){public void f() { +// Binder p = arguments(0); +// assertEquals("abc", p.get("a")); +// finishTest(); +// }}).fail(new Function(){public void f() { +// fail(); +// }}); +// } + public void testJsonValidService() { + delayTestFinish(5000); + // Use a public json service + String testJsonpUrl = "https://www.googleapis.com/blogger/v2/blogs/user_id/posts/post_id?callback=?&key=NO-KEY"; + Ajax.getJSONP(testJsonpUrl, new Function(){ + public void f() { + Binder p = arguments(0); + // It should return error since we do not use a valid key + // {"error":{"errors":[{"domain":"usageLimits","reason":"keyInvalid","message":"Bad Request"}],"code":400,"message":"Bad Request"}} + assertEquals(400, p.get("error").get("code")); + finishTest(); + } + }, null, 0); + } + +} \ No newline at end of file diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTest.java b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTest.java index a21ec648..45636766 100644 --- a/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTest.java +++ b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTest.java @@ -15,40 +15,86 @@ */ package com.google.gwt.query.client.ajax; -import java.util.Arrays; -import java.util.Date; -import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.Servlet; + +import org.mortbay.jetty.Server; +import org.mortbay.jetty.handler.HandlerWrapper; +import org.mortbay.jetty.servlet.DefaultServlet; +import org.mortbay.jetty.webapp.WebAppClassLoader; +import org.mortbay.jetty.webapp.WebAppContext; -import com.google.gwt.junit.client.GWTTestCase; -import com.google.gwt.query.client.Function; import com.google.gwt.query.client.GQ; -import com.google.gwt.query.client.Properties; -import com.google.gwt.query.client.builders.JsonBuilder; -import com.google.gwt.query.client.builders.Name; -import com.google.gwt.query.client.plugins.ajax.Ajax; +import com.google.gwt.query.servlet.GQAjaxTestServlet; /** * Tests for Deferred which can run either in JVM and GWT */ -public class AjaxTest extends GWTTestCase { +public class AjaxTest extends AjaxCommon { + + static Server server; + int port = 3333; public String getModuleName() { return null; } - public void testJsonValidService() { - delayTestFinish(5000); - // Use a public json service - String testJsonpUrl = "https://www.googleapis.com/blogger/v2/blogs/user_id/posts/post_id?callback=?&key=NO-KEY"; - Ajax.getJSONP(testJsonpUrl, new Function(){ - public void f() { - Properties p = getDataProperties(); - // It should return error since we do not use a valid key - // {"error":{"errors":[{"domain":"usageLimits","reason":"keyInvalid","message":"Bad Request"}],"code":400,"message":"Bad Request"}} - assertEquals(400, p.getJavaScriptObject("error").cast().getInt("code")); - finishTest(); + protected void gwtSetUp() throws Exception { + echoUrl = "http://127.0.0.1:" + port + "/" + servletPath; + corsUrl = "http://localhost:" + port + "/" + servletPath; + jsonData = GQ.create("data: {a: abc, d: ddd}"); + json = GQ.create("a: abc, d: ddd"); + startWebServer(); + } + + protected void startWebServer() throws Exception { + if (server == null) { + final Map> servlets = new HashMap>(); + servlets.put("/" + servletPath, GQAjaxTestServlet.class); + server = createWebServer(port, ".", null, servlets, null); + } + } + + public static Server createWebServer(final int port, final String resourceBase, final String[] classpath, + final Map> servlets, final HandlerWrapper handler) throws Exception { + + final Server server = new Server(port); + + final WebAppContext context = new WebAppContext(); + context.setContextPath("/"); + context.setResourceBase(resourceBase); + + if (servlets != null) { + for (final Map.Entry> entry : servlets.entrySet()) { + final String pathSpec = entry.getKey(); + final Class servlet = entry.getValue(); + context.addServlet(servlet, pathSpec); + + // disable defaults if someone likes to register his own root servlet + if ("/".equals(pathSpec)) { + context.setDefaultsDescriptor(null); + context.addServlet(DefaultServlet.class, "/favicon.ico"); + } + } + } + + final WebAppClassLoader loader = new WebAppClassLoader(context); + if (classpath != null) { + for (final String path : classpath) { + loader.addClassPath(path); } - }, null, 0); + } + context.setClassLoader(loader); + if (handler != null) { + handler.setHandler(context); + server.setHandler(handler); + } else { + server.setHandler(context); + } + server.start(); + return server; } } \ No newline at end of file diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTestGwt.java b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTestGwt.java index b30d53d6..022c84b1 100644 --- a/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTestGwt.java +++ b/gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTestGwt.java @@ -15,15 +15,24 @@ */ package com.google.gwt.query.client.ajax; +import com.google.gwt.core.client.GWT; +import com.google.gwt.query.client.GQ; + /** * Test for data binding shared code run in gwt */ -public class AjaxTestGwt extends AjaxTest { - +public class AjaxTestGwt extends AjaxCommon { @Override public String getModuleName() { - return "com.google.gwt.query.Query"; + return "com.google.gwt.query.QueryTest"; + } + + @Override + protected void gwtSetUp() throws Exception { + echoUrl = (GWT.isClient() ? GWT.getHostPageBaseURL() : "http://localhost:3333/") + servletPath; + corsUrl = echoUrl.replaceFirst("http://[\\d\\.]+:", "http://localhost:") + "?cors=true"; + jsonData = GQ.create("data: {a: abc, d: ddd}"); + json = GQ.create("a: abc, d: ddd"); } - } diff --git a/gwtquery-core/src/test/java/com/google/gwt/query/servlet/GQAjaxTestServlet.java b/gwtquery-core/src/test/java/com/google/gwt/query/servlet/GQAjaxTestServlet.java new file mode 100644 index 00000000..6ba602da --- /dev/null +++ b/gwtquery-core/src/test/java/com/google/gwt/query/servlet/GQAjaxTestServlet.java @@ -0,0 +1,74 @@ +package com.google.gwt.query.servlet; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Enumeration; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class GQAjaxTestServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + String name = this.getClass().getSimpleName() + " "; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + System.out.println(name + req.getMethod() + " " + req.getContentType()); + Enumeration en = req.getHeaderNames(); + while (en.hasMoreElements()) { + String s = (String) en.nextElement(); + System.out.println(name + s + " " + req.getHeader(s)); + } + + + String t = req.getParameter("timeout"); + if (t != null && t.matches("\\d+")) { + try { + int ms = Integer.parseInt(t); + System.out.println(" Sleeping: " + ms); + Thread.sleep(ms); + } catch (Exception e) { + } + } + + String data = ""; + if (req.getMethod().equalsIgnoreCase("get")) { + data = req.getParameter("data") != null ? req.getParameter("data") : ""; + if (req.getParameter("callback") != null) { + data = req.getParameter("callback") + "(" + data + ");"; + } + } else if (req.getMethod().equalsIgnoreCase("post") + && req.getContentType().toLowerCase().startsWith("application/json")) { + BufferedReader reader = req.getReader(); + String line; + while ((line = reader.readLine()) != null) + data += line; + } + + String origin = req.getHeader("Origin"); + if ("true".equals(req.getParameter("cors")) && origin != null) { + resp.addHeader("Access-Control-Allow-Origin", origin); + resp.addHeader("Access-Control-Allow-Credentials", "true"); + String method = req.getHeader("Access-Control-Request-Method"); + if (method != null) { + resp.addHeader("Access-Control-Allow-Methods", method); + resp.setHeader("Allow", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS"); + } + String headers = req.getHeader("Access-Control-Request-Headers"); + if (headers != null) { + resp.addHeader("Access-Control-Allow-Headers", headers); + } + } + + PrintWriter p = resp.getWriter(); + p.print(data); + p.flush(); + p.close(); + + System.out.println(name + "returns: " + data); + } +} -- 2.39.5