Add support multiple modules in a single browser window in hosted mode, moving much of the functionality into platform-dependent code. Added a unit
test for this functionality, although currently it passes simply by not crashing -- in the future we will add instrumentation for the module loading/unloading code (when we can easily create new objects under window.external) and verify that it is being properly handled.
Review by: scottb, knorton
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@799 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
index 5bc3fbc..7c530b1 100644
--- a/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
+++ b/dev/core/src/com/google/gwt/dev/shell/BrowserWidget.java
@@ -54,6 +54,7 @@
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
@@ -114,7 +115,13 @@
browser.stop();
} else if (evt.widget == openWebModeButton) {
// first, compile
- Set keySet = moduleSpacesByName.keySet();
+ Set keySet = new HashSet();
+ for (Iterator iter = loadedModules.entrySet().iterator();
+ iter.hasNext(); ) {
+ ModuleSpace module
+ = (ModuleSpace) ((Map.Entry) iter.next()).getValue();
+ keySet.add(module.getModuleName());
+ }
String[] moduleNames = Util.toStringArray(keySet);
if (moduleNames.length == 0) {
// A latent problem with a module.
@@ -198,20 +205,21 @@
logger.log(TreeLogger.ERROR,
"Unable to find a default external web browser", null);
- logger.log(
- TreeLogger.WARN,
- "Try setting the environment varable GWT_EXTERNAL_BROWSER to your web browser executable before launching the GWT shell",
- null);
+ logger.log(TreeLogger.WARN, "Try setting the environment variable "
+ + "GWT_EXTERNAL_BROWSER to your web browser executable before "
+ + "launching the GWT shell", null);
}
protected Browser browser;
-
+
private Color bgColor = new Color(null, 239, 237, 216);
private Button goButton;
private final BrowserWidgetHost host;
+ private final Map loadedModules = new HashMap();
+
private Text location;
private final TreeLogger logger;
@@ -220,8 +228,6 @@
private Toolbar toolbar;
- private Map moduleSpacesByName = new HashMap();
-
public BrowserWidget(Composite parent, BrowserWidgetHost host) {
super(parent, SWT.NONE);
@@ -234,7 +240,7 @@
Composite secondBar = buildLocationBar(this);
browser = new Browser(this, SWT.NONE);
-
+
{
statusBar = new Label(this, SWT.BORDER | SWT.SHADOW_IN);
statusBar.setBackground(bgColor);
@@ -304,54 +310,70 @@
/**
* Initializes and attaches module space to this browser widget. Called by
* subclasses in response to calls from JavaScript.
+ *
+ * @param space ModuleSpace instance to initialize
*/
- protected final void attachModuleSpace(String moduleName, ModuleSpace space)
+ protected final void attachModuleSpace(ModuleSpace space)
throws UnableToCompleteException {
+ Object key = space.getKey();
+ loadedModules.put(key, space);
+ logger.log(TreeLogger.SPAM, "Loading module " + space.getModuleName()
+ + " (id " + key.toString() + ")", null);
+
// Let the space do its thing.
//
space.onLoad(logger);
- // Remember this new module space so that we can dispose of it later.
- //
- moduleSpacesByName.put(moduleName, space);
-
// Enable the compile button since we successfully loaded.
//
toolbar.openWebModeButton.setEnabled(true);
}
/**
- * Disposes all the attached module spaces from the prior page (not the one
- * that just loaded). Called when this widget is disposed but, more
- * interestingly, whenever the browser's page changes.
+ * Unload one or more modules. If key is null, emulate old behavior
+ * by unloading all loaded modules.
+ *
+ * @param key unique key to identify module to unload or null for all
*/
- protected void onPageUnload() {
- for (Iterator iter = moduleSpacesByName.entrySet().iterator(); iter.hasNext();) {
- Map.Entry entry = (Map.Entry) iter.next();
- String moduleName = (String) entry.getKey();
- ModuleSpace space = (ModuleSpace) entry.getValue();
- unloadModule(space, moduleName);
+ protected void doUnload(Object key) {
+ if (key == null) {
+ // BEGIN BACKWARD COMPATIBILITY
+ // remove all modules
+ for (Iterator iter = loadedModules.entrySet().iterator();
+ iter.hasNext(); ) {
+ unloadModule((ModuleSpace) ((Map.Entry) iter.next()).getValue());
+ }
+ loadedModules.clear();
+ // END BACKWARD COMPATIBILITY
+ } else {
+ ModuleSpace moduleSpace = (ModuleSpace) loadedModules.get(key);
+ if (moduleSpace == null) {
+ throw new HostedModeException("Can't find frame window for " + key);
+ }
+ unloadModule(moduleSpace);
+ loadedModules.remove(key);
}
- moduleSpacesByName.clear();
-
- if (!toolbar.openWebModeButton.isDisposed()) {
- // Disable the compile buton.
- //
- toolbar.openWebModeButton.setEnabled(false);
+ if (loadedModules.isEmpty()) {
+ if (!toolbar.openWebModeButton.isDisposed()) {
+ // Disable the compile buton.
+ //
+ toolbar.openWebModeButton.setEnabled(false);
+ }
}
}
-
+
/**
* Unload the specified module.
*
* @param moduleSpace a ModuleSpace instance to unload.
- * @param moduleName the name of the specified module
*/
- protected void unloadModule(ModuleSpace moduleSpace, String moduleName) {
+ protected void unloadModule(ModuleSpace moduleSpace) {
+ String moduleName = moduleSpace.getModuleName();
+ Object key = moduleSpace.getKey();
moduleSpace.dispose();
- logger.log(TreeLogger.SPAM, "Cleaning up resources for module "
- + moduleName, null);
+ logger.log(TreeLogger.SPAM, "Unloading module " + moduleName
+ + " (id " + key.toString() + ")", null);
}
private Composite buildLocationBar(Composite parent) {
diff --git a/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java b/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
index 5e2724f..c8f34f1 100644
--- a/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
+++ b/dev/core/src/com/google/gwt/dev/shell/ModuleSpace.java
@@ -85,8 +85,15 @@
private final ModuleSpaceHost host;
- protected ModuleSpace(final ModuleSpaceHost host) {
+ private final String moduleName;
+
+ private final Object key;
+
+ protected ModuleSpace(ModuleSpaceHost host, String moduleName,
+ Object key) {
this.host = host;
+ this.moduleName = moduleName;
+ this.key = key;
TreeLogger hostLogger = host.getLogger();
threadLocalLogger.set(hostLogger);
}
@@ -103,6 +110,24 @@
host.getClassLoader().clear();
}
+ /**
+ * Get the unique key for this module.
+ *
+ * @return the unique key
+ */
+ public Object getKey() {
+ return key;
+ }
+
+ /**
+ * Get the module name.
+ *
+ * @return the module name
+ */
+ public String getModuleName() {
+ return moduleName;
+ }
+
public boolean invokeNativeBoolean(String name, Object jthis, Class[] types,
Object[] args) {
JsValue result = invokeNative(name, jthis, types, args);
diff --git a/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java b/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
index 1ce9b3e..81ebe73 100644
--- a/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
+++ b/dev/linux/src/com/google/gwt/dev/shell/moz/BrowserWidgetMoz.java
@@ -29,9 +29,6 @@
import org.eclipse.swt.internal.mozilla.nsIWebBrowser;
import org.eclipse.swt.widgets.Shell;
-import java.util.HashMap;
-import java.util.Map;
-
/**
* Represents an individual browser window and all of its controls.
*/
@@ -39,9 +36,6 @@
private class ExternalObjectImpl implements ExternalObject {
- private Map modulesByScriptObject = new HashMap();
- private Map namesByScriptObject = new HashMap();
-
public boolean gwtOnLoad(int scriptObject, String moduleName) {
try {
if (moduleName == null) {
@@ -51,14 +45,14 @@
return true;
}
+ Object key = new Integer(scriptObject);
// Attach a new ModuleSpace to make it programmable.
//
ModuleSpaceHost msh = getHost().createModuleSpaceHost(
BrowserWidgetMoz.this, moduleName);
- ModuleSpace moduleSpace = new ModuleSpaceMoz(msh, scriptObject);
- attachModuleSpace(moduleName, moduleSpace);
- modulesByScriptObject.put(new Integer(scriptObject), moduleSpace);
- namesByScriptObject.put(new Integer(scriptObject), moduleName);
+ ModuleSpace moduleSpace = new ModuleSpaceMoz(msh, scriptObject,
+ moduleName, key);
+ attachModuleSpace(moduleSpace);
return true;
} catch (Throwable e) {
// We do catch Throwable intentionally because there are a ton of things
@@ -74,29 +68,14 @@
/**
* Unload one or more modules.
*
- * TODO(jat): Note that currently the JS code does not unload individual
- * modules, so this change is in preparation for when the JS code is
- * fixed.
- *
* @param scriptObject window to unload, 0 if all
*/
protected void handleUnload(int scriptObject) {
- // TODO(jat): true below restores original behavior of always
- // unloading all modules until the resulting GC issues (the
- // order of destruction is undefined so JS_RemoveRoot gets
- // called on a non-existent JSContext)
- if (true || scriptObject == 0) {
- onPageUnload();
- modulesByScriptObject.clear();
- namesByScriptObject.clear();
- return;
+ Integer key = null;
+ if (scriptObject != 0) {
+ key = new Integer(scriptObject);
}
- Integer key = new Integer(scriptObject);
- ModuleSpace moduleSpace = (ModuleSpace)modulesByScriptObject.get(key);
- String moduleName = (String)namesByScriptObject.get(key);
- unloadModule(moduleSpace, moduleName);
- modulesByScriptObject.remove(key);
- namesByScriptObject.remove(key);
+ doUnload(key);
}
}
diff --git a/dev/linux/src/com/google/gwt/dev/shell/moz/ModuleSpaceMoz.java b/dev/linux/src/com/google/gwt/dev/shell/moz/ModuleSpaceMoz.java
index 7cb960e..e326fb5 100644
--- a/dev/linux/src/com/google/gwt/dev/shell/moz/ModuleSpaceMoz.java
+++ b/dev/linux/src/com/google/gwt/dev/shell/moz/ModuleSpaceMoz.java
@@ -35,8 +35,9 @@
/**
* Constructs a browser interface for use with a Mozilla global window object.
*/
- public ModuleSpaceMoz(ModuleSpaceHost host, int scriptGlobalObject) {
- super(host);
+ public ModuleSpaceMoz(ModuleSpaceHost host, int scriptGlobalObject,
+ String moduleName, Object key) {
+ super(host, moduleName, key);
// Hang on to the parent window.
//
diff --git a/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java b/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
index be49357..6d148d1 100644
--- a/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
+++ b/dev/mac/src/com/google/gwt/dev/shell/mac/BrowserWidgetSaf.java
@@ -47,18 +47,19 @@
public boolean gwtOnLoad(int scriptObject, String moduleName) {
try {
if (moduleName == null) {
- // Indicates the page is being unloaded.
- // TODO(jat): add support to unload a single module
- onPageUnload();
+ // Indicates one or more modules are being unloaded.
+ handleUnload(scriptObject);
return true;
}
// Attach a new ModuleSpace to make it programmable.
//
+ Integer key = new Integer(scriptObject);
ModuleSpaceHost msh = getHost().createModuleSpaceHost(
BrowserWidgetSaf.this, moduleName);
- ModuleSpace moduleSpace = new ModuleSpaceSaf(msh, scriptObject);
- attachModuleSpace(moduleName, moduleSpace);
+ ModuleSpace moduleSpace = new ModuleSpaceSaf(msh, scriptObject,
+ moduleName, key);
+ attachModuleSpace(moduleSpace);
return true;
} catch (Throwable e) {
// We do catch Throwable intentionally because there are a ton of things
@@ -73,6 +74,19 @@
public void setField(String name, int value) {
}
+
+ /**
+ * Unload one or more modules.
+ *
+ * @param scriptObject window to unload, 0 if all
+ */
+ protected void handleUnload(int scriptObject) {
+ Integer key = null;
+ if (scriptObject != 0) {
+ key = new Integer(scriptObject);
+ }
+ doUnload(key);
+ }
}
private static final class GwtOnLoad implements DispatchMethod {
diff --git a/dev/mac/src/com/google/gwt/dev/shell/mac/ModuleSpaceSaf.java b/dev/mac/src/com/google/gwt/dev/shell/mac/ModuleSpaceSaf.java
index 7ef03aa..51ff4d6 100644
--- a/dev/mac/src/com/google/gwt/dev/shell/mac/ModuleSpaceSaf.java
+++ b/dev/mac/src/com/google/gwt/dev/shell/mac/ModuleSpaceSaf.java
@@ -32,9 +32,13 @@
/**
* Constructs a browser interface for use with a global window object.
+ *
+ * @param moduleName name of the module
+ * @param key unique key for this instance of the module
*/
- public ModuleSpaceSaf(ModuleSpaceHost host, int scriptGlobalObject) {
- super(host);
+ public ModuleSpaceSaf(ModuleSpaceHost host, int scriptGlobalObject,
+ String moduleName, Object key) {
+ super(host, moduleName, key);
// Hang on to the global execution state.
//
diff --git a/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java b/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
index 70213ad..aafacab 100644
--- a/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
+++ b/dev/windows/src/com/google/gwt/dev/shell/ie/BrowserWidgetIE6.java
@@ -23,6 +23,7 @@
import org.eclipse.swt.SWTException;
import org.eclipse.swt.internal.ole.win32.COM;
import org.eclipse.swt.internal.ole.win32.IDispatch;
+import org.eclipse.swt.ole.win32.OleAutomation;
import org.eclipse.swt.ole.win32.Variant;
import org.eclipse.swt.widgets.Shell;
@@ -44,22 +45,29 @@
*
* @param frameWnd a reference to the IFRAME in which the module's injected
* JavaScript will live
+ * @param moduleName the name of the module to load, null if this is being
+ * unloaded
*/
public boolean gwtOnLoad(IDispatch frameWnd, String moduleName) {
try {
if (moduleName == null) {
- // Indicates the page is being unloaded.
- // TODO(jat): add support for unloading only a single module
- onPageUnload();
+ // Indicates one or more modules are being unloaded.
+ handleUnload(frameWnd);
return true;
}
+
+ // set the module ID
+ int moduleID = ++nextModuleID;
+ Integer key = new Integer(moduleID);
+ setIntProperty(frameWnd, "__gwt_module_id", moduleID);
// Attach a new ModuleSpace to make it programmable.
//
ModuleSpaceHost msh = getHost().createModuleSpaceHost(
BrowserWidgetIE6.this, moduleName);
- ModuleSpaceIE6 moduleSpace = new ModuleSpaceIE6(msh, frameWnd);
- attachModuleSpace(moduleName, moduleSpace);
+ ModuleSpaceIE6 moduleSpace = new ModuleSpaceIE6(msh, frameWnd,
+ moduleName, key);
+ attachModuleSpace(moduleSpace);
return true;
} catch (Throwable e) {
// We do catch Throwable intentionally because there are a ton of things
@@ -88,6 +96,19 @@
throw new HResultException(DISP_E_UNKNOWNNAME);
}
+ /**
+ * Unload one or more modules.
+ *
+ * @param frameWnd window to unload, null if all
+ */
+ protected void handleUnload(IDispatch frameWnd) {
+ Integer key = null;
+ if (frameWnd != null) {
+ key = new Integer(getIntProperty(frameWnd, "__gwt_module_id"));
+ }
+ doUnload(key);
+ }
+
protected Variant invoke(int dispId, int flags, Variant[] params)
throws HResultException, InvocationTargetException {
@@ -130,6 +151,74 @@
}
}
+ // counter to generate unique module IDs
+ private static int nextModuleID = 0;
+
+ /**
+ * Get a property off a window object as an integer.
+ *
+ * @param frameWnd inner code frame
+ * @param propName name of the property to get
+ * @return the property value as an integer
+ * @throws RuntimeException if the property does not exist
+ */
+ private static int getIntProperty(IDispatch frameWnd, String propName) {
+ OleAutomation window = null;
+ try {
+ window = new OleAutomation(frameWnd);
+ int[] dispID = window.getIDsOfNames(new String[] { propName });
+ if (dispID == null) {
+ throw new RuntimeException("No such property " + propName);
+ }
+ Variant value = null;
+ try {
+ value = window.getProperty(dispID[0]);
+ return value.getInt();
+ } finally {
+ if (value != null) {
+ value.dispose();
+ }
+ }
+ } finally {
+ if (window != null) {
+ window.dispose();
+ }
+ }
+ }
+
+ /**
+ * Set a property off a window object from an integer value.
+ *
+ * @param frameWnd inner code frame
+ * @param propName name of the property to set
+ * @param intValue the value to set
+ * @throws RuntimeException if the property does not exist
+ */
+ private static void setIntProperty(IDispatch frameWnd, String propName,
+ int intValue) {
+ OleAutomation window = null;
+ try {
+ window = new OleAutomation(frameWnd);
+ int[] dispID = window.getIDsOfNames(new String[] { propName });
+ if (dispID == null) {
+ throw new RuntimeException("No such property " + propName);
+ }
+ Variant value = null;
+ try {
+ value = new Variant(intValue);
+ window.setProperty(dispID[0], value);
+ } finally {
+ if (value != null) {
+ value.dispose();
+ }
+ }
+ } finally {
+ if (window != null) {
+ window.dispose();
+ }
+ }
+ }
+
public BrowserWidgetIE6(Shell shell, BrowserWidgetHost host) {
super(shell, host);
@@ -142,5 +231,4 @@
//
LowLevelIE6.init();
}
-
}
diff --git a/dev/windows/src/com/google/gwt/dev/shell/ie/ModuleSpaceIE6.java b/dev/windows/src/com/google/gwt/dev/shell/ie/ModuleSpaceIE6.java
index f841b25..d16b710 100644
--- a/dev/windows/src/com/google/gwt/dev/shell/ie/ModuleSpaceIE6.java
+++ b/dev/windows/src/com/google/gwt/dev/shell/ie/ModuleSpaceIE6.java
@@ -30,6 +30,49 @@
* Internet Explorer 6.
*/
public class ModuleSpaceIE6 extends ModuleSpace {
+ /**
+ * Invoke a JavaScript function. The static function exists to allow
+ * platform-dependent code to make JavaScript calls without having a
+ * ModuleSpaceIE6 (and all that entails) if it is not required.
+ *
+ * @param window the window containing the function
+ * @param name the name of the function
+ * @param vArgs the array of arguments. vArgs[0] is the this parameter
+ * supplied to the function, which must be null if it is static.
+ * @return the return value of the JavaScript function
+ */
+ protected static Variant doInvokeOnWindow(OleAutomation window, String name,
+ Variant[] vArgs) {
+ OleAutomation funcObj = null;
+ Variant funcObjVar = null;
+ try {
+
+ // Get the function object and its 'call' method.
+ //
+ int[] ids = window.getIDsOfNames(new String[] {name});
+ if (ids == null) {
+ throw new RuntimeException(
+ "Could not find a native method with the signature '" + name + "'");
+ }
+ int functionId = ids[0];
+ funcObjVar = window.getProperty(functionId);
+ funcObj = funcObjVar.getAutomation();
+ int callDispId = funcObj.getIDsOfNames(new String[] {"call"})[0];
+
+ // Invoke it and return the result.
+ //
+ return funcObj.invoke(callDispId, vArgs);
+
+ } finally {
+ if (funcObjVar != null) {
+ funcObjVar.dispose();
+ }
+
+ if (funcObj != null) {
+ funcObj.dispose();
+ }
+ }
+ }
// CHECKSTYLE_OFF
private static int CODE(int hresult) {
@@ -47,9 +90,11 @@
/**
* Constructs a browser interface for use with an IE6 'window' automation
* object.
+ * @param moduleName
*/
- public ModuleSpaceIE6(ModuleSpaceHost host, IDispatch scriptFrameWindow) {
- super(host);
+ public ModuleSpaceIE6(ModuleSpaceHost host, IDispatch scriptFrameWindow,
+ String moduleName, Object key) {
+ super(host, moduleName, key);
window = new OleAutomation(scriptFrameWindow);
}
@@ -134,8 +179,6 @@
*/
protected JsValue doInvoke(String name, Object jthis, Class[] types,
Object[] args) {
- OleAutomation funcObj = null;
- Variant funcObjVar = null;
Variant[] vArgs = null;
try {
// Build the argument list, including 'jthis'.
@@ -151,21 +194,7 @@
getIsolatedClassLoader(), types[i], args[i]);
}
- // Get the function object and its 'call' method.
- //
- int[] ids = window.getIDsOfNames(new String[] {name});
- if (ids == null) {
- throw new RuntimeException(
- "Could not find a native method with the signature '" + name + "'");
- }
- int functionId = ids[0];
- funcObjVar = window.getProperty(functionId);
- funcObj = funcObjVar.getAutomation();
- int callDispId = funcObj.getIDsOfNames(new String[] {"call"})[0];
-
- // Invoke it and return the result.
- //
- Variant result = funcObj.invoke(callDispId, vArgs);
+ Variant result = doInvokeOnWindow(window, name, vArgs);
try {
if (!isExceptionActive()) {
return new JsValueIE6(result);
@@ -190,14 +219,6 @@
vArgs[i].dispose();
}
}
-
- if (funcObjVar != null) {
- funcObjVar.dispose();
- }
-
- if (funcObj != null) {
- funcObj.dispose();
- }
}
}
diff --git a/user/src/com/google/gwt/core/public/hosted.html b/user/src/com/google/gwt/core/public/hosted.html
index 538cff2..9aeacec 100644
--- a/user/src/com/google/gwt/core/public/hosted.html
+++ b/user/src/com/google/gwt/core/public/hosted.html
@@ -20,6 +20,8 @@
external.gwtOnLoad(window, null);
};
+window.__gwt_module_id = 0;
+
var query = window.location.search.substr(1);
if (query && $wnd[query]) $wnd[query].onScriptLoad();
--></script></body></html>
diff --git a/user/test/com/google/gwt/dev/shell/MultiModuleTest.gwt.xml b/user/test/com/google/gwt/dev/shell/MultiModuleTest.gwt.xml
new file mode 100644
index 0000000..7d49f4d
--- /dev/null
+++ b/user/test/com/google/gwt/dev/shell/MultiModuleTest.gwt.xml
@@ -0,0 +1,4 @@
+<module>
+ <inherits name='com.google.gwt.user.User'/>
+ <source path='test'/>
+</module>
diff --git a/user/test/com/google/gwt/dev/shell/test/MultiModuleTest.java b/user/test/com/google/gwt/dev/shell/test/MultiModuleTest.java
new file mode 100644
index 0000000..35af2a8
--- /dev/null
+++ b/user/test/com/google/gwt/dev/shell/test/MultiModuleTest.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2007 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.test;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Frame;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Tests unloading individual modules when more than one are loaded on a page,
+ * including in nested frames.
+ *
+ * The test will load up the initial configuration, then when all frames are
+ * done loading will toggle the first frame, then the second frame, then both at
+ * the same time. Buttons are provided for manual testing.
+ *
+ * Currently, there isn't much it can do to actually verify the proper behavior
+ * other than not crashing (which does not verify that the removed module isn't
+ * holding a lot of memory), but it is hard to do more than that without adding
+ * a lot of hooks that would only be used for this test. When tobyr's profiling
+ * changes are merged in, we will have to create some of the hooks to allow
+ * calls into the external object which could be used for the hooks for this
+ * test.
+ */
+public class MultiModuleTest extends GWTTestCase {
+
+ /**
+ * Used to setup the variable to keep track of things to be loaded, plus the
+ * JavaScript function used to communicate with the main module (the one that
+ * sets up the frames) from other modules.
+ *
+ * @param javaThis frameTest instance to use for callback
+ */
+ private static native void setupDoneLoading(MultiModuleTest javaThis) /*-{
+ $wnd.__count_to_be_loaded = 0;
+ $wnd.__done_loading = function() {
+ javaThis.@com.google.gwt.dev.shell.test.MultiModuleTest::doneLoading()();
+ };
+ }-*/;
+
+ /**
+ * Used to setup the JavaScript function used to communicate with the main
+ * module (the one that sets up the frames) from other modules.
+ *
+ * @param javaThis frameTest instance to use for callback
+ */
+ private static native void setupTestComplete(MultiModuleTest javaThis) /*-{
+ $wnd.__test_complete = function() {
+ javaThis.@com.google.gwt.dev.shell.test.MultiModuleTest::completedTest()();
+ };
+ }-*/;
+
+ /**
+ * Child frames, which are unused in nested modules.
+ */
+ private Frame[] frame = new Frame[2];
+
+ /**
+ * Flags indicating the "A" version of frame i is displayed, used to toggle
+ * the individual frames.
+ */
+ private boolean[] frameA = new boolean[2];
+
+ /**
+ * The top-level panel for callbacks.
+ */
+ private VerticalPanel mainPanel = null;
+
+ /**
+ * The state for automated frame toggles.
+ */
+ private int state;
+
+ /**
+ * Get the name of the GWT module to use for this test.
+ *
+ * @return the fully-qualified module name
+ */
+ public String getModuleName() {
+ return "com.google.gwt.dev.shell.MultiModuleTest";
+ }
+
+ /**
+ * Create the DOM elements for the module, based on the query string. The top
+ * level (query parameter frame=top) drives the process and sets up the
+ * automated state transition hooks.
+ *
+ * This function returns with no effect if gwt.junit.testfuncname is not
+ * passed as a query parameter, which means it is being run as a real test
+ * rather than as a "submodule" of testMultipleModules.
+ */
+ public void testInnerModules() {
+ String url = getURL();
+ Map params = getURLParams(url);
+ if (!params.containsKey("gwt.junit.testfuncname")) {
+ // if this test is being run as a normal JUnit test, return success
+ return;
+ }
+
+ // we were invoked by testMultipleModules, get the frame to load
+ String frameName = (String) params.get("frame");
+
+ VerticalPanel panel = new VerticalPanel();
+ RootPanel.get().add(panel);
+ if (frameName.equals("top")) {
+ // initial load
+ setupDoneLoading(this);
+ mainPanel = panel;
+ panel.add(new Label("Top level frame"));
+ state = 0;
+ params.put("frame", "1a");
+ frame[0] = new Frame(buildURL(url, params));
+ panel.add(frame[0]);
+ params.put("frame", "2a");
+ frame[1] = new Frame(buildURL(url, params));
+ panel.add(frame[1]);
+ addToBeLoaded(0, 2);
+ } else if (frameName.equals("1a")) {
+ panel.add(new Label("Frame 1a"));
+ markLoaded(1);
+ } else if (frameName.equals("1b")) {
+ panel.add(new Label("Frame 1b"));
+ markLoaded(1);
+ } else if (frameName.equals("2a")) {
+ panel.add(new Label("Frame 2a"));
+ params.put("frame", "2suba");
+ Frame sub = new Frame(buildURL(url, params));
+ panel.add(sub);
+ } else if (frameName.equals("2b")) {
+ panel.add(new Label("Frame 2b"));
+ params.put("frame", "2subb");
+ Frame sub = new Frame(buildURL(url, params));
+ panel.add(sub);
+ } else if (frameName.equals("2suba")) {
+ panel.add(new Label("Frame 2a inner"));
+ markLoaded(2);
+ } else if (frameName.equals("2subb")) {
+ panel.add(new Label("Frame 2b inner"));
+ markLoaded(2);
+ } else {
+ GWT.log("Unexpected frame name " + frameName, null);
+ }
+ }
+
+ public void testMultipleModules() {
+ setupTestComplete(this);
+
+ // build new URL from current one
+ String url = getURL();
+ Map params = getURLParams(url);
+ params.put("frame", "top");
+ params.put("gwt.junit.testfuncname", "testInnerModules");
+
+ // open a new frame containing the module that drives the actual test
+ Frame frame = new Frame(buildURL(url, params));
+ frame.setHeight("100%");
+ frame.setWidth("100%");
+ RootPanel.get().add(frame);
+ // wait up to 30 seconds for inner frames module to do its job
+ delayTestFinish(30000);
+ }
+
+ /**
+ * Increments the number of pages to be loaded. This count is kept in the
+ * context of the top-level module, so the depth parameter is provided to find
+ * it.
+ *
+ * @param depth nesting depth of this module, 0 = top level
+ * @param count number of pages due to be loaded
+ */
+ private native void addToBeLoaded(int depth, int count) /*-{
+ var frame = $wnd;
+ while (depth-- > 0) {
+ frame = frame.parent;
+ }
+ frame.__count_to_be_loaded += count;
+ }-*/;
+
+ /**
+ * Create a URL given an old URL and a map of query parameters. The search
+ * portion of the original URL will be discarded and replaced with a string of
+ * the form ?param1¶m2=value2 etc., where param1 has a null value in the
+ * map.
+ *
+ * @param url the original URL to rewrite
+ * @param params a map of parameter names to values
+ * @return the revised URL
+ */
+ private String buildURL(String url, Map params) {
+
+ // strip off the query string if present
+ int pos = url.indexOf("?");
+ if (pos >= 0) {
+ url = url.substring(0, pos);
+ }
+
+ // flag if we are generating the first parameter in the URL
+ boolean firstParam = true;
+
+ // gwt.hybrid must be first if present
+ if (params.containsKey("gwt.hybrid")) {
+ url += "?gwt.hybrid";
+ firstParam = false;
+ }
+
+ // now add the rest of the parameters, excluding gwt.hybrid
+ for (Iterator it = params.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ String param = (String) entry.getKey();
+
+ if (param.equals("gwt.hybrid")) {
+ // we already included gwt.hybrid if it was present
+ continue;
+ }
+
+ // add the parameter name to the URL
+ if (firstParam) {
+ url += "?";
+ firstParam = false;
+ } else {
+ url += "&";
+ }
+ url += param;
+
+ // add the value if necessary
+ String value = (String) entry.getValue();
+ if (value != null) {
+ url += "=" + value;
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Called via JSNI by testInnerModules when it successfully goes through
+ * all its iterations.
+ */
+ private void completedTest() {
+ // tell JUnit that we completed successfully
+ finishTest();
+ }
+
+ /**
+ * Proceed to the next automatic state change if any. This is called in the
+ * context of the top-level module via JSNI calls when all modules being
+ * waited on are loaded.
+ */
+ private void doneLoading() {
+ String url = getURL();
+ Map params = getURLParams(url);
+ mainPanel.add(new Label("done loading"));
+ if (++state == 4) {
+ // all tests complete, notify parent
+ notifyParent();
+ }
+ if (state >= 4) {
+ return;
+ }
+ StringBuffer buf = new StringBuffer();
+ buf.append("Toggling frame(s)");
+ if ((state & 1) != 0) {
+ buf.append(" 0");
+ toggleFrame(0, url, params);
+ }
+ if ((state & 2) != 0) {
+ buf.append(" 1");
+ toggleFrame(1, url, params);
+ }
+ mainPanel.add(new Label(buf.toString()));
+ }
+
+ /**
+ * Get the query string from the URL, including the question mark if present.
+ *
+ * @return the query string
+ */
+ private native String getURL() /*-{
+ return $wnd.location.href || '';
+ }-*/;
+
+ /**
+ * Parse a URL and return a map of query parameters. If a parameter is
+ * supplied without =value, it will be defined as null.
+ *
+ * @param url the full or partial (ie, only location.search) URL to parse
+ * @return the map of parameter names to values
+ */
+ private Map getURLParams(String url) {
+ HashMap map = new HashMap();
+ int pos = url.indexOf("?");
+
+ // loop precondition: pos is the index of the next ? or & character in url
+ while (pos >= 0) {
+ // skip over the separator character
+ url = url.substring(pos + 1);
+
+ // find the end of this parameter, which is the next ? or &
+ pos = url.indexOf("?");
+ int posAlt = url.indexOf("&");
+ if (pos < 0 || (posAlt >= 0 && posAlt < pos)) {
+ pos = posAlt;
+ }
+ String param;
+ if (pos >= 0) {
+ // trim this parameter if there is a terminator
+ param = url.substring(0, pos);
+ } else {
+ param = url;
+ }
+
+ // split value from parameter name if present
+ int equals = param.indexOf("=");
+ String value = null;
+ if (equals >= 0) {
+ value = param.substring(equals + 1);
+ param = param.substring(0, equals);
+ }
+
+ map.put(param, value);
+ }
+ return map;
+ }
+
+ /**
+ * Mark this page as loaded, using JSNI to mark it in the context of the
+ * top-level module space. If all outstanding modules have loaded, call the
+ * doneLoading method in the top-level module space (using JSNI and the depth
+ * to find it).
+ *
+ * @param depth nesting depth of this module, 0 = top level
+ */
+ private native void markLoaded(int depth) /*-{
+ var frame = $wnd;
+ while (depth-- > 0) {
+ frame = frame.parent;
+ }
+ if (!--frame.__count_to_be_loaded) {
+ frame.__done_loading();
+ }
+ }-*/;
+
+ /**
+ * Notify our parent frame that the test is complete.
+ */
+ private native void notifyParent() /*-{
+ $wnd.parent.__test_complete();
+ }-*/;
+
+ /**
+ * Replace the specified frame with its alternate version.
+ *
+ * @param frameNumber the number of the frame to replace, starting with 0
+ */
+ private void toggleFrame(int frameNumber, String url, Map params) {
+ params.put("frame", (frameNumber + 1) + (frameA[frameNumber] ? "b" : "a"));
+ frame[frameNumber].setUrl(buildURL(url, params));
+ frameA[frameNumber] = !frameA[frameNumber];
+ addToBeLoaded(0, 1);
+ }
+}