Update from trunk@6270

svn merge https://google-web-toolkit.googlecode.com/svn/trunk \
  --ignore-ancestry -r6249:6270 .



git-svn-id: https://google-web-toolkit.googlecode.com/svn/branches/farewellSwt@6271 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/branch-info.txt b/branch-info.txt
index 21016ee..5d5ecba 100644
--- a/branch-info.txt
+++ b/branch-info.txt
@@ -15,3 +15,5 @@
   -r6200:6239 .
 svn merge https://google-web-toolkit.googlecode.com/svn/trunk \
   -r6239:6249 .
+svn merge https://google-web-toolkit.googlecode.com/svn/trunk \
+  --ignore-ancestry -r6249:6270 .
diff --git a/dev/common.ant.xml b/dev/common.ant.xml
index 178bc62..262d3ba 100755
--- a/dev/common.ant.xml
+++ b/dev/common.ant.xml
@@ -4,7 +4,6 @@
 
   <property.ensure name="gwt.core.root" location="../core" />
   <property.ensure name="gwt.core.build" location="${project.build}/../core" />
-  <property.ensure name="gwt.tools.soyc" location="${gwt.root}/tools/soyc-vis" />
 
   <target name="compile" description="Compile all java files">
     <mkdir dir="${javac.out}" />
@@ -29,9 +28,6 @@
         <fileset dir="${javac.out}" />
         <fileset dir="${gwt.core.build}/bin" />
         <fileset file="${gwt.core.build}/alldeps.jar" />
-        <fileset dir="${gwt.tools.soyc}/images"/>
-        <fileset file="${gwt.tools.soyc}/classLevel.css"/>
-        <fileset file="${gwt.tools.soyc}/roundedCorners.css"/>
       </sourcefiles>
       <targetfiles>
         <fileset file="${project.lib}"/>
@@ -48,10 +44,6 @@
           <fileset dir="${gwt.core.build}/bin" />
           <zipfileset src="${gwt.core.build}/alldeps.jar" />
 
-          <zipfileset dir="${gwt.tools.soyc}/images" prefix="com/google/gwt/soyc/resources/images"/>
-          <zipfileset file="${gwt.tools.soyc}/classLevel.css" prefix="com/google/gwt/soyc/resources/"/>
-          <zipfileset file="${gwt.tools.soyc}/roundedCorners.css" prefix="com/google/gwt/soyc/resources/"/>
-
           <manifest>
             <attribute name="Main-Class" value="com.google.gwt.dev.GWTMain" />
           </manifest>
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/CompilationAnalysis.java b/dev/core/src/com/google/gwt/core/ext/linker/CompilationAnalysis.java
index 615f333..982499a 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/CompilationAnalysis.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/CompilationAnalysis.java
@@ -16,6 +16,7 @@
 package com.google.gwt.core.ext.linker;
 
 import com.google.gwt.core.ext.Linker;
+import com.google.gwt.core.ext.linker.impl.StandardCompilationAnalysis.SoycArtifact;
 
 import java.util.ArrayList;
 import java.util.LinkedList;
@@ -41,6 +42,12 @@
   public abstract EmittedArtifact getDetailedStoriesFile();
 
   /**
+   * Files containing the HTML dashboard.
+   */
+
+  public abstract List<SoycArtifact> getReportFiles();
+
+  /**
    * @return a file of size maps
    */
   public abstract EmittedArtifact getSizeMapsFile();
@@ -69,9 +76,12 @@
         allFiles());
     LinkedList<EmittedArtifact> otherFiles = new LinkedList<EmittedArtifact>(
         o.allFiles());
-    assert (myFiles.size() == otherFiles.size());
 
     while (!myFiles.isEmpty()) {
+      if (otherFiles.isEmpty()) {
+        return 1;
+      }
+
       EmittedArtifact myFile = myFiles.removeFirst();
       EmittedArtifact otherFile = otherFiles.removeFirst();
       if (myFile == null && otherFile == null) {
@@ -93,6 +103,10 @@
       }
     }
 
+    if (!otherFiles.isEmpty()) {
+      return -1;
+    }
+
     return 0;
   }
 
@@ -107,6 +121,7 @@
     files.add(getDepFile());
     files.add(getSizeMapsFile());
     files.add(getDetailedStoriesFile());
+    files.addAll(getReportFiles());
     return files;
   }
 }
diff --git a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationAnalysis.java b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationAnalysis.java
index b9a8969..327c5ba 100644
--- a/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationAnalysis.java
+++ b/dev/core/src/com/google/gwt/core/ext/linker/impl/StandardCompilationAnalysis.java
@@ -19,6 +19,8 @@
 import com.google.gwt.core.ext.linker.SyntheticArtifact;
 import com.google.gwt.core.linker.SoycReportLinker;
 
+import java.util.List;
+
 /**
  * An implementation of CompilationAnalysis. This class transforms SourceInfos
  * and related data into an API suitable for public consumption via the Linker
@@ -48,9 +50,9 @@
   private SoycArtifact detailedStoriesFile;
 
   /**
-   * File containing split points.
+   * Files containing the HTML dashboard.
    */
-  private SoycArtifact splitPointsFile;
+  private List<SoycArtifact> reportFiles;
 
   /**
    * File containing size maps.
@@ -58,16 +60,22 @@
   private SoycArtifact sizeMapsFile;
 
   /**
+   * File containing split points.
+   */
+  private SoycArtifact splitPointsFile;
+
+  /**
    * Constructed by PermutationCompiler.
    */
   public StandardCompilationAnalysis(SoycArtifact dependencies,
       SoycArtifact sizeMaps, SoycArtifact splitPoints,
-      SoycArtifact detailedStories) {
+      SoycArtifact detailedStories, List<SoycArtifact> reportFiles) {
     super(StandardLinkerContext.class);
     this.depFile = dependencies;
     this.sizeMapsFile = sizeMaps;
     this.splitPointsFile = splitPoints;
     this.detailedStoriesFile = detailedStories;
+    this.reportFiles = reportFiles;
   }
 
   @Override
@@ -81,6 +89,11 @@
   }
 
   @Override
