]> source.dussan.org Git - gwtquery.git/commitdiff
Implemented Ajax for JVM, and tests
authorManuel Carrasco Moñino <manuel.carrasco.m@gmail.com>
Sat, 4 Jan 2014 21:57:15 +0000 (22:57 +0100)
committerManuel Carrasco Moñino <manuel.carrasco.m@gmail.com>
Sat, 4 Jan 2014 21:57:15 +0000 (22:57 +0100)
20 files changed:
gwtquery-core/pom.xml
gwtquery-core/src/main/java/com/google/gwt/junit/RunStyleHtmlUnit.java [new file with mode: 0644]
gwtquery-core/src/main/java/com/google/gwt/query/client/GQ.java
gwtquery-core/src/main/java/com/google/gwt/query/client/builders/JsonFactory.java
gwtquery-core/src/main/java/com/google/gwt/query/client/js/JsUtils.java
gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/Effects.java
gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/Ajax.java
gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/ajax/AjaxTransportJs.java
gwtquery-core/src/main/java/com/google/gwt/query/client/plugins/effects/Transitions.java
gwtquery-core/src/main/java/com/google/gwt/query/rebind/JsonBuilderGenerator.java
gwtquery-core/src/main/java/com/google/gwt/query/vm/AjaxTransportJre.java
gwtquery-core/src/main/java/com/google/gwt/query/vm/JsonFactoryJre.java
gwtquery-core/src/main/java/com/google/gwt/query/vm/ResponseJre.java [new file with mode: 0644]
gwtquery-core/src/main/super/com/google/gwt/query/super/com/google/gwt/query/client/GQ.java
gwtquery-core/src/test/java/com/google/gwt/dev/shell/BrowserChannel.java [new file with mode: 0644]
gwtquery-core/src/test/java/com/google/gwt/query/QueryTest.gwt.xml [new file with mode: 0644]
gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxCommon.java [new file with mode: 0644]
gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTest.java
gwtquery-core/src/test/java/com/google/gwt/query/client/ajax/AjaxTestGwt.java
gwtquery-core/src/test/java/com/google/gwt/query/servlet/GQAjaxTestServlet.java [new file with mode: 0644]

index c44b7f9b9490ee9e567f45e9ff23e09816ecc0bf..7bfbbb9aef05cfec6057f6e2db608605e92a3cb5 100644 (file)
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.7</version>
-            <type>jar</type>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>net.sourceforge.htmlunit</groupId>
+            <artifactId>htmlunit</artifactId>
+            <version>2.13</version>
+            <scope>test</scope>
+        </dependency>        
         <dependency>
             <groupId>com.google.gwt</groupId>
             <artifactId>gwt-user</artifactId>
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 (file)
index 0000000..0d8b5fe
--- /dev/null
@@ -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<String, BrowserVersion> 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<Platform> 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<String, BrowserVersion> createBrowserMap() {
+    Map<String, BrowserVersion> browserMap = new HashMap<String, BrowserVersion>();
+    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<BrowserVersion> browsers = new HashSet<BrowserVersion>();
+  private boolean developmentMode;
+  private final List<Thread> threads = new ArrayList<Thread>();
+
+  /**
+   * Create a RunStyle instance with the passed-in browser targets.
+   */
+  public RunStyleHtmlUnit(JUnitShell shell) {
+    super(shell);
+  }
+
+  @Override
+  public Set<Platform> 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<BrowserVersion> browserSet = new HashSet<BrowserVersion>();
+    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);
+  }
+}
index 0791feaf365e0c02d236ace95ad3ae3b59619385..1e6aa30487265727c20761b4c53acf32f146d2f8 100644 (file)
@@ -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 extends JsonBuilder> T create(Class<T> clz) {
-    if (jsonFactory == null) {
-      jsonFactory = GWT.isClient() ?
-          GWT.<JsonFactory>create(JsonFactory.class) :
-          new JsonFactoryJre();
-    }
-    return jsonFactory.create(clz);
+    return getFactory().create(clz);
   }
 
   public static <T extends JsonBuilder> T create(Class<T> clz, String payload) {
@@ -38,4 +37,26 @@ public class GQ {
     ret.load(payload);
     return ret;
   }
+  
+  public static <T extends Binder> 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.<JsonFactory>create(JsonFactory.class) :
+          new JsonFactoryJre();
+    }
+    return jsonFactory;
+  }
 }
index 30f08bb96f7d3ec88d87ba86254b96bfdf0894e6..adcd5212c324aa0d41a150527bc849a954ca060c 100644 (file)
@@ -1,5 +1,8 @@
 package com.google.gwt.query.client.builders;
 
+import com.google.gwt.query.client.Binder;
+
 public interface JsonFactory {
   <T extends JsonBuilder> T create(Class<T> clz);
+  <T extends Binder> T create(String s);
 }
