Support runAsync with the cross-site linker.
Review at http://gwt-code-reviews.appspot.com/213801
Review by: robertvawter@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7806 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
index 7a805d0..9580913 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/SelectionScriptLinker.java
@@ -186,7 +186,8 @@
byte[][] bytes = new byte[js.length][];
bytes[0] = generatePrimaryFragment(logger, context, result, js);
for (int i = 1; i < js.length; i++) {
- bytes[i] = Util.getBytes(js[i]);
+ bytes[i] = Util.getBytes(generateDeferredFragment(logger, context, i,
+ js[i]));
}
Collection<Artifact<?>> toReturn = new ArrayList<Artifact<?>>();
@@ -225,6 +226,11 @@
+ ".nocache.js", lastModified);
}
+ protected String generateDeferredFragment(TreeLogger logger,
+ LinkerContext context, int fragment, String js) {
+ return js;
+ }
+
/**
* Generate the primary fragment. The default implementation is based on
* {@link #getModulePrefix(TreeLogger, LinkerContext, String, int)} and
diff --git a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
index e0c972b..ccc283c 100644
--- a/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/IFrameLinker.java
@@ -57,6 +57,12 @@
private static final String CHUNK_SIZE_PROPERTY = "iframe.linker.script.chunk.size";
/**
+ * A configuration property that can be used to have the linker load from
+ * somewhere other than {@link #FRAGMENT_SUBDIR}
+ */
+ private static final String PROP_FRAGMENT_SUBDIR_OVERRIDE = "iframe.linker.deferredjs.subdir";
+
+ /**
* Split a JavaScript string into multiple chunks, at statement boundaries.
* Insert and end-script tag and a start-script tag in between each chunk.
* This method is made default access for testing.
@@ -187,23 +193,38 @@
/**
* Returns the subdirectory name to be used by getModulPrefix when requesting
- * a runAsync module. The default implementation returns the value of
- * FRAGMENT_SUDBIR. This has been factored out for test cases.
+ * a runAsync module. It is specified by
+ * {@link #PROP_FRAGMENT_SUBDIR_OVERRIDE} and, aside from test cases, is
+ * always {@link #FRAGMENT_SUBDIR}.
*/
- protected String getFragmentSubdir() {
- return FRAGMENT_SUBDIR;
+ protected final String getFragmentSubdir(TreeLogger logger,
+ LinkerContext context) throws UnableToCompleteException {
+ String subdir = null;
+ for (ConfigurationProperty prop : context.getConfigurationProperties()) {
+ if (prop.getName().equals(PROP_FRAGMENT_SUBDIR_OVERRIDE)) {
+ subdir = prop.getValues().get(0);
+ }
+ }
+
+ if (subdir == null) {
+ logger.log(TreeLogger.ERROR, "Could not find property "
+ + PROP_FRAGMENT_SUBDIR_OVERRIDE);
+ throw new UnableToCompleteException();
+ }
+
+ return subdir;
}
@Override
protected String getModulePrefix(TreeLogger logger, LinkerContext context,
- String strongName) {
- return getModulePrefix(context, strongName, true);
+ String strongName) throws UnableToCompleteException {
+ return getModulePrefix(logger, context, strongName, true);
}
@Override
protected String getModulePrefix(TreeLogger logger, LinkerContext context,
- String strongName, int numFragments) {
- return getModulePrefix(context, strongName, numFragments > 1);
+ String strongName, int numFragments) throws UnableToCompleteException {
+ return getModulePrefix(logger, context, strongName, numFragments > 1);
}
@Override
@@ -257,8 +278,9 @@
* This is the real implementation of <code>getModulePrefix</code> for this
* linker. The other versions forward to this one.
*/
- private String getModulePrefix(LinkerContext context, String strongName,
- boolean supportRunAsync) {
+ private String getModulePrefix(TreeLogger logger, LinkerContext context,
+ String strongName, boolean supportRunAsync)
+ throws UnableToCompleteException {
DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
out.print("<html>");
out.newlineOpt();
@@ -280,7 +302,7 @@
out.print("function __gwtStartLoadingFragment(frag) {");
out.indentIn();
out.newlineOpt();
- out.print(" return $moduleBase + '" + getFragmentSubdir()
+ out.print(" return $moduleBase + '" + getFragmentSubdir(logger, context)
+ "/' + $strongName + '/' + frag + '" + FRAGMENT_EXTENSION + "';");
out.indentOut();
out.newlineOpt();
diff --git a/dev/core/src/com/google/gwt/core/linker/XSLinker.java b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
index 6dbedba..1ffc1f6 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/XSLinker.java
@@ -17,18 +17,14 @@
import com.google.gwt.core.ext.LinkerContext;
import com.google.gwt.core.ext.TreeLogger;
-import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.core.ext.linker.Artifact;
-import com.google.gwt.core.ext.linker.CompilationResult;
import com.google.gwt.core.ext.linker.LinkerOrder;
import com.google.gwt.core.ext.linker.Shardable;
import com.google.gwt.core.ext.linker.LinkerOrder.Order;
import com.google.gwt.core.ext.linker.impl.SelectionScriptLinker;
import com.google.gwt.dev.About;
+import com.google.gwt.dev.js.JsToStringGenerationVisitor;
import com.google.gwt.dev.util.DefaultTextOutput;
-import java.util.Collection;
-
/**
* Generates a cross-site compatible bootstrap sequence.
*/
@@ -41,27 +37,65 @@
}
@Override
- protected Collection<Artifact<?>> doEmitCompilation(TreeLogger logger,
- LinkerContext context, CompilationResult result)
- throws UnableToCompleteException {
- if (result.getJavaScript().length != 1) {
- logger.branch(TreeLogger.ERROR,
- "The module must not have multiple fragments when using the "
- + getDescription() + " Linker.", null);
- throw new UnableToCompleteException();
- }
- return super.doEmitCompilation(logger, context, result);
+ protected String generateDeferredFragment(TreeLogger logger,
+ LinkerContext context, int fragment, String js) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(context.getModuleFunctionName());
+ sb.append(".runAsyncCallback");
+ sb.append(fragment);
+ sb.append("(");
+ sb.append(JsToStringGenerationVisitor.javaScriptString(js));
+ sb.append(");\n");
+ return sb.toString();
}
@Override
protected String getCompilationExtension(TreeLogger logger,
- LinkerContext context) throws UnableToCompleteException {
+ LinkerContext context) {
return ".cache.js";
}
@Override
protected String getModulePrefix(TreeLogger logger, LinkerContext context,
- String strongName) throws UnableToCompleteException {
+ String strongName) {
+ return getModulePrefix(context, strongName, true);
+ }
+
+ @Override
+ protected String getModulePrefix(TreeLogger logger, LinkerContext context,
+ String strongName, int numFragments) {
+ return getModulePrefix(context, strongName, numFragments > 1);
+ }
+
+ @Override
+ protected String getModuleSuffix(TreeLogger logger, LinkerContext context) {
+ DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
+
+ out.print("$stats && $stats({moduleName:'" + context.getModuleName()
+ + "',sessionId:$sessionId"
+ + ",subSystem:'startup',evtGroup:'moduleStartup'"
+ + ",millis:(new Date()).getTime(),type:'moduleEvalEnd'});");
+
+ // Generate the call to tell the bootstrap code that we're ready to go.
+ out.newlineOpt();
+ out.print("if (" + context.getModuleFunctionName() + " && "
+ + context.getModuleFunctionName() + ".onScriptLoad)"
+ + context.getModuleFunctionName() + ".onScriptLoad(gwtOnLoad);");
+ out.newlineOpt();
+ out.print("})();");
+ out.newlineOpt();
+
+ return out.toString();
+ }
+
+ @Override
+ protected String getSelectionScriptTemplate(TreeLogger logger,
+ LinkerContext context) {
+ return "com/google/gwt/core/linker/XSTemplate.js";
+ }
+
+ private String getModulePrefix(LinkerContext context, String strongName,
+ boolean supportRunAsync) {
DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
out.print("(function(){");
@@ -83,40 +117,23 @@
out.newlineOpt();
out.print("var $sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null;");
out.newlineOpt();
+
out.print("$stats && $stats({moduleName:'" + context.getModuleName()
+ "',sessionId:$sessionId"
+ ",subSystem:'startup',evtGroup:'moduleStartup'"
+ ",millis:(new Date()).getTime(),type:'moduleEvalStart'});");
out.newlineOpt();
- return out.toString();
- }
-
- @Override
- protected String getModuleSuffix(TreeLogger logger, LinkerContext context)
- throws UnableToCompleteException {
- DefaultTextOutput out = new DefaultTextOutput(context.isOutputCompact());
-
- out.print("$stats && $stats({moduleName:'" + context.getModuleName()
- + "',sessionId:$sessionId"
- + ",subSystem:'startup',evtGroup:'moduleStartup'"
- + ",millis:(new Date()).getTime(),type:'moduleEvalEnd'});");
-
- // Generate the call to tell the bootstrap code that we're ready to go.
- out.newlineOpt();
- out.print("if (" + context.getModuleFunctionName() + ") "
- + context.getModuleFunctionName() + ".onScriptLoad(gwtOnLoad);");
- out.newlineOpt();
- out.print("})();");
- out.newlineOpt();
+ if (supportRunAsync) {
+ out.print(context.getModuleFunctionName());
+ out.print(".installCode = function(code) { eval(code) };");
+ out.newlineOpt();
+ out.print("var __gwtModuleFunction = ");
+ out.print(context.getModuleFunctionName());
+ out.print(";");
+ out.newline();
+ }
return out.toString();
}
-
- @Override
- protected String getSelectionScriptTemplate(TreeLogger logger, LinkerContext context)
- throws UnableToCompleteException {
- return "com/google/gwt/core/linker/XSTemplate.js";
- }
-
}
diff --git a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
index 5200a69..81498f1 100644
--- a/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
+++ b/dev/core/src/com/google/gwt/core/linker/XSTemplate.js
@@ -21,6 +21,7 @@
var $wnd = window
,$doc = document
,$stats = $wnd.__gwtStatsEvent ? function(a) {return $wnd.__gwtStatsEvent(a);} : null
+ ,$sessionId = $wnd.__gwtStatsSessionId ? $wnd.__gwtStatsSessionId : null
// These variables gate calling gwtOnLoad; all must be true to start
,gwtOnLoad, bodyDone
@@ -163,8 +164,8 @@
// Called when the compiled script identified by moduleName is done loading.
//
__MODULE_FUNC__.onScriptLoad = function(gwtOnLoadFunc) {
- // remove this whole function from the global namespace to allow GC
- __MODULE_FUNC__ = null;
+ // remove the callback to prevent it being called twice
+ __MODULE_FUNC__.onScriptLoad = null;
gwtOnLoad = gwtOnLoadFunc;
maybeStartModule();
}
@@ -270,17 +271,17 @@
// from *within* the stats script, guaranteeing order at the expense of near
// total inscrutability :(
var compiledScriptTag = '"<script src=\\"' + base + strongName + '.cache.js\\"></scr" + "ipt>"';
- $doc.write('<script><!--\n'
+ $doc.write('<scr' + 'ipt><!-' + '-\n'
+ 'window.__gwtStatsEvent && window.__gwtStatsEvent({'
- + 'moduleName:"__MODULE_NAME__", sessionId:$sessionId, subSystem:"startup",'
+ + 'moduleName:"__MODULE_NAME__", sessionId:window.__gwtStatsSessionId, subSystem:"startup",'
+ 'evtGroup: "loadExternalRefs", millis:(new Date()).getTime(),'
+ 'type: "end"});'
+ 'window.__gwtStatsEvent && window.__gwtStatsEvent({'
- + 'moduleName:"__MODULE_NAME__", sessionId:$sessionId, subSystem:"startup",'
+ + 'moduleName:"__MODULE_NAME__", sessionId:window.__gwtStatsSessionId, subSystem:"startup",'
+ 'evtGroup: "moduleStartup", millis:(new Date()).getTime(),'
+ 'type: "moduleRequested"});'
+ 'document.write(' + compiledScriptTag + ');'
- + '\n--></script>');
+ + '\n-' + '-></scr' + 'ipt>');
}
__MODULE_FUNC__();
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index bf07384..91b1579 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -69,6 +69,7 @@
import com.google.gwt.dev.jjs.impl.FragmentLoaderCreator;
import com.google.gwt.dev.jjs.impl.GenerateJavaAST;
import com.google.gwt.dev.jjs.impl.GenerateJavaScriptAST;
+import com.google.gwt.dev.jjs.impl.HandleCrossFragmentReferences;
import com.google.gwt.dev.jjs.impl.JavaScriptObjectNormalizer;
import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
import com.google.gwt.dev.jjs.impl.JsFunctionClusterer;
@@ -234,8 +235,8 @@
JProgram jprogram = ast.getJProgram();
JsProgram jsProgram = ast.getJsProgram();
JJSOptions options = unifiedAst.getOptions();
- Map<StandardSymbolData, JsName> symbolTable = new TreeMap<StandardSymbolData, JsName>(
- new SymbolData.ClassIdentComparator());
+ Map<StandardSymbolData, JsName> symbolTable =
+ new TreeMap<StandardSymbolData, JsName>(new SymbolData.ClassIdentComparator());
ResolveRebinds.exec(jprogram, permutation.getOrderedRebindAnswers());
@@ -345,6 +346,11 @@
default:
throw new InternalCompilerException("Unknown output mode");
}
+
+ // (10.8) Handle cross-island references.
+ // No new JsNames or references to JSNames can be introduced after this
+ // point.
+ HandleCrossFragmentReferences.exec(logger, jsProgram, propertyOracles);
// (11) Perform any post-obfuscation normalizations.
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
index da27c6d..bfa8289 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/FragmentLoaderCreator.java
@@ -144,8 +144,8 @@
srcWriter.println("if (!" + BROWSER_LOADER + ".isLoading(" + entryNumber
+ ")) {");
srcWriter.println(" " + BROWSER_LOADER + ".inject(" + entryNumber + ",");
- srcWriter.println(" new AsyncFragmentLoader.LoadErrorHandler() {");
- srcWriter.println(" public void loadFailed(Throwable reason) {");
+ srcWriter.println(" new AsyncFragmentLoader.LoadTerminatedHandler() {");
+ srcWriter.println(" public void loadTerminated(Throwable reason) {");
srcWriter.println(" runCallbackOnFailures(reason);");
srcWriter.println(" }");
srcWriter.println(" });");
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java b/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
new file mode 100644
index 0000000..31a9b49
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/HandleCrossFragmentReferences.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2010 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.jjs.impl;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.dev.jjs.SourceInfo;
+import com.google.gwt.dev.js.ast.JsBinaryOperation;
+import com.google.gwt.dev.js.ast.JsBinaryOperator;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsExpression;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsModVisitor;
+import com.google.gwt.dev.js.ast.JsName;
+import com.google.gwt.dev.js.ast.JsNameRef;
+import com.google.gwt.dev.js.ast.JsObjectLiteral;
+import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsStatement;
+import com.google.gwt.dev.js.ast.JsVars;
+import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.js.ast.JsVars.JsVar;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Rewrite JavaScript to better handle references from one code fragment to
+ * another. For any function defined off the initial download and accessed from
+ * a different island than the one it's defined on, predefine a variable in the
+ * initial download to hold its definition.
+ */
+public class HandleCrossFragmentReferences {
+ /**
+ * Find out which islands define and use each named function or variable.
+ * This visitor is not smart about which definitions and uses matter. It
+ * blindly records all of them.
+ */
+ private class FindNameReferences extends JsVisitor {
+ Map<JsName, Set<Integer>> islandsDefining = new LinkedHashMap<JsName, Set<Integer>>();
+ Map<JsName, Set<Integer>> islandsUsing = new LinkedHashMap<JsName, Set<Integer>>();
+ private int currentIsland;
+
+ @Override
+ public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+ JsName name = x.getName();
+ if (name != null) {
+ definitionSeen(name);
+ }
+ }
+
+ @Override
+ public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+ if (x.getQualifier() == null) {
+ JsName name = x.getName();
+ if (name != null) {
+ referenceSeen(name);
+ }
+ }
+ }
+
+ @Override
+ public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+ for (JsVar var : x) {
+ JsName name = var.getName();
+ if (name != null) {
+ definitionSeen(name);
+ }
+ }
+ }
+
+ @Override
+ public boolean visit(JsProgram x, JsContext<JsProgram> ctx) {
+ for (int i = 0; i < x.getFragmentCount(); i++) {
+ currentIsland = i;
+ accept(x.getFragmentBlock(i));
+ }
+
+ return false;
+ }
+
+ private void definitionSeen(JsName name) {
+ /*
+ * Support multiple definitions, because local variables can reuse the
+ * same name.
+ */
+ Set<Integer> defs = islandsDefining.get(name);
+ if (defs == null) {
+ defs = new LinkedHashSet<Integer>();
+ islandsDefining.put(name, defs);
+ }
+ defs.add(currentIsland);
+ }
+
+ private void referenceSeen(JsName name) {
+ Set<Integer> refs = islandsUsing.get(name);
+ if (refs == null) {
+ refs = new HashSet<Integer>();
+ islandsUsing.put(name, refs);
+ }
+ refs.add(currentIsland);
+ }
+ }
+
+ /**
+ * Rewrite var and function declarations as assignments, if their name is
+ * accessed cross-island. Rewrite refs to such names correspondingly.
+ */
+ private class RewriteDeclsAndRefs extends JsModVisitor {
+ @Override
+ public void endVisit(JsFunction x, JsContext<JsExpression> ctx) {
+ if (namesToPredefine.contains(x.getName())) {
+ JsBinaryOperation asg = new JsBinaryOperation(x.getSourceInfo(),
+ JsBinaryOperator.ASG, makeRefViaJslink(x.getName(),
+ x.getSourceInfo()), x);
+ x.setName(null);
+ ctx.replaceMe(asg);
+ }
+ }
+
+ @Override
+ public void endVisit(JsNameRef x, JsContext<JsExpression> ctx) {
+ if (namesToPredefine.contains(x.getName())) {
+ ctx.replaceMe(makeRefViaJslink(x.getName(), x.getSourceInfo()));
+ }
+ }
+
+ @Override
+ public void endVisit(JsVars x, JsContext<JsStatement> ctx) {
+ if (!ctx.canInsert()) {
+ return;
+ }
+
+ /*
+ * Loop through each var and see if it was predefined. If so, then remove
+ * the var. If the var has an initializer, then add back an assignment
+ * statement to initialize it. If there is no initializer, then don't add
+ * anything back; the var will still have undefined as its initial value,
+ * just like before.
+ *
+ * A complication is that the variables that are predefined might be
+ * interspersed with variables that are not. That means the general result
+ * of this transformation has alternating var lists and assignment
+ * statements. The currentVar variable holds the most recently inserted
+ * statement, if that statement was a JsVars; otherwise it holds null.
+ */
+
+ JsVars currentVar = null;
+ Iterator<JsVar> varsIterator = x.iterator();
+ while (varsIterator.hasNext()) {
+ JsVar var = varsIterator.next();
+ if (namesToPredefine.contains(var.getName())) {
+ // The var was predefined
+ if (var.getInitExpr() != null) {
+ // If it has an initializer, add an assignment statement
+ JsBinaryOperation asg = new JsBinaryOperation(var.getSourceInfo(),
+ JsBinaryOperator.ASG, makeRefViaJslink(var.getName(),
+ var.getSourceInfo()), var.getInitExpr());
+ ctx.insertBefore(asg.makeStmt());
+ currentVar = null;
+ }
+ } else {
+ // The var was not predefined; add it to a var list
+ if (currentVar == null) {
+ currentVar = new JsVars(x.getSourceInfo());
+ ctx.insertBefore(currentVar);
+ }
+ currentVar.add(var);
+ }
+ }
+
+ ctx.removeMe();
+ }
+
+ private JsNameRef makeRefViaJslink(JsName name, SourceInfo sourceInfo) {
+ JsNameRef ref = name.makeRef(sourceInfo);
+ ref.setQualifier(jslink.makeRef(sourceInfo));
+ return ref;
+ }
+ }
+
+ public static String PROP_PREDECLARE_VARS = "compiler.predeclare.cross.fragment.references";
+
+ public static void exec(TreeLogger logger, JsProgram jsProgram,
+ PropertyOracle[] propertyOracles) {
+ new HandleCrossFragmentReferences(logger, jsProgram, propertyOracles).execImpl();
+ }
+
+ private static boolean containsOtherThan(Set<Integer> set, int allowed) {
+ for (int elem : set) {
+ if (elem != allowed) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private JsName jslink;
+ private final JsProgram jsProgram;
+ private final Set<JsName> namesToPredefine = new LinkedHashSet<JsName>();
+ private final PropertyOracle[] propertyOracles;
+ private final TreeLogger logger;
+
+ private HandleCrossFragmentReferences(TreeLogger logger, JsProgram jsProgram,
+ PropertyOracle[] propertyOracles) {
+ this.logger = logger;
+ this.jsProgram = jsProgram;
+ this.propertyOracles = propertyOracles;
+ }
+
+ private void chooseNamesToPredefine(Map<JsName, Set<Integer>> map,
+ Map<JsName, Set<Integer>> islandsUsing) {
+ for (Entry<JsName, Set<Integer>> entry : map.entrySet()) {
+ JsName name = entry.getKey();
+ Set<Integer> defIslands = entry.getValue();
+ if (defIslands.size() != 1) {
+ // Only rewrite global variables, which should have exactly one
+ // definition
+ continue;
+ }
+ int defIsland = defIslands.iterator().next();
+ if (defIsland == 0) {
+ // Variables defined on the base island can be accessed directly from
+ // other islands
+ continue;
+ }
+ Set<Integer> useIslands = islandsUsing.get(name);
+ if (useIslands == null) {
+ // The variable is never used. Leave it alone.
+ continue;
+ }
+
+ if (containsOtherThan(islandsUsing.get(name), defIsland)) {
+ namesToPredefine.add(name);
+ }
+ }
+ }
+
+ /**
+ * Define the jslink object that will be used to fix up cross-island
+ * references.
+ */
+ private void defineJsLink() {
+ SourceInfo info = jsProgram.createSourceInfoSynthetic(
+ HandleCrossFragmentReferences.class, "defining jslink");
+ jslink = jsProgram.getScope().declareName("jslink");
+ JsVars vars = new JsVars(info);
+ JsVar var = new JsVar(info, jslink);
+ var.setInitExpr(new JsObjectLiteral(info));
+ vars.add(var);
+ jsProgram.getFragmentBlock(0).getStatements().add(0, vars);
+ }
+
+ private void execImpl() {
+ if (jsProgram.getFragmentCount() == 1) {
+ return;
+ }
+ if (!shouldPredeclareReferences()) {
+ return;
+ }
+ defineJsLink();
+ FindNameReferences findNameReferences = new FindNameReferences();
+ findNameReferences.accept(jsProgram);
+ chooseNamesToPredefine(findNameReferences.islandsDefining,
+ findNameReferences.islandsUsing);
+ new RewriteDeclsAndRefs().accept(jsProgram);
+ }
+
+ /**
+ * Check the property oracles for whether references should be predeclared or
+ * not. If any of them say yes, then do the rewrite.
+ */
+ private boolean shouldPredeclareReferences() {
+ for (PropertyOracle props : propertyOracles) {
+ try {
+ String propValue = props.getSelectionProperty(logger,
+ PROP_PREDECLARE_VARS).getCurrentValue();
+ if (Boolean.parseBoolean(propValue)) {
+ return true;
+ }
+ } catch (BadPropertyValueException e) {
+ // Property not defined; don't rewrite
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
index 9e56801..4bdaff8 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/JsFunctionClusterer.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -23,15 +23,19 @@
import java.util.List;
import java.util.LinkedList;
import java.util.Iterator;
+import java.util.regex.Pattern;
/**
* Re-orders function declarations according to a given metric and clustering
* algorithm in order to boost gzip/deflation compression efficiency. This
* version uses the edit-distance algorithm as a metric, and a semi-greedy
- * strategy for grouping functions together.
+ * strategy for grouping functions together.
*/
public class JsFunctionClusterer extends JsAbstractTextTransformer {
+ private static Pattern functionPattern =
+ Pattern.compile("^(function |[A-Za-z0-9_$]+=function)");
+
/**
* Limit edit-distance search to MAX_DIST.
*/
@@ -39,25 +43,25 @@
private static final int MAX_DISTANCE_LIMIT = 100;
- private List<Integer> functionIndices;
-
private int[] clusteredIndices;
- public JsFunctionClusterer(String js, StatementRanges statementRanges) {
- super(js, statementRanges);
- }
+ private List<Integer> functionIndices;
public JsFunctionClusterer(JsAbstractTextTransformer xformer) {
super(xformer);
}
+ public JsFunctionClusterer(String js, StatementRanges statementRanges) {
+ super(js, statementRanges);
+ }
+
public void exec() {
functionIndices = new LinkedList<Integer>();
// gather up all of the indices of function decl statements
for (int i = 0; i < statementRanges.numStatements(); i++) {
String code = getJsForRange(i);
- if (code.startsWith("function")) {
+ if (functionPattern.matcher(code).find()) {
functionIndices.add(i);
}
}
@@ -88,8 +92,7 @@
Iterator<Integer> it = functionIndices.iterator();
int count = 0;
// search up to MAX_DIST functions for the best match
- while (it.hasNext() &&
- count < Math.min(MAX_DIST, functionIndices.size())) {
+ while (it.hasNext() && count < Math.min(MAX_DIST, functionIndices.size())) {
int functionIndex = it.next();
String testCode = getJsForRange(functionIndex);
int distanceLimit = Math.min(bestDistance, MAX_DISTANCE_LIMIT);
@@ -172,8 +175,8 @@
while (j <= jmax) {
char c2 = str2.charAt(j - 1);
int costSwap = c1 == c2 ? 0 : 1;
- nextRow[j] = Math.min(Math.min(lastRow[j] + 1, nextRow[j - 1] + 1),
- lastRow[j - 1] + costSwap);
+ nextRow[j] =
+ Math.min(Math.min(lastRow[j] + 1, nextRow[j - 1] + 1), lastRow[j - 1] + costSwap);
j = j + 1;
}
int tmpRow[] = nextRow;
diff --git a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
index 56b8200..a363f88 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsToStringGenerationVisitor.java
@@ -125,8 +125,136 @@
*/
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("[a-zA-Z_$][\\w$]*");
- protected boolean needSemi = true;
+ /**
+ * Generate JavaScript code that evaluates to the supplied string. Adapted
+ * from {@link com.google.gwt.dev.js.rhino.ScriptRuntime#escapeString(String)}
+ * . The difference is that we quote with either " or ' depending on
+ * which one is used less inside the string.
+ */
+ public static String javaScriptString(String value) {
+ char[] chars = value.toCharArray();
+ final int n = chars.length;
+ int quoteCount = 0;
+ int aposCount = 0;
+ for (int i = 0; i < n; ++i) {
+ switch (chars[i]) {
+ case '"':
+ ++quoteCount;
+ break;
+ case '\'':
+ ++aposCount;
+ break;
+ }
+ }
+ StringBuffer result = new StringBuffer(value.length() + 16);
+
+ char quoteChar = (quoteCount < aposCount) ? '"' : '\'';
+ result.append(quoteChar);
+
+ for (int i = 0; i < n; ++i) {
+ char c = chars[i];
+
+ if (' ' <= c && c <= '~' && c != quoteChar && c != '\\') {
+ // an ordinary print character (like C isprint())
+ result.append(c);
+ continue;
+ }
+
+ int escape = -1;
+ switch (c) {
+ case '\b':
+ escape = 'b';
+ break;
+ case '\f':
+ escape = 'f';
+ break;
+ case '\n':
+ escape = 'n';
+ break;
+ case '\r':
+ escape = 'r';
+ break;
+ case '\t':
+ escape = 't';
+ break;
+ case '"':
+ escape = '"';
+ break; // only reach here if == quoteChar
+ case '\'':
+ escape = '\'';
+ break; // only reach here if == quoteChar
+ case '\\':
+ escape = '\\';
+ break;
+ }
+
+ if (escape >= 0) {
+ // an \escaped sort of character
+ result.append('\\');
+ result.append((char) escape);
+ } else {
+ /*
+ * Emit characters from 0 to 31 that don't have a single character
+ * escape sequence in octal where possible. This saves one or two
+ * characters compared to the hexadecimal format '\xXX'.
+ *
+ * These short octal sequences may only be used at the end of the string
+ * or where the following character is a non-digit. Otherwise, the
+ * following character would be incorrectly interpreted as belonging to
+ * the sequence.
+ */
+ if (c < ' ' && (i == n - 1 || chars[i + 1] < '0' || chars[i + 1] > '9')) {
+ result.append('\\');
+ if (c > 0x7) {
+ result.append((char) ('0' + (0x7 & (c >> 3))));
+ }
+ result.append((char) ('0' + (0x7 & c)));
+ } else {
+ int hexSize;
+ if (c < 256) {
+ // 2-digit hex
+ result.append("\\x");
+ hexSize = 2;
+ } else {
+ // Unicode.
+ result.append("\\u");
+ hexSize = 4;
+ }
+ // append hexadecimal form of ch left-padded with 0
+ for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
+ int digit = 0xf & (c >> shift);
+ result.append(HEX_DIGITS[digit]);
+ }
+ }
+ }
+ }
+ result.append(quoteChar);
+ escapeClosingTags(result);
+ String resultString = result.toString();
+ return resultString;
+ }
+
+ /**
+ * Escapes any closing XML tags embedded in <code>str</code>, which could
+ * potentially cause a parse failure in a browser, for example, embedding a
+ * closing <code><script></code> tag.
+ *
+ * @param str an unescaped literal; May be null
+ */
+ private static void escapeClosingTags(StringBuffer str) {
+ if (str == null) {
+ return;
+ }
+
+ int index = 0;
+
+ while ((index = str.indexOf("</", index)) != -1) {
+ str.insert(index + 1, '\\');
+ }
+ }
+
+ protected boolean needSemi = true;
/**
* "Global" blocks are either the global block of a fragment, or a block
* nested directly within some other global block. This definition matters
@@ -135,7 +263,9 @@
*/
private Set<JsBlock> globalBlocks = new HashSet<JsBlock>();
private final TextOutput p;
+
private ArrayList<Integer> statementEnds = new ArrayList<Integer>();
+
private ArrayList<Integer> statementStarts = new ArrayList<Integer>();
public JsToStringGenerationVisitor(TextOutput out) {
@@ -564,8 +694,7 @@
accept(q);
if (q instanceof JsNumberLiteral) {
/**
- * Fix for Issue #3796. "42.foo" is not allowed, but
- * "42 .foo" is.
+ * Fix for Issue #3796. "42.foo" is not allowed, but "42 .foo" is.
*/
_space();
}
@@ -592,8 +721,8 @@
}
/*
- * If a constructor call has no arguments, it may simply be
- * replaced with "new Constructor" with no parentheses.
+ * If a constructor call has no arguments, it may simply be replaced with
+ * "new Constructor" with no parentheses.
*/
List<JsExpression> args = x.getArguments();
if (args.size() > 0) {
@@ -1232,25 +1361,6 @@
// CHECKSTYLE_NAMING_ON
- /**
- * Escapes any closing XML tags embedded in <code>str</code>, which could
- * potentially cause a parse failure in a browser, for example, embedding a
- * closing <code><script></code> tag.
- *
- * @param str an unescaped literal; May be null
- */
- private void escapeClosingTags(StringBuffer str) {
- if (str == null) {
- return;
- }
-
- int index = 0;
-
- while ((index = str.indexOf("</", index)) != -1) {
- str.insert(index + 1, '\\');
- }
- }
-
private void indent() {
p.indentIn();
}
@@ -1259,113 +1369,8 @@
p.indentOut();
}
- /**
- * Adapted from
- * {@link com.google.gwt.dev.js.rhino.ScriptRuntime#escapeString(String)}. The
- * difference is that we quote with either " or ' depending on which
- * one is used less inside the string.
- */
private void printStringLiteral(String value) {
- char[] chars = value.toCharArray();
- final int n = chars.length;
- int quoteCount = 0;
- int aposCount = 0;
- for (int i = 0; i < n; ++i) {
- switch (chars[i]) {
- case '"':
- ++quoteCount;
- break;
- case '\'':
- ++aposCount;
- break;
- }
- }
-
- StringBuffer result = new StringBuffer(value.length() + 16);
-
- char quoteChar = (quoteCount < aposCount) ? '"' : '\'';
- p.print(quoteChar);
-
- for (int i = 0; i < n; ++i) {
- char c = chars[i];
-
- if (' ' <= c && c <= '~' && c != quoteChar && c != '\\') {
- // an ordinary print character (like C isprint())
- result.append(c);
- continue;
- }
-
- int escape = -1;
- switch (c) {
- case '\b':
- escape = 'b';
- break;
- case '\f':
- escape = 'f';
- break;
- case '\n':
- escape = 'n';
- break;
- case '\r':
- escape = 'r';
- break;
- case '\t':
- escape = 't';
- break;
- case '"':
- escape = '"';
- break; // only reach here if == quoteChar
- case '\'':
- escape = '\'';
- break; // only reach here if == quoteChar
- case '\\':
- escape = '\\';
- break;
- }
-
- if (escape >= 0) {
- // an \escaped sort of character
- result.append('\\');
- result.append((char) escape);
- } else {
- /*
- * Emit characters from 0 to 31 that don't have a single character
- * escape sequence in octal where possible. This saves one or two
- * characters compared to the hexadecimal format '\xXX'.
- *
- * These short octal sequences may only be used at the end of the string
- * or where the following character is a non-digit. Otherwise, the
- * following character would be incorrectly interpreted as belonging to
- * the sequence.
- */
- if (c < ' ' &&
- (i == n - 1 || chars[i + 1] < '0' || chars[i + 1] > '9')) {
- result.append('\\');
- if (c > 0x7) {
- result.append((char) ('0' + (0x7 & (c >> 3))));
- }
- result.append((char) ('0' + (0x7 & c)));
- } else {
- int hexSize;
- if (c < 256) {
- // 2-digit hex
- result.append("\\x");
- hexSize = 2;
- } else {
- // Unicode.
- result.append("\\u");
- hexSize = 4;
- }
- // append hexadecimal form of ch left-padded with 0
- for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
- int digit = 0xf & (c >> shift);
- result.append(HEX_DIGITS[digit]);
- }
- }
- }
- }
- result.append(quoteChar);
- escapeClosingTags(result);
- p.print(result.toString());
+ String resultString = javaScriptString(value);
+ p.print(resultString);
}
}
diff --git a/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java b/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java
new file mode 100644
index 0000000..f91996f
--- /dev/null
+++ b/dev/core/test/com/google/gwt/dev/js/JavaScriptStringTest.java
@@ -0,0 +1,40 @@
+// Copyright 2010 Google Inc. All Rights Reserved.
+
+package com.google.gwt.dev.js;
+
+import com.google.gwt.dev.js.JsToStringGenerationVisitor;
+import com.google.gwt.dev.js.rhino.TokenStream;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+/**
+ * Tests {@link JsToStringGenerationVisitor#javaScriptString(String)}.
+ */
+public class JavaScriptStringTest extends TestCase {
+ private void test(String original) throws IOException {
+ String escaped = JsToStringGenerationVisitor.javaScriptString(original);
+
+ // Parse it back
+ TokenStream tokenStream = new TokenStream(new StringReader(escaped),
+ "virtual file", 1);
+ assertEquals(TokenStream.STRING, tokenStream.getToken());
+ assertEquals(original, tokenStream.getString());
+
+ // It should be the only token
+ assertEquals(TokenStream.EOF, tokenStream.getToken());
+ }
+
+ public void testBasic() throws IOException {
+ test("abc");
+ test("");
+ test("abc\0def");
+ test("abc\\def");
+ test("\u00CC\u1234\5678\uabcd");
+ test("'''");
+ test("\"\"\"");
+ test("\b\f\n\r\t");
+ }
+}
diff --git a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
index f8bbb27..0e1a9db 100644
--- a/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
+++ b/user/src/com/google/gwt/core/CompilerParameters.gwt.xml
@@ -22,6 +22,17 @@
<define-configuration-property name='compiler.splitpoint.initial.sequence'
is-multi-valued='true' />
+ <!--
+ Whether or not the compiler should predeclare variables that are defined
+ outside the initial download and are referenced from a different code
+ fragment than the one defining them. This is usually determined by which
+ linker is used and is not directly meaningful to users.
+ -->
+ <define-property name="compiler.predeclare.cross.fragment.references"
+ values="true,false" />
+ <set-property name="compiler.predeclare.cross.fragment.references"
+ value="false" />
+
<!-- From here down, the properties are unsupported and are only available for test cases -->
<!--
@@ -42,7 +53,17 @@
JsIEBlockSizeVisitor has its usual effect.
-->
<define-configuration-property name="iframe.linker.script.chunk.size"
- is-multi-valued="false" />
+ is-multi-valued="false" />
<set-configuration-property name="iframe.linker.script.chunk.size"
- value="30000" />
+ value="30000" />
+
+ <!--
+ The compiler emits deferred code into a deferredjs subdirectory of the
+ output. This property allows test cases to have the iframe linker
+ load the deferred code from a different subdirectory.
+ -->
+ <define-configuration-property name="iframe.linker.deferredjs.subdir"
+ is-multi-valued="false" />
+ <set-configuration-property name="iframe.linker.deferredjs.subdir"
+ value="deferredjs" />
</module>
diff --git a/user/src/com/google/gwt/core/Core.gwt.xml b/user/src/com/google/gwt/core/Core.gwt.xml
index cb351e9..443f3c4 100644
--- a/user/src/com/google/gwt/core/Core.gwt.xml
+++ b/user/src/com/google/gwt/core/Core.gwt.xml
@@ -29,7 +29,7 @@
<define-linker name="sso" class="com.google.gwt.core.linker.SingleScriptLinker" />
<define-linker name="std" class="com.google.gwt.core.linker.IFrameLinker" />
- <define-linker name="xs" class="com.google.gwt.core.linker.XSLinker" />
+ <inherits name="com.google.gwt.core.XSLinker" />
<define-linker name="soycReport" class="com.google.gwt.core.linker.SoycReportLinker" />
<define-linker name="symbolMaps" class="com.google.gwt.core.linker.SymbolMapsLinker" />
diff --git a/user/src/com/google/gwt/core/XSLinker.gwt.xml b/user/src/com/google/gwt/core/XSLinker.gwt.xml
new file mode 100644
index 0000000..854a585
--- /dev/null
+++ b/user/src/com/google/gwt/core/XSLinker.gwt.xml
@@ -0,0 +1,28 @@
+<!-- -->
+<!-- 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 -->
+<!-- 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. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<!-- Defines the cross-site linker -->
+<module>
+ <define-linker name="xs" class="com.google.gwt.core.linker.XSLinker" />
+
+ <replace-with class="com.google.gwt.core.client.impl.CrossSiteLoadingStrategy">
+ <when-type-is
+ class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy" />
+ <when-linker-added name="xs" />
+ </replace-with>
+
+ <set-property name="compiler.predeclare.cross.fragment.references" value="true">
+ <when-linker-added name="xs" />
+ </set-property>
+</module>
diff --git a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
index 89119dc..a7c3bdc 100644
--- a/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
+++ b/user/src/com/google/gwt/core/client/impl/AsyncFragmentLoader.java
@@ -35,39 +35,29 @@
* </ul>
*
* <p>
- * Since the precise way to load code depends on the linker, each linker should
- * provide functions for fragment loading for any compilation that includes more
- * than one fragment. Linkers should always provide a function
- * <code>__gwtStartLoadingFragment</code>. This function is called by
- * AsyncFragmentLoader with two arguments: an integer fragment number that needs
- * to be downloaded, and a one-argument loadFailed function that can be called
- * if the load fails. If the load fails, that function should be called with a
- * descriptive exception as the argument. If the mechanism for loading the
- * contents of fragments is provided by the linker, the
- * <code>__gwtStartLoadingFragment</code> function should return
- * <code>null</code> or <code>undefined</code>.
- * </p>
- * <p>
- * Alternatively, the function can return a URL designating from where the code
- * for the requested fragment can be downloaded. In that case, the linker should
- * also provide a function <code>__gwtInstallCode</code> for actually installing
- * the code once it is downloaded. That function will be passed the loaded code
- * once it has been downloaded.
- * </p>
+ * Since the precise way to load code depends on the linker, linkers should
+ * specify a rebind of {@link LoadingStrategy}. The default rebind is
+ * {@link XhrLoadingStrategy}.
*/
public class AsyncFragmentLoader {
/**
- * An interface for handlers of load errors.
+ * An interface for handlers of load completion. On a failed download,
+ * this callback should be invoked or else the requested download will
+ * hang indefinitely. On a successful download, it's optional to call
+ * this method. If it is called at all, it must be called after
+ * the downloaded code has been installed, so that {@link AsyncFragmentLoader}
+ * can distinguish successful from unsuccessful downloads.
*/
- public static interface LoadErrorHandler {
- void loadFailed(Throwable reason);
+ public static interface LoadTerminatedHandler {
+ void loadTerminated(Throwable reason);
}
/**
* A strategy for loading code fragments.
*/
public interface LoadingStrategy {
- void startLoadingFragment(int fragment, LoadErrorHandler loadErrorHandler);
+ void startLoadingFragment(int fragment,
+ LoadTerminatedHandler loadTerminatedHandler);
}
/**
@@ -99,6 +89,77 @@
}
/**
+ * The standard logger used in a web browser. It uses the lightweight metrics
+ * system.
+ */
+ public static class StandardLogger implements Logger {
+ /**
+ * Always use this as {@link isStatsAvailable} &&
+ * {@link #stats(JavaScriptObject)}.
+ */
+ private static native boolean stats(JavaScriptObject data) /*-{
+ return $stats(data);
+ }-*/;
+
+ public void logEventProgress(String eventGroup, String type,
+ int fragment, int size) {
+ @SuppressWarnings("unused")
+ boolean toss = isStatsAvailable()
+ && stats(createStatsEvent(eventGroup, type, fragment, size));
+ }
+
+ private native JavaScriptObject createStatsEvent(String eventGroup,
+ String type, int fragment, int size) /*-{
+ var evt = {
+ moduleName: @com.google.gwt.core.client.GWT::getModuleName()(),
+ sessionId: $sessionId,
+ subSystem: 'runAsync',
+ evtGroup: eventGroup,
+ millis: (new Date()).getTime(),
+ type: type
+ };
+ if (fragment >= 0) {
+ evt.fragment = fragment;
+ }
+ if (size >= 0) {
+ evt.size = size;
+ }
+ return evt;
+ }-*/;
+
+ private native boolean isStatsAvailable() /*-{
+ return !!$stats;
+ }-*/;
+ }
+
+ /**
+ * An exception indicating than at HTTP download failed.
+ */
+ static class HttpDownloadFailure extends RuntimeException {
+ private final int statusCode;
+
+ public HttpDownloadFailure(String url, int statusCode, String statusText) {
+ super("Download of " + url + " failed with status " + statusCode + "("
+ + statusText + ")");
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+ }
+
+ /**
+ * An exception indicating than at HTTP download succeeded, but installing
+ * its body failed.
+ */
+ static class HttpInstallFailure extends RuntimeException {
+ public HttpInstallFailure(String url, String text, Throwable rootCause) {
+ super("Install of " + url + " failed with text " + text, rootCause);
+ }
+ }
+
+ /**
* A trivial queue of int's that should compile much better than a
* LinkedList<Integer>. It assumes that it has a bound on the number of
* items added to the queue. Removing items does not free up more space, but
@@ -143,39 +204,21 @@
}
/**
- * An exception indicating than at HTTP download failed.
- */
- static class HttpDownloadFailure extends RuntimeException {
- private final int statusCode;
-
- public HttpDownloadFailure(String url, int statusCode, String statusText) {
- super("Download of " + url + " failed with status " + statusCode + "("
- + statusText + ")");
- this.statusCode = statusCode;
- }
-
- public int getStatusCode() {
- return statusCode;
- }
- }
-
- /**
- * An exception indicating than at HTTP download succeeded, but installing
- * its body failed.
- */
- static class HttpInstallFailure extends RuntimeException {
- public HttpInstallFailure(String url, String text, Throwable rootCause) {
- super("Install of " + url + " failed with text " + text, rootCause);
- }
- }
-
- /**
* Internal load error handler. This calls all user-provided error handlers
* and cancels all pending downloads.
*/
- private class ResetAfterDownloadFailure implements LoadErrorHandler {
- public void loadFailed(Throwable reason) {
- assert fragmentLoading >= 0;
+ private class ResetAfterDownloadFailure implements LoadTerminatedHandler {
+ private final int fragment;
+
+ public ResetAfterDownloadFailure(int myFragment) {
+ this.fragment = myFragment;
+ }
+
+ public void loadTerminated(Throwable reason) {
+ if (fragmentLoading != fragment) {
+ // fragment already loaded successfully
+ return;
+ }
// Cancel all pending downloads.
@@ -183,8 +226,8 @@
* Make a local list of the handlers to run, in case one of them calls
* another runAsync
*/
- LoadErrorHandler[] handlersToRun = pendingDownloadErrorHandlers;
- pendingDownloadErrorHandlers = new LoadErrorHandler[numEntries + 1];
+ LoadTerminatedHandler[] handlersToRun = pendingDownloadErrorHandlers;
+ pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntries + 1];
/*
* Call clear() here so that requestedExclusives makes all of its space
@@ -200,10 +243,10 @@
*/
RuntimeException lastException = null;
- for (LoadErrorHandler handler : handlersToRun) {
+ for (LoadTerminatedHandler handler : handlersToRun) {
if (handler != null) {
try {
- handler.loadFailed(reason);
+ handler.loadTerminated(reason);
} catch (RuntimeException e) {
lastException = e;
}
@@ -217,50 +260,6 @@
}
/**
- * The standard logger used in a web browser. It uses the lightweight metrics
- * system.
- */
- private static class StandardLogger implements Logger {
- /**
- * Always use this as {@link isStatsAvailable} &&
- * {@link #stats(JavaScriptObject)}.
- */
- private static native boolean stats(JavaScriptObject data) /*-{
- return $stats(data);
- }-*/;
-
- public void logEventProgress(String eventGroup, String type,
- int fragment, int size) {
- @SuppressWarnings("unused")
- boolean toss = isStatsAvailable()
- && stats(createStatsEvent(eventGroup, type, fragment, size));
- }
-
- private native JavaScriptObject createStatsEvent(String eventGroup,
- String type, int fragment, int size) /*-{
- var evt = {
- moduleName: @com.google.gwt.core.client.GWT::getModuleName()(),
- sessionId: $sessionId,
- subSystem: 'runAsync',
- evtGroup: eventGroup,
- millis: (new Date()).getTime(),
- type: type
- };
- if (fragment >= 0) {
- evt.fragment = fragment;
- }
- if (size >= 0) {
- evt.size = size;
- }
- return evt;
- }-*/;
-
- private native boolean isStatsAvailable() /*-{
- return !!$stats;
- }-*/;
- }
-
- /**
* The standard instance of AsyncFragmentLoader used in a web browser. If
* not in GWT (i.e our vanilla JUnit tests, or if referenced in a server
* context), this filed is {@code null}. In GWT, the parameters to this call
@@ -307,7 +306,7 @@
* loaded. This array will hold the initial sequence of bases followed by the
* leftovers fragment. It is filled in by
* {@link com.google.gwt.dev.jjs.impl.CodeSplitter} modifying the initializer
- * to {@link #INSTANCE}. The list does <em>not</em> include the leftovers
+ * to {@link #BROWSER_LOADER}. The list does <em>not</em> include the leftovers
* fragment, which must be loaded once all of these are finished.
*/
private final int[] initialLoadSequence;
@@ -331,7 +330,7 @@
* Externally provided handlers for all outstanding and queued download
* requests.
*/
- private LoadErrorHandler[] pendingDownloadErrorHandlers;
+ private LoadTerminatedHandler[] pendingDownloadErrorHandlers;
/**
* Whether prefetching is currently enabled.
@@ -367,7 +366,7 @@
int numEntriesPlusOne = numEntries + 1;
requestedExclusives = new BoundedIntQueue(numEntriesPlusOne);
isLoaded = new boolean[numEntriesPlusOne];
- pendingDownloadErrorHandlers = new LoadErrorHandler[numEntriesPlusOne];
+ pendingDownloadErrorHandlers = new LoadTerminatedHandler[numEntriesPlusOne];
}
/**
@@ -402,7 +401,7 @@
*
* @param splitPoint the split point whose code needs to be loaded
*/
- public void inject(int splitPoint, LoadErrorHandler loadErrorHandler) {
+ public void inject(int splitPoint, LoadTerminatedHandler loadErrorHandler) {
pendingDownloadErrorHandlers[splitPoint] = loadErrorHandler;
if (!isInitial(splitPoint)) {
requestedExclusives.add(splitPoint);
@@ -560,7 +559,7 @@
fragmentLoading = fragment;
logDownloadStart(fragment);
loadingStrategy.startLoadingFragment(fragment,
- new ResetAfterDownloadFailure());
+ new ResetAfterDownloadFailure(fragment));
}
/**
diff --git a/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java b/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java
new file mode 100644
index 0000000..7529055
--- /dev/null
+++ b/user/src/com/google/gwt/core/client/impl/CrossSiteLoadingStrategy.java
@@ -0,0 +1,161 @@
+/*
+ * 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.core.client.impl;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
+
+/**
+ * Load runAsync code using a script tag. The
+ * {@link com.google.gwt.core.linker.XSLinker} sets
+ * <code>__gwtModuleFunction</code> to point at the function that wraps the
+ * initially downloaded code. On that function is a property
+ * <code>installCode</code> that can be invoked to eval more code in a scope
+ * nested somewhere within that function. The loaded script for fragment 123 is
+ * expected to invoke __gwtModuleFunction.runAsyncCallback123 with the code to
+ * be installed.
+ */
+public class CrossSiteLoadingStrategy implements LoadingStrategy {
+ /**
+ * A trivial JavaScript map from ints to ints.
+ */
+ private static final class IntToIntMap extends JavaScriptObject {
+ public static IntToIntMap create() {
+ return (IntToIntMap) JavaScriptObject.createArray();
+ }
+
+ protected IntToIntMap() {
+ }
+
+ /**
+ * Get an entry. If there is no such entry, return 0.
+ */
+ public native int get(int x) /*-{
+ return this[x] ? this[x] : 0;
+ }-*/;
+
+ public native void put(int x, int y) /*-{
+ this[x] = y;
+ }-*/;
+ }
+
+ @SuppressWarnings("unused")
+ private static RuntimeException LoadTerminated = new RuntimeException(
+ "Code download terminated");
+
+ /**
+ * Clear the success callback for fragment <code>fragment</code>.
+ */
+ @SuppressWarnings("unused")
+ private static native void clearOnSuccess(int fragment) /*-{
+ delete __gwtModuleFunction['runAsyncCallback'+fragment];
+ }-*/;
+
+ private static native JavaScriptObject createScriptTag(String url) /*-{
+ var head = document.getElementsByTagName('head').item(0);
+ var script = document.createElement('script');
+ script.src = url;
+ return script;
+ }-*/;
+
+ private static native void installScriptTag(JavaScriptObject script) /*-{
+ var head = document.getElementsByTagName('head').item(0);
+ head.appendChild(script);
+ }-*/;
+
+ private static native JavaScriptObject removeTagAndCallErrorHandler(
+ int fragment, JavaScriptObject tag,
+ LoadTerminatedHandler loadFinishedHandler) /*-{
+ return function(exception) {
+ var head = document.getElementsByTagName('head').item(0);
+ head.removeChild(tag);
+ @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
+ loadFinishedHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler::loadTerminated(*)(
+ exception);
+ }
+ }-*/;
+
+ private static native JavaScriptObject removeTagAndEvalCode(int fragment,
+ JavaScriptObject tag) /*-{
+ return function(code) {
+ var head = document.getElementsByTagName('head').item(0);
+ head.removeChild(tag);
+ @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::clearOnSuccess(*)(fragment);
+ __gwtModuleFunction.installCode(code);
+ }
+ }-*/;
+
+ private static native void setOnFailure(JavaScriptObject script,
+ JavaScriptObject callback) /*-{
+ var exception = @com.google.gwt.core.client.impl.CrossSiteLoadingStrategy::LoadTerminated;
+ script.onerror = function() {
+ callback(exception);
+ }
+ script.onload = function() {
+ callback(exception);
+ }
+ script.onreadystatechange = function () {
+ if (script.readyState == 'loaded' || script.readyState == 'complete') {
+ script.onreadystatechange = function () { }
+ callback(exception);
+ }
+ }
+ }-*/;
+
+ /**
+ * Set the success callback for fragment <code>fragment</code>
+ * to the supplied JavaScript function.
+ */
+ private static native void setOnSuccess(int fragment, JavaScriptObject callback) /*-{
+ __gwtModuleFunction['runAsyncCallback'+fragment] = callback;
+ }-*/;
+
+ private final IntToIntMap serialNumbers = IntToIntMap.create();
+
+ @Override
+ public void startLoadingFragment(int fragment,
+ LoadTerminatedHandler loadFinishedHandler) {
+ JavaScriptObject tag = createScriptTag(getUrl(fragment));
+ setOnSuccess(fragment, removeTagAndEvalCode(fragment, tag));
+ setOnFailure(tag, removeTagAndCallErrorHandler(fragment, tag,
+ loadFinishedHandler));
+ installScriptTag(tag);
+ }
+
+ protected String getDeferredJavaScriptDirectory() {
+ return "deferredjs/";
+ }
+
+ private int getSerial(int fragment) {
+ int ser = serialNumbers.get(fragment);
+ serialNumbers.put(fragment, ser + 1);
+ return ser;
+ }
+
+ /**
+ * The URL to retrieve a fragment of code from. NOTE: this function is not
+ * stable. It tweaks the URL with each call so that browsers are not tempted
+ * to cache a download failure.
+ */
+ private String getUrl(int fragment) {
+ return GWT.getModuleBaseURL() + getDeferredJavaScriptDirectory()
+ + GWT.getPermutationStrongName() + "/" + fragment + ".cache.js?serial="
+ + getSerial(fragment);
+ }
+}
diff --git a/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java b/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
index bdd7a38..e56de67 100644
--- a/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
+++ b/user/src/com/google/gwt/core/client/impl/XhrLoadingStrategy.java
@@ -1,240 +1,262 @@
-/*
- * 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.core.client.impl;
-
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure;
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
-import com.google.gwt.xhr.client.ReadyStateChangeHandler;
-import com.google.gwt.xhr.client.XMLHttpRequest;
-
-/**
- * The standard loading strategy used in a web browser.
- */
-public class XhrLoadingStrategy implements LoadingStrategy {
-
- /**
- * A {@link MockableXMLHttpRequest} that is really just a vanilla
- * XMLHttpRequest. This wrapper (and thus {@code MockableXMLHttpRequest) is
- * needed because so much of {@link XMLHttpRequest} is final, which in turn
- * is because it extends {@code JavaScriptObject} and is subject to its
- * restrictions.
- *
- * It is important that these methods be simple enough to be inlined away.
- */
- class DelegatingXMLHttpRequest implements MockableXMLHttpRequest {
- private final XMLHttpRequest delegate;
-
- public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) {
- delegate = xmlHttpRequest;
- }
-
- public void clearOnReadyStateChange() {
- delegate.clearOnReadyStateChange();
- }
-
- public int getReadyState() {
- return delegate.getReadyState();
- }
-
- public String getResponseText() {
- return delegate.getResponseText();
- }
-
- public int getStatus() {
- return delegate.getStatus();
- }
-
- public String getStatusText() {
- return delegate.getStatusText();
- }
-
- public void open(String method, String url) {
- delegate.open(method, url);
- }
-
- public void send() {
- delegate.send();
- }
-
- public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
- delegate.setOnReadyStateChange(handler);
- }
-
- public void setRequestHeader(String header, String value) {
- delegate.setRequestHeader(header, value);
- }
- }
-
- /**
- * Delegates to the real XMLHttpRequest, except in test when we make a mock
- * to jump through error/retry hoops.
- */
- interface MockableXMLHttpRequest {
- void clearOnReadyStateChange();
- int getReadyState();
- String getResponseText();
- int getStatus();
- String getStatusText();
- void open(String method, String url);
- void send();
- void setOnReadyStateChange(ReadyStateChangeHandler handler);
- void setRequestHeader(String header, String value);
- }
-
- /**
- * Since LoadingStrategy must support concurrent requests, including figuring
- * which is which in the onLoadError handling, we need to keep track of this
- * data for each outstanding request, which we index by xhr object.
- */
- protected class RequestData {
- String url;
- int retryCount;
- LoadErrorHandler errorHandler = null;
-
- public RequestData(String url, LoadErrorHandler errorHandler) {
- this.url = url;
- this.errorHandler = errorHandler;
- this.retryCount = 0;
- }
- }
-
- static final String HTTP_GET = "GET";
-
- /**
- * Some UA's like Safari will have a "0" status code when loading from file:
- * URLs. Additionally, the "0" status code is used sometimes if the server
- * does not respond, e.g. if there is a connection refused.
- */
- static final int HTTP_STATUS_NON_HTTP = 0;
-
- static final int HTTP_STATUS_OK = 200;
-
- /**
- * For error logging, max length of fragment response text to include in
- * failed-to-install exception message.
- */
- private static final int MAX_LOG_LENGTH = 200;
-
- /**
- * Number of retry attempts for a single fragment. If a fragment download
- * fails, we try again this many times before "really" failing out to user
- * error-handling code. If a fragment downloads but doesn't install, we
- * don't retry at all.
- */
- private static final int MAX_RETRY_COUNT = 3;
-
- public void startLoadingFragment(int fragment,
- final LoadErrorHandler loadErrorHandler) {
- String url = gwtStartLoadingFragment(fragment, loadErrorHandler);
- if (url == null) {
- // The download has already started; nothing more to do
- return;
- }
-
- RequestData request = new RequestData(url, loadErrorHandler);
- tryLoad(request);
- }
-
- /**
- * Overridable for tests.
- */
- protected MockableXMLHttpRequest createXhr() {
- return new DelegatingXMLHttpRequest(XMLHttpRequest.create());
- }
-
- /**
- * Call the linker-supplied <code>__gwtInstallCode</code> method. See the
- * {@link AsyncFragmentLoader class comment} for more details.
- */
- protected native void gwtInstallCode(String text) /*-{
- __gwtInstallCode(text);
- }-*/;
-
- /**
- * Call the linker-supplied __gwtStartLoadingFragment function. It should
- * either start the download and return null or undefined, or it should
- * return a URL that should be downloaded to get the code. If it starts the
- * download itself, it can synchronously load it, e.g. from cache, if that
- * makes sense.
- */
- protected native String gwtStartLoadingFragment(int fragment,
- LoadErrorHandler loadErrorHandler) /*-{
- function loadFailed(e) {
- loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadErrorHandler::loadFailed(Ljava/lang/Throwable;)(e);
- }
- return __gwtStartLoadingFragment(fragment, loadFailed);
- }-*/;
-
- /**
- * Error recovery from loading or installing code.
- * @param request the requestData of this request
- * @param e exception of the error
- * @param mayRetry {@code true} if retrying might be helpful
- */
- protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) {
- if (mayRetry) {
- request.retryCount++;
- if (request.retryCount < MAX_RETRY_COUNT) {
- tryLoad(request);
- return;
- }
- }
- request.errorHandler.loadFailed(e);
- }
-
- /**
- * Makes a single load-and-install attempt.
- */
- protected void tryLoad(final RequestData request) {
- final MockableXMLHttpRequest xhr = createXhr();
-
- xhr.open(HTTP_GET, request.url);
- if (request.retryCount > 0) {
- // disable caching if we have to retry; one cause could be bad cache
- xhr.setRequestHeader("Cache-Control", "no-cache");
- }
-
- xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
- public void onReadyStateChange(XMLHttpRequest ignored) {
- if (xhr.getReadyState() == XMLHttpRequest.DONE) {
- xhr.clearOnReadyStateChange();
- if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
- && xhr.getResponseText() != null
- && xhr.getResponseText().length() != 0) {
- try {
- gwtInstallCode(xhr.getResponseText());
- } catch (RuntimeException e) {
- String textIntro = xhr.getResponseText();
- if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {
- textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";
- }
- onLoadError(request,
- new HttpInstallFailure(request.url, textIntro, e), false);
- }
- } else {
- onLoadError(request,
- new HttpDownloadFailure(request.url, xhr.getStatus(),
- xhr.getStatusText()), true);
- }
- }
- }
- });
-
- xhr.send();
- }
+/*
+ * 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.core.client.impl;
+
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpDownloadFailure;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.HttpInstallFailure;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+import com.google.gwt.xhr.client.XMLHttpRequest;
+
+/**
+ * The standard loading strategy used in a web browser. The linker it is used
+ * with should provide JavaScript-level functions to indicate how to handle
+ * downloading and installing code. There is support to use XHR for the
+ * download.
+ *
+ * Linkers should always provide a function
+ * <code>__gwtStartLoadingFragment</code>. This function is called by
+ * AsyncFragmentLoader with two arguments: an integer fragment number that needs
+ * to be downloaded, and a one-argument loadFinished function. If the load
+ * fails, that function should be called with a descriptive exception as the
+ * argument. If the load succeeds, that function may also be called, so long as
+ * it isn't called until the downloaded code has been installed.
+ *
+ *
+ * If the mechanism for loading the contents of fragments is provided by the
+ * linker, the <code>__gwtStartLoadingFragment</code> function should return
+ * <code>null</code> or <code>undefined</code>.
+ *
+ * Alternatively, the function can return a URL designating from where the code
+ * for the requested fragment can be downloaded. In that case, the linker should
+ * also provide a function <code>__gwtInstallCode</code> for actually installing
+ * the code once it is downloaded. That function will be passed the loaded code
+ * once it has been downloaded.
+ */
+public class XhrLoadingStrategy implements LoadingStrategy {
+
+ /**
+ * A {@link MockableXMLHttpRequest} that is really just a vanilla
+ * XMLHttpRequest. This wrapper (and thus {@code MockableXMLHttpRequest} is
+ * needed because so much of {@link XMLHttpRequest} is final, which in turn
+ * is because it extends {@code JavaScriptObject} and is subject to its
+ * restrictions.
+ *
+ * It is important that these methods be simple enough to be inlined away.
+ */
+ class DelegatingXMLHttpRequest implements MockableXMLHttpRequest {
+ private final XMLHttpRequest delegate;
+
+ public DelegatingXMLHttpRequest(XMLHttpRequest xmlHttpRequest) {
+ delegate = xmlHttpRequest;
+ }
+
+ public void clearOnReadyStateChange() {
+ delegate.clearOnReadyStateChange();
+ }
+
+ public int getReadyState() {
+ return delegate.getReadyState();
+ }
+
+ public String getResponseText() {
+ return delegate.getResponseText();
+ }
+
+ public int getStatus() {
+ return delegate.getStatus();
+ }
+
+ public String getStatusText() {
+ return delegate.getStatusText();
+ }
+
+ public void open(String method, String url) {
+ delegate.open(method, url);
+ }
+
+ public void send() {
+ delegate.send();
+ }
+
+ public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
+ delegate.setOnReadyStateChange(handler);
+ }
+
+ public void setRequestHeader(String header, String value) {
+ delegate.setRequestHeader(header, value);
+ }
+ }
+
+ /**
+ * Delegates to the real XMLHttpRequest, except in test when we make a mock
+ * to jump through error/retry hoops.
+ */
+ interface MockableXMLHttpRequest {
+ void clearOnReadyStateChange();
+ int getReadyState();
+ String getResponseText();
+ int getStatus();
+ String getStatusText();
+ void open(String method, String url);
+ void send();
+ void setOnReadyStateChange(ReadyStateChangeHandler handler);
+ void setRequestHeader(String header, String value);
+ }
+
+ /**
+ * Since LoadingStrategy must support concurrent requests, including figuring
+ * which is which in the onLoadError handling, we need to keep track of this
+ * data for each outstanding request, which we index by xhr object.
+ */
+ protected class RequestData {
+ String url;
+ int retryCount;
+ LoadTerminatedHandler errorHandler = null;
+
+ public RequestData(String url, LoadTerminatedHandler errorHandler) {
+ this.url = url;
+ this.errorHandler = errorHandler;
+ this.retryCount = 0;
+ }
+ }
+
+ static final String HTTP_GET = "GET";
+
+ /**
+ * Some UA's like Safari will have a "0" status code when loading from file:
+ * URLs. Additionally, the "0" status code is used sometimes if the server
+ * does not respond, e.g. if there is a connection refused.
+ */
+ static final int HTTP_STATUS_NON_HTTP = 0;
+
+ static final int HTTP_STATUS_OK = 200;
+
+ /**
+ * For error logging, max length of fragment response text to include in
+ * failed-to-install exception message.
+ */
+ private static final int MAX_LOG_LENGTH = 200;
+
+ /**
+ * Number of retry attempts for a single fragment. If a fragment download
+ * fails, we try again this many times before "really" failing out to user
+ * error-handling code. If a fragment downloads but doesn't install, we
+ * don't retry at all.
+ */
+ private static final int MAX_RETRY_COUNT = 3;
+
+ public void startLoadingFragment(int fragment,
+ final LoadTerminatedHandler loadErrorHandler) {
+ String url = gwtStartLoadingFragment(fragment, loadErrorHandler);
+ if (url == null) {
+ // The download has already started; nothing more to do
+ return;
+ }
+
+ RequestData request = new RequestData(url, loadErrorHandler);
+ tryLoad(request);
+ }
+
+ /**
+ * Overridable for tests.
+ */
+ protected MockableXMLHttpRequest createXhr() {
+ return new DelegatingXMLHttpRequest(XMLHttpRequest.create());
+ }
+
+ /**
+ * Call the linker-supplied <code>__gwtInstallCode</code> method. See the
+ * {@link AsyncFragmentLoader class comment} for more details.
+ */
+ protected native void gwtInstallCode(String text) /*-{
+ __gwtInstallCode(text);
+ }-*/;
+
+ /**
+ * Call the linker-supplied __gwtStartLoadingFragment function. It should
+ * either start the download and return null or undefined, or it should
+ * return a URL that should be downloaded to get the code. If it starts the
+ * download itself, it can synchronously load it, e.g. from cache, if that
+ * makes sense.
+ */
+ protected native String gwtStartLoadingFragment(int fragment,
+ LoadTerminatedHandler loadErrorHandler) /*-{
+ function loadFailed(e) {
+ loadErrorHandler.@com.google.gwt.core.client.impl.AsyncFragmentLoader$LoadTerminatedHandler::loadTerminated(*)(e);
+ }
+ return __gwtStartLoadingFragment(fragment, loadFailed);
+ }-*/;
+
+ /**
+ * Error recovery from loading or installing code.
+ * @param request the requestData of this request
+ * @param e exception of the error
+ * @param mayRetry {@code true} if retrying might be helpful
+ */
+ protected void onLoadError(RequestData request, Throwable e, boolean mayRetry) {
+ if (mayRetry) {
+ request.retryCount++;
+ if (request.retryCount < MAX_RETRY_COUNT) {
+ tryLoad(request);
+ return;
+ }
+ }
+ request.errorHandler.loadTerminated(e);
+ }
+
+ /**
+ * Makes a single load-and-install attempt.
+ */
+ protected void tryLoad(final RequestData request) {
+ final MockableXMLHttpRequest xhr = createXhr();
+
+ xhr.open(HTTP_GET, request.url);
+ if (request.retryCount > 0) {
+ // disable caching if we have to retry; one cause could be bad cache
+ xhr.setRequestHeader("Cache-Control", "no-cache");
+ }
+
+ xhr.setOnReadyStateChange(new ReadyStateChangeHandler() {
+ public void onReadyStateChange(XMLHttpRequest ignored) {
+ if (xhr.getReadyState() == XMLHttpRequest.DONE) {
+ xhr.clearOnReadyStateChange();
+ if ((xhr.getStatus() == HTTP_STATUS_OK || xhr.getStatus() == HTTP_STATUS_NON_HTTP)
+ && xhr.getResponseText() != null
+ && xhr.getResponseText().length() != 0) {
+ try {
+ gwtInstallCode(xhr.getResponseText());
+ } catch (RuntimeException e) {
+ String textIntro = xhr.getResponseText();
+ if (textIntro != null && textIntro.length() > MAX_LOG_LENGTH) {
+ textIntro = textIntro.substring(0, MAX_LOG_LENGTH) + "...";
+ }
+ onLoadError(request,
+ new HttpInstallFailure(request.url, textIntro, e), false);
+ }
+ } else {
+ onLoadError(request,
+ new HttpDownloadFailure(request.url, xhr.getStatus(),
+ xhr.getStatusText()), true);
+ }
+ }
+ }
+ });
+
+ xhr.send();
+ }
}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
index b14c3c2..cac5944 100644
--- a/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
+++ b/user/test/com/google/gwt/core/client/impl/AsyncFragmentLoaderTest.java
@@ -15,7 +15,7 @@
*/
package com.google.gwt.core.client.impl;
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy;
import com.google.gwt.core.client.impl.AsyncFragmentLoader.Logger;
@@ -33,20 +33,20 @@
* correct lightweight metrics under a variety of request patterns.
*/
public class AsyncFragmentLoaderTest extends TestCase {
- private static class MockErrorHandler implements LoadErrorHandler {
+ private static class MockErrorHandler implements LoadTerminatedHandler {
private boolean wasCalled = false;
public boolean getWasCalled() {
return wasCalled;
}
- public void loadFailed(Throwable reason) {
+ public void loadTerminated(Throwable reason) {
wasCalled = true;
}
}
private static class MockLoadStrategy implements LoadingStrategy {
- public final Map<Integer, LoadErrorHandler> errorHandlers = new HashMap<Integer, LoadErrorHandler>();
+ public final Map<Integer, LoadTerminatedHandler> errorHandlers = new HashMap<Integer, LoadTerminatedHandler>();
private List<Integer> loadRequests = new LinkedList<Integer>();
public void assertFragmentsRequested(int... expectedAry) {
@@ -61,7 +61,7 @@
}
public void startLoadingFragment(int fragment,
- LoadErrorHandler loadErrorHandler) {
+ LoadTerminatedHandler loadErrorHandler) {
errorHandlers.put(fragment, loadErrorHandler);
loadRequests.add(fragment);
}
@@ -138,8 +138,8 @@
private static final String END = "end";
private static final String LEFTOVERS_DOWNLOAD = "leftoversDownload";
- private static final LoadErrorHandler NULL_ERROR_HANDLER = new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
+ private static final LoadTerminatedHandler NULL_ERROR_HANDLER = new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
}
};
@@ -285,7 +285,11 @@
reqs.assertFragmentsRequested();
assertFalse(error6try2.getWasCalled());
progress.assertEvent("download6", END, 6);
-
+
+ // a finish event should do nothing if the fragment has already succeeded
+ progress.assertNoEvents();
+ loadFailed(reqs, 6);
+ assertFalse(error6try2.getWasCalled());
progress.assertNoEvents();
}
@@ -726,6 +730,6 @@
}
private void loadFailed(MockLoadStrategy reqs, int fragment) {
- reqs.errorHandlers.get(fragment).loadFailed(makeLoadFailedException());
+ reqs.errorHandlers.get(fragment).loadTerminated(makeLoadFailedException());
}
}
diff --git a/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java b/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
index 4ca245b..de9d2a8 100644
--- a/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
+++ b/user/test/com/google/gwt/core/client/impl/XhrLoadingStrategyTest.java
@@ -1,278 +1,270 @@
-/*
- * 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.core.client.impl;
-
-import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadErrorHandler;
-import com.google.gwt.core.client.impl.XhrLoadingStrategy.MockableXMLHttpRequest;
-import com.google.gwt.xhr.client.ReadyStateChangeHandler;
-
-import junit.framework.TestCase;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-
-/**
- * Tests the default loading strategy and its retry behavior.
- */
-public class XhrLoadingStrategyTest extends TestCase {
-
- static class MockXhr implements MockableXMLHttpRequest {
- public static final String SUCCESSFUL_RESPONSE_TEXT =
- "successful response text";
- public static final String INSTALL_FAILED_RESPONSE_TEXT =
- "install failed response text";
-
- private ReadyStateChangeHandler handler;
- private int httpStatus;
- private int state;
- private String statusText;
- private String text;
- private HashMap<String,String> headers;
-
- public MockXhr(int status, String statusText, boolean loads,
- boolean installs, String... headers) {
- this.httpStatus = status;
- this.statusText = statusText;
- if (installs) {
- text = SUCCESSFUL_RESPONSE_TEXT;
- } else if (loads) {
- text = INSTALL_FAILED_RESPONSE_TEXT;
- } else {
- text = null;
- }
- handler = null;
- state = 0;
- assert headers.length % 2 == 0;
- this.headers = new HashMap<String,String>();
- for (int i = 0; i < headers.length; i += 2) {
- this.headers.put(headers[i], headers[i + 1]);
- }
- }
-
- public void clearOnReadyStateChange() {
- handler = null;
- }
-
- public int getReadyState() {
- return state;
- }
-
- public String getResponseText() {
- return state > 3 ? text : null;
- }
-
- public int getStatus() {
- return state > 1 ? httpStatus : 0;
- }
-
- public String getStatusText() {
- return state > 1 ? statusText : null;
- }
-
- public void open(String method, String url) {
- state = 1;
- }
-
- public void send() {
- state = 4;
- if (headers.size() != 0) {
- throw new IllegalStateException("not all expected headers set");
- }
- if (handler != null) {
- /* This is brittle, but I don't have a better idea. The problem is
- * that onReadyStateChange takes a REAL XMLHttpRequest, which I can't
- * mock because it's all final. I don't want to open
- * ReadyStateChangeHandler's long-standing API to let it take a
- * non-real XMLHttpRequest, just for my wee test here, so instead I
- * admit that null works 'cause the handler won't *use* its argument.
- */
- handler.onReadyStateChange(null);
- }
- }
-
- public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
- this.handler = handler;
- }
-
- public void setRequestHeader(String header, String value) {
- String val = headers.get(header);
- if (val == null) {
- throw new IllegalArgumentException("set of unexpected header "
- + header);
- }
- if (!val.equals(value)) {
- throw new IllegalArgumentException("set of header "
- + header + " to unexpected value " + value + ", not " + val);
- }
- headers.remove(header);
- }
- }
-
- /**
- * {@link XhrLoadingStrategy}, but without actual live XHRs.
- */
- static class MockXhrLoadingStrategy extends XhrLoadingStrategy {
- private static final String FRAGMENT_URL = "http://nowhere.net/fragment";
- private ArrayList<MockXhr> xhrs;
-
- public MockXhrLoadingStrategy(MockXhr... input) {
- xhrs = new ArrayList<MockXhr>(Arrays.asList(input));
- }
-
- public void assertDone() {
- if (xhrs.size() != 0) {
- throw new IllegalStateException("leftover createXhr() data" +
- " (too few load retries?)");
- }
- }
-
- /**
- * Test stub; install succeeds unless text says otherwise.
- */
- @Override
- protected void gwtInstallCode(String text) {
- if (MockXhr.INSTALL_FAILED_RESPONSE_TEXT.equals(text)) {
- throw new RuntimeException(text);
- }
- }
-
- /**
- * Test stub; bypass the JSNI, but we're returning a (mock) URL.
- */
- @Override
- protected String gwtStartLoadingFragment(int fragment,
- LoadErrorHandler loadErrorHandler) {
- return FRAGMENT_URL;
- }
-
- @Override
- protected MockableXMLHttpRequest createXhr() {
- if (xhrs.size() == 0) {
- throw new IllegalStateException("createXhr() underflow" +
- " (too many load retries?)");
- }
- return xhrs.remove(0);
- }
- }
-
- /**
- * Basic succeeds-on-first-try case.
- */
- public void testNoRetrySucceeds() {
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(200, "200 Ok", true, true));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- fail();
- }
- });
- xls.assertDone();
- }
-
- /**
- * Fails irrevocably on first try; doesn't retry.
- */
- public void testNoRetryFails() {
- final boolean loadFailedCalled[] = new boolean[1];
- loadFailedCalled[0] = false;
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(200, "Ok", true, false));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- loadFailedCalled[0] = true;
- }
- });
- xls.assertDone();
- if (!loadFailedCalled[0]) {
- fail("should have failed to install, but didn't");
- }
- }
-
- /**
- * Needs some retries, but succeeds.
- */
- public void testRetrySucceeds() {
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(0, "Could not connect", false, false),
- new MockXhr(200, "Ok", true, true, "Cache-Control", "no-cache"));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- fail();
- }
- });
- xls.assertDone();
- }
-
- /**
- * Needs retries, and never succeeds.
- */
- public void testRetryFails() {
- final boolean loadFailedCalled[] = new boolean[1];
- loadFailedCalled[0] = false;
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(0, "Could not connect", false, false),
- new MockXhr(0, "Could not connect", false, false,
- "Cache-Control", "no-cache"),
- new MockXhr(0, "Could not connect", false, false,
- "Cache-Control", "no-cache"));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- loadFailedCalled[0] = true;
- }
- });
- xls.assertDone();
- if (!loadFailedCalled[0]) {
- fail("should have failed to install, but didn't");
- }
- }
-
- /**
- * A bizarre case we've seen in the wild...
- */
- public void testNull200Case() {
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(200, "Ok", false, false),
- new MockXhr(200, "Ok", false, false,
- "Cache-Control", "no-cache"),
- new MockXhr(200, "Ok", true, true,
- "Cache-Control", "no-cache"));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- fail();
- }
- });
- xls.assertDone();
- }
-
- /**
- * Check some HTTP status codes....
- */
- public void testRetryCodes() {
- MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(
- new MockXhr(500, "Server Error", false, false),
- new MockXhr(404, "Not Found", false, false,
- "Cache-Control", "no-cache"),
- new MockXhr(200, "Ok", true, true,
- "Cache-Control", "no-cache"));
- xls.startLoadingFragment(1, new LoadErrorHandler() {
- public void loadFailed(Throwable reason) {
- fail();
- }
- });
- xls.assertDone();
- }
-}
+/*
+ * 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.core.client.impl;
+
+import com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadTerminatedHandler;
+import com.google.gwt.core.client.impl.XhrLoadingStrategy.MockableXMLHttpRequest;
+import com.google.gwt.xhr.client.ReadyStateChangeHandler;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+
+/**
+ * Tests the default loading strategy and its retry behavior.
+ */
+public class XhrLoadingStrategyTest extends TestCase {
+
+ static class MockXhr implements MockableXMLHttpRequest {
+ public static final String SUCCESSFUL_RESPONSE_TEXT = "successful response text";
+ public static final String INSTALL_FAILED_RESPONSE_TEXT = "install failed response text";
+
+ private ReadyStateChangeHandler handler;
+ private int httpStatus;
+ private int state;
+ private String statusText;
+ private String text;
+ private HashMap<String, String> headers;
+
+ public MockXhr(int status, String statusText, boolean loads,
+ boolean installs, String... headers) {
+ this.httpStatus = status;
+ this.statusText = statusText;
+ if (installs) {
+ text = SUCCESSFUL_RESPONSE_TEXT;
+ } else if (loads) {
+ text = INSTALL_FAILED_RESPONSE_TEXT;
+ } else {
+ text = null;
+ }
+ handler = null;
+ state = 0;
+ assert headers.length % 2 == 0;
+ this.headers = new HashMap<String, String>();
+ for (int i = 0; i < headers.length; i += 2) {
+ this.headers.put(headers[i], headers[i + 1]);
+ }
+ }
+
+ public void clearOnReadyStateChange() {
+ handler = null;
+ }
+
+ public int getReadyState() {
+ return state;
+ }
+
+ public String getResponseText() {
+ return state > 3 ? text : null;
+ }
+
+ public int getStatus() {
+ return state > 1 ? httpStatus : 0;
+ }
+
+ public String getStatusText() {
+ return state > 1 ? statusText : null;
+ }
+
+ public void open(String method, String url) {
+ state = 1;
+ }
+
+ public void send() {
+ state = 4;
+ if (headers.size() != 0) {
+ throw new IllegalStateException("not all expected headers set");
+ }
+ if (handler != null) {
+ /*
+ * This is brittle, but I don't have a better idea. The problem is that
+ * onReadyStateChange takes a REAL XMLHttpRequest, which I can't mock
+ * because it's all final. I don't want to open
+ * ReadyStateChangeHandler's long-standing API to let it take a non-real
+ * XMLHttpRequest, just for my wee test here, so instead I admit that
+ * null works 'cause the handler won't *use* its argument.
+ */
+ handler.onReadyStateChange(null);
+ }
+ }
+
+ public void setOnReadyStateChange(ReadyStateChangeHandler handler) {
+ this.handler = handler;
+ }
+
+ public void setRequestHeader(String header, String value) {
+ String val = headers.get(header);
+ if (val == null) {
+ throw new IllegalArgumentException("set of unexpected header " + header);
+ }
+ if (!val.equals(value)) {
+ throw new IllegalArgumentException("set of header " + header
+ + " to unexpected value " + value + ", not " + val);
+ }
+ headers.remove(header);
+ }
+ }
+
+ /**
+ * {@link XhrLoadingStrategy}, but without actual live XHRs.
+ */
+ static class MockXhrLoadingStrategy extends XhrLoadingStrategy {
+ private static final String FRAGMENT_URL = "http://nowhere.net/fragment";
+ private ArrayList<MockXhr> xhrs;
+
+ public MockXhrLoadingStrategy(MockXhr... input) {
+ xhrs = new ArrayList<MockXhr>(Arrays.asList(input));
+ }
+
+ public void assertDone() {
+ if (xhrs.size() != 0) {
+ throw new IllegalStateException("leftover createXhr() data"
+ + " (too few load retries?)");
+ }
+ }
+
+ /**
+ * Test stub; install succeeds unless text says otherwise.
+ */
+ @Override
+ protected void gwtInstallCode(String text) {
+ if (MockXhr.INSTALL_FAILED_RESPONSE_TEXT.equals(text)) {
+ throw new RuntimeException(text);
+ }
+ }
+
+ /**
+ * Test stub; bypass the JSNI, but we're returning a (mock) URL.
+ */
+ @Override
+ protected String gwtStartLoadingFragment(int fragment,
+ LoadTerminatedHandler LoadFinishedHandler) {
+ return FRAGMENT_URL;
+ }
+
+ @Override
+ protected MockableXMLHttpRequest createXhr() {
+ if (xhrs.size() == 0) {
+ throw new IllegalStateException("createXhr() underflow"
+ + " (too many load retries?)");
+ }
+ return xhrs.remove(0);
+ }
+ }
+
+ /**
+ * Basic succeeds-on-first-try case.
+ */
+ public void testNoRetrySucceeds() {
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+ "200 Ok", true, true));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ fail();
+ }
+ });
+ xls.assertDone();
+ }
+
+ /**
+ * Fails irrevocably on first try; doesn't retry.
+ */
+ public void testNoRetryFails() {
+ final boolean loadFailedCalled[] = new boolean[1];
+ loadFailedCalled[0] = false;
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+ "Ok", true, false));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ loadFailedCalled[0] = true;
+ }
+ });
+ xls.assertDone();
+ if (!loadFailedCalled[0]) {
+ fail("should have failed to install, but didn't");
+ }
+ }
+
+ /**
+ * Needs some retries, but succeeds.
+ */
+ public void testRetrySucceeds() {
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(0,
+ "Could not connect", false, false), new MockXhr(200, "Ok", true, true,
+ "Cache-Control", "no-cache"));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ fail();
+ }
+ });
+ xls.assertDone();
+ }
+
+ /**
+ * Needs retries, and never succeeds.
+ */
+ public void testRetryFails() {
+ final boolean loadFailedCalled[] = new boolean[1];
+ loadFailedCalled[0] = false;
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(0,
+ "Could not connect", false, false), new MockXhr(0, "Could not connect",
+ false, false, "Cache-Control", "no-cache"), new MockXhr(0,
+ "Could not connect", false, false, "Cache-Control", "no-cache"));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ loadFailedCalled[0] = true;
+ }
+ });
+ xls.assertDone();
+ if (!loadFailedCalled[0]) {
+ fail("should have failed to install, but didn't");
+ }
+ }
+
+ /**
+ * A bizarre case we've seen in the wild...
+ */
+ public void testNull200Case() {
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(200,
+ "Ok", false, false), new MockXhr(200, "Ok", false, false,
+ "Cache-Control", "no-cache"), new MockXhr(200, "Ok", true, true,
+ "Cache-Control", "no-cache"));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ fail();
+ }
+ });
+ xls.assertDone();
+ }
+
+ /**
+ * Check some HTTP status codes....
+ */
+ public void testRetryCodes() {
+ MockXhrLoadingStrategy xls = new MockXhrLoadingStrategy(new MockXhr(500,
+ "Server Error", false, false), new MockXhr(404, "Not Found", false,
+ false, "Cache-Control", "no-cache"), new MockXhr(200, "Ok", true, true,
+ "Cache-Control", "no-cache"));
+ xls.startLoadingFragment(1, new LoadTerminatedHandler() {
+ public void loadTerminated(Throwable reason) {
+ fail();
+ }
+ });
+ xls.assertDone();
+ }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml b/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml
new file mode 100644
index 0000000..fa7ed94
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CompilerSuiteCrossSite.gwt.xml
@@ -0,0 +1,18 @@
+<!-- -->
+<!-- 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 -->
+<!-- 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. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<module>
+ <inherits name="com.google.gwt.dev.jjs.CompilerSuite"/>
+ <add-linker name="xs"/>
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml
new file mode 100644
index 0000000..0a83b65
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncFailure.gwt.xml
@@ -0,0 +1,25 @@
+<!-- -->
+<!-- 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 -->
+<!-- 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. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<module>
+ <inherits name="com.google.gwt.dev.jjs.RunAsyncFailure" />
+
+ <add-linker name="xs" />
+
+ <replace-with
+ class="com.google.gwt.user.client.runasync.CrossSiteLoadingStrategyForRunAsyncFailureTest">
+ <when-type-is
+ class="com.google.gwt.core.client.impl.AsyncFragmentLoader.LoadingStrategy" />
+ </replace-with>
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml
new file mode 100644
index 0000000..ad0e7d7
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncMetrics.gwt.xml
@@ -0,0 +1,20 @@
+<!-- -->
+<!-- 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 -->
+<!-- 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. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<module>
+ <inherits name="com.google.gwt.dev.jjs.RunAsyncMetricsIntegrationTest" />
+ <add-linker name="xs" />
+
+ <public path="public" />
+</module>
diff --git a/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java
new file mode 100644
index 0000000..ffd4eac
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/CrossSiteRunAsyncSuite.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2010 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.jjs;
+
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncFailureTest;
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncMetricsTest;
+import com.google.gwt.dev.jjs.test.CrossSiteRunAsyncTest;
+import com.google.gwt.junit.tools.GWTTestSuite;
+
+import junit.framework.Test;
+
+/**
+ * Tests that GWT.runAsync works with the cross-site linker.
+ */
+public class CrossSiteRunAsyncSuite {
+
+ public static Test suite() {
+ GWTTestSuite suite = new GWTTestSuite("Test for com.google.gwt.dev.jjs");
+
+ suite.addTestSuite(CrossSiteRunAsyncMetricsTest.class);
+ suite.addTestSuite(CrossSiteRunAsyncFailureTest.class);
+ suite.addTestSuite(CrossSiteRunAsyncTest.class);
+
+ return suite;
+ }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
index 29935b6..9e5ae07 100644
--- a/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncFailure.gwt.xml
@@ -13,10 +13,10 @@
<!-- limitations under the License. -->
<module>
- <source path='test' />
- <servlet path="/runAsyncFailure"
- class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet"/>
- <define-linker name="runAsyncFailure"
- class="com.google.gwt.user.server.runasync.RunAsyncFailureIFrameLinker"/>
- <add-linker name="runAsyncFailure"/>
+ <inherits name="com.google.gwt.core.Core" />
+ <source path="test" />
+ <servlet path="/runAsyncFailure"
+ class="com.google.gwt.user.server.runasync.RunAsyncFailureServlet" />
+ <set-configuration-property name="iframe.linker.deferredjs.subdir"
+ value="runAsyncFailure/deferredjs" />
</module>
diff --git a/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml b/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
index 622cf88..d76e53c 100644
--- a/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
+++ b/user/test/com/google/gwt/dev/jjs/RunAsyncMetricsIntegrationTest.gwt.xml
@@ -18,4 +18,5 @@
<!-- order of those events could change. -->
<module>
<inherits name="com.google.gwt.dev.jjs.CompilerSuite"/>
+ <script src="empty-gwt-stats.js" />
</module>
diff --git a/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js b/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js
new file mode 100644
index 0000000..c0b1f28
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/public/empty-gwt-stats.js
@@ -0,0 +1,17 @@
+<!-- -->
+<!-- Copyright 2010 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 -->
+<!-- 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. License for the specific language governing permissions and -->
+<!-- limitations under the License. -->
+
+<!-- Define an empty __gwtStatsEvent just to make sure that the -->
+<!-- lightweight metrics machinery runs during bootstrapping -->
+function __gwtStatsEvent(evt) { return true; }
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java
new file mode 100644
index 0000000..9d690ba
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncFailureTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2010 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.jjs.test;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests that the cross-site linker handles download failures.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncFailureTest extends RunAsyncFailureTest {
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.dev.jjs.CrossSiteRunAsyncFailure";
+ }
+
+ @Override
+ public void testHttpFailureRetries() {
+ if (!GWT.isScript()) {
+ // Cross-site linker is not supported in dev mode
+ return;
+ }
+ super.testHttpFailureRetries();
+ }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java
new file mode 100644
index 0000000..07d9d2e
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncMetricsTest.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2010 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.jjs.test;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests runAsync lightweight metrics when the cross-site linker is used.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncMetricsTest extends
+ RunAsyncMetricsIntegrationTest {
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.dev.jjs.CrossSiteRunAsyncMetrics";
+ }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java
new file mode 100644
index 0000000..b6c3ee2
--- /dev/null
+++ b/user/test/com/google/gwt/dev/jjs/test/CrossSiteRunAsyncTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2010 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.jjs.test;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+
+/**
+ * Tests GWT.runAsync when used with the cross-site linker.
+ */
+@DoNotRunWith(Platform.Devel)
+public class CrossSiteRunAsyncTest extends RunAsyncTest {
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.dev.jjs.CompilerSuiteCrossSite";
+ }
+}
diff --git a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
index 05e80d0..aaff5aa 100644
--- a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
@@ -17,12 +17,17 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;
/**
* Tests runAsync server/network failure handling.
+ *
+ * This is skipped in dev mode because runAsync never fails in dev mode.
*/
+@DoNotRunWith(Platform.Devel)
public class RunAsyncFailureTest extends GWTTestCase {
abstract static class MyRunAsyncCallback implements RunAsyncCallback {
diff --git a/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java b/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java
new file mode 100644
index 0000000..20fa0d5
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/runasync/CrossSiteLoadingStrategyForRunAsyncFailureTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2010 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.user.client.runasync;
+
+import com.google.gwt.core.client.impl.CrossSiteLoadingStrategy;
+
+/**
+ * A variant of {@link CrossSiteLoadingStrategy} used for the
+ * {@link com.google.gwt.dev.jjs.test.RunAsyncFailureTest}.
+ * It downloads code fragments via a faulty servlet
+ * instead of the normal "deferredjs" location.
+ */
+public class CrossSiteLoadingStrategyForRunAsyncFailureTest extends
+ CrossSiteLoadingStrategy {
+ @Override
+ protected String getDeferredJavaScriptDirectory() {
+ return "runAsyncFailure/deferredjs/";
+ }
+}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
deleted file mode 100644
index 56b3222..0000000
--- a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureIFrameLinker.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.user.server.runasync;
-
-import com.google.gwt.core.ext.linker.Shardable;
-import com.google.gwt.core.linker.IFrameLinker;
-
-/**
- * Load modules from a custom servlet in order to test download failure
- * behavior.
- */
-@Shardable
-public class RunAsyncFailureIFrameLinker extends IFrameLinker {
-
- @Override
- protected String getFragmentSubdir() {
- return "runAsyncFailure/" + FRAGMENT_SUBDIR;
- }
-}
diff --git a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
index a4a28e5..ce19d82 100644
--- a/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
+++ b/user/test/com/google/gwt/user/server/runasync/RunAsyncFailureServlet.java
@@ -31,48 +31,51 @@
* Servlet for {@link com.google.gwt.dev.jjs.test.RunAsyncFailureTest}.
*/
public class RunAsyncFailureServlet extends HttpServlet {
-
+
private static final boolean DEBUG = false;
private static final HashSet<String> errorFragments = new HashSet<String>();
+
+ /**
+ * Sequence of response codes to send back. SC_OK must be last.
+ */
private static final int[] responses = {
HttpServletResponse.SC_SERVICE_UNAVAILABLE,
HttpServletResponse.SC_GATEWAY_TIMEOUT,
HttpServletResponse.SC_SERVICE_UNAVAILABLE,
- HttpServletResponse.SC_GATEWAY_TIMEOUT,
- HttpServletResponse.SC_OK // must be last
- };
-
+ HttpServletResponse.SC_GATEWAY_TIMEOUT, HttpServletResponse.SC_OK};
+
static {
errorFragments.add("2.cache.js");
}
-
+
private static void debug(String message) {
if (DEBUG) {
System.out.println(message);
}
}
-
- HashMap<String,Integer> triesMap = new HashMap<String,Integer>();
-
+
+ HashMap<String, Integer> triesMap = new HashMap<String, Integer>();
+
private int sSerial = 0;
-
+
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String originalUri = req.getRequestURI();
debug("doGet: " + originalUri);
String uri = originalUri.replace("/runAsyncFailure", "");
-
+
int response = getDesiredResponse(uri);
String fragment = uri.substring(uri.lastIndexOf('/') + 1);
if (!errorFragments.contains(fragment)
|| response == HttpServletResponse.SC_OK) {
// Delegate the actual data fetch to the main servlet
+
String host = req.getLocalName();
int port = req.getLocalPort();
String realUrl = "http://" + host + ":" + port + uri;
debug("Fetching: " + realUrl);
-
+
int bytes = 0;
try {
URL url = new URL(realUrl);
@@ -91,29 +94,29 @@
debug("IOException fetching real data: " + e);
throw e;
}
-
+
+ resp.setContentType("text/javascript");
resp.setHeader("Cache-Control", "no-cache");
resp.setContentLength(bytes);
- resp.setContentType("text/html");
resp.setStatus(HttpServletResponse.SC_OK);
-
+
debug("doGet: served " + uri + " (" + bytes + " bytes)");
} else {
resp.setHeader("Cache-Control", "no-cache");
resp.sendError(response, "serial=" + getNextSerial());
-
+
debug("doGet: sent error " + response + " for " + uri);
}
}
-
+
private int getDesiredResponse(String resource) {
Integer t = triesMap.get(resource);
int tries = t == null ? 0 : t.intValue();
triesMap.put(resource, tries + 1);
-
+
return responses[tries % responses.length];
}
-
+
private synchronized int getNextSerial() {
return sSerial++;
}