+  public List<SoycArtifact> getReportFiles() {
+    return reportFiles;
+  }
+
+  @Override
   public SoycArtifact getSizeMapsFile() {
     return sizeMapsFile;
   }
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
index e58edce..a8f648e 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/DependencyRecorder.java
@@ -108,7 +108,7 @@
   protected void recordDependenciesImpl(TreeLogger logger, JProgram jprogram) {
 
     logger = logger.branch(TreeLogger.INFO,
-        "Creating Dependencies file for SOYC");
+        "Creating dependencies file for the compile report");
 
     ControlFlowAnalyzer dependencyAnalyzer = new ControlFlowAnalyzer(jprogram);
     dependencyAnalyzer.setDependencyRecorder(this);
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/SplitPointRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/SplitPointRecorder.java
index 597ad33..044de32 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/SplitPointRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/SplitPointRecorder.java
@@ -40,7 +40,7 @@
       TreeLogger logger) {
 
     logger = logger.branch(TreeLogger.TRACE,
-        "Creating Split Point Map file for SOYC");
+        "Creating split point map file for the compile report");
 
     try {
       OutputStreamWriter writer = new OutputStreamWriter(new GZIPOutputStream(
diff --git a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
index f1708d2..9ad7601 100644
--- a/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
+++ b/dev/core/src/com/google/gwt/core/ext/soyc/impl/StoryRecorder.java
@@ -108,7 +108,7 @@
   protected void recordStoriesImpl(TreeLogger logger, OutputStream out,
       List<Map<Range, SourceInfo>> sourceInfoMaps, String[] js) {
 
-    logger = logger.branch(TreeLogger.INFO, "Creating Stories file for SOYC");
+    logger = logger.branch(TreeLogger.INFO, "Creating Stories file for the compile report");
 
     this.js = js;
 
diff --git a/dev/core/src/com/google/gwt/core/linker/SoycReportLinker.java b/dev/core/src/com/google/gwt/core/linker/SoycReportLinker.java
index bb32174..2f63ff1 100644
--- a/dev/core/src/com/google/gwt/core/linker/SoycReportLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/SoycReportLinker.java
@@ -20,9 +20,17 @@
 import com.google.gwt.core.ext.TreeLogger;

 import com.google.gwt.core.ext.UnableToCompleteException;

 import com.google.gwt.core.ext.linker.ArtifactSet;

+import com.google.gwt.core.ext.linker.CompilationResult;

 import com.google.gwt.core.ext.linker.LinkerOrder;

+import com.google.gwt.core.ext.linker.SelectionProperty;

 import com.google.gwt.core.ext.linker.LinkerOrder.Order;

 import com.google.gwt.core.ext.linker.impl.StandardCompilationAnalysis;

+import com.google.gwt.soyc.SoycDashboard;

+import com.google.gwt.soyc.io.ArtifactsOutputDirectory;

+

+import java.io.IOException;

+import java.util.Map;

+import java.util.TreeMap;

 

 /**

  * Converts SOYC report files into emitted private artifacts.

@@ -32,13 +40,15 @@
 

   @Override

   public String getDescription() {

-    return "Emit SOYC artifacts";

+    return "Emit compile report artifacts";

   }

 

   @Override

   public ArtifactSet link(TreeLogger logger, LinkerContext context,

       ArtifactSet artifacts) throws UnableToCompleteException {

     ArtifactSet results = new ArtifactSet(artifacts);

+    boolean foundReports = false;

+

     for (StandardCompilationAnalysis soycFiles : artifacts.find(StandardCompilationAnalysis.class)) {

       if (soycFiles.getDepFile() != null) {

         results.add(soycFiles.getDepFile());

@@ -50,8 +60,42 @@
         results.add(soycFiles.getDetailedStoriesFile());

       }

       results.add(soycFiles.getSplitPointsFile());

+      if (!soycFiles.getReportFiles().isEmpty()) {

+        results.addAll(soycFiles.getReportFiles());

+        foundReports = true;

+      }

+    }

+

+    if (foundReports) {

+      // run the final step of the dashboard to generate top-level files

+      ArtifactsOutputDirectory out = new ArtifactsOutputDirectory();

+      try {

+        new SoycDashboard(out).generateCrossPermutationFiles(extractPermutationDescriptions(artifacts));

+      } catch (IOException e) {

+        logger.log(TreeLogger.ERROR,

+            "Error while generating a Story of Your Compile", e);

+        e.printStackTrace();

+      }

+      results.addAll(out.getArtifacts());

     }

     return results;

   }

 

+  private Map<String, String> extractPermutationDescriptions(

+      ArtifactSet artifacts) {

+    Map<String, String> permDescriptions = new TreeMap<String, String>();

+

+    for (CompilationResult res : artifacts.find(CompilationResult.class)) {

+      String permId = Integer.toString(res.getPermutationId());

+      /*

+       * TODO(kprobst,spoon) support permutations that collapsed to the same

+       * compilation result

+       */

+      Map<SelectionProperty, String> propertyMap = res.getPropertyMap().iterator().next();

+      String permDesc = SymbolMapsLinker.propertyMapToString(propertyMap);

+      permDescriptions.put(permId, permDesc);

+    }

+

+    return permDescriptions;

+  }

 }

diff --git a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
index 3d3ed90..a3dcad3 100644
--- a/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
+++ b/dev/core/src/com/google/gwt/core/linker/SymbolMapsLinker.java
@@ -29,6 +29,7 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.Map;
 import java.util.SortedMap;
 
@@ -47,6 +48,33 @@
    */
   public static final String STRONG_NAME_SUFFIX = ".symbolMap";
 
+  public static String propertyMapToString(
+      Map<SelectionProperty, String> propertyMap) {
+    StringWriter writer = new StringWriter();
+    PrintWriter pw = new PrintWriter(writer);
+    printPropertyMap(pw, propertyMap);
+    pw.flush();
+    return writer.toString();
+  }
+
+  private static void printPropertyMap(PrintWriter pw,
+      Map<SelectionProperty, String> map) {
+    boolean needsComma = false;
+    for (Map.Entry<SelectionProperty, String> entry : map.entrySet()) {
+      if (needsComma) {
+        pw.print(" , ");
+      } else {
+        needsComma = true;
+      }
+
+      pw.print("'");
+      pw.print(entry.getKey().getName());
+      pw.print("' : '");
+      pw.print(entry.getValue());
+      pw.print("'");
+    }
+  }
+
   @Override
   public String getDescription() {
     return "Export CompilationResult symbol maps";
@@ -89,26 +117,11 @@
    */
   protected void doWriteSymbolMap(TreeLogger logger, CompilationResult result,
       PrintWriter pw) throws UnableToCompleteException {
-    
     pw.println("# { " + result.getPermutationId() + " }");
-    
+
     for (SortedMap<SelectionProperty, String> map : result.getPropertyMap()) {
       pw.print("# { ");
-
-      boolean needsComma = false;
-      for (Map.Entry<SelectionProperty, String> entry : map.entrySet()) {
-        if (needsComma) {
-          pw.print(" , ");
-        } else {
-          needsComma = true;
-        }
-
-        pw.print("'");
-        pw.print(entry.getKey().getName());
-        pw.print("' : '");
-        pw.print(entry.getValue());
-        pw.print("'");
-      }
+      printPropertyMap(pw, map);
       pw.println(" }");
     }
 
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 c37c6c3..3f83f66 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -20,6 +20,7 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.linker.ArtifactSet;
+import com.google.gwt.core.ext.linker.EmittedArtifact;
 import com.google.gwt.core.ext.linker.StatementRanges;
 import com.google.gwt.core.ext.linker.SymbolData;
 import com.google.gwt.core.ext.linker.impl.StandardCompilationAnalysis;
@@ -57,7 +58,6 @@
 import com.google.gwt.dev.jjs.impl.CastNormalizer;
 import com.google.gwt.dev.jjs.impl.CatchBlockNormalizer;
 import com.google.gwt.dev.jjs.impl.CodeSplitter;
-import com.google.gwt.dev.jjs.impl.CodeSplitter.MultipleDependencyGraphRecorder;
 import com.google.gwt.dev.jjs.impl.ControlFlowAnalyzer;
 import com.google.gwt.dev.jjs.impl.DeadCodeElimination;
 import com.google.gwt.dev.jjs.impl.EqualityNormalizer;
@@ -85,6 +85,7 @@
 import com.google.gwt.dev.jjs.impl.SourceGenerationVisitor;
 import com.google.gwt.dev.jjs.impl.TypeMap;
 import com.google.gwt.dev.jjs.impl.TypeTightener;
+import com.google.gwt.dev.jjs.impl.CodeSplitter.MultipleDependencyGraphRecorder;
 import com.google.gwt.dev.js.EvalFunctionsAtTopScope;
 import com.google.gwt.dev.js.JsBreakUpLargeVarStatements;
 import com.google.gwt.dev.js.JsIEBlockSizeVisitor;
@@ -110,12 +111,16 @@
 import com.google.gwt.dev.util.PerfLogger;
 import com.google.gwt.dev.util.TextOutput;
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.dev.util.collect.Lists;
 import com.google.gwt.dev.util.collect.Maps;
+import com.google.gwt.soyc.SoycDashboard;
+import com.google.gwt.soyc.io.ArtifactsOutputDirectory;
 
 import org.eclipse.jdt.core.compiler.IProblem;
 import org.eclipse.jdt.internal.compiler.CompilationResult;
 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration;
 import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
+import org.xml.sax.SAXException;
 
 import java.io.ByteArrayOutputStream;
 import java.io.FileOutputStream;
@@ -130,13 +135,15 @@
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.zip.GZIPInputStream;
+
+import javax.xml.parsers.ParserConfigurationException;
 
 /**
  * Compiles the Java <code>JProgram</code> representation into its corresponding
  * JavaScript source.
  */
 public class JavaToJavaScriptCompiler {
-
   private static class PermutationResultImpl implements PermutationResult {
     private final ArtifactSet artifacts = new ArtifactSet();
     private final byte[][] js;
@@ -318,7 +325,7 @@
       // Work around an IE7 bug,
       // http://code.google.com/p/google-web-toolkit/issues/detail?id=1440
       // note, JsIEBlockTextTransformer now handles restructuring top level
-      // blocks, this class now handles non-top level blocks only. 
+      // blocks, this class now handles non-top level blocks only.
       SelectionProperty userAgentProperty = null;
       for (PropertyOracle oracle : propertyOracles) {
         userAgentProperty = oracle.getSelectionProperty(logger, "user.agent");
@@ -327,9 +334,8 @@
         }
       }
       // if user agent is known or ie6, split overly large blocks
-      boolean splitBlocks = userAgentProperty == null || (
-          userAgentProperty != null &&
-              "ie6".equals(userAgentProperty.getCurrentValue()));
+      boolean splitBlocks = userAgentProperty == null
+          || (userAgentProperty != null && "ie6".equals(userAgentProperty.getCurrentValue()));
 
       if (splitBlocks) {
         JsIEBlockSizeVisitor.exec(jsProgram);
@@ -853,7 +859,7 @@
         v = new JsSourceGenerationVisitorWithSizeBreakdown(out, jjsMap);
       }
       v.accept(jsProgram.getFragmentBlock(i));
-      
+
       /**
        * Reorder function decls to improve compression ratios. Also restructures
        * the top level blocks into sub-blocks if they exceed 32767 statements.
@@ -929,10 +935,10 @@
       SizeBreakdown[] sizeBreakdowns,
       List<Map<Range, SourceInfo>> sourceInfoMaps, SoycArtifact dependencies,
       JavaToJavaScriptMap jjsmap, Map<JsName, String> obfuscateMap)
-      throws IOException {
+      throws IOException, UnableToCompleteException {
     ByteArrayOutputStream baos = new ByteArrayOutputStream();
 
-    PerfLogger.start("Recording SOYC output");
+    PerfLogger.start("Recording compile report output");
 
     PerfLogger.start("Record split points");
     SplitPointRecorder.recordSplitPoints(jprogram, baos, logger);
@@ -964,8 +970,34 @@
 
     PerfLogger.end();
 
+    List<SoycArtifact> reportArtifacts = Lists.create();
+    if (sizeBreakdowns != null) {
+      PerfLogger.start("Generating compile report");
+      ArtifactsOutputDirectory outDir = new ArtifactsOutputDirectory();
+      SoycDashboard dashboard = new SoycDashboard(outDir);
+      dashboard.startNewPermutation(Integer.toString(permutationId));
+      try {
+        dashboard.readSplitPoints(openWithGunzip(splitPoints));
+        if (sizeMaps != null) {
+          dashboard.readSizeMaps(openWithGunzip(sizeMaps));
+        }
+        if (dependencies != null) {
+          dashboard.readDependencies(openWithGunzip(dependencies));
+        }
+      } catch (ParserConfigurationException e) {
+        throw new InternalCompilerException(
+            "Error reading compile report information that was just generated", e);
+      } catch (SAXException e) {
+        throw new InternalCompilerException(
+            "Error reading compile report information that was just generated", e);
+      }
+      dashboard.generateForOnePermutation();
+      reportArtifacts = outDir.getArtifacts();
+      PerfLogger.end();
+    }
+
     return new StandardCompilationAnalysis(dependencies, sizeMaps, splitPoints,
-        detailedStories);
+        detailedStories, reportArtifacts);
   }
 
   /**
@@ -1032,6 +1064,14 @@
   }
 
   /**
+   * Open an emitted artifact and gunzip its contents.
+   */
+  private static GZIPInputStream openWithGunzip(EmittedArtifact artifact)
+      throws IOException, UnableToCompleteException {
+    return new GZIPInputStream(artifact.getContents(TreeLogger.NULL));
+  }
+
+  /**
    * Dependency information is normally recorded during code splitting, and it
    * results in multiple dependency graphs. If the code splitter doesn't run,
    * then this method can be used instead to record a single dependency graph
diff --git a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSoycDetailed.java b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSoycDetailed.java
index d8b78c9..c278399 100644
--- a/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSoycDetailed.java
+++ b/dev/core/src/com/google/gwt/dev/util/arg/ArgHandlerSoycDetailed.java
@@ -29,7 +29,7 @@
 
   @Override
   public String getPurpose() {
-    return "Emit extra, detailed SOYC information at the expense of compile time";
+    return "Emit extra, detailed compile-report information at the expense of compile time";
   }
 
   @Override
diff --git a/dev/core/src/com/google/gwt/soyc/GlobalInformation.java b/dev/core/src/com/google/gwt/soyc/GlobalInformation.java
index aaf9edf..34262db 100644
--- a/dev/core/src/com/google/gwt/soyc/GlobalInformation.java
+++ b/dev/core/src/com/google/gwt/soyc/GlobalInformation.java
@@ -25,10 +25,11 @@
 import java.util.TreeSet;
 
 /**
- * Information global to the entire SOYC report generator.
+ * SOYC information about a compiled module.
  */
 public class GlobalInformation {
   private static final SizeBreakdown[] EMPTY_SIZE_BREAKDOWN = new SizeBreakdown[0];
+  public Map<String, Map<String, String>> dependencies = null;
   private HashMap<String, String> classToPackage = new HashMap<String, String>();
   private HashMap<String, HashSet<String>> classToWhatItDependsOn = new HashMap<String, HashSet<String>>();
   private Map<Integer, SizeBreakdown> exclusiveCodeBreakdowns = new HashMap<Integer, SizeBreakdown>();
@@ -38,11 +39,16 @@
       "Leftovers code, code not in any other category", "leftovers");
   private int numSplitPoints = 0;
   private Map<String, TreeSet<String>> packageToClasses = new TreeMap<String, TreeSet<String>>();
+  private final String permutationId;
   private ArrayList<Integer> splitPointInitialLoadSequence = new ArrayList<Integer>();
   private HashMap<Integer, String> splitPointToLocation = new HashMap<Integer, String>();
   private SizeBreakdown totalCodeBreakdown = new SizeBreakdown("Total program",
       "total");
 
+  public GlobalInformation(String permutationId) {
+    this.permutationId = permutationId;
+  }
+
   public SizeBreakdown[] allSizeBreakdowns() {
     List<SizeBreakdown> breakdowns = new ArrayList<SizeBreakdown>();
     breakdowns.add(totalCodeBreakdown);
@@ -128,6 +134,10 @@
     return packageToClasses;
   }
 
+  public String getPermutationId() {
+    return permutationId;
+  }
+
   /**
    * Gets the initial load sequence.
    * 
diff --git a/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java b/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
index 9c63ffb..79d9258 100644
--- a/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
+++ b/dev/core/src/com/google/gwt/soyc/MakeTopLevelHtmlForPerm.java
@@ -17,13 +17,9 @@
 package com.google.gwt.soyc;
 
 import com.google.gwt.dev.util.Util;
+import com.google.gwt.soyc.io.OutputDirectory;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
@@ -33,9 +29,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
-import java.util.jar.JarEntry;
-import java.util.jar.JarFile;
-import java.util.jar.JarInputStream;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -44,24 +37,14 @@
  */
 public class MakeTopLevelHtmlForPerm {
   /**
-   * A dependency linker for exclusive fragments. It links to nothing.
-   */
-  public class DependencyLinkerForExclusiveFragment implements DependencyLinker {
-    public String dependencyLinkForClass(String className, String permutationId) {
-      return null;
-    }
-  }
-
-  /**
    * A dependency linker for the initial code download. It links to the
    * dependencies for the initial download.
    */
   public class DependencyLinkerForInitialCode implements DependencyLinker {
-    public String dependencyLinkForClass(String className, String permutationId) {
+    public String dependencyLinkForClass(String className) {
       String packageName = globalInformation.getClassToPackage().get(className);
       assert packageName != null;
-      return dependenciesFileName("initial", packageName, permutationId) + "#"
-          + className;
+      return dependenciesFileName("initial", packageName) + "#" + className;
     }
   }
 
@@ -70,8 +53,8 @@
    * status pages.
    */
   public class DependencyLinkerForLeftoversFragment implements DependencyLinker {
-    public String dependencyLinkForClass(String className, String permutationId) {
-      return leftoversStatusFileName(className, permutationId);
+    public String dependencyLinkForClass(String className) {
+      return leftoversStatusFileName(className);
     }
   }
 
@@ -81,13 +64,22 @@
    * 
    */
   public class DependencyLinkerForTotalBreakdown implements DependencyLinker {
-    public String dependencyLinkForClass(String className, String permutationId) {
-      return splitStatusFileName(className, permutationId);
+    public String dependencyLinkForClass(String className) {
+      return splitStatusFileName(className);
+    }
+  }
+
+  /**
+   * A dependency linker that never links to anything.
+   */
+  public static class NullDependencyLinker implements DependencyLinker {
+    public String dependencyLinkForClass(String className) {
+      return null;
     }
   }
 
   interface DependencyLinker {
-    String dependencyLinkForClass(String className, String permutationId);
+    String dependencyLinkForClass(String className);
   }
 
   /**
@@ -110,10 +102,6 @@
    */
   private static final Pattern PATTERN_SP_INT = Pattern.compile("sp([0-9]+)");
 
-  private static String RESOURCES_PATH = MakeTopLevelHtmlForPerm.class.getPackage().getName().replace(
-      '.', '/')
-      + "/resources/";
-
   public static String escapeXml(String unescaped) {
     String escaped = unescaped.replaceAll("\\&", "&amp;");
     escaped = escaped.replaceAll("\\<", "&lt;");
@@ -123,6 +111,46 @@
     return escaped;
   }
 
+  public static void makeTopLevelHtmlForAllPerms(
+      Map<String, String> allPermsInfo, OutputDirectory outDir)
+      throws IOException {
+    PrintWriter outFile = new PrintWriter(outDir.getOutputStream("index.html"));
+
+    outFile.println("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">");
+    outFile.println("<html>");
+    outFile.println("<head>");
+    outFile.println("  <title>Story of Your Compile - Top Level Dashboard for all Permutations</title>");
+    outFile.println("<style type=\"text/css\">");
+    outFile.println("body {background-color: #728FCE}");
+    outFile.println("h2 {background-color: transparent}");
+    outFile.println("</style>");
+    outFile.println("</head>");
+
+    outFile.println("<body>");
+    outFile.println("<center>");
+    outFile.println("<h1>Story of Your Compile</h1>");
+    outFile.println("<hr>");
+    outFile.println("<h3>Story of Your Compile - Overview of Permutations</h3>");
+    outFile.println("<hr>");
+
+    outFile.println("<div style='overflow:auto; background-color:white'>");
+    outFile.println("<center>");
+    for (String permutationId : allPermsInfo.keySet()) {
+      String permutationInfo = allPermsInfo.get(permutationId);
+      outFile.print("<p><a href=\"SoycDashboard" + "-" + permutationId
+          + "-index.html\">Permutation " + permutationId);
+      if (permutationInfo.length() > 0) {
+        outFile.println(" (" + permutationInfo + ")" + "</a>");
+      } else {
+        outFile.println("</a>");
+      }
+    }
+    outFile.println("</center>");
+    outFile.println("</div>");
+    addStandardHtmlEnding(outFile);
+    outFile.close();
+  }
+
   private static void addCenteredHeader(final PrintWriter outFile, String header) {
     outFile.println("<hr>");
     outFile.println("<b>" + header + "</b>");
@@ -137,6 +165,37 @@
     addCenteredHeader(outFile, headerLineForBreakdown(breakdown));
   }
 
+  private static void addStandardHtmlEnding(final PrintWriter out) {
+    out.println("</div>");
+    out.println("</body>");
+    out.println("</html>");
+  }
+
+  private static void addStandardHtmlProlog(final PrintWriter out,
+      String title, String header) {
+    out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
+    out.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
+    out.println("<html>");
+    out.println("<head>");
+    out.println("<meta http-equiv=\"content-type\" content=\"text/html;charset=ISO-8859-1\">");
+    out.println("<title>" + title + "</title>");
+    out.println("</head>");
+
+    out.println("<style type=\"text/css\">");
+    out.println("body {background-color: #728FCE}");
+    out.println("h2 {background-color: transparent}");
+    out.println("p {background-color: fuchsia}");
+    out.println("</style>");
+
+    out.println("<body>");
+    out.println("<center>");
+    out.println("<h2>" + title + "</h2>");
+    if (header != null) {
+      addCenteredHeader(out, header);
+    }
+    out.println("</center>");
+  }
+
   private static String classesInPackageFileName(SizeBreakdown breakdown,
       String packageName, String permutationId) {
     return breakdown.getId() + "_" + packageName + "-" + permutationId
@@ -167,155 +226,22 @@
   /**
    * Global information for this permutation.
    */
-  private GlobalInformation globalInformation = new GlobalInformation();
+  private final GlobalInformation globalInformation;
 
-  /**
-   * Settings for this permutation.
-   */
-  private Settings settings = new Settings();
+  private final OutputDirectory outDir;
 
-  /**
-   * Default constructor. Will be used for all permutations.
-   */
-  MakeTopLevelHtmlForPerm() {
-    this.globalInformation = new GlobalInformation();
-    this.settings = new Settings();
-  }
-
-  /**
-   * Constructor for a specific permutation.
-   * 
-   * @param globalInformation All the information about this permutation
-   */
-  MakeTopLevelHtmlForPerm(final GlobalInformation globalInformation) {
+  MakeTopLevelHtmlForPerm(GlobalInformation globalInformation,
+      OutputDirectory outDir) {
     this.globalInformation = globalInformation;
+    this.outDir = outDir;
   }
 
-  public void copyFileOrDirectory(File srcPath, File dstPath, String classPath,
-      String inputFileName, boolean isDirectory) throws IOException {
-    if (srcPath.isDirectory()) {
-      if (!dstPath.exists()) {
-        dstPath.mkdir();
-      }
-      String files[] = srcPath.list();
-      for (int i = 0; i < files.length; i++) {
-        copyFileOrDirectory(new File(srcPath, files[i]), new File(dstPath,
-            files[i]), classPath, inputFileName, isDirectory);
-      }
-    } else {
-      if (!srcPath.exists()) {
-        copyFileOrDirectoryFromJar(classPath, inputFileName, dstPath,
-            isDirectory);
-      } else {
-        InputStream in = new FileInputStream(srcPath);
-        OutputStream out = new FileOutputStream(dstPath);
-        // Transfer bytes from in to out
-        byte[] buf = new byte[1024];
-        int len;
-        while ((len = in.read(buf)) > 0) {
-          out.write(buf, 0, len);
-        }
-        in.close();
-        out.close();
-      }
-    }
-  }
-
-  public void copyFileOrDirectoryFromJar(String jarFileName,
-      String inputFileName, File dstPath, boolean isDirectory)
-      throws IOException {
-
-    JarFile jarFile = new JarFile(jarFileName);
-    if (isDirectory) {
-      dstPath.mkdir();
-
-      JarInputStream jarFileIS = new JarInputStream(new FileInputStream(
-          jarFileName));
-      JarEntry jarEntry = jarFileIS.getNextJarEntry();
-      while (jarEntry != null) {
-        if (!inputFileName.endsWith("/")) {
-          inputFileName += "/";
-        }
-        if ((jarEntry.getName().compareTo(inputFileName) != 0)
-            && (jarEntry.getName().startsWith(inputFileName))) {
-          File newDstPath = getOutFile(jarEntry.getName());
-          copyFileOrDirectoryFromJar(jarFileName, jarEntry.getName(),
-              newDstPath, false);
-        }
-        jarEntry = jarFileIS.getNextJarEntry();
-      }
-      jarFileIS.close();
-    } else {
-      InputStream in = jarFile.getInputStream(jarFile.getEntry(inputFileName));
-      OutputStream out = new FileOutputStream(dstPath);
-
-      int c;
-      while ((c = in.read()) != -1) {
-        out.write(c);
-      }
-      in.close();
-      out.close();
-      jarFile.close();
-    }
-  }
-
-  public GlobalInformation getGlobalInformation() {
-    return globalInformation;
-  }
-
-  public void makeBreakdownShell(SizeBreakdown breakdown, String permutationId)
-      throws IOException {
-    // this will contain the place holder iframes where the actual information
-    // is going to go.
-
+  public void makeBreakdownShell(SizeBreakdown breakdown) throws IOException {
     Map<String, CodeCollection> nameToCodeColl = breakdown.nameToCodeColl;
     Map<String, LiteralsCollection> nameToLitColl = breakdown.nameToLitColl;
 
-    // copy from the bin directory to the current directory
-    String classPath = settings.resources.get();
-    if (classPath == null) {
-      classPath = System.getProperty("java.class.path");
-    }
-    if (!classPath.endsWith("/")) {
-      classPath += "/";
-    }
-    String inputFileName = "roundedCorners.css";
-    File inputFile = new File(classPath + RESOURCES_PATH + inputFileName);
-    if (!inputFile.exists()) {
-      inputFile = new File(classPath + inputFileName);
-    }
-    File outputFile = getOutFile("roundedCorners.css");
-    copyFileOrDirectory(inputFile, outputFile, classPath, RESOURCES_PATH
-        + inputFileName, false);
-
-    inputFileName = "classLevel.css";
-    File inputFile2 = new File(classPath + RESOURCES_PATH + inputFileName);
-    if (!inputFile2.exists()) {
-      inputFile2 = new File(classPath + inputFileName);
-    }
-    File outputFile2 = getOutFile("classLevel.css");
-    copyFileOrDirectory(inputFile2, outputFile2, classPath, RESOURCES_PATH
-        + inputFileName, false);
-
-    inputFileName = "common.css";
-    File inputFile3 = new File(classPath + RESOURCES_PATH + inputFileName);
-    if (!inputFile3.exists()) {
-      inputFile3 = new File(classPath + inputFileName);
-    }
-    File outputFile3 = getOutFile("common.css");
-    copyFileOrDirectory(inputFile3, outputFile3, classPath, RESOURCES_PATH
-        + inputFileName, false);
-
-    inputFileName = "images";
-    File inputDir = new File(classPath + RESOURCES_PATH + "images");
-    if (!inputDir.exists()) {
-      inputDir = new File(classPath + "images");
-    }
-    File outputDir = getOutFile("images");
-    copyFileOrDirectory(inputDir, outputDir, classPath, inputFileName, true);
-
-    final PrintWriter outFile = new PrintWriter(getOutFile(shellFileName(
-        breakdown, permutationId)));
+    PrintWriter outFile = new PrintWriter(getOutFile(shellFileName(breakdown,
+        getPermutationId())));
 
     outFile.println("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">");
     outFile.println("<html>");
@@ -389,7 +315,7 @@
 
     outFile.println("<div class='abs header'>Package breakdown</div>");
     outFile.println("<div class='abs innerContent'>");
-    String packageBreakdownFileName = makePackageHtml(breakdown, permutationId);
+    String packageBreakdownFileName = makePackageHtml(breakdown);
     outFile.println(" <iframe class='frame' src=\"" + packageBreakdownFileName
         + "\" scrolling=auto></iframe>");
     outFile.println("</div>");
@@ -400,7 +326,7 @@
 
     outFile.println("<div class='abs innerContent'>");
     String codeTypeBreakdownFileName = makeCodeTypeHtml(breakdown,
-        nameToCodeColl, nameToLitColl, permutationId);
+        nameToCodeColl, nameToLitColl);
     outFile.println("<iframe class='frame' src=\"" + codeTypeBreakdownFileName
         + "\" scrolling=auto></iframe>");
     outFile.println("</div>");
@@ -411,15 +337,15 @@
     outFile.close();
   }
 
-  public void makeCodeTypeClassesHtmls(SizeBreakdown breakdown,
-      String permutationId) throws IOException {
+  public void makeCodeTypeClassesHtmls(SizeBreakdown breakdown)
+      throws IOException {
     HashMap<String, CodeCollection> nameToCodeColl = breakdown.nameToCodeColl;
 
     for (String codeType : nameToCodeColl.keySet()) {
 
       // construct file name
       String outFileName = breakdown.getId() + "_" + codeType + "-"
-          + permutationId + "Classes.html";
+          + getPermutationId() + "Classes.html";
 
       float maxSize = 0f;
       TreeMap<Float, String> sortedClasses = new TreeMap<Float, String>(
@@ -540,28 +466,26 @@
     }
   }
 
-  public void makeDependenciesHtml(
-      Map<String, Map<String, String>> allDependencies, String permutationId)
-      throws IOException {
-    for (String depGraphName : allDependencies.keySet()) {
-      makeDependenciesHtml(depGraphName, allDependencies.get(depGraphName),
-          permutationId);
+  public void makeDependenciesHtml() throws IOException {
+    for (String depGraphName : globalInformation.dependencies.keySet()) {
+      makeDependenciesHtml(depGraphName,
+          globalInformation.dependencies.get(depGraphName));
     }
   }
 
-  public void makeLeftoverStatusPages(String permutationId) throws IOException {
+  public void makeLeftoverStatusPages() throws IOException {
     for (String className : globalInformation.getClassToPackage().keySet()) {
-      makeLeftoversStatusPage(className, permutationId);
+      makeLeftoversStatusPage(className);
     }
   }
 
-  public void makeLiteralsClassesTableHtmls(SizeBreakdown breakdown,
-      String permutationId) throws IOException {
+  public void makeLiteralsClassesTableHtmls(SizeBreakdown breakdown)
+      throws IOException {
     Map<String, LiteralsCollection> nameToLitColl = breakdown.nameToLitColl;
 
     for (String literalType : nameToLitColl.keySet()) {
 
-      String outFileName = literalType + "-" + permutationId + "Lits.html";
+      String outFileName = literalType + "-" + getPermutationId() + "Lits.html";
       final PrintWriter outFile = new PrintWriter(getOutFile(breakdown.getId()
           + "_" + outFileName));
 
@@ -625,9 +549,8 @@
    * Make size breakdowns for each package for one code collection.
    */
   public void makePackageClassesHtmls(SizeBreakdown breakdown,
-      DependencyLinker depLinker, String permutationId) throws IOException {
+      DependencyLinker depLinker) throws IOException {
     for (String packageName : globalInformation.getPackageToClasses().keySet()) {
-
       TreeMap<Float, String> sortedClasses = new TreeMap<Float, String>(
           Collections.reverseOrder());
       float maxSize = 0f;
@@ -642,7 +565,6 @@
         }
 
         if (curSize != 0f) {
-
           sumSize += curSize;
           sortedClasses.put(curSize, className);
           if (curSize > maxSize) {
@@ -653,7 +575,7 @@
 
       PrintWriter outFile = new PrintWriter(
           getOutFile(classesInPackageFileName(breakdown, packageName,
-              permutationId)));
+              getPermutationId())));
 
       outFile.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
       outFile.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
@@ -726,9 +648,8 @@
       outFile.println("<th class='barlabel'></th>");
       outFile.println("<th style='width:100%' class='barlabel'></th>");
       outFile.println("</thead>");
-//
-      for (Float size : sortedClasses.keySet()) {
 
+      for (Float size : sortedClasses.keySet()) {
         String className = sortedClasses.get(size);
         float ratio = (size / maxSize) * 85;
         if (ratio < 5) {
@@ -736,12 +657,11 @@
         }
         float perc = (size / sumSize) * 100;
 
-        String dependencyLink = depLinker.dependencyLinkForClass(className,
-            permutationId);
+        String dependencyLink = depLinker.dependencyLinkForClass(className);
         outFile.println("<tr>");
         outFile.println("<td class=\"barlabel\">" + size + "</td>");
         outFile.println("<td class=\"barlabel\">" + perc + "%</td>");
-        if ((settings.displayDependencies) && (dependencyLink != null)) {
+        if (dependencyLink != null) {
           outFile.println("<td class=\"barlabel\"><a href=\"" + dependencyLink
               + "\" target=\"_top\">" + className + "</a></td>");
         } else {
@@ -761,54 +681,15 @@
     }
   }
 
-  public void makeSplitStatusPages(String permutationId) throws IOException {
+  public void makeSplitStatusPages() throws IOException {
     for (String className : globalInformation.getClassToPackage().keySet()) {
-      makeSplitStatusPage(className, permutationId);
+      makeSplitStatusPage(className);
     }
   }
 
-  public void makeTopLevelHtmlForAllPerms() throws FileNotFoundException {
-
-    PrintWriter outFile = new PrintWriter(getOutFile("index.html"));
-
-    outFile.println("<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML//EN\">");
-    outFile.println("<html>");
-    outFile.println("<head>");
-    outFile.println("  <title>Story of Your Compile - Top Level Dashboard for all Permutations</title>");
-    outFile.println("<style type=\"text/css\">");
-    outFile.println("body {background-color: #728FCE}");
-    outFile.println("h2 {background-color: transparent}");
-    outFile.println("</style>");
-    outFile.println("</head>");
-
-    outFile.println("<body>");
-    outFile.println("<center>");
-    outFile.println("<h1>Story of Your Compile</h1>");
-    outFile.println("<hr>");
-    outFile.println("<h3>Story of Your Compile - Overview of Permutations</h3>");
-    outFile.println("<hr>");
-
-    outFile.println("<div style='overflow:auto; background-color:white'>");
-    outFile.println("<center>");
-    for (String permutationId : settings.allPermsInfo.keySet()) {
-      String permutationInfo = settings.allPermsInfo.get(permutationId);
-      outFile.print("<p><a href=\"SoycDashboard" + "-" + permutationId
-          + "-index.html\">Permutation " + permutationId);
-      if (permutationInfo.length() > 0) {
-        outFile.println(" (" + permutationInfo + ")" + "</a>");
-      } else {
-        outFile.println("</a>");
-      }
-    }
-    outFile.println("</center>");
-    outFile.println("</div>");
-    addStandardHtmlEnding(outFile);
-    outFile.close();
-  }
-
-  public void makeTopLevelShell(String permutationId) throws IOException {
+  public void makeTopLevelShell() throws IOException {
     PrintWriter outFile = new PrintWriter(getOutFile("SoycDashboard" + "-"
-        + permutationId + "-index.html"));
+        + getPermutationId() + "-index.html"));
 
     outFile.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
     outFile.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
@@ -868,8 +749,10 @@
     outFile.println("<body>");
     outFile.println("<div class='abs mainHeader'>");
     outFile.println("<h2>Story of Your Compile Dashboard</h2>");
-    String permutationInfo = settings.allPermsInfo.get(permutationId);
-    outFile.print("<h3>Permutation " + permutationId);
+    // String permutationInfo = settings.allPermsInfo.get(getPermutationId());
+    String permutationInfo = ""; // TODO(spoon) pass in permutation information
+    // here
+    outFile.print("<h3>Permutation " + getPermutationId());
     if (permutationInfo.length() > 0) {
       outFile.println(" (" + permutationInfo + ")");
     }
@@ -928,7 +811,7 @@
         breakdown = globalInformation.splitPointCodeBreakdown(i);
       }
 
-      String drillDownFileName = shellFileName(breakdown, permutationId);
+      String drillDownFileName = shellFileName(breakdown, getPermutationId());
       String splitPointDescription = breakdown.getDescription();
 
       int size = breakdown.sizeAllCode;
@@ -962,14 +845,6 @@
     outFile.close();
   }
 
-  public void setGlobalInformation(GlobalInformation globalInformation) {
-    this.globalInformation = globalInformation;
-  }
-
-  public void setSettings(Settings settings) {
-    this.settings = settings;
-  }
-
   private void addDependenciesHtmlProlog(final PrintWriter out, String title,
       String header) {
     out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
@@ -1023,65 +898,39 @@
   }
 
   private void addLefttoversStatus(String className, String packageName,
-      PrintWriter out, String permutationId) {
-    if (settings.displayDependencies) {
+      PrintWriter out) {
+    if (globalInformation.dependencies != null) {
       out.println("<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\""
-          + dependenciesFileName("total", packageName, permutationId) + "#"
-          + className + "\">See why it's live</a></td></tr>");
+          + dependenciesFileName("total", packageName) + "#" + className
+          + "\">See why it's live</a></td></tr>");
       for (int sp = 1; sp <= globalInformation.getNumSplitPoints(); sp++) {
         out.println("<tr><td>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\""
-            + dependenciesFileName("sp" + sp, packageName, permutationId) + "#"
-            + className + "\">See why it's not exclusive to s.p. #" + sp + " ("
+            + dependenciesFileName("sp" + sp, packageName) + "#" + className
+            + "\">See why it's not exclusive to s.p. #" + sp + " ("
             + globalInformation.getSplitPointToLocation().get(sp)
             + ")</a></td></tr>");
       }
     }
   }
 
-  private void addStandardHtmlEnding(final PrintWriter out) {
-    out.println("</div>");
-    out.println("</body>");
-    out.println("</html>");
-  }
-
-  private void addStandardHtmlProlog(final PrintWriter out, String title,
-      String header) {
-    out.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\"");
-    out.println("\"http://www.w3.org/TR/html4/strict.dtd\">");
-    out.println("<html>");
-    out.println("<head>");
-    out.println("<meta http-equiv=\"content-type\" content=\"text/html;charset=ISO-8859-1\">");
-    out.println("<title>" + title + "</title>");
-    out.println("</head>");
-
-    out.println("<style type=\"text/css\">");
-    out.println("body {background-color: #728FCE}");
-    out.println("h2 {background-color: transparent}");
-    out.println("p {background-color: fuchsia}");
-    out.println("</style>");
-
-    out.println("<body>");
-    out.println("<center>");
-    out.println("<h2>" + title + "</h2>");
-    if (header != null) {
-      addCenteredHeader(out, header);
-    }
-    out.println("</center>");
-  }
-
-  private String dependenciesFileName(String depGraphName, String packageName,
-      String permutationId) {
+  private String dependenciesFileName(String depGraphName, String packageName) {
     return "methodDependencies-" + depGraphName + "-" + filename(packageName)
-        + "-" + permutationId + ".html";
+        + "-" + getPermutationId() + ".html";
   }
 
   /**
    * Return a {@link File} object for a file to be emitted into the output
    * directory.
    */
-  private File getOutFile(String localFileName) {
-    File outDir = new File(settings.out.get());
-    return new File(outDir, localFileName);
+  private OutputStream getOutFile(String localFileName) throws IOException {
+    return outDir.getOutputStream(localFileName);
+  }
+
+  /**
+   * @return
+   */
+  private String getPermutationId() {
+    return globalInformation.getPermutationId();
   }
 
   /**
@@ -1115,16 +964,15 @@
         splitPoint);
   }
 
-  private String leftoversStatusFileName(String className, String permutationId) {
-    return "leftoverStatus-" + filename(className) + "-" + permutationId
+  private String leftoversStatusFileName(String className) {
+    return "leftoverStatus-" + filename(className) + "-" + getPermutationId()
         + ".html";
   }
 
   private String makeCodeTypeHtml(SizeBreakdown breakdown,
       Map<String, CodeCollection> nameToCodeColl,
-      Map<String, LiteralsCollection> nameToLitColl, String permutationId)
-      throws IOException {
-    String outFileName = breakdown.getId() + "-" + permutationId
+      Map<String, LiteralsCollection> nameToLitColl) throws IOException {
+    String outFileName = breakdown.getId() + "-" + getPermutationId()
         + "_codeTypeBreakdown.html";
     float maxSize = 0f;
     float sumSize = 0f;
@@ -1167,7 +1015,7 @@
 
       String codeType = sortedCodeTypes.get(size);
       String drillDownFileName = breakdown.getId() + "_" + codeType + "-"
-          + permutationId + "Classes.html";
+          + getPermutationId() + "Classes.html";
 
       float ratio = (size / maxSize) * 79;
       float perc = (size / sumSize) * 100;
@@ -1210,7 +1058,7 @@
     for (Float size : sortedLitTypes.keySet()) {
       String literal = sortedLitTypes.get(size);
       String drillDownFileName = breakdown.getId() + "_" + literal + "-"
-          + permutationId + "Lits.html";
+          + getPermutationId() + "Lits.html";
 
       float ratio = (size / maxSize) * 79;
       float perc = (size / sumSize) * 100;
@@ -1241,8 +1089,7 @@
   }
 
   private void makeDependenciesHtml(String depGraphName,
-      Map<String, String> dependencies, String permutationId)
-      throws FileNotFoundException {
+      Map<String, String> dependencies) throws IOException {
     String depGraphDescription = inferDepGraphDescription(depGraphName);
     PrintWriter outFile = null;
     String curPackageName = "";
@@ -1268,8 +1115,7 @@
           outFile.close();
         }
 
-        String outFileName = dependenciesFileName(depGraphName, curPackageName,
-            permutationId);
+        String outFileName = dependenciesFileName(depGraphName, curPackageName);
         outFile = new PrintWriter(getOutFile(outFileName));
 
         String packageDescription = packageName.length() == 0
@@ -1305,11 +1151,10 @@
     outFile.close();
   }
 
-  private void makeLeftoversStatusPage(String className, String permutationId)
-      throws IOException {
+  private void makeLeftoversStatusPage(String className) throws IOException {
     String packageName = globalInformation.getClassToPackage().get(className);
-    PrintWriter out = new PrintWriter(getOutFile(leftoversStatusFileName(
-        className, permutationId)));
+    PrintWriter out = new PrintWriter(
+        getOutFile(leftoversStatusFileName(className)));
 
     addStandardHtmlProlog(out, "Leftovers page for " + className, null);
 
@@ -1317,7 +1162,7 @@
     out.println("<table border=\"1\" width=\"80%\" style=\"font-size: 11pt;\" bgcolor=\"white\">");
 
     out.println("<tr><td>This class has some leftover code, neither initial nor exclusive to any split point:</td></tr>");
-    addLefttoversStatus(className, packageName, out, permutationId);
+    addLefttoversStatus(className, packageName, out);
     out.println("</table>");
 
     addStandardHtmlEnding(out);
@@ -1325,9 +1170,8 @@
     out.close();
   }
 
-  private String makePackageHtml(SizeBreakdown breakdown, String permutationId)
-      throws FileNotFoundException {
-    String outFileName = breakdown.getId() + "-" + permutationId + "_"
+  private String makePackageHtml(SizeBreakdown breakdown) throws IOException {
+    String outFileName = breakdown.getId() + "-" + getPermutationId() + "_"
         + "packageBreakdown.html";
     Map<String, Integer> packageToPartialSize = breakdown.packageToSize;
     TreeMap<Integer, String> sortedPackages = new TreeMap<Integer, String>(
@@ -1364,7 +1208,7 @@
     for (int size : sortedPackages.keySet()) {
       String packageName = sortedPackages.get(size);
       String drillDownFileName = classesInPackageFileName(breakdown,
-          packageName, permutationId);
+          packageName, getPermutationId());
 
       float ratio = (size / maxSize) * 79;
       if (ratio < 5) {
@@ -1393,11 +1237,10 @@
     return outFileName;
   }
 
-  private void makeSplitStatusPage(String className, String permutationId)
-      throws IOException {
+  private void makeSplitStatusPage(String className) throws IOException {
     String packageName = globalInformation.getClassToPackage().get(className);
-    PrintWriter out = new PrintWriter(getOutFile(splitStatusFileName(className,
-        permutationId)));
+    PrintWriter out = new PrintWriter(
+        getOutFile(splitStatusFileName(className)));
 
     addStandardHtmlProlog(out, "Split point status for " + className, null);
 
@@ -1405,10 +1248,10 @@
     out.println("<table border=\"1\" width=\"80%\" style=\"font-size: 11pt;\" bgcolor=\"white\">");
 
     if (globalInformation.getInitialCodeBreakdown().classToSize.containsKey(className)) {
-      if (settings.displayDependencies) {
+      if (globalInformation.dependencies != null) {
         out.println("<tr><td>Some code is initial (<a href=\""
-            + dependenciesFileName("initial", packageName, permutationId) + "#"
-            + className + "\">see why</a>)</td></tr>");
+            + dependenciesFileName("initial", packageName) + "#" + className
+            + "\">see why</a>)</td></tr>");
       } else {
         out.println("<tr><td>Some code is initial</td></tr>");
       }
@@ -1419,7 +1262,7 @@
     }
     if (globalInformation.getLeftoversBreakdown().classToSize.containsKey(className)) {
       out.println("<tr><td>Some code is left over:</td></tr>");
-      addLefttoversStatus(className, packageName, out, permutationId);
+      addLefttoversStatus(className, packageName, out);
     }
     out.println("</table>");
 
@@ -1442,7 +1285,8 @@
     return sps;
   }
 
-  private String splitStatusFileName(String className, String permutationId) {
-    return "splitStatus-" + filename(className) + "-" + permutationId + ".html";
+  private String splitStatusFileName(String className) {
+    return "splitStatus-" + filename(className) + "-" + getPermutationId()
+        + ".html";
   }
 }
diff --git a/dev/core/src/com/google/gwt/soyc/Settings.java b/dev/core/src/com/google/gwt/soyc/Settings.java
index 7f5b9bb..01bd6e8 100644
--- a/dev/core/src/com/google/gwt/soyc/Settings.java
+++ b/dev/core/src/com/google/gwt/soyc/Settings.java
@@ -16,15 +16,10 @@
 package com.google.gwt.soyc;
 
 import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.TreeMap;
 
 /**
  * Command-line settings for SOYC.
@@ -123,10 +118,6 @@
       break; // No setting wanted the remaining arguments
     }
 
-    if (settings.resources.get() == null) {
-      throw new ArgumentListException("The -resources option is required");
-    }
-
     if ((settings.soycDir.get() == null)
         && (settings.symbolMapsDir.get() == null)) {
 
@@ -184,17 +175,16 @@
     return help.toString();
   }
 
-  public Map<String, String> allPermsInfo = new TreeMap<String, String>();
   public String depFileName;
-  public Boolean displayDependencies = false;
-  public Boolean displaySplitPoints = false;
 
   public final Setting<String> out = addSetting(new StringSetting("-out",
       "dir", ".", "output directory"));
 
   public final Setting<String> resources = addSetting(new StringSetting(
-      "-resources", "jarfile", null,
-      " directory or jar file with CSS, etc., resources"));
+      "-resources",
+      "dir",
+      null,
+      "present only for backwards compatibility; directory or jar file with CSS, etc., resources"));
 
   public final Setting<String> soycDir = addSetting(new StringSetting(
       "-soycDir", "dir", null, " directory for soyc files"));
@@ -207,47 +197,6 @@
       "-symbolMapsDir", "dir", null, " directory or symbol maps files"));
 
   private List<Setting<?>> allSettings;
-  public void readPermutationInfo() throws FileNotFoundException {
-
-    if (symbolMapsDir.get() == null) {
-      // get the permutation id from settings
-      String permutationId = storiesFileName;
-      permutationId = permutationId.replaceAll(".*/stories", "");
-      permutationId = permutationId.replaceAll("\\.xml(\\.gz)?", "");
-      allPermsInfo.put(permutationId, "");
-    } else {
-      File dir = new File(symbolMapsDir.get());
-      String files[] = dir.list();
-      for (Integer i = 0; i < files.length; i++) {
-        String permFileName = symbolMapsDir.get() + "/" + files[i];
-        FileReader fir = new FileReader(permFileName);
-
-        Scanner sc = new Scanner(fir);
-
-        String permutationId = "";
-        String permutationInfo = "";
-        int lineCount = 0;
-        while ((sc.hasNextLine()) && (lineCount < 2)) {
-
-          String curLine = sc.nextLine();
-          curLine = curLine.trim();
-
-          if (curLine.startsWith("# {")) {
-            curLine = curLine.replace("# {", "");
-            curLine = curLine.replace("}", "");
-            curLine = curLine.trim();
-            if (lineCount == 0) {
-              permutationId = curLine;
-            } else {
-              permutationInfo = curLine;
-            }
-            lineCount++;
-          }
-        }
-        allPermsInfo.put(permutationId, permutationInfo);
-      }
-    }
-  }
 
   private <T> Setting<T> addSetting(Setting<T> setting) {
     if (allSettings == null) {
diff --git a/dev/core/src/com/google/gwt/soyc/SoycDashboard.java b/dev/core/src/com/google/gwt/soyc/SoycDashboard.java
index 25aeffd..190256f 100644
--- a/dev/core/src/com/google/gwt/soyc/SoycDashboard.java
+++ b/dev/core/src/com/google/gwt/soyc/SoycDashboard.java
@@ -17,6 +17,9 @@
 package com.google.gwt.soyc;
 
 import com.google.gwt.soyc.MakeTopLevelHtmlForPerm.DependencyLinker;
+import com.google.gwt.soyc.MakeTopLevelHtmlForPerm.NullDependencyLinker;
+import com.google.gwt.soyc.io.FileSystemOutputDirectory;
+import com.google.gwt.soyc.io.OutputDirectory;
 
 import org.xml.sax.Attributes;
 import org.xml.sax.SAXException;
@@ -26,6 +29,7 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
@@ -33,6 +37,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Scanner;
 import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.Map.Entry;
@@ -43,10 +48,9 @@
 import javax.xml.parsers.SAXParserFactory;
 
 /**
- * The command-line entry point for creating a SOYC report.
+ * The command-line entry point for creating a compile report.
  */
 public class SoycDashboard {
-
   private static class FormatException extends RuntimeException {
     public FormatException() {
       super();
@@ -62,136 +66,47 @@
   }
 
   public static void main(final String[] args) {
+    Settings settings;
     try {
-      System.out.println("Generating the Story of Your Compile...");
-      Settings settings = Settings.fromArgumentList(args);
+      settings = Settings.fromArgumentList(args);
+    } catch (Settings.ArgumentListException e) {
+      System.err.println(e.getMessage());
+      System.err.println("Usage: "
+          + "java com.google.gwt.soyc.SoycDashboard -resources dir -soycDir dir -symbolMaps dir [-out dir]");
+      System.err.println("(Legacy usage: "
+          + "java com.google.gwt.soyc.SoycDashboard options stories0.xml[.gz] [dependencies0.xml[.gz]] [splitpoints0.xml[.gz]])");
+      System.err.println("Options:");
+      System.err.println(Settings.settingsHelp());
+      System.exit(1);
+      return; // not reached
+    }
 
-      MakeTopLevelHtmlForPerm makeTopLevelHtml = new MakeTopLevelHtmlForPerm();
-      makeTopLevelHtml.setSettings(settings);
+    System.out.println("Generating the Story of Your Compile...");
 
-      // read in all the symbol maps
-      settings.readPermutationInfo();
-      makeTopLevelHtml.makeTopLevelHtmlForAllPerms();
-      for (String permutationId : settings.allPermsInfo.keySet()) {
-        GlobalInformation globalInformation = new GlobalInformation();
-        MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm = new MakeTopLevelHtmlForPerm(
-            globalInformation);
-        makeTopLevelHtmlForPerm.setSettings(settings);
+    OutputDirectory outDir = new FileSystemOutputDirectory(new File(
+        settings.out.get()));
 
-        String storiesFileName = settings.storiesFileName;
-
-        String depFileName = settings.depFileName;
-        if (depFileName == null) {
-          depFileName = "";
+    try {
+      Map<String, String> permInfo = readPermutationInfo(settings);
+      SoycDashboard dashboard = new SoycDashboard(outDir);
+      for (String permutationId : permInfo.keySet()) {
+        dashboard.startNewPermutation(permutationId);
+        if (settings.symbolMapsDir.get() == null) {
+          dashboard.readFromFilesNamed(settings.storiesFileName,
+              settings.depFileName, settings.splitPointsFileName);
+        } else {
+          String soycDir = settings.soycDir.get();
+          dashboard.readFromFilesNamed(soycInputFile(soycDir, "stories",
+              permutationId), soycInputFile(soycDir, "dependencies",
+              permutationId), soycInputFile(soycDir, "splitPoints",
+              permutationId));
         }
-        String splitPointsFileName = settings.splitPointsFileName;
-        if (splitPointsFileName == null) {
-          splitPointsFileName = "";
-        }
-
-        if (settings.symbolMapsDir.get() != null) {
-          storiesFileName = settings.soycDir.get() + "/stories" + permutationId
-              + ".xml.gz";
-
-          if (!(new File(storiesFileName).exists())) {
-            storiesFileName = settings.soycDir.get() + "/stories"
-                + permutationId + ".xml";
-          }
-          depFileName = settings.soycDir.get() + "/dependencies"
-              + permutationId + ".xml.gz";
-          if (!(new File(depFileName).exists())) {
-            depFileName = settings.soycDir.get() + "/dependencies"
-                + permutationId + ".xml";
-          }
-          splitPointsFileName = settings.soycDir.get() + "/splitPoints"
-              + permutationId + ".xml.gz";
-          if (!(new File(splitPointsFileName).exists())) {
-            splitPointsFileName = settings.soycDir.get() + "/splitPoints"
-                + permutationId + ".xml";
-          }
-        }
-
-        settings.displayDependencies = (new File(depFileName)).exists();
-        settings.displaySplitPoints = (new File(splitPointsFileName)).exists();
-
-        new File(settings.out.get()).mkdir();
-        if (settings.displayDependencies) {
-          /**
-           * handle dependencies
-           */
-          Map<String, Map<String, String>> dependencies = new TreeMap<String, Map<String, String>>();
-          DefaultHandler depHandler = parseXMLDocumentDependencies(dependencies);
-          SAXParserFactory depFactoryMain = SAXParserFactory.newInstance();
-          depFactoryMain.setNamespaceAware(true);
-          SAXParser saxParser = depFactoryMain.newSAXParser();
-          InputStream in = new FileInputStream(depFileName);
-          if (depFileName.endsWith(".gz")) {
-            in = new GZIPInputStream(in);
-          }
-          in = new BufferedInputStream(in);
-          saxParser.parse(in, depHandler);
-
-          makeTopLevelHtmlForPerm.makeDependenciesHtml(dependencies,
-              permutationId);
-        }
-
-        if (settings.displaySplitPoints) {
-          /**
-           * handle runAsync split points
-           */
-
-          DefaultHandler splitPointHandler = parseXMLDocumentSplitPoints(globalInformation);
-          SAXParserFactory splitPointsFactoryMain = SAXParserFactory.newInstance();
-          splitPointsFactoryMain.setNamespaceAware(true);
-
-          SAXParser saxParser = splitPointsFactoryMain.newSAXParser();
-          InputStream in = new FileInputStream(splitPointsFileName);
-          if (depFileName.endsWith(".gz")) {
-            in = new GZIPInputStream(in);
-          }
-          in = new BufferedInputStream(in);
-          saxParser.parse(in, splitPointHandler);
-        }
-
-        /**
-         * handle everything else
-         */
-
-        DefaultHandler handler = parseXMLDocumentSizeMap(globalInformation);
-        SAXParserFactory factoryMain = SAXParserFactory.newInstance();
-        factoryMain.setNamespaceAware(true);
-        SAXParser saxParser = factoryMain.newSAXParser();
-        InputStream in = new FileInputStream(storiesFileName);
-        if (storiesFileName.endsWith(".gz")) {
-          in = new GZIPInputStream(in);
-        }
-        in = new BufferedInputStream(in);
-        saxParser.parse(in, handler);
-
-        // add to "All Other Code" if none of the special categories apply
-        for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
-          updateAllOtherCodeType(breakdown.nameToCodeColl, globalInformation);
-        }
-        globalInformation.computePackageSizes();
-
-        // clean up the RPC categories
-        for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
-          foldInRPCHeuristic(breakdown.nameToCodeColl);
-        }
-
-        // generate all the html files
-        makeTopLevelHtmlForPerm.makeSplitStatusPages(permutationId);
-        makeTopLevelHtmlForPerm.makeLeftoverStatusPages(permutationId);
-        for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
-          DependencyLinker linker = chooseDependencyLinker(
-              makeTopLevelHtmlForPerm, breakdown);
-          makeHTMLFiles(makeTopLevelHtmlForPerm, breakdown, linker,
-              permutationId);
-        }
+        dashboard.generateForOnePermutation();
         System.out.println("Finished creating reports for permutation.");
       }
-      System.out.println("Finished creating reports. To see the dashboard, open index.html in your browser.");
 
+      dashboard.generateCrossPermutationFiles(permInfo);
+      System.out.println("Finished creating reports. To see the dashboard, open index.html in your browser.");
     } catch (ParserConfigurationException e) {
       System.err.println("Could not parse document. " + e.getMessage());
       System.exit(1);
@@ -204,45 +119,21 @@
     } catch (IOException e) {
       System.err.println("Error creating html file. " + e.getMessage());
       System.exit(1);
-    } catch (Settings.ArgumentListException e) {
-      System.err.println(e.getMessage());
-      System.err.println("Usage: "
-          + "java com.google.gwt.soyc.SoycDashboard -resources dir -soycDir dir -symbolMaps dir [-out dir]");
-      System.err.println("(Legacy usage: "
-          + "java com.google.gwt.soyc.SoycDashboard options stories0.xml[.gz] [dependencies0.xml[.gz]] [splitpoints0.xml[.gz]])");
-      System.err.println("Options:");
-      System.err.println(Settings.settingsHelp());
-      System.exit(1);
     }
   }
 
-  private static Collection<SizeBreakdown> breakdownsForFragment(
-      Integer fragment, GlobalInformation globalInformation) {
-    List<SizeBreakdown> breakdowns = new ArrayList<SizeBreakdown>();
-    breakdowns.add(globalInformation.getTotalCodeBreakdown());
-    if (fragment == 0) {
-      breakdowns.add(globalInformation.getInitialCodeBreakdown());
+  /**
+   * Open a file for reading. If the filename ends in .gz, then wrap the stream
+   * with a {@link GZIPInputStream}.
+   */
+  public static InputStream openPossiblyGzippedFile(String filename)
+      throws IOException {
+    InputStream in = new FileInputStream(filename);
+    if (filename.endsWith(".gz")) {
+      in = new GZIPInputStream(in);
     }
-    if (fragment == (globalInformation.getNumSplitPoints() + 1)) {
-      breakdowns.add(globalInformation.getLeftoversBreakdown());
-    }
-    if (fragment >= 1 && fragment <= globalInformation.getNumSplitPoints()) {
-      breakdowns.add(globalInformation.splitPointCodeBreakdown(fragment));
-    }
-    return breakdowns;
-  }
-
-  private static DependencyLinker chooseDependencyLinker(
-      MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm, SizeBreakdown breakdown) {
-    if (breakdown == makeTopLevelHtmlForPerm.getGlobalInformation().getTotalCodeBreakdown()) {
-      return makeTopLevelHtmlForPerm.new DependencyLinkerForTotalBreakdown();
-    } else if (breakdown == makeTopLevelHtmlForPerm.getGlobalInformation().getInitialCodeBreakdown()) {
-      return makeTopLevelHtmlForPerm.new DependencyLinkerForInitialCode();
-    } else if (breakdown == makeTopLevelHtmlForPerm.getGlobalInformation().getLeftoversBreakdown()) {
-      return makeTopLevelHtmlForPerm.new DependencyLinkerForLeftoversFragment();
-    } else {
-      return makeTopLevelHtmlForPerm.new DependencyLinkerForExclusiveFragment();
-    }
+    in = new BufferedInputStream(in);
+    return in;
   }
 
   /*
@@ -279,27 +170,6 @@
     }
   }
 
-  /**
-   * Generates all the HTML files for one size breakdown.
-   * 
-   * @param makeTopLevelHtmlForPerm
-   * @param breakdown
-   * @param depLinker
-   * @param permutationId
-   * @throws IOException
-   */
-  private static void makeHTMLFiles(
-      MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm, SizeBreakdown breakdown,
-      DependencyLinker depLinker, String permutationId) throws IOException {
-    makeTopLevelHtmlForPerm.makePackageClassesHtmls(breakdown, depLinker,
-        permutationId);
-    makeTopLevelHtmlForPerm.makeCodeTypeClassesHtmls(breakdown, permutationId);
-    makeTopLevelHtmlForPerm.makeLiteralsClassesTableHtmls(breakdown,
-        permutationId);
-    makeTopLevelHtmlForPerm.makeBreakdownShell(breakdown, permutationId);
-    makeTopLevelHtmlForPerm.makeTopLevelShell(permutationId);
-  }
-
   private static DefaultHandler parseXMLDocumentDependencies(
       final Map<String, Map<String, String>> allDependencies) {
     DefaultHandler handler = new DefaultHandler() {
@@ -357,7 +227,193 @@
     return handler;
   }
 
-  private static DefaultHandler parseXMLDocumentSizeMap(
+  private static Map<String, String> readPermutationInfo(Settings settings)
+      throws FileNotFoundException {
+    Map<String, String> allPermsInfo = new TreeMap<String, String>();
+    if (settings.symbolMapsDir.get() == null) {
+      String permutationId = settings.storiesFileName;
+      permutationId = permutationId.replaceAll(".*/stories", "");
+      permutationId = permutationId.replaceAll("\\.xml(\\.gz)?", "");
+      allPermsInfo.put(permutationId, "");
+    } else {
+      File dir = new File(settings.symbolMapsDir.get());
+      String files[] = dir.list();
+      for (Integer i = 0; i < files.length; i++) {
+        String permFileName = settings.symbolMapsDir.get() + "/" + files[i];
+        FileReader fir = new FileReader(permFileName);
+
+        Scanner sc = new Scanner(fir);
+
+        String permutationId = "";
+        String permutationInfo = "";
+        int lineCount = 0;
+        while ((sc.hasNextLine()) && (lineCount < 2)) {
+
+          String curLine = sc.nextLine();
+          curLine = curLine.trim();
+
+          if (curLine.startsWith("# {")) {
+            curLine = curLine.replace("# {", "");
+            curLine = curLine.replace("}", "");
+            curLine = curLine.trim();
+            if (lineCount == 0) {
+              permutationId = curLine;
+            } else {
+              permutationInfo = curLine;
+            }
+            lineCount++;
+          }
+        }
+        allPermsInfo.put(permutationId, permutationInfo);
+      }
+    }
+    return allPermsInfo;
+  }
+
+  private static String soycInputFile(String soycDir, String baseName,
+      String permutationId) {
+    String name = soycDir + "/" + baseName + permutationId + ".xml.gz";
+    if (new File(name).exists()) {
+      return name;
+    }
+    return soycDir + "/" + baseName + permutationId + ".xml";
+  }
+
+  /**
+   * Global information for the current permutation being emitted.
+   */
+  private GlobalInformation globalInformation;
+
+  /**
+   * HTML emitter for the current permutation being emitted.
+   */
+  private MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm;
+
+  private final OutputDirectory outDir;
+
+  public SoycDashboard(OutputDirectory outDir) {
+    this.outDir = outDir;
+  }
+
+  public void generateCrossPermutationFiles(Map<String, String> permInfo)
+      throws IOException {
+    StaticResources.emit(outDir);
+    MakeTopLevelHtmlForPerm.makeTopLevelHtmlForAllPerms(permInfo, outDir);
+  }
+
+  public void generateForOnePermutation() throws IOException {
+    if (globalInformation.dependencies != null) {
+      makeTopLevelHtmlForPerm.makeDependenciesHtml();
+    }
+
+    if (globalInformation.getNumSplitPoints() > 0) {
+      makeTopLevelHtmlForPerm.makeSplitStatusPages();
+      makeTopLevelHtmlForPerm.makeLeftoverStatusPages();
+    }
+    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
+      DependencyLinker linker = chooseDependencyLinker(breakdown);
+      makeHTMLFiles(makeTopLevelHtmlForPerm, breakdown, linker);
+    }
+  }
+
+  public void readDependencies(InputStream stream)
+      throws ParserConfigurationException, SAXException, IOException {
+    globalInformation.dependencies = new TreeMap<String, Map<String, String>>();
+    DefaultHandler depHandler = parseXMLDocumentDependencies(globalInformation.dependencies);
+    SAXParserFactory depFactoryMain = SAXParserFactory.newInstance();
+    depFactoryMain.setNamespaceAware(true);
+    SAXParser saxParser = depFactoryMain.newSAXParser();
+    saxParser.parse(stream, depHandler);
+  }
+
+  public void readSizeMaps(InputStream stream)
+      throws ParserConfigurationException, SAXException, IOException {
+    DefaultHandler handler = parseXMLDocumentSizeMap(globalInformation);
+    SAXParserFactory factoryMain = SAXParserFactory.newInstance();
+    factoryMain.setNamespaceAware(true);
+    SAXParser saxParser = factoryMain.newSAXParser();
+    saxParser.parse(stream, handler);
+
+    // Now clean up the information that has been read in various ways
+    globalInformation.computePackageSizes();
+
+    // add to "All Other Code" if none of the special categories apply
+    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
+      updateAllOtherCodeType(breakdown.nameToCodeColl);
+    }
+
+    // clean up the RPC categories
+    for (SizeBreakdown breakdown : globalInformation.allSizeBreakdowns()) {
+      foldInRPCHeuristic(breakdown.nameToCodeColl);
+    }
+  }
+
+  public void readSplitPoints(InputStream stream)
+      throws ParserConfigurationException, SAXException, IOException {
+    DefaultHandler splitPointHandler = parseXMLDocumentSplitPoints();
+    SAXParserFactory splitPointsFactoryMain = SAXParserFactory.newInstance();
+    splitPointsFactoryMain.setNamespaceAware(true);
+
+    SAXParser saxParser = splitPointsFactoryMain.newSAXParser();
+    saxParser.parse(stream, splitPointHandler);
+  }
+
+  public void startNewPermutation(String permutationId) {
+    globalInformation = new GlobalInformation(permutationId);
+    makeTopLevelHtmlForPerm = new MakeTopLevelHtmlForPerm(globalInformation,
+        outDir);
+  }
+
+  private Collection<SizeBreakdown> breakdownsForFragment(Integer fragment) {
+    List<SizeBreakdown> breakdowns = new ArrayList<SizeBreakdown>();
+    breakdowns.add(globalInformation.getTotalCodeBreakdown());
+    if (fragment == 0) {
+      breakdowns.add(globalInformation.getInitialCodeBreakdown());
+    }
+    if (fragment == (globalInformation.getNumSplitPoints() + 1)) {
+      breakdowns.add(globalInformation.getLeftoversBreakdown());
+    }
+    if (fragment >= 1 && fragment <= globalInformation.getNumSplitPoints()) {
+      breakdowns.add(globalInformation.splitPointCodeBreakdown(fragment));
+    }
+    return breakdowns;
+  }
+
+  private DependencyLinker chooseDependencyLinker(SizeBreakdown breakdown) {
+    if (globalInformation.dependencies == null) {
+      // no dependencies are available
+      return new NullDependencyLinker();
+    }
+
+    if (breakdown == globalInformation.getTotalCodeBreakdown()) {
+      if (globalInformation.getNumSplitPoints() > 0) {
+        return makeTopLevelHtmlForPerm.new DependencyLinkerForTotalBreakdown();
+      } else {
+        return makeTopLevelHtmlForPerm.new DependencyLinkerForInitialCode();
+      }
+    } else if (breakdown == globalInformation.getInitialCodeBreakdown()) {
+      return makeTopLevelHtmlForPerm.new DependencyLinkerForInitialCode();
+    } else if (breakdown == globalInformation.getLeftoversBreakdown()) {
+      assert globalInformation.getNumSplitPoints() > 0;
+      return makeTopLevelHtmlForPerm.new DependencyLinkerForLeftoversFragment();
+    } else {
+      return new NullDependencyLinker();
+    }
+  }
+
+  /**
+   * Generates all the HTML files for one size breakdown.
+   */
+  private void makeHTMLFiles(MakeTopLevelHtmlForPerm makeTopLevelHtmlForPerm,
+      SizeBreakdown breakdown, DependencyLinker depLinker) throws IOException {
+    makeTopLevelHtmlForPerm.makePackageClassesHtmls(breakdown, depLinker);
+    makeTopLevelHtmlForPerm.makeCodeTypeClassesHtmls(breakdown);
+    makeTopLevelHtmlForPerm.makeLiteralsClassesTableHtmls(breakdown);
+    makeTopLevelHtmlForPerm.makeBreakdownShell(breakdown);
+    makeTopLevelHtmlForPerm.makeTopLevelShell();
+  }
+
+  private DefaultHandler parseXMLDocumentSizeMap(
       final GlobalInformation globalInformation) {
     return new DefaultHandler() {
       int fragment = -1;
@@ -393,8 +449,7 @@
           } catch (NumberFormatException e) {
             throw new FormatException(e);
           }
-          for (SizeBreakdown breakdown : breakdownsForFragment(fragment,
-              globalInformation)) {
+          for (SizeBreakdown breakdown : breakdownsForFragment(fragment)) {
             breakdown.sizeAllCode += size;
           }
         } else if (localName.compareTo("size") == 0) {
@@ -491,17 +546,14 @@
 
       private void recordSize(String refType, String ref, int size,
           GlobalInformation globalInformation) {
-        for (SizeBreakdown breakdown : breakdownsForFragment(fragment,
-            globalInformation)) {
+        for (SizeBreakdown breakdown : breakdownsForFragment(fragment)) {
           accountForSize(breakdown, refType, ref, size, globalInformation);
         }
       }
     };
   }
 
-  private static DefaultHandler parseXMLDocumentSplitPoints(
-      final GlobalInformation globalInformation) {
-
+  private DefaultHandler parseXMLDocumentSplitPoints() {
     DefaultHandler handler = new DefaultHandler() {
 
       private boolean inInitialLoadSequence = false;
@@ -558,12 +610,25 @@
     return handler;
   }
 
+  private void readFromFilesNamed(String storiesFileName,
+      String dependenciesFileName, String splitPointsFileName)
+      throws ParserConfigurationException, SAXException, IOException {
+    if (dependenciesFileName != null && new File(dependenciesFileName).exists()) {
+      readDependencies(openPossiblyGzippedFile(dependenciesFileName));
+    }
+
+    if (splitPointsFileName != null && new File(splitPointsFileName).exists()) {
+      readSplitPoints(openPossiblyGzippedFile(splitPointsFileName));
+    }
+
+    readSizeMaps(openPossiblyGzippedFile(storiesFileName));
+  }
+
   /*
    * assigns code to "all other code" if none of the special categories apply
    */
-  private static void updateAllOtherCodeType(
-      final HashMap<String, CodeCollection> nameToCodeColl,
-      GlobalInformation globalInformation) {
+  private void updateAllOtherCodeType(
+      final HashMap<String, CodeCollection> nameToCodeColl) {
     // all classes not in any of the other categories
     for (String className : globalInformation.getClassToPackage().keySet()) {
       if ((!nameToCodeColl.get("widget").classes.contains(className))
@@ -576,5 +641,4 @@
       }
     }
   }
-
 }
diff --git a/dev/core/src/com/google/gwt/soyc/StaticResources.java b/dev/core/src/com/google/gwt/soyc/StaticResources.java
new file mode 100644
index 0000000..5e63e6c
--- /dev/null
+++ b/dev/core/src/com/google/gwt/soyc/StaticResources.java
@@ -0,0 +1,61 @@
+/*
+ * 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.soyc;
+
+import com.google.gwt.soyc.io.OutputDirectory;
+import com.google.gwt.util.tools.Utility;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * This class handles static resources such as CSS and GIF files that support
+ * the generated HTML. The resources are expected to be available via this
+ * class's class loader.
+ */
+public class StaticResources {
+  /**
+   * A list of all static resources. Storing it this way allows the resources to
+   * be loaded via a Java class loader, which is often convenient. Class loaders
+   * cannot be iterated over.
+   */
+  private static String[] resourceNames = new String[] {
+      "classLevel.css", "common.css", "roundedCorners.css", "images/1bl.gif",
+      "images/1br.gif", "images/1tl.gif", "images/1tr.gif", "images/bb.gif",
+      "images/blc.gif", "images/brc.gif", "images/l.gif", "images/r.gif",
+      "images/roundedbox_lo.gif", "images/roundedbox_lu.gif",
+      "images/roundedbox_ro.gif", "images/roundedbox_ru.gif", "images/tb.gif",
+      "images/tlc.gif", "images/trc.gif",};
+
+  public static void emit(OutputDirectory outDir) throws IOException {
+    String prefix = StaticResources.class.getPackage().getName().replace('.',
+        '/')
+        + "/resources/";
+    ClassLoader loader = StaticResources.class.getClassLoader();
+    for (String resourceName : resourceNames) {
+      InputStream in = loader.getResourceAsStream(prefix + resourceName);
+      if (in == null) {
+        throw new Error("Could not find resource via my class loader: "
+            + resourceName);
+      }
+      OutputStream out = outDir.getOutputStream(resourceName);
+      Utility.streamOut(in, out, 10240);
+      in.close();
+      out.close();
+    }
+  }
+}
diff --git a/dev/core/src/com/google/gwt/soyc/io/ArtifactsOutputDirectory.java b/dev/core/src/com/google/gwt/soyc/io/ArtifactsOutputDirectory.java
new file mode 100644
index 0000000..fb774b7
--- /dev/null
+++ b/dev/core/src/com/google/gwt/soyc/io/ArtifactsOutputDirectory.java
@@ -0,0 +1,87 @@
+/*
+ * 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.soyc.io;
+
+import com.google.gwt.core.ext.linker.impl.StandardCompilationAnalysis.SoycArtifact;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An {@link OutputDirectory} that writes its output as a list of GWT compiler
+ * artifacts.
+ */
+public class ArtifactsOutputDirectory implements OutputDirectory {
+  /**
+   * An in-memory output stream. When it is closed, its contents are saved to an
+   * artifact.
+   */
+  private class OutputStreamForArtifact extends OutputStream {
+    private static final String OUTPUT_DIRECTORY_NAME = "compile-report";
+
+    private ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    private boolean closed = false;
+    private final String path;
+
+    public OutputStreamForArtifact(String path) {
+      this.path = path;
+    }
+
+    @Override
+    public void close() {
+      if (!closed) {
+        closed = true;
+        SoycArtifact newArtifact = new SoycArtifact(OUTPUT_DIRECTORY_NAME + "/"
+            + path, baos.toByteArray());
+        newArtifact.setPrivate(false);
+        artifacts.add(newArtifact);
+        baos = null;
+      }
+    }
+
+    @Override
+    public void write(byte b[]) throws IOException {
+      baos.write(b);
+    }
+
+    @Override
+    public void write(byte b[], int off, int len) throws IOException {
+      baos.write(b, off, len);
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+      baos.write(b);
+    }
+  }
+
+  private List<SoycArtifact> artifacts = new ArrayList<SoycArtifact>();
+
+  /**
+   * Return the list of artifacts that have been written so far.
+   */
+  public List<SoycArtifact> getArtifacts() {
+    return artifacts;
+  }
+
+  public OutputStream getOutputStream(String path) throws IOException {
+    return new OutputStreamForArtifact(path);
+  }
+
+}
diff --git a/dev/core/src/com/google/gwt/soyc/io/FileSystemOutputDirectory.java b/dev/core/src/com/google/gwt/soyc/io/FileSystemOutputDirectory.java
new file mode 100644
index 0000000..6914c00
--- /dev/null
+++ b/dev/core/src/com/google/gwt/soyc/io/FileSystemOutputDirectory.java
@@ -0,0 +1,38 @@
+/*
+ * 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.soyc.io;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An {@link OutputDirectory} that writes directly to the file system.
+ */
+public class FileSystemOutputDirectory implements OutputDirectory {
+  private final File outDir;
+
+  public FileSystemOutputDirectory(File outDir) {
+    this.outDir = outDir;
+  }
+
+  public OutputStream getOutputStream(String path) throws IOException {
+    File outFile = new File(outDir, path);
+    outFile.getParentFile().mkdirs();
+    return new FileOutputStream(outFile);
+  }
+}
diff --git a/dev/core/src/com/google/gwt/soyc/io/OutputDirectory.java b/dev/core/src/com/google/gwt/soyc/io/OutputDirectory.java
new file mode 100644
index 0000000..e80777d
--- /dev/null
+++ b/dev/core/src/com/google/gwt/soyc/io/OutputDirectory.java
@@ -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 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.soyc.io;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An abstraction over output directories. The SOYC dashboard writes to this
+ * interface so that it can, depending on how it is invoked, both write to the
+ * filesystem and generate GWT compiler artifacts.
+ */
+public interface OutputDirectory {
+  OutputStream getOutputStream(String path) throws IOException;
+}
diff --git a/tools/soyc-vis/classLevel.css b/dev/core/src/com/google/gwt/soyc/resources/classLevel.css
similarity index 100%
rename from tools/soyc-vis/classLevel.css
rename to dev/core/src/com/google/gwt/soyc/resources/classLevel.css
diff --git a/tools/soyc-vis/common.css b/dev/core/src/com/google/gwt/soyc/resources/common.css
similarity index 100%
rename from tools/soyc-vis/common.css
rename to dev/core/src/com/google/gwt/soyc/resources/common.css
diff --git a/tools/soyc-vis/images/1bl.gif b/dev/core/src/com/google/gwt/soyc/resources/images/1bl.gif
similarity index 100%
rename from tools/soyc-vis/images/1bl.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/1bl.gif
Binary files differ
diff --git a/tools/soyc-vis/images/1br.gif b/dev/core/src/com/google/gwt/soyc/resources/images/1br.gif
similarity index 100%
rename from tools/soyc-vis/images/1br.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/1br.gif
Binary files differ
diff --git a/tools/soyc-vis/images/1tl.gif b/dev/core/src/com/google/gwt/soyc/resources/images/1tl.gif
similarity index 100%
rename from tools/soyc-vis/images/1tl.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/1tl.gif
Binary files differ
diff --git a/tools/soyc-vis/images/1tr.gif b/dev/core/src/com/google/gwt/soyc/resources/images/1tr.gif
similarity index 100%
rename from tools/soyc-vis/images/1tr.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/1tr.gif
Binary files differ
diff --git a/tools/soyc-vis/images/bb.gif b/dev/core/src/com/google/gwt/soyc/resources/images/bb.gif
similarity index 100%
rename from tools/soyc-vis/images/bb.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/bb.gif
Binary files differ
diff --git a/tools/soyc-vis/images/blc.gif b/dev/core/src/com/google/gwt/soyc/resources/images/blc.gif
similarity index 100%
rename from tools/soyc-vis/images/blc.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/blc.gif
Binary files differ
diff --git a/tools/soyc-vis/images/brc.gif b/dev/core/src/com/google/gwt/soyc/resources/images/brc.gif
similarity index 100%
rename from tools/soyc-vis/images/brc.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/brc.gif
Binary files differ
diff --git a/tools/soyc-vis/images/l.gif b/dev/core/src/com/google/gwt/soyc/resources/images/l.gif
similarity index 100%
rename from tools/soyc-vis/images/l.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/l.gif
Binary files differ
diff --git a/tools/soyc-vis/images/r.gif b/dev/core/src/com/google/gwt/soyc/resources/images/r.gif
similarity index 100%
rename from tools/soyc-vis/images/r.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/r.gif
Binary files differ
diff --git a/tools/soyc-vis/images/roundedbox_lo.gif b/dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_lo.gif
similarity index 100%
rename from tools/soyc-vis/images/roundedbox_lo.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_lo.gif
Binary files differ
diff --git a/tools/soyc-vis/images/roundedbox_lu.gif b/dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_lu.gif
similarity index 100%
rename from tools/soyc-vis/images/roundedbox_lu.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_lu.gif
Binary files differ
diff --git a/tools/soyc-vis/images/roundedbox_ro.gif b/dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_ro.gif
similarity index 100%
rename from tools/soyc-vis/images/roundedbox_ro.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_ro.gif
Binary files differ
diff --git a/tools/soyc-vis/images/roundedbox_ru.gif b/dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_ru.gif
similarity index 100%
rename from tools/soyc-vis/images/roundedbox_ru.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/roundedbox_ru.gif
Binary files differ
diff --git a/tools/soyc-vis/images/tb.gif b/dev/core/src/com/google/gwt/soyc/resources/images/tb.gif
similarity index 100%
rename from tools/soyc-vis/images/tb.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/tb.gif
Binary files differ
diff --git a/tools/soyc-vis/images/tlc.gif b/dev/core/src/com/google/gwt/soyc/resources/images/tlc.gif
similarity index 100%
rename from tools/soyc-vis/images/tlc.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/tlc.gif
Binary files differ
diff --git a/tools/soyc-vis/images/trc.gif b/dev/core/src/com/google/gwt/soyc/resources/images/trc.gif
similarity index 100%
rename from tools/soyc-vis/images/trc.gif
rename to dev/core/src/com/google/gwt/soyc/resources/images/trc.gif
Binary files differ
diff --git a/tools/soyc-vis/roundedCorners.css b/dev/core/src/com/google/gwt/soyc/resources/roundedCorners.css
similarity index 100%
rename from tools/soyc-vis/roundedCorners.css
rename to dev/core/src/com/google/gwt/soyc/resources/roundedCorners.css
diff --git a/plugins/ie/oophm/oophm/oophm.vcproj b/plugins/ie/oophm/oophm/oophm.vcproj
index a0cae8c..288d48c 100644
--- a/plugins/ie/oophm/oophm/oophm.vcproj
+++ b/plugins/ie/oophm/oophm/oophm.vcproj
@@ -56,7 +56,7 @@
 			<Tool

 				Name="VCCLCompilerTool"

 				Optimization="0"

-				AdditionalIncludeDirectories="../../../common"

+				AdditionalIncludeDirectories="&quot;$(ProjectDir)&quot;;../../../platform/Win;../../../common"

 				PreprocessorDefinitions="WIN32;_DEBUG;_WINDOWS;_USRDLL;GWT_DEBUGLEVEL=Spam"

 				MinimalRebuild="true"

 				BasicRuntimeChecks="3"

@@ -151,7 +151,7 @@
 			<Tool

 				Name="VCCLCompilerTool"

 				Optimization="0"

-				AdditionalIncludeDirectories="../../../common"

+				AdditionalIncludeDirectories="&quot;$(ProjectDir)&quot;;../../../platform/Win;../../../common"

 				PreprocessorDefinitions="WIN32;_DEBUG;_WINDOWS;_USRDLL;GWT_DEBUGLEVEL=Spam"

 				MinimalRebuild="true"

 				BasicRuntimeChecks="3"

@@ -342,7 +342,7 @@
 				Name="VCCLCompilerTool"

 				Optimization="2"

 				FavorSizeOrSpeed="1"

-				AdditionalIncludeDirectories="../../../platform/Win;../../../common"

+				AdditionalIncludeDirectories="&quot;$(ProjectDir)&quot;;../../../platform/Win;../../../common"

 				PreprocessorDefinitions="_WINDOWS;GWT_DEBUGDISABLE"

 				RuntimeLibrary="0"

 				UsePrecompiledHeader="0"

diff --git a/plugins/ie/oophm/oophm/plugin.cpp b/plugins/ie/oophm/oophm/plugin.cpp
index a372cdf..d48dc7d 100644
--- a/plugins/ie/oophm/oophm/plugin.cpp
+++ b/plugins/ie/oophm/oophm/plugin.cpp
@@ -92,7 +92,12 @@
     return S_OK;
   }
 
-  std::string tabKey = ""; // TODO(jat): add support for tab identity
+  // Use the current thread ID as a proxy for tab ID.
+  DWORD threadId = GetCurrentThreadId();
+  char buf[20];
+  snprintf(buf, sizeof(buf), "%lu", threadId);
+  std::string tabKey = buf;
+
   std::string sessionKey = BSTRToUTF8(bsessionKey);
   std::string moduleName = BSTRToUTF8(bmoduleName);
   IOmNavigator* navigator;
diff --git a/plugins/ie/prebuilt/oophm.dll b/plugins/ie/prebuilt/oophm.dll
old mode 100644
new mode 100755
index ca398aa..f2aac8a
--- a/plugins/ie/prebuilt/oophm.dll
+++ b/plugins/ie/prebuilt/oophm.dll
Binary files differ
diff --git a/plugins/ie/prebuilt/oophm64.dll b/plugins/ie/prebuilt/oophm64.dll
old mode 100644
new mode 100755
Binary files differ
diff --git a/tools/soyc-vis/build.xml b/tools/soyc-vis/build.xml
index 51b8833..24274b4 100644
--- a/tools/soyc-vis/build.xml
+++ b/tools/soyc-vis/build.xml
@@ -13,13 +13,9 @@
   </target>
 
   <target name="compile">
-    <mkdir dir="${javac.out}/com/google/gwt/soyc/resources/images"/>
-    <copy todir="${javac.out}/com/google/gwt/soyc/resources/images">
-      <fileset dir="images"/>
+    <copy todir="${javac.out}/com/google/gwt/soyc/resources">
+        <fileset dir="${gwt.root}/dev/core/src/com/google/gwt/soyc/resources"/>
     </copy>
-    <copy file="classLevel.css" tofile="${javac.out}/com/google/gwt/soyc/resources/classLevel.css"/>
-    <copy file="roundedCorners.css" tofile="${javac.out}/com/google/gwt/soyc/resources/roundedCorners.css"/>
-    <copy file="common.css" tofile="${javac.out}/com/google/gwt/soyc/resources/common.css"/>
   </target>
 
   <target name="build" depends="compile">
diff --git a/user/javadoc/com/google/gwt/examples/AsyncJUnitExample.java b/user/javadoc/com/google/gwt/examples/AsyncJUnitExample.java
index fb73afb..275ff84 100644
--- a/user/javadoc/com/google/gwt/examples/AsyncJUnitExample.java
+++ b/user/javadoc/com/google/gwt/examples/AsyncJUnitExample.java
@@ -30,6 +30,11 @@
    * Tests the Timer class asynchronously.
    */
   public void testTimer() {
+
+    // Set a delay period significantly longer than the
+    // event is expected to take.
+    delayTestFinish(500);
+
     // Setup an asynchronous event handler.
     Timer timer = new Timer() {
       @Override
@@ -41,10 +46,6 @@
       }
     };
 
-    // Set a delay period significantly longer than the
-    // event is expected to take.
-    delayTestFinish(500);
-
     // Schedule the event and return control to the test system.
     timer.schedule(100);
   }
diff --git a/user/src/com/google/gwt/junit/BatchingStrategy.java b/user/src/com/google/gwt/junit/BatchingStrategy.java
index 1932854..14a5a11 100644
--- a/user/src/com/google/gwt/junit/BatchingStrategy.java
+++ b/user/src/com/google/gwt/junit/BatchingStrategy.java
@@ -19,6 +19,7 @@
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -37,6 +38,24 @@
    * @return an ordered list of test blocks to run
    */
   public abstract List<TestInfo[]> getTestBlocks(String syntheticModuleName);
+
+  /**
+   * Get the set of tests for this module, minus tests that should not be
+   * executed.
+   * 
+   * @return the set of tests to execute
+   */
+  protected final Set<TestInfo> getTestsForModule(String syntheticModuleName) {
+    Set<TestInfo> toExecute = GWTTestCase.getTestsForModule(syntheticModuleName).getTests();
+    Set<TestInfo> toRemove = new HashSet<TestInfo>();
+    for (TestInfo info : toExecute) {
+      if (JUnitShell.mustNotExecuteTest(info)) {
+        toRemove.add(info);
+      }
+    }
+    toExecute.removeAll(toRemove);
+    return toExecute;
+  }
 }
 
 /**
@@ -46,8 +65,7 @@
 class NoBatchingStrategy extends BatchingStrategy {
   @Override
   public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
-    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
-        syntheticModuleName).getTests();
+    Set<TestInfo> allTestsInModule = getTestsForModule(syntheticModuleName);
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
     for (TestInfo testInfo : allTestsInModule) {
       testBlocks.add(new TestInfo[] {testInfo});
@@ -62,8 +80,7 @@
 class ClassBatchingStrategy extends BatchingStrategy {
   @Override
   public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
-    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
-        syntheticModuleName).getTests();
+    Set<TestInfo> allTestsInModule = getTestsForModule(syntheticModuleName);
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
     String lastTestClass = null;
     List<TestInfo> lastTestBlock = null;
@@ -96,11 +113,12 @@
 class ModuleBatchingStrategy extends BatchingStrategy {
   @Override
   public List<TestInfo[]> getTestBlocks(String syntheticModuleName) {
-    Set<TestInfo> allTestsInModule = GWTTestCase.getTestsForModule(
-        syntheticModuleName).getTests();
-    TestInfo[] testBlock = allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]);
+    Set<TestInfo> allTestsInModule = getTestsForModule(syntheticModuleName);
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
-    testBlocks.add(testBlock);
+    if (allTestsInModule.size() > 0) {
+      TestInfo[] testBlock = allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]);
+      testBlocks.add(testBlock);
+    }
     return testBlocks;
   }
 }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 59bf1e6..6158789 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -396,19 +396,34 @@
    * @return the list of remote user agents
    */
   public static String[] getRemoteUserAgents() {
-    return getUnitTestShell().remoteUserAgents;
+    if (unitTestShell == null) {
+      return null;
+    }
+    return unitTestShell.remoteUserAgents;
   }
 
   /**
    * Checks if a testCase should not be executed. Currently, a test is either
    * executed on all clients (mentioned in this test) or on no clients.
    * 
-   * @param testCase current testCase.
+   * @param testInfo the test info to check
    * @return true iff the test should not be executed on any of the specified
    *         clients.
    */
-  public static boolean mustNotExecuteTest(TestCase testCase) {
-    return getUnitTestShell().mustNotExecuteTest(getBannedPlatforms(testCase));
+  public static boolean mustNotExecuteTest(TestInfo testInfo) {
+    if (unitTestShell == null) {
+      throw new IllegalStateException(
+          "mustNotExecuteTest cannot be called before runTest()");
+    }
+    try {
+      Class<?> testClass = TestCase.class.getClassLoader().loadClass(
+          testInfo.getTestClass());
+      return unitTestShell.mustNotExecuteTest(getBannedPlatforms(testClass,
+          testInfo.getTestMethod()));
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException("Could not load test class: "
+          + testInfo.getTestClass());
+    }
   }
 
   /**
@@ -488,16 +503,19 @@
   }
 
   /**
-   * returns the set of banned {@code Platform} for a test method.
+   * Returns the set of banned {@code Platform} for a test method.
+   * 
+   * @param testClass the testClass
+   * @param methodName the name of the test method
    */
-  private static Set<Platform> getBannedPlatforms(TestCase testCase) {
-    Class<?> testClass = testCase.getClass();
+  private static Set<Platform> getBannedPlatforms(Class<?> testClass,
+      String methodName) {
     Set<Platform> bannedSet = EnumSet.noneOf(Platform.class);
     if (testClass.isAnnotationPresent(DoNotRunWith.class)) {
       bannedSet.addAll(Arrays.asList(testClass.getAnnotation(DoNotRunWith.class).value()));
     }
     try {
-      Method testMethod = testClass.getMethod(testCase.getName());
+      Method testMethod = testClass.getMethod(methodName);
       if (testMethod.isAnnotationPresent(DoNotRunWith.class)) {
         bannedSet.addAll(Arrays.asList(testMethod.getAnnotation(
             DoNotRunWith.class).value()));
@@ -536,6 +554,10 @@
       // TODO: install a shutdown hook? Not necessary with GWTShell.
       unitTestShell.lastLaunchFailed = false;
     }
+    if (unitTestShell.thread != Thread.currentThread()) {
+      throw new IllegalThreadStateException(
+          "JUnitShell can only be accessed from the thread that created it.");
+    }
 
     return unitTestShell;
   }
@@ -641,10 +663,16 @@
   private long testMethodTimeout;
 
   /**
+   * The thread that created the JUnitShell.
+   */
+  private Thread thread;
+
+  /**
    * Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces
    * server mode and disables processing extra arguments as URLs to be shown.
    */
   private JUnitShell() {
+    thread = Thread.currentThread();
     setRunTomcat(true);
     setHeadless(true);
   }
@@ -900,7 +928,8 @@
   private void runTestImpl(GWTTestCase testCase, TestResult testResult)
       throws UnableToCompleteException {
 
-    if (mustNotExecuteTest(testCase)) {
+    if (mustNotExecuteTest(getBannedPlatforms(testCase.getClass(),
+        testCase.getName()))) {
       return;
     }
 
diff --git a/user/src/com/google/gwt/junit/client/GWTTestCase.java b/user/src/com/google/gwt/junit/client/GWTTestCase.java
index 4ad6866..176c8a0 100644
--- a/user/src/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/src/com/google/gwt/junit/client/GWTTestCase.java
@@ -273,11 +273,6 @@
   public void setName(String name) {
     super.setName(name);
 
-    // If we can't run this test, don't add it to the map of all tests to batch.
-    if (JUnitShell.mustNotExecuteTest(this)) {
-      return;
-    }
-
     synchronized (ALL_GWT_TESTS_LOCK) {
       // Once the name is set, we can add ourselves to the global set.
       String syntheticModuleName = getSyntheticModuleName();
diff --git a/user/src/com/google/gwt/resources/Resources.gwt.xml b/user/src/com/google/gwt/resources/Resources.gwt.xml
index 1db713a..3c66d64 100644
--- a/user/src/com/google/gwt/resources/Resources.gwt.xml
+++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -16,6 +16,8 @@
 <module>
   <!-- Pull in the necessary base support, including user.agent detection -->
   <inherits name="com.google.gwt.core.Core" />
+  <!-- Pull in StyleInjector for CssResource -->
+  <inherits name="com.google.gwt.dom.DOM" />
   <!-- Used by ExternalTextResource -->
   <inherits name="com.google.gwt.http.HTTP" />
 
diff --git a/user/src/com/google/gwt/resources/client/CssResource.java b/user/src/com/google/gwt/resources/client/CssResource.java
index 908c777..5020448 100644
--- a/user/src/com/google/gwt/resources/client/CssResource.java
+++ b/user/src/com/google/gwt/resources/client/CssResource.java
@@ -285,6 +285,16 @@
   }
 
   /**
+   * Calls
+   * {@link com.google.gwt.dom.client.StyleInjector#injectStylesheet(String)} to
+   * inject the contents of the CssResource into the DOM. Repeated calls to this
+   * method on an instance of a CssResources will have no effect.
+   * 
+   * @return <code>true</code> if this method mutated the DOM.
+   */
+  boolean ensureInjected();
+
+  /**
    * Provides the contents of the CssResource.
    */
   String getText();
diff --git a/user/src/com/google/gwt/resources/css/ClassRenamer.java b/user/src/com/google/gwt/resources/css/ClassRenamer.java
new file mode 100644
index 0000000..a78ccbd
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ClassRenamer.java
@@ -0,0 +1,271 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.CssResource.ClassName;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssSelector;
+import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+/**
+ * Renames class selectors to their obfuscated names.
+ */
+public class ClassRenamer extends CssVisitor {
+
+  /**
+   * A tag to indicate that an externally-defined CSS class has no JMethod that
+   * is used to access it.
+   */
+  private static final Replacement UNREFERENCED_EXTERNAL = new Replacement(
+      null, null);
+
+  /*
+   * TODO: Replace with Pair<A, B>.
+   */
+  private static class Replacement {
+
+    private JMethod method;
+    private String obfuscatedClassName;
+
+    public Replacement(JMethod method, String obfuscatedClassName) {
+      this.method = method;
+      this.obfuscatedClassName = obfuscatedClassName;
+    }
+
+    public JMethod getMethod() {
+      return method;
+    }
+
+    public String getObfuscatedClassName() {
+      return obfuscatedClassName;
+    }
+
+    /**
+     * For debugging use only.
+     */
+    public String toString() {
+      if (this == UNREFERENCED_EXTERNAL) {
+        return "Unreferenced external class name";
+      } else {
+        return method.getName() + "=" + obfuscatedClassName;
+      }
+    }
+  }
+
+  /**
+   * Records replacements that have actually been performed.
+   */
+  private final Map<JMethod, String> actualReplacements = new IdentityHashMap<JMethod, String>();
+  private final Set<String> cssDefs = new HashSet<String>();
+
+  /**
+   * The task-list of replacements to perform in the stylesheet.
+   */
+  private final Map<String, Replacement> potentialReplacements;
+  private final TreeLogger logger;
+  private final Set<JMethod> missingClasses;
+  private final boolean strict;
+  private final Set<String> unknownClasses = new HashSet<String>();
+
+  public ClassRenamer(TreeLogger logger,
+      Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+      boolean strict, Set<String> externalClasses) {
+    this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
+    this.strict = strict;
+
+    potentialReplacements = computeReplacements(classReplacementsWithPrefix,
+        externalClasses);
+
+    // Require a definition for all classes in the default namespace
+    assert classReplacementsWithPrefix.containsKey("");
+    missingClasses = new HashSet<JMethod>(
+        classReplacementsWithPrefix.get("").keySet());
+  }
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    cssDefs.add(x.getKey());
+  }
+
+  @Override
+  public void endVisit(CssSelector x, Context ctx) {
+
+    String sel = x.getSelector();
+    int originalLength = sel.length();
+
+    Matcher ma = CssSelector.CLASS_SELECTOR_PATTERN.matcher(sel);
+    StringBuilder sb = new StringBuilder(originalLength);
+    int start = 0;
+
+    while (ma.find()) {
+      String sourceClassName = ma.group(1);
+
+      Replacement entry = potentialReplacements.get(sourceClassName);
+
+      if (entry == null) {
+        unknownClasses.add(sourceClassName);
+        continue;
+
+      } else if (entry == UNREFERENCED_EXTERNAL) {
+        // An @external without an accessor method. This is OK.
+        continue;
+      }
+
+      JMethod method = entry.getMethod();
+      String obfuscatedClassName = entry.getObfuscatedClassName();
+
+      // Consume the interstitial portion of the original selector
+      sb.append(sel.subSequence(start, ma.start(1)));
+      sb.append(obfuscatedClassName);
+      start = ma.end(1);
+
+      actualReplacements.put(method, obfuscatedClassName);
+      missingClasses.remove(method);
+    }
+
+    if (start != 0) {
+      // Consume the remainder and update the selector
+      sb.append(sel.subSequence(start, originalLength));
+      x.setSelector(sb.toString());
+    }
+  }
+
+  @Override
+  public void endVisit(CssStylesheet x, Context ctx) {
+    boolean stop = false;
+
+    // Skip names corresponding to @def entries. They too can be declared as
+    // String accessors.
+    List<JMethod> toRemove = new ArrayList<JMethod>();
+    for (JMethod method : missingClasses) {
+      if (cssDefs.contains(method.getName())) {
+        toRemove.add(method);
+      }
+    }
+    for (JMethod method : toRemove) {
+      missingClasses.remove(method);
+    }
+
+    if (!missingClasses.isEmpty()) {
+      stop = true;
+      TreeLogger errorLogger = logger.branch(TreeLogger.INFO,
+          "The following obfuscated style classes were missing from "
+              + "the source CSS file:");
+      for (JMethod m : missingClasses) {
+        String name = m.getName();
+        ClassName className = m.getAnnotation(ClassName.class);
+        if (className != null) {
+          name = className.value();
+        }
+        errorLogger.log(TreeLogger.ERROR, name + ": Fix by adding ." + name
+            + "{}");
+      }
+    }
+
+    if (strict && !unknownClasses.isEmpty()) {
+      stop = true;
+      TreeLogger errorLogger = logger.branch(TreeLogger.ERROR,
+          "The following unobfuscated classes were present in a strict CssResource:");
+      for (String s : unknownClasses) {
+        errorLogger.log(TreeLogger.ERROR, s);
+      }
+      errorLogger.log(TreeLogger.INFO, "Fix by adding String accessor "
+          + "method(s) to the CssResource interface for obfuscated classes, "
+          + "or using an @external declaration for unobfuscated classes.");
+    }
+
+    if (stop) {
+      throw new CssCompilerException("Missing a CSS replacement");
+    }
+  }
+
+  /**
+   * Reports the replacements that were actually performed by this visitor.
+   */
+  public Map<JMethod, String> getReplacements() {
+    return actualReplacements;
+  }
+
+  /**
+   * Flatten class name lookups to speed selector rewriting.
+   * 
+   * @param classReplacementsWithPrefix a map of local prefixes to the
+   *          obfuscated names of imported methods. If a CssResource makes use
+   *          of the {@link CssResource.Import} annotation, the keys of this map
+   *          will correspond to the {@link CssResource.ImportedWithPrefix}
+   *          value defined on the imported CssResource. The zero-length string
+   *          key holds the obfuscated names for the CssResource that is being
+   *          generated.
+   * @return A flattened version of the classReplacementWithPrefix map, where
+   *         the keys are the source class name (with prefix included), and
+   *         values have the obfuscated class name and associated JMethod.
+   */
+  private Map<String, Replacement> computeReplacements(
+      Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+      Set<String> externalClasses) {
+
+    Map<String, Replacement> toReturn = new HashMap<String, Replacement>();
+
+    for (String externalClass : externalClasses) {
+      toReturn.put(externalClass, UNREFERENCED_EXTERNAL);
+    }
+
+    for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
+      String prefix = outerEntry.getKey();
+
+      for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
+        JMethod method = entry.getKey();
+        String sourceClassName = method.getName();
+        String obfuscatedClassName = entry.getValue();
+
+        ClassName className = method.getAnnotation(ClassName.class);
+        if (className != null) {
+          sourceClassName = className.value();
+        }
+
+        sourceClassName = prefix + sourceClassName;
+
+        if (externalClasses.contains(sourceClassName)) {
+          /*
+           * It simplifies the sanity-checking logic to treat external classes
+           * as though they were simply obfuscated to exactly the value the user
+           * wants.
+           */
+          obfuscatedClassName = sourceClassName;
+        }
+
+        toReturn.put(sourceClassName, new Replacement(method,
+            obfuscatedClassName));
+      }
+    }
+    return Collections.unmodifiableMap(toReturn);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/DefsCollector.java b/user/src/com/google/gwt/resources/css/DefsCollector.java
new file mode 100644
index 0000000..4cb11c8
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/DefsCollector.java
@@ -0,0 +1,40 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Collects the names of all user-defined {@literal @def} constants in the
+ * stylesheet.
+ */
+public class DefsCollector extends CssVisitor {
+  private final Set<String> defs = new HashSet<String>();
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    defs.add(x.getKey());
+  }
+
+  public Set<String> getDefs() {
+    return defs;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java b/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java
new file mode 100644
index 0000000..d6cb7bf
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ExternalClassesCollector.java
@@ -0,0 +1,39 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssExternalSelectors;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Collects all {@code @external} declarations in the stylesheet.
+ */
+public class ExternalClassesCollector extends CssVisitor {
+  private final Set<String> classes = new HashSet<String>();
+
+  @Override
+  public void endVisit(CssExternalSelectors x, Context ctx) {
+    classes.addAll(x.getClasses());
+  }
+
+  public Set<String> getClasses() {
+    return classes;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/IfEvaluator.java b/user/src/com/google/gwt/resources/css/IfEvaluator.java
new file mode 100644
index 0000000..5e02ad3
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/IfEvaluator.java
@@ -0,0 +1,85 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.ConfigurationProperty;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.SelectionProperty;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+
+import java.util.Arrays;
+
+/**
+ * Statically evaluates {@literal @if} rules.
+ */
+public class IfEvaluator extends CssModVisitor {
+  private final TreeLogger logger;
+  private final PropertyOracle oracle;
+
+  public IfEvaluator(TreeLogger logger, PropertyOracle oracle) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Replacing property-based @if blocks");
+    this.oracle = oracle;
+  }
+
+  @Override
+  public void endVisit(CssIf x, Context ctx) {
+    if (x.getExpression() != null) {
+      // This gets taken care of by the runtime substitution visitor
+    } else {
+      try {
+        String propertyName = x.getPropertyName();
+        String propValue = null;
+        try {
+          SelectionProperty selProp = oracle.getSelectionProperty(logger,
+              propertyName);
+          propValue = selProp.getCurrentValue();
+        } catch (BadPropertyValueException e) {
+          ConfigurationProperty confProp = oracle.getConfigurationProperty(propertyName);
+          propValue = confProp.getValues().get(0);
+        }
+
+        /*
+         * If the deferred binding property's value is in the list of values in
+         * the @if rule, move the rules into the @if's context.
+         */
+        if (Arrays.asList(x.getPropertyValues()).contains(propValue)
+            ^ x.isNegated()) {
+          for (CssNode n : x.getNodes()) {
+            ctx.insertBefore(n);
+          }
+        } else {
+          // Otherwise, move the else block into the if statement's position
+          for (CssNode n : x.getElseNodes()) {
+            ctx.insertBefore(n);
+          }
+        }
+
+        // Always delete @if rules that we can statically evaluate
+        ctx.removeMe();
+      } catch (BadPropertyValueException e) {
+        logger.log(TreeLogger.ERROR, "Unable to evaluate @if block", e);
+        throw new CssCompilerException("Unable to parse CSS", e);
+      }
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java b/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java
new file mode 100644
index 0000000..08a03ed
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/MergeIdenticalSelectorsVisitor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSelector;
+import com.google.gwt.resources.rg.CssResourceGenerator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Merges rules that have matching selectors.
+ */
+public class MergeIdenticalSelectorsVisitor extends CssModVisitor {
+  private final Map<String, CssRule> canonicalRules = new HashMap<String, CssRule>();
+  private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
+
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    visitInNewContext(x.getElseNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    // Assumed to run immediately after SplitRulesVisitor
+    assert x.getSelectors().size() == 1;
+    CssSelector sel = x.getSelectors().get(0);
+
+    if (canonicalRules.containsKey(sel.getSelector())) {
+      CssRule canonical = canonicalRules.get(sel.getSelector());
+
+      // Check everything between the canonical rule and this rule for common
+      // properties. If there are common properties, it would be unsafe to
+      // promote the rule.
+      boolean hasCommon = false;
+      int index = rulesInOrder.indexOf(canonical) + 1;
+      assert index != 0;
+
+      for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
+          && !hasCommon;) {
+        hasCommon = CssResourceGenerator.haveCommonProperties(i.next(), x);
+      }
+
+      if (!hasCommon) {
+        // It's safe to promote the rule
+        canonical.getProperties().addAll(x.getProperties());
+        ctx.removeMe();
+        return false;
+      }
+    }
+
+    canonicalRules.put(sel.getSelector(), x);
+    rulesInOrder.add(x);
+    return false;
+  }
+
+  private void visitInNewContext(List<CssNode> nodes) {
+    MergeIdenticalSelectorsVisitor v = new MergeIdenticalSelectorsVisitor();
+    v.acceptWithInsertRemove(nodes);
+    rulesInOrder.addAll(v.rulesInOrder);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java b/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java
new file mode 100644
index 0000000..ea3e276
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/MergeRulesByContentVisitor.java
@@ -0,0 +1,93 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.rg.CssResourceGenerator;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Merges rules that have identical content.
+ */
+public class MergeRulesByContentVisitor extends CssModVisitor {
+  private Map<String, CssRule> rulesByContents = new HashMap<String, CssRule>();
+  private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
+
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    visitInNewContext(x.getElseNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    visitInNewContext(x.getNodes());
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    StringBuilder b = new StringBuilder();
+    for (CssProperty p : x.getProperties()) {
+      b.append(p.getName()).append(":").append(p.getValues().getExpression());
+    }
+
+    String content = b.toString();
+    CssRule canonical = rulesByContents.get(content);
+
+    // Check everything between the canonical rule and this rule for common
+    // properties. If there are common properties, it would be unsafe to
+    // promote the rule.
+    if (canonical != null) {
+      boolean hasCommon = false;
+      int index = rulesInOrder.indexOf(canonical) + 1;
+      assert index != 0;
+
+      for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
+          && !hasCommon;) {
+        hasCommon = CssResourceGenerator.haveCommonProperties(i.next(), x);
+      }
+
+      if (!hasCommon) {
+        canonical.getSelectors().addAll(x.getSelectors());
+        ctx.removeMe();
+        return false;
+      }
+    }
+
+    rulesByContents.put(content, x);
+    rulesInOrder.add(x);
+    return false;
+  }
+
+  private void visitInNewContext(List<CssNode> nodes) {
+    MergeRulesByContentVisitor v = new MergeRulesByContentVisitor();
+    v.acceptWithInsertRemove(nodes);
+    rulesInOrder.addAll(v.rulesInOrder);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/RequirementsCollector.java b/user/src/com/google/gwt/resources/css/RequirementsCollector.java
new file mode 100644
index 0000000..125e348
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/RequirementsCollector.java
@@ -0,0 +1,53 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssVisitor;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+
+/**
+ * Analyzes a stylesheet to update the ClientBundleRequirements interface.
+ */
+public class RequirementsCollector extends CssVisitor {
+  private final TreeLogger logger;
+  private final ClientBundleRequirements requirements;
+
+  public RequirementsCollector(TreeLogger logger,
+      ClientBundleRequirements requirements) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Scanning CSS for requirements");
+    this.requirements = requirements;
+  }
+
+  @Override
+  public void endVisit(CssIf x, Context ctx) {
+    String propertyName = x.getPropertyName();
+    if (propertyName != null) {
+      try {
+        requirements.addPermutationAxis(propertyName);
+      } catch (BadPropertyValueException e) {
+        logger.log(TreeLogger.ERROR, "Unknown deferred-binding property "
+            + propertyName, e);
+        throw new CssCompilerException("Unknown deferred-binding property", e);
+      }
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/RtlVisitor.java b/user/src/com/google/gwt/resources/css/RtlVisitor.java
new file mode 100644
index 0000000..3994231
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/RtlVisitor.java
@@ -0,0 +1,290 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNoFlip;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.css.ast.CssProperty.NumberValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Applies RTL transforms to a stylesheet.
+ */
+public class RtlVisitor extends CssModVisitor {
+  /**
+   * Records if we're currently visiting a CssRule whose only selector is
+   * "body".
+   */
+  private boolean inBodyRule;
+
+  @Override
+  public void endVisit(CssProperty x, Context ctx) {
+    String name = x.getName();
+
+    if (name.equalsIgnoreCase("left")) {
+      x.setName("right");
+    } else if (name.equalsIgnoreCase("right")) {
+      x.setName("left");
+    } else if (name.endsWith("-left")) {
+      int len = name.length();
+      x.setName(name.substring(0, len - 4) + "right");
+    } else if (name.endsWith("-right")) {
+      int len = name.length();
+      x.setName(name.substring(0, len - 5) + "left");
+    } else if (name.contains("-right-")) {
+      x.setName(name.replace("-right-", "-left-"));
+    } else if (name.contains("-left-")) {
+      x.setName(name.replace("-left-", "-right-"));
+    } else {
+      List<Value> values = new ArrayList<Value>(x.getValues().getValues());
+      invokePropertyHandler(x.getName(), values);
+      x.setValue(new CssProperty.ListValue(values));
+    }
+  }
+
+  @Override
+  public boolean visit(CssNoFlip x, Context ctx) {
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    inBodyRule = x.getSelectors().size() == 1
+        && x.getSelectors().get(0).getSelector().equals("body");
+    return true;
+  }
+
+  void propertyHandlerBackground(List<Value> values) {
+    /*
+     * The first numeric value will be treated as the left position only if we
+     * havn't seen any value that could potentially be the left value.
+     */
+    boolean seenLeft = false;
+
+    for (ListIterator<Value> it = values.listIterator(); it.hasNext();) {
+      Value v = it.next();
+      Value maybeFlipped = flipLeftRightIdentValue(v);
+      NumberValue nv = v.isNumberValue();
+      if (v != maybeFlipped) {
+        it.set(maybeFlipped);
+        seenLeft = true;
+
+      } else if (isIdent(v, "center")) {
+        seenLeft = true;
+
+      } else if (!seenLeft && (nv != null)) {
+        seenLeft = true;
+        if ("%".equals(nv.getUnits())) {
+          float position = 100f - nv.getValue();
+          it.set(new NumberValue(position, "%"));
+          break;
+        }
+      }
+    }
+  }
+
+  void propertyHandlerBackgroundPosition(List<Value> values) {
+    propertyHandlerBackground(values);
+  }
+
+  Value propertyHandlerBackgroundPositionX(Value v) {
+    ArrayList<Value> list = new ArrayList<Value>(1);
+    list.add(v);
+    propertyHandlerBackground(list);
+    return list.get(0);
+  }
+
+  /**
+   * Note there should be no propertyHandlerBorder(). The CSS spec states that
+   * the border property must set all values at once.
+   */
+  void propertyHandlerBorderColor(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerBorderStyle(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerBorderWidth(List<Value> values) {
+    swapFour(values);
+  }
+
+  Value propertyHandlerClear(Value v) {
+    return propertyHandlerFloat(v);
+  }
+
+  Value propertyHandlerCursor(Value v) {
+    IdentValue identValue = v.isIdentValue();
+    if (identValue == null) {
+      return v;
+    }
+
+    String ident = identValue.getIdent().toLowerCase();
+    if (!ident.endsWith("-resize")) {
+      return v;
+    }
+
+    StringBuffer newIdent = new StringBuffer();
+
+    if (ident.length() == 9) {
+      if (ident.charAt(0) == 'n') {
+        newIdent.append('n');
+        ident = ident.substring(1);
+      } else if (ident.charAt(0) == 's') {
+        newIdent.append('s');
+        ident = ident.substring(1);
+      } else {
+        return v;
+      }
+    }
+
+    if (ident.length() == 8) {
+      if (ident.charAt(0) == 'e') {
+        newIdent.append("w-resize");
+      } else if (ident.charAt(0) == 'w') {
+        newIdent.append("e-resize");
+      } else {
+        return v;
+      }
+      return new IdentValue(newIdent.toString());
+    } else {
+      return v;
+    }
+  }
+
+  Value propertyHandlerDirection(Value v) {
+    if (inBodyRule) {
+      if (isIdent(v, "ltr")) {
+        return new IdentValue("rtl");
+      } else if (isIdent(v, "rtl")) {
+        return new IdentValue("ltr");
+      }
+    }
+    return v;
+  }
+
+  Value propertyHandlerFloat(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  void propertyHandlerMargin(List<Value> values) {
+    swapFour(values);
+  }
+
+  void propertyHandlerPadding(List<Value> values) {
+    swapFour(values);
+  }
+
+  Value propertyHandlerPageBreakAfter(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  Value propertyHandlerPageBreakBefore(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  Value propertyHandlerTextAlign(Value v) {
+    return flipLeftRightIdentValue(v);
+  }
+
+  private Value flipLeftRightIdentValue(Value v) {
+    if (isIdent(v, "right")) {
+      return new IdentValue("left");
+
+    } else if (isIdent(v, "left")) {
+      return new IdentValue("right");
+    }
+    return v;
+  }
+
+  /**
+   * Reflectively invokes a propertyHandler method for the named property.
+   * Dashed names are transformed into camel-case names; only letters following
+   * a dash will be capitalized when looking for a method to prevent
+   * <code>fooBar<code> and <code>foo-bar</code> from colliding.
+   */
+  private void invokePropertyHandler(String name, List<Value> values) {
+    // See if we have a property-handler function
+    try {
+      String[] parts = name.toLowerCase().split("-");
+      StringBuffer methodName = new StringBuffer("propertyHandler");
+      for (String part : parts) {
+        methodName.append(Character.toUpperCase(part.charAt(0)));
+        methodName.append(part, 1, part.length());
+      }
+
+      try {
+        // Single-arg for simplicity
+        Method m = getClass().getDeclaredMethod(methodName.toString(),
+            Value.class);
+        assert Value.class.isAssignableFrom(m.getReturnType());
+        Value newValue = (Value) m.invoke(this, values.get(0));
+        values.set(0, newValue);
+      } catch (NoSuchMethodException e) {
+        // OK
+      }
+
+      try {
+        // Or the whole List for completeness
+        Method m = getClass().getDeclaredMethod(methodName.toString(),
+            List.class);
+        m.invoke(this, values);
+      } catch (NoSuchMethodException e) {
+        // OK
+      }
+
+    } catch (SecurityException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (IllegalArgumentException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (IllegalAccessException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    } catch (InvocationTargetException e) {
+      throw new CssCompilerException(
+          "Unable to invoke property handler function for " + name, e);
+    }
+  }
+
+  private boolean isIdent(Value value, String query) {
+    IdentValue v = value.isIdentValue();
+    return v != null && v.getIdent().equalsIgnoreCase(query);
+  }
+
+  /**
+   * Swaps the second and fourth values in a list of four values.
+   */
+  private void swapFour(List<Value> values) {
+    if (values.size() == 4) {
+      Collections.swap(values, 1, 3);
+    }
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java b/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java
new file mode 100644
index 0000000..646bed7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SplitRulesVisitor.java
@@ -0,0 +1,45 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssNodeCloner;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSelector;
+
+/**
+ * Splits rules with compound selectors into multiple rules.
+ */
+public class SplitRulesVisitor extends CssModVisitor {
+  @Override
+  public void endVisit(CssRule x, Context ctx) {
+    if (x.getSelectors().size() == 1) {
+      return;
+    }
+
+    for (CssSelector sel : x.getSelectors()) {
+      CssRule newRule = new CssRule();
+      newRule.getSelectors().add(sel);
+      newRule.getProperties().addAll(
+          CssNodeCloner.clone(CssProperty.class, x.getProperties()));
+      ctx.insertBefore(newRule);
+    }
+    ctx.removeMe();
+    return;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/Spriter.java b/user/src/com/google/gwt/resources/css/Spriter.java
new file mode 100644
index 0000000..6e48d37
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/Spriter.java
@@ -0,0 +1,150 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.resources.client.ImageResource.ImageOptions;
+import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssModVisitor;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssRule;
+import com.google.gwt.resources.css.ast.CssSprite;
+import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import java.util.List;
+
+/**
+ * Replaces CssSprite nodes with CssRule nodes that will display the sprited
+ * image. The real trick with spriting the images is to reuse the ImageResource
+ * processing framework by requiring the sprite to be defined in terms of an
+ * ImageResource.
+ */
+public class Spriter extends CssModVisitor {
+  private final ResourceContext context;
+  private final TreeLogger logger;
+
+  public Spriter(TreeLogger logger, ResourceContext context) {
+    this.logger = logger.branch(TreeLogger.DEBUG,
+        "Creating image sprite classes");
+    this.context = context;
+  }
+
+  @Override
+  public void endVisit(CssSprite x, Context ctx) {
+    JClassType bundleType = context.getClientBundleType();
+    String functionName = x.getResourceFunction();
+
+    if (functionName == null) {
+      logger.log(TreeLogger.ERROR, "The @sprite rule " + x.getSelectors()
+          + " must specify the " + CssSprite.IMAGE_PROPERTY_NAME + " property");
+      throw new CssCompilerException("No image property specified");
+    }
+
+    // Find the image accessor method
+    JMethod imageMethod = null;
+    JMethod[] allMethods = bundleType.getOverridableMethods();
+    for (int i = 0; imageMethod == null && i < allMethods.length; i++) {
+      JMethod candidate = allMethods[i];
+      // If the function name matches and takes no parameters
+      if (candidate.getName().equals(functionName)
+          && candidate.getParameters().length == 0) {
+        // We have a match
+        imageMethod = candidate;
+      }
+    }
+
+    // Method unable to be located
+    if (imageMethod == null) {
+      logger.log(TreeLogger.ERROR, "Unable to find ImageResource method "
+          + functionName + " in " + bundleType.getQualifiedSourceName());
+      throw new CssCompilerException("Cannot find image function");
+    }
+
+    JClassType imageResourceType = context.getGeneratorContext().getTypeOracle().findType(
+        ImageResource.class.getName());
+    assert imageResourceType != null;
+
+    if (!imageResourceType.isAssignableFrom(imageMethod.getReturnType().isClassOrInterface())) {
+      logger.log(TreeLogger.ERROR, "The return type of " + functionName
+          + " is not assignable to " + imageResourceType.getSimpleSourceName());
+      throw new CssCompilerException("Incorrect return type for "
+          + CssSprite.IMAGE_PROPERTY_NAME + " method");
+    }
+
+    ImageOptions options = imageMethod.getAnnotation(ImageOptions.class);
+    RepeatStyle repeatStyle;
+    if (options != null) {
+      repeatStyle = options.repeatStyle();
+    } else {
+      repeatStyle = RepeatStyle.None;
+    }
+
+    String instance = "(" + context.getImplementationSimpleSourceName()
+        + ".this." + functionName + "())";
+
+    CssRule replacement = new CssRule();
+    replacement.getSelectors().addAll(x.getSelectors());
+    List<CssProperty> properties = replacement.getProperties();
+
+    if (repeatStyle == RepeatStyle.None
+        || repeatStyle == RepeatStyle.Horizontal) {
+      properties.add(new CssProperty("height", new ExpressionValue(instance
+          + ".getHeight() + \"px\""), false));
+    }
+
+    if (repeatStyle == RepeatStyle.None || repeatStyle == RepeatStyle.Vertical) {
+      properties.add(new CssProperty("width", new ExpressionValue(instance
+          + ".getWidth() + \"px\""), false));
+    }
+    properties.add(new CssProperty("overflow", new IdentValue("hidden"), false));
+
+    String repeatText;
+    switch (repeatStyle) {
+      case None:
+        repeatText = " no-repeat";
+        break;
+      case Horizontal:
+        repeatText = " repeat-x";
+        break;
+      case Vertical:
+        repeatText = " repeat-y";
+        break;
+      case Both:
+        repeatText = " repeat";
+        break;
+      default:
+        throw new RuntimeException("Unknown repeatStyle " + repeatStyle);
+    }
+
+    String backgroundExpression = "\"url(\\\"\" + " + instance
+        + ".getURL() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + "
+        + instance + ".getTop() + \"px " + repeatText + "\"";
+    properties.add(new CssProperty("background", new ExpressionValue(
+        backgroundExpression), false));
+
+    // Retain any user-specified properties
+    properties.addAll(x.getProperties());
+
+    ctx.replaceMe(replacement);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/SubstitutionCollector.java b/user/src/com/google/gwt/resources/css/SubstitutionCollector.java
new file mode 100644
index 0000000..d6cf2a7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SubstitutionCollector.java
@@ -0,0 +1,51 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssEval;
+import com.google.gwt.resources.css.ast.CssUrl;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collects all user-defined constant nodes in the stylesheet.
+ */
+public class SubstitutionCollector extends CssVisitor {
+  private final Map<String, CssDef> substitutions = new HashMap<String, CssDef>();
+
+  @Override
+  public void endVisit(CssDef x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  @Override
+  public void endVisit(CssEval x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  @Override
+  public void endVisit(CssUrl x, Context ctx) {
+    substitutions.put(x.getKey(), x);
+  }
+
+  public Map<String, CssDef> getSubstitutions() {
+    return substitutions;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java b/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java
new file mode 100644
index 0000000..9cbbcc4
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/SubstitutionReplacer.java
@@ -0,0 +1,119 @@
+/*
+ * 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.resources.css;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.css.ast.Context;
+import com.google.gwt.resources.css.ast.CssCompilerException;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssProperty;
+import com.google.gwt.resources.css.ast.CssUrl;
+import com.google.gwt.resources.css.ast.CssVisitor;
+import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.css.ast.CssProperty.ListValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * Substitute symbolic replacements into string values.
+ */
+public class SubstitutionReplacer extends CssVisitor {
+  private final ResourceContext context;
+  private final TreeLogger logger;
+  private final Map<String, CssDef> substitutions;
+
+  public SubstitutionReplacer(TreeLogger logger, ResourceContext context,
+      Map<String, CssDef> substitutions) {
+    this.context = context;
+    this.logger = logger;
+    this.substitutions = substitutions;
+  }
+
+  @Override
+  public void endVisit(CssProperty x, Context ctx) {
+    if (x.getValues() == null) {
+      // Nothing to do
+      return;
+    }
+
+    List<Value> values = new ArrayList<Value>(x.getValues().getValues());
+
+    for (ListIterator<Value> i = values.listIterator(); i.hasNext();) {
+      IdentValue v = i.next().isIdentValue();
+
+      if (v == null) {
+        // Don't try to substitute into anything other than idents
+        continue;
+      }
+
+      String value = v.getIdent();
+      CssDef def = substitutions.get(value);
+
+      if (def == null) {
+        continue;
+      } else if (def instanceof CssUrl) {
+        assert def.getValues().size() == 1;
+        assert def.getValues().get(0).isIdentValue() != null;
+        String functionName = def.getValues().get(0).isIdentValue().getIdent();
+
+        // Find the method
+        JMethod methods[] = context.getClientBundleType().getOverridableMethods();
+        boolean foundMethod = false;
+        if (methods != null) {
+          for (JMethod method : methods) {
+            if (method.getName().equals(functionName)) {
+              foundMethod = true;
+              break;
+            }
+          }
+        }
+
+        if (!foundMethod) {
+          logger.log(TreeLogger.ERROR, "Unable to find DataResource method "
+              + functionName + " in "
+              + context.getClientBundleType().getQualifiedSourceName());
+          throw new CssCompilerException("Cannot find data function");
+        }
+
+        String instance = "((" + DataResource.class.getName() + ")("
+            + context.getImplementationSimpleSourceName() + ".this."
+            + functionName + "()))";
+
+        StringBuilder expression = new StringBuilder();
+        expression.append("\"url('\" + ");
+        expression.append(instance).append(".getUrl()");
+        expression.append(" + \"')\"");
+        i.set(new ExpressionValue(expression.toString()));
+
+      } else {
+        i.remove();
+        for (Value defValue : def.getValues()) {
+          i.add(defValue);
+        }
+      }
+    }
+
+    x.setValue(new ListValue(values));
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
index adbf29c..85846d4 100644
--- a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -19,7 +19,6 @@
 import com.google.gwt.core.ext.ConfigurationProperty;
 import com.google.gwt.core.ext.Generator;
 import com.google.gwt.core.ext.PropertyOracle;
-import com.google.gwt.core.ext.SelectionProperty;
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JClassType;
@@ -31,43 +30,39 @@
 import com.google.gwt.dev.util.DefaultTextOutput;
 import com.google.gwt.dev.util.Util;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.StyleInjector;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.resources.client.CssResource;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.resources.client.CssResource.ClassName;
 import com.google.gwt.resources.client.CssResource.Import;
 import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
 import com.google.gwt.resources.client.CssResource.NotStrict;
 import com.google.gwt.resources.client.CssResource.Shared;
 import com.google.gwt.resources.client.CssResource.Strict;
-import com.google.gwt.resources.client.ImageResource.ImageOptions;
-import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.resources.css.ClassRenamer;
 import com.google.gwt.resources.css.CssGenerationVisitor;
+import com.google.gwt.resources.css.DefsCollector;
+import com.google.gwt.resources.css.ExternalClassesCollector;
 import com.google.gwt.resources.css.GenerateCssAst;
+import com.google.gwt.resources.css.IfEvaluator;
+import com.google.gwt.resources.css.MergeIdenticalSelectorsVisitor;
+import com.google.gwt.resources.css.MergeRulesByContentVisitor;
+import com.google.gwt.resources.css.RequirementsCollector;
+import com.google.gwt.resources.css.RtlVisitor;
+import com.google.gwt.resources.css.SplitRulesVisitor;
+import com.google.gwt.resources.css.Spriter;
+import com.google.gwt.resources.css.SubstitutionCollector;
+import com.google.gwt.resources.css.SubstitutionReplacer;
 import com.google.gwt.resources.css.ast.CollapsedNode;
-import com.google.gwt.resources.css.ast.Context;
 import com.google.gwt.resources.css.ast.CssCompilerException;
 import com.google.gwt.resources.css.ast.CssDef;
-import com.google.gwt.resources.css.ast.CssEval;
-import com.google.gwt.resources.css.ast.CssExternalSelectors;
 import com.google.gwt.resources.css.ast.CssIf;
-import com.google.gwt.resources.css.ast.CssMediaRule;
-import com.google.gwt.resources.css.ast.CssModVisitor;
-import com.google.gwt.resources.css.ast.CssNoFlip;
 import com.google.gwt.resources.css.ast.CssNode;
-import com.google.gwt.resources.css.ast.CssNodeCloner;
 import com.google.gwt.resources.css.ast.CssProperty;
 import com.google.gwt.resources.css.ast.CssRule;
-import com.google.gwt.resources.css.ast.CssSelector;
-import com.google.gwt.resources.css.ast.CssSprite;
 import com.google.gwt.resources.css.ast.CssStylesheet;
-import com.google.gwt.resources.css.ast.CssUrl;
-import com.google.gwt.resources.css.ast.CssVisitor;
 import com.google.gwt.resources.css.ast.HasNodes;
 import com.google.gwt.resources.css.ast.CssProperty.DotPathValue;
-import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
-import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
 import com.google.gwt.resources.css.ast.CssProperty.ListValue;
 import com.google.gwt.resources.css.ast.CssProperty.NumberValue;
 import com.google.gwt.resources.css.ast.CssProperty.Value;
@@ -79,341 +74,23 @@
 import com.google.gwt.user.rebind.StringSourceWriter;
 
 import java.io.Serializable;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
 import java.net.URL;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.List;
-import java.util.ListIterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import java.util.regex.Matcher;
 import java.util.zip.Adler32;
 
 /**
  * Provides implementations of CSSResources.
  */
 public final class CssResourceGenerator extends AbstractResourceGenerator {
-  static class ClassRenamer extends CssVisitor {
-
-    /**
-     * A tag to indicate that an externally-defined CSS class has no JMethod
-     * that is used to access it.
-     */
-    private static final Replacement UNREFERENCED_EXTERNAL = new Replacement(
-        null, null);
-
-    /*
-     * TODO: Replace with Pair<A, B>.
-     */
-    private static class Replacement {
-
-      private JMethod method;
-      private String obfuscatedClassName;
-
-      public Replacement(JMethod method, String obfuscatedClassName) {
-        this.method = method;
-        this.obfuscatedClassName = obfuscatedClassName;
-      }
-
-      public JMethod getMethod() {
-        return method;
-      }
-
-      public String getObfuscatedClassName() {
-        return obfuscatedClassName;
-      }
-
-      /**
-       * For debugging use only.
-       */
-      public String toString() {
-        if (this == UNREFERENCED_EXTERNAL) {
-          return "Unreferenced external class name";
-        } else {
-          return method.getName() + "=" + obfuscatedClassName;
-        }
-      }
-    }
-
-    /**
-     * Records replacements that have actually been performed.
-     */
-    private final Map<JMethod, String> actualReplacements = new IdentityHashMap<JMethod, String>();
-    private final Set<String> cssDefs = new HashSet<String>();
-
-    /**
-     * The task-list of replacements to perform in the stylesheet.
-     */
-    private final Map<String, Replacement> potentialReplacements;
-    private final TreeLogger logger;
-    private final Set<JMethod> missingClasses;
-    private final boolean strict;
-    private final Set<String> unknownClasses = new HashSet<String>();
-
-    public ClassRenamer(TreeLogger logger,
-        Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
-        boolean strict, Set<String> externalClasses) {
-      this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
-      this.strict = strict;
-
-      potentialReplacements = computeReplacements(classReplacementsWithPrefix,
-          externalClasses);
-
-      // Require a definition for all classes in the default namespace
-      assert classReplacementsWithPrefix.containsKey("");
-      missingClasses = new HashSet<JMethod>(
-          classReplacementsWithPrefix.get("").keySet());
-    }
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      cssDefs.add(x.getKey());
-    }
-
-    @Override
-    public void endVisit(CssSelector x, Context ctx) {
-
-      String sel = x.getSelector();
-      int originalLength = sel.length();
-
-      Matcher ma = CssSelector.CLASS_SELECTOR_PATTERN.matcher(sel);
-      StringBuilder sb = new StringBuilder(originalLength);
-      int start = 0;
-
-      while (ma.find()) {
-        String sourceClassName = ma.group(1);
-
-        Replacement entry = potentialReplacements.get(sourceClassName);
-
-        if (entry == null) {
-          unknownClasses.add(sourceClassName);
-          continue;
-
-        } else if (entry == UNREFERENCED_EXTERNAL) {
-          // An @external without an accessor method. This is OK.
-          continue;
-        }
-
-        JMethod method = entry.getMethod();
-        String obfuscatedClassName = entry.getObfuscatedClassName();
-
-        // Consume the interstitial portion of the original selector
-        sb.append(sel.subSequence(start, ma.start(1)));
-        sb.append(obfuscatedClassName);
-        start = ma.end(1);
-
-        actualReplacements.put(method, obfuscatedClassName);
-        missingClasses.remove(method);
-      }
-
-      if (start != 0) {
-        // Consume the remainder and update the selector
-        sb.append(sel.subSequence(start, originalLength));
-        x.setSelector(sb.toString());
-      }
-    }
-
-    @Override
-    public void endVisit(CssStylesheet x, Context ctx) {
-      boolean stop = false;
-
-      // Skip names corresponding to @def entries. They too can be declared as
-      // String accessors.
-      List<JMethod> toRemove = new ArrayList<JMethod>();
-      for (JMethod method : missingClasses) {
-        if (cssDefs.contains(method.getName())) {
-          toRemove.add(method);
-        }
-      }
-      for (JMethod method : toRemove) {
-        missingClasses.remove(method);
-      }
-
-      if (!missingClasses.isEmpty()) {
-        stop = true;
-        TreeLogger errorLogger = logger.branch(TreeLogger.INFO,
-            "The following obfuscated style classes were missing from "
-                + "the source CSS file:");
-        for (JMethod m : missingClasses) {
-          String name = m.getName();
-          ClassName className = m.getAnnotation(ClassName.class);
-          if (className != null) {
-            name = className.value();
-          }
-          errorLogger.log(TreeLogger.ERROR, name + ": Fix by adding ." + name
-              + "{}");
-        }
-      }
-
-      if (strict && !unknownClasses.isEmpty()) {
-        stop = true;
-        TreeLogger errorLogger = logger.branch(TreeLogger.ERROR,
-            "The following unobfuscated classes were present in a strict CssResource:");
-        for (String s : unknownClasses) {
-          errorLogger.log(TreeLogger.ERROR, s);
-        }
-        errorLogger.log(TreeLogger.INFO, "Fix by adding String accessor "
-            + "method(s) to the CssResource interface for obfuscated classes, "
-            + "or using an @external declaration for unobfuscated classes.");
-      }
-
-      if (stop) {
-        throw new CssCompilerException("Missing a CSS replacement");
-      }
-    }
-
-    /**
-     * Reports the replacements that were actually performed by this visitor.
-     */
-    public Map<JMethod, String> getReplacements() {
-      return actualReplacements;
-    }
-
-    /**
-     * Flatten class name lookups to speed selector rewriting.
-     * 
-     * @param classReplacementsWithPrefix a map of local prefixes to the
-     *          obfuscated names of imported methods. If a CssResource makes use
-     *          of the {@link Import} annotation, the keys of this map will
-     *          correspond to the {@link ImportedWithPrefix} value defined on
-     *          the imported CssResource. The zero-length string key holds the
-     *          obfuscated names for the CssResource that is being generated.
-     * @return A flattened version of the classReplacementWithPrefix map, where
-     *         the keys are the source class name (with prefix included), and
-     *         values have the obfuscated class name and associated JMethod.
-     */
-    private Map<String, Replacement> computeReplacements(
-        Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
-        Set<String> externalClasses) {
-
-      Map<String, Replacement> toReturn = new HashMap<String, Replacement>();
-
-      for (String externalClass : externalClasses) {
-        toReturn.put(externalClass, UNREFERENCED_EXTERNAL);
-      }
-
-      for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
-        String prefix = outerEntry.getKey();
-
-        for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
-          JMethod method = entry.getKey();
-          String sourceClassName = method.getName();
-          String obfuscatedClassName = entry.getValue();
-
-          ClassName className = method.getAnnotation(ClassName.class);
-          if (className != null) {
-            sourceClassName = className.value();
-          }
-
-          sourceClassName = prefix + sourceClassName;
-
-          if (externalClasses.contains(sourceClassName)) {
-            /*
-             * It simplifies the sanity-checking logic to treat external classes
-             * as though they were simply obfuscated to exactly the value the
-             * user wants.
-             */
-            obfuscatedClassName = sourceClassName;
-          }
-
-          toReturn.put(sourceClassName, new Replacement(method,
-              obfuscatedClassName));
-        }
-      }
-      return Collections.unmodifiableMap(toReturn);
-    }
-  }
-
-  static class DefsCollector extends CssVisitor {
-    private final Set<String> defs = new HashSet<String>();
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      defs.add(x.getKey());
-    }
-  }
-
-  /**
-   * Collects all {@code @external} declarations in the stylesheet.
-   */
-  static class ExternalClassesCollector extends CssVisitor {
-    private final Set<String> classes = new HashSet<String>();
-
-    @Override
-    public void endVisit(CssExternalSelectors x, Context ctx) {
-      classes.addAll(x.getClasses());
-    }
-
-    public Set<String> getClasses() {
-      return classes;
-    }
-  }
-
-  /**
-   * Statically evaluates {@literal @if} rules.
-   */
-  static class IfEvaluator extends CssModVisitor {
-    private final TreeLogger logger;
-    private final PropertyOracle oracle;
-
-    public IfEvaluator(TreeLogger logger, PropertyOracle oracle) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Replacing property-based @if blocks");
-      this.oracle = oracle;
-    }
-
-    @Override
-    public void endVisit(CssIf x, Context ctx) {
-      if (x.getExpression() != null) {
-        // This gets taken care of by the runtime substitution visitor
-      } else {
-        try {
-          String propertyName = x.getPropertyName();
-          String propValue = null;
-          try {
-            SelectionProperty selProp = oracle.getSelectionProperty(logger,
-                propertyName);
-            propValue = selProp.getCurrentValue();
-          } catch (BadPropertyValueException e) {
-            ConfigurationProperty confProp = oracle.getConfigurationProperty(propertyName);
-            propValue = confProp.getValues().get(0);
-          }
-
-          /*
-           * If the deferred binding property's value is in the list of values
-           * in the @if rule, move the rules into the @if's context.
-           */
-          if (Arrays.asList(x.getPropertyValues()).contains(propValue)
-              ^ x.isNegated()) {
-            for (CssNode n : x.getNodes()) {
-              ctx.insertBefore(n);
-            }
-          } else {
-            // Otherwise, move the else block into the if statement's position
-            for (CssNode n : x.getElseNodes()) {
-              ctx.insertBefore(n);
-            }
-          }
-
-          // Always delete @if rules that we can statically evaluate
-          ctx.removeMe();
-        } catch (BadPropertyValueException e) {
-          logger.log(TreeLogger.ERROR, "Unable to evaluate @if block", e);
-          throw new CssCompilerException("Unable to parse CSS", e);
-        }
-      }
-    }
-  }
 
   @SuppressWarnings("serial")
   static class JClassOrderComparator implements Comparator<JClassType>,
@@ -424,654 +101,6 @@
   }
 
   /**
-   * Merges rules that have matching selectors.
-   */
-  static class MergeIdenticalSelectorsVisitor extends CssModVisitor {
-    private final Map<String, CssRule> canonicalRules = new HashMap<String, CssRule>();
-    private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
-
-    @Override
-    public boolean visit(CssIf x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      visitInNewContext(x.getElseNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssMediaRule x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      // Assumed to run immediately after SplitRulesVisitor
-      assert x.getSelectors().size() == 1;
-      CssSelector sel = x.getSelectors().get(0);
-
-      if (canonicalRules.containsKey(sel.getSelector())) {
-        CssRule canonical = canonicalRules.get(sel.getSelector());
-
-        // Check everything between the canonical rule and this rule for common
-        // properties. If there are common properties, it would be unsafe to
-        // promote the rule.
-        boolean hasCommon = false;
-        int index = rulesInOrder.indexOf(canonical) + 1;
-        assert index != 0;
-
-        for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
-            && !hasCommon;) {
-          hasCommon = haveCommonProperties(i.next(), x);
-        }
-
-        if (!hasCommon) {
-          // It's safe to promote the rule
-          canonical.getProperties().addAll(x.getProperties());
-          ctx.removeMe();
-          return false;
-        }
-      }
-
-      canonicalRules.put(sel.getSelector(), x);
-      rulesInOrder.add(x);
-      return false;
-    }
-
-    private void visitInNewContext(List<CssNode> nodes) {
-      MergeIdenticalSelectorsVisitor v = new MergeIdenticalSelectorsVisitor();
-      v.acceptWithInsertRemove(nodes);
-      rulesInOrder.addAll(v.rulesInOrder);
-    }
-  }
-
-  /**
-   * Merges rules that have identical content.
-   */
-  static class MergeRulesByContentVisitor extends CssModVisitor {
-    private Map<String, CssRule> rulesByContents = new HashMap<String, CssRule>();
-    private final List<CssRule> rulesInOrder = new ArrayList<CssRule>();
-
-    @Override
-    public boolean visit(CssIf x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      visitInNewContext(x.getElseNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssMediaRule x, Context ctx) {
-      visitInNewContext(x.getNodes());
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      StringBuilder b = new StringBuilder();
-      for (CssProperty p : x.getProperties()) {
-        b.append(p.getName()).append(":").append(p.getValues().getExpression());
-      }
-
-      String content = b.toString();
-      CssRule canonical = rulesByContents.get(content);
-
-      // Check everything between the canonical rule and this rule for common
-      // properties. If there are common properties, it would be unsafe to
-      // promote the rule.
-      if (canonical != null) {
-        boolean hasCommon = false;
-        int index = rulesInOrder.indexOf(canonical) + 1;
-        assert index != 0;
-
-        for (Iterator<CssRule> i = rulesInOrder.listIterator(index); i.hasNext()
-            && !hasCommon;) {
-          hasCommon = haveCommonProperties(i.next(), x);
-        }
-
-        if (!hasCommon) {
-          canonical.getSelectors().addAll(x.getSelectors());
-          ctx.removeMe();
-          return false;
-        }
-      }
-
-      rulesByContents.put(content, x);
-      rulesInOrder.add(x);
-      return false;
-    }
-
-    private void visitInNewContext(List<CssNode> nodes) {
-      MergeRulesByContentVisitor v = new MergeRulesByContentVisitor();
-      v.acceptWithInsertRemove(nodes);
-      rulesInOrder.addAll(v.rulesInOrder);
-    }
-  }
-
-  static class RequirementsCollector extends CssVisitor {
-    private final TreeLogger logger;
-    private final ClientBundleRequirements requirements;
-
-    public RequirementsCollector(TreeLogger logger,
-        ClientBundleRequirements requirements) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Scanning CSS for requirements");
-      this.requirements = requirements;
-    }
-
-    @Override
-    public void endVisit(CssIf x, Context ctx) {
-      String propertyName = x.getPropertyName();
-      if (propertyName != null) {
-        try {
-          requirements.addPermutationAxis(propertyName);
-        } catch (BadPropertyValueException e) {
-          logger.log(TreeLogger.ERROR, "Unknown deferred-binding property "
-              + propertyName, e);
-          throw new CssCompilerException("Unknown deferred-binding property", e);
-        }
-      }
-    }
-  }
-
-  static class RtlVisitor extends CssModVisitor {
-    /**
-     * Records if we're currently visiting a CssRule whose only selector is
-     * "body".
-     */
-    private boolean inBodyRule;
-
-    @Override
-    public void endVisit(CssProperty x, Context ctx) {
-      String name = x.getName();
-
-      if (name.equalsIgnoreCase("left")) {
-        x.setName("right");
-      } else if (name.equalsIgnoreCase("right")) {
-        x.setName("left");
-      } else if (name.endsWith("-left")) {
-        int len = name.length();
-        x.setName(name.substring(0, len - 4) + "right");
-      } else if (name.endsWith("-right")) {
-        int len = name.length();
-        x.setName(name.substring(0, len - 5) + "left");
-      } else if (name.contains("-right-")) {
-        x.setName(name.replace("-right-", "-left-"));
-      } else if (name.contains("-left-")) {
-        x.setName(name.replace("-left-", "-right-"));
-      } else {
-        List<Value> values = new ArrayList<Value>(x.getValues().getValues());
-        invokePropertyHandler(x.getName(), values);
-        x.setValue(new CssProperty.ListValue(values));
-      }
-    }
-
-    @Override
-    public boolean visit(CssNoFlip x, Context ctx) {
-      return false;
-    }
-
-    @Override
-    public boolean visit(CssRule x, Context ctx) {
-      inBodyRule = x.getSelectors().size() == 1
-          && x.getSelectors().get(0).getSelector().equals("body");
-      return true;
-    }
-
-    void propertyHandlerBackground(List<Value> values) {
-      /*
-       * The first numeric value will be treated as the left position only if we
-       * havn't seen any value that could potentially be the left value.
-       */
-      boolean seenLeft = false;
-
-      for (ListIterator<Value> it = values.listIterator(); it.hasNext();) {
-        Value v = it.next();
-        Value maybeFlipped = flipLeftRightIdentValue(v);
-        NumberValue nv = v.isNumberValue();
-        if (v != maybeFlipped) {
-          it.set(maybeFlipped);
-          seenLeft = true;
-
-        } else if (isIdent(v, "center")) {
-          seenLeft = true;
-
-        } else if (!seenLeft && (nv != null)) {
-          seenLeft = true;
-          if ("%".equals(nv.getUnits())) {
-            float position = 100f - nv.getValue();
-            it.set(new NumberValue(position, "%"));
-            break;
-          }
-        }
-      }
-    }
-
-    void propertyHandlerBackgroundPosition(List<Value> values) {
-      propertyHandlerBackground(values);
-    }
-
-    Value propertyHandlerBackgroundPositionX(Value v) {
-      ArrayList<Value> list = new ArrayList<Value>(1);
-      list.add(v);
-      propertyHandlerBackground(list);
-      return list.get(0);
-    }
-
-    /**
-     * Note there should be no propertyHandlerBorder(). The CSS spec states that
-     * the border property must set all values at once.
-     */
-    void propertyHandlerBorderColor(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerBorderStyle(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerBorderWidth(List<Value> values) {
-      swapFour(values);
-    }
-
-    Value propertyHandlerClear(Value v) {
-      return propertyHandlerFloat(v);
-    }
-
-    Value propertyHandlerCursor(Value v) {
-      IdentValue identValue = v.isIdentValue();
-      if (identValue == null) {
-        return v;
-      }
-
-      String ident = identValue.getIdent().toLowerCase();
-      if (!ident.endsWith("-resize")) {
-        return v;
-      }
-
-      StringBuffer newIdent = new StringBuffer();
-
-      if (ident.length() == 9) {
-        if (ident.charAt(0) == 'n') {
-          newIdent.append('n');
-          ident = ident.substring(1);
-        } else if (ident.charAt(0) == 's') {
-          newIdent.append('s');
-          ident = ident.substring(1);
-        } else {
-          return v;
-        }
-      }
-
-      if (ident.length() == 8) {
-        if (ident.charAt(0) == 'e') {
-          newIdent.append("w-resize");
-        } else if (ident.charAt(0) == 'w') {
-          newIdent.append("e-resize");
-        } else {
-          return v;
-        }
-        return new IdentValue(newIdent.toString());
-      } else {
-        return v;
-      }
-    }
-
-    Value propertyHandlerDirection(Value v) {
-      if (inBodyRule) {
-        if (isIdent(v, "ltr")) {
-          return new IdentValue("rtl");
-        } else if (isIdent(v, "rtl")) {
-          return new IdentValue("ltr");
-        }
-      }
-      return v;
-    }
-
-    Value propertyHandlerFloat(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    void propertyHandlerMargin(List<Value> values) {
-      swapFour(values);
-    }
-
-    void propertyHandlerPadding(List<Value> values) {
-      swapFour(values);
-    }
-
-    Value propertyHandlerPageBreakAfter(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    Value propertyHandlerPageBreakBefore(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    Value propertyHandlerTextAlign(Value v) {
-      return flipLeftRightIdentValue(v);
-    }
-
-    private Value flipLeftRightIdentValue(Value v) {
-      if (isIdent(v, "right")) {
-        return new IdentValue("left");
-
-      } else if (isIdent(v, "left")) {
-        return new IdentValue("right");
-      }
-      return v;
-    }
-
-    /**
-     * Reflectively invokes a propertyHandler method for the named property.
-     * Dashed names are transformed into camel-case names; only letters
-     * following a dash will be capitalized when looking for a method to prevent
-     * <code>fooBar<code> and <code>foo-bar</code> from colliding.
-     */
-    private void invokePropertyHandler(String name, List<Value> values) {
-      // See if we have a property-handler function
-      try {
-        String[] parts = name.toLowerCase().split("-");
-        StringBuffer methodName = new StringBuffer("propertyHandler");
-        for (String part : parts) {
-          methodName.append(Character.toUpperCase(part.charAt(0)));
-          methodName.append(part, 1, part.length());
-        }
-
-        try {
-          // Single-arg for simplicity
-          Method m = getClass().getDeclaredMethod(methodName.toString(),
-              Value.class);
-          assert Value.class.isAssignableFrom(m.getReturnType());
-          Value newValue = (Value) m.invoke(this, values.get(0));
-          values.set(0, newValue);
-        } catch (NoSuchMethodException e) {
-          // OK
-        }
-
-        try {
-          // Or the whole List for completeness
-          Method m = getClass().getDeclaredMethod(methodName.toString(),
-              List.class);
-          m.invoke(this, values);
-        } catch (NoSuchMethodException e) {
-          // OK
-        }
-
-      } catch (SecurityException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (IllegalArgumentException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (IllegalAccessException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      } catch (InvocationTargetException e) {
-        throw new CssCompilerException(
-            "Unable to invoke property handler function for " + name, e);
-      }
-    }
-
-    private boolean isIdent(Value value, String query) {
-      IdentValue v = value.isIdentValue();
-      return v != null && v.getIdent().equalsIgnoreCase(query);
-    }
-
-    /**
-     * Swaps the second and fourth values in a list of four values.
-     */
-    private void swapFour(List<Value> values) {
-      if (values.size() == 4) {
-        Collections.swap(values, 1, 3);
-      }
-    }
-  }
-
-  /**
-   * Splits rules with compound selectors into multiple rules.
-   */
-  static class SplitRulesVisitor extends CssModVisitor {
-    @Override
-    public void endVisit(CssRule x, Context ctx) {
-      if (x.getSelectors().size() == 1) {
-        return;
-      }
-
-      for (CssSelector sel : x.getSelectors()) {
-        CssRule newRule = new CssRule();
-        newRule.getSelectors().add(sel);
-        newRule.getProperties().addAll(
-            CssNodeCloner.clone(CssProperty.class, x.getProperties()));
-        ctx.insertBefore(newRule);
-      }
-      ctx.removeMe();
-      return;
-    }
-  }
-
-  /**
-   * Replaces CssSprite nodes with CssRule nodes that will display the sprited
-   * image. The real trick with spriting the images is to reuse the
-   * ImageResource processing framework by requiring the sprite to be defined in
-   * terms of an ImageResource.
-   */
-  static class Spriter extends CssModVisitor {
-    private final ResourceContext context;
-    private final TreeLogger logger;
-
-    public Spriter(TreeLogger logger, ResourceContext context) {
-      this.logger = logger.branch(TreeLogger.DEBUG,
-          "Creating image sprite classes");
-      this.context = context;
-    }
-
-    @Override
-    public void endVisit(CssSprite x, Context ctx) {
-      JClassType bundleType = context.getClientBundleType();
-      String functionName = x.getResourceFunction();
-
-      if (functionName == null) {
-        logger.log(TreeLogger.ERROR, "The @sprite rule " + x.getSelectors()
-            + " must specify the " + CssSprite.IMAGE_PROPERTY_NAME
-            + " property");
-        throw new CssCompilerException("No image property specified");
-      }
-
-      // Find the image accessor method
-      JMethod imageMethod = null;
-      JMethod[] allMethods = bundleType.getOverridableMethods();
-      for (int i = 0; imageMethod == null && i < allMethods.length; i++) {
-        JMethod candidate = allMethods[i];
-        // If the function name matches and takes no parameters
-        if (candidate.getName().equals(functionName)
-            && candidate.getParameters().length == 0) {
-          // We have a match
-          imageMethod = candidate;
-        }
-      }
-
-      // Method unable to be located
-      if (imageMethod == null) {
-        logger.log(TreeLogger.ERROR, "Unable to find ImageResource method "
-            + functionName + " in " + bundleType.getQualifiedSourceName());
-        throw new CssCompilerException("Cannot find image function");
-      }
-
-      JClassType imageResourceType = context.getGeneratorContext().getTypeOracle().findType(
-          ImageResource.class.getName());
-      assert imageResourceType != null;
-
-      if (!imageResourceType.isAssignableFrom(imageMethod.getReturnType().isClassOrInterface())) {
-        logger.log(TreeLogger.ERROR, "The return type of " + functionName
-            + " is not assignable to "
-            + imageResourceType.getSimpleSourceName());
-        throw new CssCompilerException("Incorrect return type for "
-            + CssSprite.IMAGE_PROPERTY_NAME + " method");
-      }
-
-      ImageOptions options = imageMethod.getAnnotation(ImageOptions.class);
-      RepeatStyle repeatStyle;
-      if (options != null) {
-        repeatStyle = options.repeatStyle();
-      } else {
-        repeatStyle = RepeatStyle.None;
-      }
-
-      String instance = "(" + context.getImplementationSimpleSourceName()
-          + ".this." + functionName + "())";
-
-      CssRule replacement = new CssRule();
-      replacement.getSelectors().addAll(x.getSelectors());
-      List<CssProperty> properties = replacement.getProperties();
-
-      if (repeatStyle == RepeatStyle.None
-          || repeatStyle == RepeatStyle.Horizontal) {
-        properties.add(new CssProperty("height", new ExpressionValue(instance
-            + ".getHeight() + \"px\""), false));
-      }
-
-      if (repeatStyle == RepeatStyle.None
-          || repeatStyle == RepeatStyle.Vertical) {
-        properties.add(new CssProperty("width", new ExpressionValue(instance
-            + ".getWidth() + \"px\""), false));
-      }
-      properties.add(new CssProperty("overflow", new IdentValue("hidden"),
-          false));
-
-      String repeatText;
-      switch (repeatStyle) {
-        case None:
-          repeatText = " no-repeat";
-          break;
-        case Horizontal:
-          repeatText = " repeat-x";
-          break;
-        case Vertical:
-          repeatText = " repeat-y";
-          break;
-        case Both:
-          repeatText = " repeat";
-          break;
-        default:
-          throw new RuntimeException("Unknown repeatStyle " + repeatStyle);
-      }
-
-      String backgroundExpression = "\"url(\\\"\" + " + instance
-          + ".getURL() + \"\\\") -\" + " + instance
-          + ".getLeft() + \"px -\" + " + instance + ".getTop() + \"px "
-          + repeatText + "\"";
-      properties.add(new CssProperty("background", new ExpressionValue(
-          backgroundExpression), false));
-
-      // Retain any user-specified properties
-      properties.addAll(x.getProperties());
-
-      ctx.replaceMe(replacement);
-    }
-  }
-
-  static class SubstitutionCollector extends CssVisitor {
-    private final Map<String, CssDef> substitutions = new HashMap<String, CssDef>();
-
-    @Override
-    public void endVisit(CssDef x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-
-    @Override
-    public void endVisit(CssEval x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-
-    @Override
-    public void endVisit(CssUrl x, Context ctx) {
-      substitutions.put(x.getKey(), x);
-    }
-  }
-
-  /**
-   * Substitute symbolic replacements into string values.
-   */
-  static class SubstitutionReplacer extends CssVisitor {
-    private final ResourceContext context;
-    private final TreeLogger logger;
-    private final Map<String, CssDef> substitutions;
-
-    public SubstitutionReplacer(TreeLogger logger, ResourceContext context,
-        Map<String, CssDef> substitutions) {
-      this.context = context;
-      this.logger = logger;
-      this.substitutions = substitutions;
-    }
-
-    @Override
-    public void endVisit(CssProperty x, Context ctx) {
-      if (x.getValues() == null) {
-        // Nothing to do
-        return;
-      }
-
-      List<Value> values = new ArrayList<Value>(x.getValues().getValues());
-
-      for (ListIterator<Value> i = values.listIterator(); i.hasNext();) {
-        IdentValue v = i.next().isIdentValue();
-
-        if (v == null) {
-          // Don't try to substitute into anything other than idents
-          continue;
-        }
-
-        String value = v.getIdent();
-        CssDef def = substitutions.get(value);
-
-        if (def == null) {
-          continue;
-        } else if (def instanceof CssUrl) {
-          assert def.getValues().size() == 1;
-          assert def.getValues().get(0).isIdentValue() != null;
-          String functionName = def.getValues().get(0).isIdentValue().getIdent();
-
-          // Find the method
-          JMethod methods[] = context.getClientBundleType().getOverridableMethods();
-          boolean foundMethod = false;
-          if (methods != null) {
-            for (JMethod method : methods) {
-              if (method.getName().equals(functionName)) {
-                foundMethod = true;
-                break;
-              }
-            }
-          }
-
-          if (!foundMethod) {
-            logger.log(TreeLogger.ERROR, "Unable to find DataResource method "
-                + functionName + " in "
-                + context.getClientBundleType().getQualifiedSourceName());
-            throw new CssCompilerException("Cannot find data function");
-          }
-
-          String instance = "((" + DataResource.class.getName() + ")("
-              + context.getImplementationSimpleSourceName() + ".this."
-              + functionName + "()))";
-
-          StringBuilder expression = new StringBuilder();
-          expression.append("\"url('\" + ");
-          expression.append(instance).append(".getUrl()");
-          expression.append(" + \"')\"");
-          i.set(new ExpressionValue(expression.toString()));
-
-        } else {
-          i.remove();
-          for (Value defValue : def.getValues()) {
-            i.add(defValue);
-          }
-        }
-      }
-
-      x.setValue(new ListValue(values));
-    }
-  }
-
-  /**
    * A lookup table of base-32 chars we use to encode CSS idents. Because CSS
    * class selectors may be case-insensitive, we don't have enough characters to
    * use a base-64 encoding.
@@ -1096,13 +125,7 @@
   private static final String KEY_CLASS_PREFIX = "prefix";
   private static final String KEY_CLASS_COUNTER = "counter";
 
-  public static void main(String[] args) {
-    for (int i = 0; i < 1000; i++) {
-      System.out.println(makeIdent(i));
-    }
-  }
-
-  static boolean haveCommonProperties(CssRule a, CssRule b) {
+  public static boolean haveCommonProperties(CssRule a, CssRule b) {
     if (a.getProperties().size() == 0 || b.getProperties().size() == 0) {
       return false;
     }
@@ -1149,6 +172,12 @@
     return false;
   }
 
+  public static void main(String[] args) {
+    for (int i = 0; i < 1000; i++) {
+      System.out.println(makeIdent(i));
+    }
+  }
+
   /**
    * Create a Java expression that evaluates to a string representation of the
    * given node. Visible only for testing.
@@ -1377,6 +406,10 @@
       }
     }
 
+    // Methods defined by CssResource interface
+    writeEnsureInjected(sw);
+    writeGetName(method, sw);
+
     sw.println("public String getText() {");
     sw.indent();
     boolean strict = isStrict(logger, context, method);
@@ -1388,12 +421,6 @@
     sw.outdent();
     sw.println("}");
 
-    sw.println("public String getName() {");
-    sw.indent();
-    sw.println("return \"" + method.getName() + "\";");
-    sw.outdent();
-    sw.println("}");
-
     /*
      * getOverridableMethods is used to handle CssResources extending
      * non-CssResource types. See the discussion in computeReplacementsForType.
@@ -1763,7 +790,7 @@
       SubstitutionCollector collector = new SubstitutionCollector();
       collector.accept(sheet);
 
-      (new SubstitutionReplacer(logger, context, collector.substitutions)).accept(sheet);
+      (new SubstitutionReplacer(logger, context, collector.getSubstitutions())).accept(sheet);
 
       // Evaluate @if statements based on deferred binding properties
       (new IfEvaluator(logger,
@@ -1834,7 +861,7 @@
     String name = toImplement.getName();
     // TODO: Annotation for override
 
-    CssDef def = collector.substitutions.get(name);
+    CssDef def = collector.getSubstitutions().get(name);
     if (def == null) {
       logger.log(TreeLogger.ERROR, "No @def rule for name " + name);
       throw new UnableToCompleteException();
@@ -1884,6 +911,28 @@
     sw.println("}");
   }
 
+  private void writeEnsureInjected(SourceWriter sw) {
+    sw.println("private boolean injected;");
+    sw.println("public boolean ensureInjected() {");
+    sw.indent();
+    sw.println("if (!injected) {");
+    sw.indentln("injected = true;");
+    sw.indentln(StyleInjector.class.getName() + ".injectStylesheet(getText());");
+    sw.indentln("return true;");
+    sw.println("}");
+    sw.println("return false;");
+    sw.outdent();
+    sw.println("}");
+  }
+
+  private void writeGetName(JMethod method, SourceWriter sw) {
+    sw.println("public String getName() {");
+    sw.indent();
+    sw.println("return \"" + method.getName() + "\";");
+    sw.outdent();
+    sw.println("}");
+  }
+
   /**
    * Write all of the user-defined methods in the CssResource subtype.
    */
@@ -1895,22 +944,23 @@
     // Get list of @defs
     DefsCollector collector = new DefsCollector();
     collector.accept(sheet);
+    Set<String> defs = collector.getDefs();
 
     for (JMethod toImplement : methods) {
       String name = toImplement.getName();
-      if ("getName".equals(name) || "getText".equals(name)) {
+      if ("getName".equals(name) || "getText".equals(name)
+          || "ensureInjected".equals(name)) {
         continue;
       }
 
       // Bomb out if there is a collision between @def and a style name
-      if (collector.defs.contains(name)
-          && obfuscatedClassNames.containsKey(toImplement)) {
+      if (defs.contains(name) && obfuscatedClassNames.containsKey(toImplement)) {
         logger.log(TreeLogger.ERROR, "@def shadows CSS class name: " + name
             + ". Fix by renaming the @def name or the CSS class name.");
         throw new UnableToCompleteException();
       }
 
-      if (collector.defs.contains(toImplement.getName())
+      if (defs.contains(toImplement.getName())
           && toImplement.getParameters().length == 0) {
         writeDefAssignment(logger, sw, toImplement, sheet);
       } else if (toImplement.getReturnType().equals(stringType)
diff --git a/user/test/com/google/gwt/animation/client/AnimationTest.java b/user/test/com/google/gwt/animation/client/AnimationTest.java
index 1ed09cf..1249dfc 100644
--- a/user/test/com/google/gwt/animation/client/AnimationTest.java
+++ b/user/test/com/google/gwt/animation/client/AnimationTest.java
@@ -141,6 +141,7 @@
   public void testCancelBeforeStarted() {
     final TestAnimation anim = new TestAnimation();
     double curTime = Duration.currentTimeMillis();
+    delayTestFinish(20 * DELAY_MULTIPLIER);
     anim.run(10 * DELAY_MULTIPLIER, curTime + 10 * DELAY_MULTIPLIER);
 
     // Check progress
@@ -168,9 +169,6 @@
         finishTest();
       }
     }.schedule(15 * DELAY_MULTIPLIER);
-
-    // Wait for test to finish
-    delayTestFinish(20 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -178,6 +176,7 @@
    */
   public void testCancelWhenComplete() {
     final TestAnimation anim = new TestAnimation();
+    delayTestFinish(25 * DELAY_MULTIPLIER);
     anim.run(10 * DELAY_MULTIPLIER);
 
     // Check progress
@@ -204,9 +203,6 @@
         finishTest();
       }
     }.schedule(20 * DELAY_MULTIPLIER);
-
-    // Wait for test to finish
-    delayTestFinish(25 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -214,6 +210,7 @@
    */
   public void testCancelWhileRunning() {
     final TestAnimation anim = new TestAnimation();
+    delayTestFinish(20 * DELAY_MULTIPLIER);
     anim.run(50 * DELAY_MULTIPLIER);
 
     // Check progress
@@ -239,9 +236,6 @@
         finishTest();
       }
     }.schedule(15 * DELAY_MULTIPLIER);
-
-    // Wait for test to finish
-    delayTestFinish(20 * DELAY_MULTIPLIER);
   }
 
   /**
@@ -328,6 +322,7 @@
     final TestAnimation animPast = new TestAnimation();
     final TestAnimation animFuture = new TestAnimation();
 
+    delayTestFinish(50 * DELAY_MULTIPLIER);
     // Run animations
     double curTime = Duration.currentTimeMillis();
     animNow.run(30 * DELAY_MULTIPLIER);
@@ -416,8 +411,6 @@
         animFuture.assertStarted(true);
         animFuture.assertCompleted(false);
         animFuture.assertProgressRange(0.0, 1.0);
-
-        finishTest();
       }
     }.schedule(35 * DELAY_MULTIPLIER);
 
@@ -440,8 +433,5 @@
         finishTest();
       }
     }.schedule(45 * DELAY_MULTIPLIER);
-
-    // Wait for the test to finish
-    delayTestFinish(50 * DELAY_MULTIPLIER);
   }
 }
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 26357df..05e80d0 100644
--- a/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
+++ b/user/test/com/google/gwt/dev/jjs/test/RunAsyncFailureTest.java
@@ -46,7 +46,7 @@
   }
   
   // Repeated runAsync using a Timer
-  public void runAsync1(final int attempt) {
+  private void runAsync1(final int attempt) {
     log("runAsync1: attempt = " + attempt);
     GWT.runAsync(new MyRunAsyncCallback() {
       public void onFailure(Throwable caught) {
diff --git a/user/test/com/google/gwt/junit/BatchingStrategyTest.java b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
index bd86c18..8a44b09 100644
--- a/user/test/com/google/gwt/junit/BatchingStrategyTest.java
+++ b/user/test/com/google/gwt/junit/BatchingStrategyTest.java
@@ -26,10 +26,35 @@
 import java.util.Set;
 
 /**
- * Tests of {@link BatchingStrategy}.
+ * Tests of {@link BatchingStrategy}. This test must run after a
+ * {@link GWTTestCase} to ensure that JUnitShell is already initialized.
  */
 public class BatchingStrategyTest extends TestCase {
 
+  private static class TestClass0 {
+    public void testMethod0() {
+    }
+
+    public void testMethod1() {
+    }
+  }
+
+  private static class TestClass1 {
+    public void testMethod2() {
+    }
+
+    public void testMethod3() {
+    }
+
+    public void testMethod4() {
+    }
+  }
+
+  private static class TestClass2 {
+    public void testMethod5() {
+    }
+  }
+
   /**
    * The synthetic name of the module used for this test.
    */
@@ -41,17 +66,17 @@
   private static TestModuleInfo fakeModuleInfo;
 
   private static final TestInfo TEST_INFO_0_0 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass0", "testMethod0");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass0.class.getName(), "testMethod0");
   private static final TestInfo TEST_INFO_0_1 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass0", "testMethod1");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass0.class.getName(), "testMethod1");
   private static final TestInfo TEST_INFO_1_2 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod2");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass1.class.getName(), "testMethod2");
   private static final TestInfo TEST_INFO_1_3 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod3");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass1.class.getName(), "testMethod3");
   private static final TestInfo TEST_INFO_1_4 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass1", "testMethod4");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass1.class.getName(), "testMethod4");
   private static final TestInfo TEST_INFO_2_5 = new TestInfo(
-      FAKE_MODULE_SYNTHETIC_NAME, "testClass2", "testMethod5");
+      FAKE_MODULE_SYNTHETIC_NAME, TestClass2.class.getName(), "testMethod5");
 
   public void testClassBatchingStrategy() {
     List<TestInfo[]> testBlocks = new ArrayList<TestInfo[]>();
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index ec1c25b..40f468e 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -27,17 +27,19 @@
   public static Test suite() {
     GWTTestSuite suite = new GWTTestSuite("Test for suite for com.google.gwt.junit");
 
-    suite.addTestSuite(FakeMessagesMakerTest.class);
-    suite.addTestSuite(BatchingStrategyTest.class);
-    suite.addTestSuite(JUnitMessageQueueTest.class);
-    suite.addTestSuite(GWTTestCaseNoClientTest.class);
-    suite.addTestSuite(BenchmarkNoClientTest.class);
-
     // client
     // Suppressed due to flakiness on Linux
     // suite.addTestSuite(BenchmarkTest.class);
     suite.addTestSuite(GWTTestCaseTest.class);
 
+    // Must run after a GWTTestCase so JUnitShell is initialized.
+    suite.addTestSuite(BatchingStrategyTest.class);
+
+    suite.addTestSuite(FakeMessagesMakerTest.class);
+    suite.addTestSuite(JUnitMessageQueueTest.class);
+    suite.addTestSuite(GWTTestCaseNoClientTest.class);
+    suite.addTestSuite(BenchmarkNoClientTest.class);
+
     // These two are intended only to be run manually. See class comments
     // suite.addTestSuite(ParallelRemoteTest.class);
     // suite.addTestSuite(TestManualAsync.class);
diff --git a/user/test/com/google/gwt/resources/ResourcesSuite.java b/user/test/com/google/gwt/resources/ResourcesSuite.java
index 7826df2..98538b9 100644
--- a/user/test/com/google/gwt/resources/ResourcesSuite.java
+++ b/user/test/com/google/gwt/resources/ResourcesSuite.java
@@ -21,10 +21,10 @@
 import com.google.gwt.resources.client.ImageResourceTest;
 import com.google.gwt.resources.client.NestedBundleTest;
 import com.google.gwt.resources.client.TextResourceTest;
+import com.google.gwt.resources.css.CssNodeClonerTest;
+import com.google.gwt.resources.css.CssReorderTest;
+import com.google.gwt.resources.css.CssRtlTest;
 import com.google.gwt.resources.css.ExtractClassNamesVisitorTest;
-import com.google.gwt.resources.rg.CssNodeClonerTest;
-import com.google.gwt.resources.rg.CssReorderTest;
-import com.google.gwt.resources.rg.CssRtlTest;
 
 import junit.framework.Test;
 
diff --git a/user/test/com/google/gwt/resources/client/CSSResourceTest.java b/user/test/com/google/gwt/resources/client/CSSResourceTest.java
index b0c4664..3bb69ec 100644
--- a/user/test/com/google/gwt/resources/client/CSSResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/CSSResourceTest.java
@@ -135,6 +135,10 @@
     @Strict
     HasDescendants descendants();
 
+    // Make sure an empty, no-op CssResource works
+    @Strict
+    CssResource empty();
+
     @Source("16x16.png")
     ImageResource spriteMethod();
   }
@@ -292,6 +296,17 @@
     assertFalse("10".equals(defines.overrideIntClass()));
   }
 
+  public void testEnsureInjected() {
+    Resources r = GWT.create(Resources.class);
+    assertTrue(r.empty().ensureInjected());
+
+    r = GWT.create(Resources.class);
+    assertFalse(r.empty().ensureInjected());
+
+    r = GWT.create(ChildResources.class);
+    assertTrue(r.empty().ensureInjected());
+  }
+
   public void testMultipleBundles() {
     Resources r1 = GWT.create(Resources.class);
     SiblingResources r2 = GWT.create(SiblingResources.class);
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
index 21e53fe..9da158a 100644
--- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -113,6 +113,7 @@
     assertEquals(a.getLeft(), b.getTop());
     assertEquals(a.getLeft(), c.getTop());
 
+    delayTestFinish(1000);
     // See if the size of the image strip is what we expect
     Image i = new Image(a.getURL());
     i.addLoadHandler(new LoadHandler() {
@@ -127,7 +128,6 @@
     });
 
     RootPanel.get().add(i);
-    delayTestFinish(1000);
   }
 
   public void testPacking() {
diff --git a/user/test/com/google/gwt/resources/client/TextResourceTest.java b/user/test/com/google/gwt/resources/client/TextResourceTest.java
index 2b4ad40..2c9093f 100644
--- a/user/test/com/google/gwt/resources/client/TextResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/TextResourceTest.java
@@ -47,6 +47,8 @@
   public void testExternal() throws ResourceException {
     final Resources r = GWT.create(Resources.class);
 
+    delayTestFinish(2000);
+
     ResourceCallback<TextResource> c = new ResourceCallback<TextResource>() {
 
       public void onError(ResourceException e) {
@@ -61,7 +63,6 @@
       }
     };
 
-    delayTestFinish(2000);
     r.helloWorldExternal().getText(c);
   }
 
diff --git a/user/test/com/google/gwt/resources/client/empty.css b/user/test/com/google/gwt/resources/client/empty.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/empty.css
diff --git a/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java b/user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
similarity index 96%
rename from user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
rename to user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
index 24c5042..517de81 100644
--- a/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
+++ b/user/test/com/google/gwt/resources/css/CssNodeClonerTest.java
@@ -13,16 +13,16 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
-import com.google.gwt.resources.css.GenerateCssAst;
 import com.google.gwt.resources.css.ast.CssNode;
 import com.google.gwt.resources.css.ast.CssNodeCloner;
 import com.google.gwt.resources.css.ast.CssProperty;
 import com.google.gwt.resources.css.ast.CssSelector;
 import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.rg.CssTestCase;
 
 import java.net.URL;
 import java.util.List;
diff --git a/user/test/com/google/gwt/resources/rg/CssReorderTest.java b/user/test/com/google/gwt/resources/css/CssReorderTest.java
similarity index 82%
rename from user/test/com/google/gwt/resources/rg/CssReorderTest.java
rename to user/test/com/google/gwt/resources/css/CssReorderTest.java
index 2777548..0b3a62e 100644
--- a/user/test/com/google/gwt/resources/rg/CssReorderTest.java
+++ b/user/test/com/google/gwt/resources/css/CssReorderTest.java
@@ -13,14 +13,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.resources.css.ast.CssVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.MergeIdenticalSelectorsVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.MergeRulesByContentVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.SplitRulesVisitor;
+import com.google.gwt.resources.rg.CssTestCase;
 
 /**
  * Tests CSS reordering visitors.
diff --git a/user/test/com/google/gwt/resources/rg/CssRtlTest.java b/user/test/com/google/gwt/resources/css/CssRtlTest.java
similarity index 94%
rename from user/test/com/google/gwt/resources/rg/CssRtlTest.java
rename to user/test/com/google/gwt/resources/css/CssRtlTest.java
index ac0bc8e..fa75951 100644
--- a/user/test/com/google/gwt/resources/rg/CssRtlTest.java
+++ b/user/test/com/google/gwt/resources/css/CssRtlTest.java
@@ -13,12 +13,12 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.resources.rg;
+package com.google.gwt.resources.css;
 
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.resources.css.ast.CssVisitor;
-import com.google.gwt.resources.rg.CssResourceGenerator.RtlVisitor;
+import com.google.gwt.resources.rg.CssTestCase;
 
 /**
  * This is a static test of the automatic RTL support.
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css b/user/test/com/google/gwt/resources/css/backgroundProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css
rename to user/test/com/google/gwt/resources/css/backgroundProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_test.css b/user/test/com/google/gwt/resources/css/backgroundProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/backgroundProperties_test.css
rename to user/test/com/google/gwt/resources/css/backgroundProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_expected.css b/user/test/com/google/gwt/resources/css/cursorProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/cursorProperties_expected.css
rename to user/test/com/google/gwt/resources/css/cursorProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_test.css b/user/test/com/google/gwt/resources/css/cursorProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/cursorProperties_test.css
rename to user/test/com/google/gwt/resources/css/cursorProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_expected.css b/user/test/com/google/gwt/resources/css/directionProperty_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/directionProperty_expected.css
rename to user/test/com/google/gwt/resources/css/directionProperty_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_test.css b/user/test/com/google/gwt/resources/css/directionProperty_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/directionProperty_test.css
rename to user/test/com/google/gwt/resources/css/directionProperty_test.css
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css b/user/test/com/google/gwt/resources/css/fourValuedProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css
rename to user/test/com/google/gwt/resources/css/fourValuedProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css b/user/test/com/google/gwt/resources/css/fourValuedProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css
rename to user/test/com/google/gwt/resources/css/fourValuedProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css b/user/test/com/google/gwt/resources/css/leftRightProperties_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css
rename to user/test/com/google/gwt/resources/css/leftRightProperties_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_test.css b/user/test/com/google/gwt/resources/css/leftRightProperties_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/leftRightProperties_test.css
rename to user/test/com/google/gwt/resources/css/leftRightProperties_test.css
diff --git a/user/test/com/google/gwt/resources/rg/noflip_expected.css b/user/test/com/google/gwt/resources/css/noflip_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/noflip_expected.css
rename to user/test/com/google/gwt/resources/css/noflip_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/noflip_test.css b/user/test/com/google/gwt/resources/css/noflip_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/noflip_test.css
rename to user/test/com/google/gwt/resources/css/noflip_test.css
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_expected.css b/user/test/com/google/gwt/resources/css/propertyMerging_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/propertyMerging_expected.css
rename to user/test/com/google/gwt/resources/css/propertyMerging_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_test.css b/user/test/com/google/gwt/resources/css/propertyMerging_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/propertyMerging_test.css
rename to user/test/com/google/gwt/resources/css/propertyMerging_test.css
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_expected.css b/user/test/com/google/gwt/resources/css/selectorMerging_expected.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/selectorMerging_expected.css
rename to user/test/com/google/gwt/resources/css/selectorMerging_expected.css
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_test.css b/user/test/com/google/gwt/resources/css/selectorMerging_test.css
similarity index 100%
rename from user/test/com/google/gwt/resources/rg/selectorMerging_test.css
rename to user/test/com/google/gwt/resources/css/selectorMerging_test.css
diff --git a/user/test/com/google/gwt/resources/rg/CssTestCase.java b/user/test/com/google/gwt/resources/rg/CssTestCase.java
index 01e7555..33cbc0b 100644
--- a/user/test/com/google/gwt/resources/rg/CssTestCase.java
+++ b/user/test/com/google/gwt/resources/rg/CssTestCase.java
@@ -38,13 +38,17 @@
  * modifications to the CSS AST.
  */
 public class CssTestCase extends TestCase {
+  /*
+   * NB: This class is in the resources.rg package so that it can acess
+   * package-protected methods in CssResourceGenerator.
+   */
 
   /**
    * Triggers an assertion if a CssNode is traversed more than once.
    * 
    * @see CssTestCase#assertNoAliasing(CssNode)
    */
-  protected static class AliasDetector extends CssVisitor {
+  public static class AliasDetector extends CssVisitor {
     private final Map<CssNode, Void> seen = new IdentityHashMap<CssNode, Void>();
 
     @Override
diff --git a/user/test/com/google/gwt/user/client/CommandExecutorTest.java b/user/test/com/google/gwt/user/client/CommandExecutorTest.java
index ca18dc7..1e970d9 100644
--- a/user/test/com/google/gwt/user/client/CommandExecutorTest.java
+++ b/user/test/com/google/gwt/user/client/CommandExecutorTest.java
@@ -114,13 +114,13 @@
           }
         });
 
+        delayTestFinish(TEST_FINISH_DELAY_MILLIS);
         ce.submit(new Command() {
           public void execute() {
             finishTest();
           }
         });
 
-        delayTestFinish(TEST_FINISH_DELAY_MILLIS);
         ce.doExecuteCommands(Duration.currentTimeMillis());
       }
     };
diff --git a/user/test/com/google/gwt/user/client/CookieTest.java b/user/test/com/google/gwt/user/client/CookieTest.java
index 7ecef1b..a51ef3d 100644
--- a/user/test/com/google/gwt/user/client/CookieTest.java
+++ b/user/test/com/google/gwt/user/client/CookieTest.java
@@ -76,6 +76,7 @@
     Cookies.setCookie(lateCookie, "late", expiresLate);
     Cookies.setCookie(sessionCookie, "forever", null);
 
+    delayTestFinish(6 * 1000);
     // Wait until the cookie expires before checking it
     Timer timer = new Timer() {
       @Override
@@ -96,7 +97,6 @@
       }
     };
     timer.schedule(5010);
-    delayTestFinish(6 * 1000);
   }
 
   /**
diff --git a/user/test/com/google/gwt/user/client/WindowTest.java b/user/test/com/google/gwt/user/client/WindowTest.java
index f25219c..b50e698 100644
--- a/user/test/com/google/gwt/user/client/WindowTest.java
+++ b/user/test/com/google/gwt/user/client/WindowTest.java
@@ -149,6 +149,7 @@
     final Label largeDOM = new Label();
     largeDOM.setPixelSize(oldClientWidth + 100, oldClientHeight + 100);
     RootPanel.get().add(largeDOM);
+    delayTestFinish(200);
     DeferredCommand.addCommand(new Command() {
       public void execute() {
         int newClientHeight = Window.getClientHeight();
@@ -159,7 +160,6 @@
         finishTest();
       }
     });
-    delayTestFinish(200);
   }
 
   /**
diff --git a/user/test/com/google/gwt/user/client/rpc/InheritanceTest.java b/user/test/com/google/gwt/user/client/rpc/InheritanceTest.java
index 6023899..6544b75 100644
--- a/user/test/com/google/gwt/user/client/rpc/InheritanceTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/InheritanceTest.java
@@ -180,6 +180,7 @@
   }
 
   public void testSerializationExceptionPreventsCall() {
+    delayTestFinish(TEST_DELAY);
     final boolean serializationExceptionCaught[] = new boolean[1];
     new Timer() {
       @Override
@@ -189,7 +190,6 @@
         finishTest();
       }
     }.schedule(TEST_DELAY / 2);
-    delayTestFinish(TEST_DELAY);
 
     InheritanceTestServiceAsync service = getServiceAsync();
     service.echo(new AnonymousClassInterface() {
diff --git a/user/test/com/google/gwt/user/client/ui/DialogBoxTest.java b/user/test/com/google/gwt/user/client/ui/DialogBoxTest.java
index d794eea..3cc558c 100644
--- a/user/test/com/google/gwt/user/client/ui/DialogBoxTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DialogBoxTest.java
@@ -102,6 +102,7 @@
     UIObjectTest.assertDebugId("myDialogBox-content",
         DOM.getParent(content.getElement()));
 
+    delayTestFinish(250);
     // Check the header IDs
     DeferredCommand.addCommand(new Command() {
       public void execute() {
@@ -110,7 +111,6 @@
         finishTest();
       }
     });
-    delayTestFinish(250);
   }
 
   @Override
diff --git a/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java b/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
index 8e12014..98f8eb5 100644
--- a/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/DisclosurePanelTest.java
@@ -48,6 +48,7 @@
 
     panel.setOpen(true);
 
+    delayTestFinish(500);
     // Allow the animation time to finish
     Timer t = new Timer() {
       @Override
@@ -59,7 +60,6 @@
       }
     };
     t.schedule(450);
-    delayTestFinish(500);
   }
 
   public void testAttachDetachOrder() {
diff --git a/user/test/com/google/gwt/user/client/ui/HistoryTest.java b/user/test/com/google/gwt/user/client/ui/HistoryTest.java
index 34f5373..0d1bd85 100644
--- a/user/test/com/google/gwt/user/client/ui/HistoryTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HistoryTest.java
@@ -125,6 +125,7 @@
    * Verify that no events are issued via newItem if there were not reqeuested.
    */
   public void testNoEvents() {
+    delayTestFinish(5000);
     addHistoryListenerImpl(new HistoryListener() {
       {
         timer = new Timer() {
@@ -139,7 +140,6 @@
         fail("onHistoryChanged should not have been called");
       }
     });
-    delayTestFinish(5000);
     History.newItem("testNoEvents", false);
   }
 
@@ -197,9 +197,10 @@
    * called once per {@link History#newItem(String)}. 
    */
   public void testHistoryChangedCount() {
+    delayTestFinish(5000);
     timer = new Timer() {
       private int count = 0;
-
+      
       public void run() {
         if (count++ == 0) {
           // verify that duplicates don't issue another event
@@ -222,7 +223,6 @@
         timer.schedule(500);
       }
     });
-    delayTestFinish(5000);
     History.newItem("testHistoryChangedCount");
   }
 
diff --git a/user/test/com/google/gwt/user/client/ui/ImageTest.java b/user/test/com/google/gwt/user/client/ui/ImageTest.java
index 995c57d..156feae 100644
--- a/user/test/com/google/gwt/user/client/ui/ImageTest.java
+++ b/user/test/com/google/gwt/user/client/ui/ImageTest.java
@@ -99,6 +99,7 @@
     final Image image = new Image("counting-forwards.png", 12, 13, 8, 8);
     assertEquals("clipped", getCurrentImageStateName(image));
 
+    delayTestFinish(5000);
     image.addLoadListener(new LoadListener() {
       private int onLoadEventCount = 0;
 
@@ -121,7 +122,6 @@
       }
     });
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
   }
 
@@ -134,6 +134,7 @@
     final Image image = new Image("counting-forwards.png");
     assertEquals("unclipped", getCurrentImageStateName(image));
 
+    delayTestFinish(5000);
     image.addLoadListener(new LoadListener() {
       private int onLoadEventCount = 0;
 
@@ -157,7 +158,6 @@
       }
     });
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
   }
 
@@ -169,6 +169,7 @@
   public void disabledTestCreateImage() {
     final Image image = new Image("counting-forwards.png");
 
+    delayTestFinish(5000);
     image.addLoadListener(new LoadListener() {
       private int onLoadEventCount = 0;
 
@@ -185,7 +186,6 @@
       }
     });
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     assertEquals(0, image.getOriginLeft());
     assertEquals(0, image.getOriginTop());
@@ -202,6 +202,7 @@
   public void disabledTestSetUrlAndLoadEventsOnUnclippedImage() {
     final Image image = new Image();
 
+    delayTestFinish(5000);
     image.addLoadListener(new LoadListener() {
       private int onLoadEventCount = 0;
 
@@ -218,7 +219,6 @@
       }
     });
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     image.setUrl("counting-backwards.png");
   }
@@ -233,6 +233,7 @@
   public void disabledTestSetUrlAndVisibleRectOnUnclippedImage() {
     final Image image = new Image("counting-backwards.png");
 
+    delayTestFinish(5000);
     image.addLoadListener(new LoadListener() {
       private int onLoadEventCount = 0;
 
@@ -256,7 +257,6 @@
       }
     });
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     assertEquals("unclipped", getCurrentImageStateName(image));
   }
@@ -273,6 +273,7 @@
   public void testCreateClippedImage() {
     final Image image = new Image("counting-forwards.png", 16, 16, 16, 16);
 
+    delayTestFinish(5000);
     final TestLoadListener listener = new TestLoadListener(image) {
       private int onLoadEventCount = 0;
 
@@ -303,7 +304,6 @@
     });
     image.addErrorHandler(new TestErrorHandler(image));
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     assertEquals(16, image.getOriginLeft());
     assertEquals(16, image.getOriginTop());
@@ -344,6 +344,7 @@
   @DoNotRunWith({Platform.Htmlunit})
   public void testSetUrlAndVisibleRectOnClippedImage() {
     final Image image = new Image("counting-backwards.png", 12, 12, 12, 12);
+    delayTestFinish(5000);
 
     final TestLoadListener listener = new TestLoadListener(image) {
       private int onLoadEventCount = 0;
@@ -381,7 +382,6 @@
     });
     image.addErrorHandler(new TestErrorHandler(image));
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     assertEquals("clipped", getCurrentImageStateName(image));
     image.setUrlAndVisibleRect("counting-forwards.png", 0, 16, 16, 16);
@@ -396,6 +396,7 @@
   public void testSetVisibleRectAndLoadEventsOnClippedImage() {
     final Image image = new Image("counting-backwards.png", 16, 16, 16, 16);
 
+    delayTestFinish(5000);
     final TestLoadListener listener = new TestLoadListener(image) {
       private int onLoadEventCount = 0;
 
@@ -422,7 +423,6 @@
     });
     image.addErrorHandler(new TestErrorHandler(image));
 
-    delayTestFinish(5000);
     RootPanel.get().add(image);
     image.setVisibleRect(0, 0, 16, 16);
     image.setVisibleRect(0, 0, 16, 16);
diff --git a/user/test/com/google/gwt/user/client/ui/ListBoxTest.java b/user/test/com/google/gwt/user/client/ui/ListBoxTest.java
index 5b116de..97e9501 100644
--- a/user/test/com/google/gwt/user/client/ui/ListBoxTest.java
+++ b/user/test/com/google/gwt/user/client/ui/ListBoxTest.java
@@ -49,6 +49,7 @@
     list.ensureDebugId("myList");
     UIObjectTest.assertDebugId("myList", list.getElement());
 
+    delayTestFinish(250);
     DeferredCommand.addCommand(new Command() {
       public void execute() {
         UIObjectTest.assertDebugIdContents("myList-item0", "option0");   
@@ -58,7 +59,6 @@
         finishTest();
       }
     });
-    delayTestFinish(250);
   }
 
   public void testInsert() {
diff --git a/user/test/com/google/gwt/user/client/ui/MenuBarTest.java b/user/test/com/google/gwt/user/client/ui/MenuBarTest.java
index 1785aeb..a33f64c 100644
--- a/user/test/com/google/gwt/user/client/ui/MenuBarTest.java
+++ b/user/test/com/google/gwt/user/client/ui/MenuBarTest.java
@@ -236,6 +236,7 @@
     bar.ensureDebugId("myMenu");
     UIObjectTest.assertDebugId("myMenu", bar.getElement());
 
+    delayTestFinish(250);
     DeferredCommand.addCommand(new Command() {
       public void execute() {
         UIObjectTest.assertDebugIdContents("myMenu-item0", "top0");
@@ -248,7 +249,6 @@
         finishTest();
       }
     });
-    delayTestFinish(250);
   }
 
   /**
diff --git a/user/test/com/google/gwt/user/client/ui/PopupTest.java b/user/test/com/google/gwt/user/client/ui/PopupTest.java
index 0ec6231..eb64dc0 100644
--- a/user/test/com/google/gwt/user/client/ui/PopupTest.java
+++ b/user/test/com/google/gwt/user/client/ui/PopupTest.java
@@ -333,6 +333,7 @@
       }
     }.schedule(1000);
 
+    delayTestFinish(5000);
     // Give time for any errors to occur
     new Timer() {
       @Override
@@ -340,7 +341,5 @@
         finishTest();
       }
     }.schedule(2000);
-
-    delayTestFinish(5000);
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
index cce59a8..d504f19 100644
--- a/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
+++ b/user/test/com/google/gwt/user/client/ui/RichTextAreaTest.java
@@ -228,6 +228,7 @@
   public void testSetTextBeforeInit() {
     final RichTextArea richTextArea = new RichTextArea();
     richTextArea.setText("foo");
+    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
     richTextArea.addInitializeHandler(new InitializeHandler() {
       public void onInitialize(InitializeEvent event) {
         assertEquals("foo", richTextArea.getText());
@@ -236,6 +237,5 @@
     });
     RootPanel.get().add(richTextArea);
     assertEquals("foo", richTextArea.getText());
-    delayTestFinish(RICH_TEXT_ASYNC_DELAY);
   }
 }
diff --git a/user/test/com/google/gwt/user/client/ui/StackPanelTest.java b/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
index 0e6ab61..4bf82e6 100644
--- a/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/StackPanelTest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2008 Google Inc.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -75,6 +75,8 @@
     UIObjectTest.assertDebugId("myStack-content2",
         DOM.getParent(c.getElement()));
 
+    delayTestFinish(5000);
+
     // Check the header IDs
     DeferredCommand.addCommand(new Command() {
       public void execute() {
@@ -97,7 +99,6 @@
         finishTest();
       }
     });
-    delayTestFinish(5000);
   }
 
   /**
diff --git a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
index 75c634e..0db70a3 100644
--- a/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/TabPanelTest.java
@@ -202,6 +202,7 @@
     p.add(new Button("foo"), "foo");
     p.add(new Button("bar"), "bar");
 
+    this.delayTestFinish(1000);
     // Make sure selecting a tab fires both events in the right order.
     p.addTabListener(new TabListener() {
       private boolean onBeforeFired;
@@ -217,7 +218,6 @@
       }
     });
 
-    this.delayTestFinish(1000);
     p.selectTab(1);
   }