index be06f79b5387273dace28adf974c3706b2fa3d04..ac01f7caeb477c6d4873ae25d250532ae58930d6 100644 (file)
@@ -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)) {
index c7b66d5bd7593d4a76e2d88b45d7badbcfe979c7..7f6ce9270e3c76694e360ae08e3fb17a0d6c9e12 100755 (executable)
@@ -166,7 +166,8 @@ public class Effects extends QueuePlugin<Effects> {
   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");
index 9da8e7dd6a45e09de5dd275410ec55764416df5e..ecf5b626d716bcc70596f0ec5a704af6837cef10 100644 (file)
@@ -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.<JavaScriptObject>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<Void, Exception>() {
-          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("</?html[\\s\\S]*?>\\s*", "")
             .replaceAll("<head[\\s\\S]*?</head>\\s*", "")
             .replaceAll("<script[\\s\\S]*?</script>\\s*", "")
index bc7a72efa2dbcbf0ead342046f511cccf20bb97b..fe4e91b89312a60809b7f40fefdbbc98b04c4594 100644 (file)
@@ -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);
+  }
   
 }
index 5740350135eeabc80d00b7ee55d1eda28abf2c09..ac9581cda195c4724d94748042c746cc72e0513c 100644 (file)
@@ -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();
index 2e66bc3f706928eb83138c5be4761b06c37c6e77..e1c905af4d5d21727d2a92df80595ac68fa71eed 100644 (file)
@@ -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 extends Binder> T create(String s) {");
+    sw.indent();
+    sw.println("return (T)" + Properties.class.getName() + ".create(s);");
+    sw.outdent();
+    sw.println("}");
   }
 }
index 7e882d625676dc2f4b27765eb49bb572f0f2273f..822da4c2e5c7e8a95ccf0937ae77966868004133 100644 (file)
@@ -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());
+  }
 }
index ec1150865c814d26672acabc6e0f54255b43b9cb..b81ca2d473d988829ef4c47fc44d14602d7f5bfe 100644 (file)
@@ -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 extends Binder> 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 (file)
index 0000000..36aafd3
--- /dev/null
@@ -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<String,List<String>> headers;
+  private String responseText;
+  private String statusText;
+  
+  public ResponseJre(int status, String statusText, String text, Map<String,List<String>> headers) {
+    this.status = status;
+    this.headers = headers;
+    this.responseText = text;
+    this.statusText = statusText;
+  }
+  
+  public String getHeader(String header) {
+    List<String> 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
index edad9154e9eb61c58a65c39a166f01a6ad892fa1..d694afa5dd99b4c8922173cfe827c72ec60215a7 100644 (file)
 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 extends JsonBuilder> T create(Class<T> clz) {
-    if (jsonFactory == null) {
-      jsonFactory = GWT.<JsonFactory>create(JsonFactory.class);
-    }
-    return jsonFactory.create(clz);
+    return getFactory().create(clz);
   }
 
   public static <T extends JsonBuilder> T create(Class<T> clz, String payload) {
@@ -36,4 +38,22 @@ public class GQ {
     ret.load(payload);
     return ret;
   }
+  
+  public static <T extends Binder> 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.<JsonFactory>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 (file)
index 0000000..9a5a925
--- /dev/null
@@ -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.
+   * 
+   * <p>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<T extends BrowserChannel> {
+
+    /**
+     * 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).
+     * 
+     * <p>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<Integer> 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.
+   * <p>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.
+   * <p>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<Integer> 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<Integer> 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 (file)
index 0000000..5b2b1d3
--- /dev/null
@@ -0,0 +1,21 @@
+<!--
+  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.
+-->
+
+<module>
+    <inherits name='com.google.gwt.query.Query'/>
+    <servlet class='com.google.gwt.query.servlet.GQAjaxTestServlet' path="/test.json"/>
+    <servlet class='com.google.gwt.query.servlet.GQAjaxTestServlet' path="/test.upld"/>
+</module>
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 (file)
index 0000000..2ff3095
--- /dev/null
@@ -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.<Binder>get("error").get("code"));
+        finishTest();
+      }
+    }, null, 0);
+  }
+
+}
\ No newline at end of file
index a21ec64839a7837cc9f1320b3a92f7b4680832d6..456367667efac97e919b2684af836fc10b45f5a4 100644 (file)
  */
 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").<Properties>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<String, Class<? extends Servlet>> servlets = new HashMap<String, Class<? extends Servlet>>();
+      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<String, Class<? extends Servlet>> 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<String, Class<? extends Servlet>> entry : servlets.entrySet()) {
+        final String pathSpec = entry.getKey();
+        final Class<? extends Servlet> 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
index b30d53d65f5a1c7a2fde320df33337a74eee488a..022c84b180018adfef2e3ec41bf6c62fca4bff86 100644 (file)
  */
 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 (file)
index 0000000..6ba602d
--- /dev/null
@@ -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<String> 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);
+  }
+}