Initial add of ClientBundle.

Patch by: bobv
Review by: jgw, rjrjr, ecc


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5083 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/common.ant.xml b/common.ant.xml
index 2e2c78a..8a65f84 100755
--- a/common.ant.xml
+++ b/common.ant.xml
@@ -174,6 +174,8 @@
           <pathelement location="${gwt.dev.staging.jar}" />
           <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
           <pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
+          <pathelement location="${gwt.tools.lib}/w3c/sac/sac-1.3.jar" />
+          <pathelement location="${gwt.tools.lib}/w3c/flute/flute-1.3.jar" />
           <extraclasspaths />
         </classpath>
 
diff --git a/dev/core/src/com/google/gwt/core/ext/Generator.java b/dev/core/src/com/google/gwt/core/ext/Generator.java
index 1e5e470..524dddb 100644
--- a/dev/core/src/com/google/gwt/core/ext/Generator.java
+++ b/dev/core/src/com/google/gwt/core/ext/Generator.java
@@ -31,6 +31,7 @@
     int extra = 0;
     for (int in = 0, n = unescaped.length(); in < n; ++in) {
       switch (unescaped.charAt(in)) {
+        case '\0':
         case '\n':
         case '\r':
         case '\"':
@@ -49,6 +50,10 @@
     for (int in = 0, out = 0, n = oldChars.length; in < n; ++in, ++out) {
       char c = oldChars[in];
       switch (c) {
+        case '\0':
+          newChars[out++] = '\\';
+          c = '0';
+          break;
         case '\n':
           newChars[out++] = '\\';
           c = 'n';
diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath
index 60629c2..3f063b0 100644
--- a/eclipse/user/.classpath
+++ b/eclipse/user/.classpath
@@ -8,6 +8,8 @@
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/tomcat/servlet-api-2.5.jar"/>
 	<classpathentry kind="var" path="GWT_TOOLS/lib/selenium/selenium-java-client-driver.jar"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/w3c/sac/sac-1.3.jar"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/w3c/flute/flute-1.3.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-windows"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/user/build.xml b/user/build.xml
index 196959a..2ff72cb 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -35,6 +35,8 @@
         <pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
         <pathelement location="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar" />
         <pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
+        <pathelement location="${gwt.tools.lib}/w3c/sac/sac-1.3.jar" />
+        <pathelement location="${gwt.tools.lib}/w3c/flute/flute-1.3.jar" />
         <pathelement location="${gwt.dev.jar}" />
       </classpath>
     </gwt.javac>
@@ -73,6 +75,8 @@
       <fileset dir="super" excludes="**/package.html" />
       <fileset dir="${javac.out}" />
       <zipfileset src="${gwt.tools.lib}/tomcat/servlet-api-2.5.jar" />
+      <zipfileset src="${gwt.tools.lib}/w3c/sac/sac-1.3.jar" />
+      <zipfileset src="${gwt.tools.lib}/w3c/flute/flute-1.3.jar" />
     </gwt.jar>
   </target>
 
diff --git a/user/src/com/google/gwt/resources/Resources.gwt.xml b/user/src/com/google/gwt/resources/Resources.gwt.xml
new file mode 100644
index 0000000..482d555
--- /dev/null
+++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -0,0 +1,75 @@
+<!--                                                                        -->
+<!-- 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   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<!-- ClientBundle supports renaming files with strong names at compile time  -->
+<module>
+  <!-- Pull in the necessary base support, including user.agent detection -->
+  <inherits name="com.google.gwt.core.Core" />
+  <!-- Used by ExternalTextResource -->
+  <inherits name="com.google.gwt.http.HTTP" />
+
+  <!--  This acts as a switch to disable the use of data: URLs -->
+  <define-property name="ClientBundle.enableInlining" values="true,false" />
+  <set-property name="ClientBundle.enableInlining" value="true" />
+
+  <!-- Specify the default behavior -->
+  <generate-with
+    class="com.google.gwt.resources.rebind.context.StaticClientBundleGenerator">
+
+    <!-- We have to specify on which types to execute the Generator -->
+    <when-type-assignable
+      class="com.google.gwt.resources.client.ClientBundle" />
+  </generate-with>
+
+  <!-- Last-matches wins, so this will selectively override the previous rule -->
+  <generate-with
+    class="com.google.gwt.resources.rebind.context.InlineClientBundleGenerator">
+
+    <!-- We have a number of conditions that must be satisfied -->
+    <all>
+      <!-- Is inlining enabled? -->
+      <when-property-is name="ClientBundle.enableInlining" value="true" />
+
+      <!-- Again, it's necessary to specify which types the generator runs on -->
+      <when-type-assignable
+        class="com.google.gwt.resources.client.ClientBundle" />
+
+      <!-- Only some browsers support RFC 2397 data: URLs -->
+      <any>
+        <when-property-is name="user.agent" value="safari" />
+        <when-property-is name="user.agent" value="opera" />
+        <when-property-is name="user.agent" value="gecko" />
+        <when-property-is name="user.agent" value="gecko1_8" />
+      </any>
+    </all>
+  </generate-with>
+
+  <!-- This can be used to disable the use of strongly-named files -->
+  <set-configuration-property name="ClientBundle.enableRenaming" value="true" />
+
+  <!-- This allows merging of CSS rules to be disabled. -->
+  <set-configuration-property name="CssResource.mergeEnabled" value="true" />
+
+  <!-- This forces all CssResource accessor functions to have the @Strict -->
+  <!-- annotation. This is intended primarily for application developers and -->
+  <!-- the library test code. -->
+  <set-configuration-property name="CssResource.strictAccessors" value="false" />
+
+  <!-- This allows the developer to use shorter obfuscated class names. -->
+  <!-- Is is valid to extend this property to use a custom name. -->
+  <set-configuration-property name="CssResource.obfuscationPrefix" value="default" />
+
+  <!-- This can be used to make CssResource produce human-readable CSS -->
+  <set-configuration-property name="CssResource.style" value="obf" />
+</module>
diff --git a/user/src/com/google/gwt/resources/client/ClientBundle.java b/user/src/com/google/gwt/resources/client/ClientBundle.java
new file mode 100644
index 0000000..a25cba5
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ClientBundle.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.BundleResourceGenerator;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * The use of this interface is similar to that of ImageBundle. Declare
+ * no-argument functions that return subclasses of {@link ResourcePrototype},
+ * which are annotated with {@link ClientBundle.Source} annotations specifying
+ * the classpath location of the resource to include in the output. At runtime,
+ * the functions will return an object that can be used to access the data in
+ * the original resource.
+ */
+@ResourceGeneratorType(BundleResourceGenerator.class)
+public interface ClientBundle {
+  /**
+   * Specifies the classpath location of the resource or resources associated
+   * with the {@link ResourcePrototype}.
+   */
+  @Target(ElementType.METHOD)
+  public @interface Source {
+    String[] value();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/client/ClientBundleWithLookup.java b/user/src/com/google/gwt/resources/client/ClientBundleWithLookup.java
new file mode 100644
index 0000000..90a7b6e
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ClientBundleWithLookup.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.client;
+
+/**
+ * This is an extension of ClientBundle that allows for name-based lookup of
+ * resources. Note that the use of the methods defined within this interface
+ * will prevent the compiler from pruning any of the resources declared in the
+ * ClientBundle.
+ */
+public interface ClientBundleWithLookup extends ClientBundle {
+
+  /**
+   * Find a resource by the name of the function in which it is declared.
+   * 
+   * @param name the name of the desired resource
+   * @return the resource, or <code>null</code> if no such resource is defined.
+   */
+  ResourcePrototype getResource(String name);
+
+  /**
+   * A convenience method to iterate over all ResourcePrototypes contained in
+   * the ClientBundle.
+   */
+  ResourcePrototype[] getResources();
+}
diff --git a/user/src/com/google/gwt/resources/client/CssResource.java b/user/src/com/google/gwt/resources/client/CssResource.java
new file mode 100644
index 0000000..a133977
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/CssResource.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.CssResourceGenerator;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Aggregates and minifies CSS stylesheets. A CssResource represents a regular
+ * CSS file with GWT-specific at-rules.
+ * <p>
+ * Currently-supported accessor functions:
+ * 
+ * <ul>
+ * <li>{@code String someClassName();} will allow the css class
+ * <code>.someClassName</code> to be obfuscated at runtime. The function will
+ * return the obfuscated class name.</li>
+ * <li>{@code <primitive numeric type> someDefName();} will allow access to the
+ * values defined by {@literal @def} rules within the CSS file. The defined
+ * value must be a raw number, a CSS length, or a percentage value.
+ * </ul>
+ * 
+ * <p>
+ * Currently-supported rules:
+ * 
+ * <ul>
+ * <li>{@code @def NAME replacement-expression; .myClass background: NAME;}
+ * Define a static constant. The replacement expression may be any CSS that
+ * would be valid in a property value context. A {@code @def} may refer to
+ * previously-defined rules, but no forward-references will be honored.</li>
+ * <li>{@code @eval NAME Java-expression; .myClass background: NAME;} Define a
+ * constant based on a Java expression.</li>
+ * <li><code>{@literal @if} [!]property list of values {ruleBlock}</code> Include or
+ * exclude CSS rules based on the value of a deferred-binding property. Also
+ * {@code @elif} and {@code @else} follow the same pattern.<br/>
+ * This might look like {@code @if user.agent ie6 safari ...}.</li>
+ * <li><code>{@literal @if} (Java-expression) {ruleBlock}</code> Include or exclude
+ * CSS rules based on a boolean Java expression.</li>
+ * <li><code>{@literal @noflip} { rules }</code> will suppress the automatic
+ * right-to-left transformation applied to the CSS when the module is compiled
+ * for an RTL language.</li>
+ * <li>
+ * <code>{@literal @}sprite .any .selector {gwt-image: "imageResourceFunction";}</code>
+ * . The appearance, size, and height of the sprite will be affected by any
+ * {@link ImageResource.ImageOptions} annotations present on the related
+ * {@link ImageResource} accessor function. Additional properties may be
+ * specified in the rule block.</li>
+ * <li>{@code @url NAME siblingDataResource; .myClass background: NAME
+ * repeat-x;} Use a {@link DataResource} to generate a <code>url('...'}</code> value.</li>
+ * </ul>
+ * 
+ * <p>
+ * Currently-supported CSS functions:
+ * 
+ * <ul>
+ * <li>{@code literal("expression")} substitutes a property value that does not
+ * conform to CSS2 parsing rules. The escape sequences {@code \"} and {@code \\}
+ * will be replaced with {@code "} and {@code \} respectively.
+ * <li>{@code value("bundleFunction.someFunction[.other[...]]" [, "suffix"])}
+ * substitute the value of a sequence of named zero-arg function invocations. An
+ * optional suffix will be appended to the return value of the function. The
+ * first name is resolved relative to the bundle interface passed to
+ * {@link com.google.gwt.core.client.GWT#create(Class)}. An example:
+ * 
+ * <pre>
+ * .bordersTheSizeOfAnImage {
+ *   border-left: value('leftBorderImageResource.getWidth', 'px') solid blue;
+ * }
+ * </pre>
+ * </li>
+ * </ul>
+ * 
+ * @see <a
+ *      href="http://code.google.com/p/google-web-toolkit-incubator/wiki/CssResource"
+ *      >CssResource design doc</a>
+ */
+@ResourceGeneratorType(CssResourceGenerator.class)
+public interface CssResource extends ResourcePrototype {
+  /**
+   * The original CSS class name specified in the resource. This allows CSS
+   * classes that do not correspond to Java identifiers to be mapped onto
+   * obfuscated class accessors.
+   * 
+   * <pre>
+   * .some-non-java-ident { background: blue; }
+   * 
+   * interface MyCssResource extends CssResource {
+   *   {@literal @}ClassName("some-non-java-ident")
+   *   String classAccessor();
+   * }
+   * </pre>
+   */
+  @Documented
+  @Target(ElementType.METHOD)
+  public @interface ClassName {
+    String value();
+  }
+
+  /**
+   * Makes class selectors from other CssResource types available in the raw
+   * source of a CssResource. String accessor methods can be referred to using
+   * the value of the imported type's {@link ImportedWithPrefix} value.
+   * <p>
+   * This is an example of creating a descendant selector with two unrelated
+   * types:
+   * 
+   * <pre>
+   *{@literal @ImportedWithPrefix}("some-prefix")
+   * interface ToImport extends CssResource {
+   *   String widget();
+   * }
+   * 
+   *{@literal @ImportedWithPrefix}("other-import")
+   * interface OtherImport extends CssResource {
+   *   String widget();
+   * }
+   * 
+   * interface Resources extends ClientBundle {
+   *  {@literal @Import}(value = {ToImport.class, OtherImport.class})
+   *  {@literal @Source}("my.css")
+   *   CssResource usesImports();
+   * }
+   * 
+   * my.css:
+   * // Now I can refer to these classes defined elsewhere with no 
+   * // fear of name collisions
+   * .some-prefix-widget .other-import-widget {...}
+   * </pre>
+   * 
+   * If the imported CssResource type is lacking an {@link ImportedWithPrefix}
+   * annotation, the simple name of the type will be used instead. In the above
+   * example, without the annotation on <code>ToImport</code>, the class
+   * selector would have been <code>.ToImport-widget</code>. Notice also that
+   * both interfaces defined a method called <code>widget()</code>, which would
+   * prevent meaningful composition of the original interfaces.
+   * <p>
+   * It is an error to import multiple classes with the same prefix into one
+   * CssResource.
+   */
+  @Documented
+  @Target(ElementType.METHOD)
+  public @interface Import {
+    Class<? extends CssResource>[] value();
+  }
+
+  /**
+   * Specifies the string prefix to use when one CssResource is imported into
+   * the scope of another CssResource.
+   * 
+   * @see Import
+   */
+  @Documented
+  @Target(ElementType.TYPE)
+  public @interface ImportedWithPrefix {
+    String value();
+  }
+
+  /**
+   * Indicates that the String accessor methods defined in a CssResource will
+   * return the same values across all implementations of that type.
+   * <p>
+   * This is an example of "stateful" class selectors being used:
+   * 
+   * <pre>
+   *{@literal @Shared}
+   * interface FocusCss extends CssResource {
+   *   String focused();
+   *   String unfocused();
+   * }
+   * 
+   * interface PanelCss extends CssResource, FocusCss {
+   *   String widget();
+   * }
+   * 
+   * interface InputCss extends CssResource, FocusCss {
+   *   String widget();
+   * }
+   * 
+   * input.css:
+   * *.focused .widget {border: thin solid blue;}
+   * 
+   * Application.java:
+   * myPanel.add(myInputWidget);
+   * myPanel.addStyleName(instanceOfPanelCss.focused());
+   * </pre>
+   * 
+   * Because the <code>FocusCss</code> interface is tagged with {@code @Shared},
+   * the <code>focused()</code> method on the instance of <code>PanelCss</code>
+   * will match the <code>.focused</code> parent selector in
+   * <code>input.css</code>.
+   * <p>
+   * The effect of inheriting an {@code Shared} interface can be replicated by
+   * use use of the {@link Import} annotation (e.g. {@code .FocusCss-focused
+   * .widget}), however the use of state-bearing descendant selectors is common
+   * enough to warrant an easier use-case.
+   */
+  @Documented
+  @Target(ElementType.TYPE)
+  public @interface Shared {
+  }
+
+  /**
+   * The presence of this annotation on a CssResource accessor method indicates
+   * that any class selectors that do not correspond with a String accessor
+   * method in the return type should trigger a compilation error. In the normal
+   * case, any unobfuscatable class selectors will be emitted as-is. This
+   * annotation ensures that the resource does not contribute any unobfuscated
+   * class selectors into the global CSS namespace and is recommended as the
+   * default for library-provided CssResource instances.
+   * <p>
+   * Given these interfaces:
+   * 
+   * <pre>
+   * interface MyCss extends CssResource {
+   *   String someClass();
+   * }
+   * 
+   * interface MyBundle extends ClientBundle {
+   *  {@literal @Source("my.css")}
+   *  {@literal @Strict}
+   *   MyCss css();
+   * }
+   * </pre>
+   * 
+   * the source CSS will fail to compile if it does not contain exactly the one
+   * class selector defined in the MyCss type.
+   */
+  @Documented
+  @Target(ElementType.METHOD)
+  public @interface Strict {
+  }
+
+  /**
+   * Provides the contents of the CssResource.
+   */
+  String getText();
+}
diff --git a/user/src/com/google/gwt/resources/client/DataResource.java b/user/src/com/google/gwt/resources/client/DataResource.java
new file mode 100644
index 0000000..fd3d2cb
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/DataResource.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.DataResourceGenerator;
+
+/**
+ * A non-text resource.
+ */
+@ResourceGeneratorType(DataResourceGenerator.class)
+public interface DataResource extends ResourcePrototype {
+  /**
+   * Retrieves a URL by which the contents of the resource can be obtained. This
+   * will be an absolute URL.
+   */
+  String getUrl();
+}
diff --git a/user/src/com/google/gwt/resources/client/ExternalTextResource.java b/user/src/com/google/gwt/resources/client/ExternalTextResource.java
new file mode 100644
index 0000000..0e5d4cb
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ExternalTextResource.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.ExternalTextResourceGenerator;
+
+/**
+ * Identical to {@link TextResource}, except the contents of the resource are
+ * not inlined into the compiled output. This is suitable for resources that are
+ * not required as part of program initialization.
+ */
+@ResourceGeneratorType(ExternalTextResourceGenerator.class)
+public interface ExternalTextResource extends ResourcePrototype {
+  void getText(ResourceCallback<TextResource> callback)
+      throws ResourceException;
+}
diff --git a/user/src/com/google/gwt/resources/client/GwtCreateResource.java b/user/src/com/google/gwt/resources/client/GwtCreateResource.java
new file mode 100644
index 0000000..a13b32a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/GwtCreateResource.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.GwtCreateResourceGenerator;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * This resource type allows any class that can be instantiated via a call to
+ * {@link com.google.gwt.core.client.GWT#create(Class)} to be used within an
+ * ClientBundle. Example uses include the I18N support classes, RPC
+ * endpoints, or any type that supports default instantiation. If no
+ * {@link ClassType} annotation is present on the resource accessor method, the
+ * type parameter <code>T</code> will be used as the class literal passed to
+ * <code>GWT.create()</code>.
+ * 
+ * @param <T> The type that should be returned from the
+ *          <code>GWT.create()</code> call
+ */
+@ResourceGeneratorType(GwtCreateResourceGenerator.class)
+public interface GwtCreateResource<T> extends ResourcePrototype {
+  /**
+   * This annotation can be applied to the resource getter method in order to
+   * call <code>GWT.create</code> with a class literal other than that of the
+   * return type parameterization. This annotation would be used with RPC
+   * endpoints:
+   * 
+   * <pre>{@literal @ClassType}(Service.class)
+   * GwtCreateResource&lt;ServiceAsync&gt; rpc();
+   * </pre>
+   */
+  @Documented
+  @Target(ElementType.METHOD)
+  public @interface ClassType {
+    Class<?> value();
+  }
+
+  /**
+   * Invokes <code>GWT.create()</code>. Multiple invocations of this method will
+   * return different instances of the <code>T</code> type.
+   */
+  T create();
+}
diff --git a/user/src/com/google/gwt/resources/client/ImageResource.java b/user/src/com/google/gwt/resources/client/ImageResource.java
new file mode 100644
index 0000000..75c2572
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ImageResource.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.ImageResourceGenerator;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Provides access to image resources at runtime.
+ */
+@ResourceGeneratorType(ImageResourceGenerator.class)
+public interface ImageResource extends ResourcePrototype {
+
+  /**
+   * Specifies additional options to control how an image is bundled.
+   */
+  @Documented
+  @Target(ElementType.METHOD)
+  public @interface ImageOptions {
+    /**
+     * This option affects the image bundling optimization to allow the image to
+     * be used with the {@link CssResource} {@code @sprite} rule where
+     * repetition of the image is desired.
+     * 
+     * @see "CssResource documentation"
+     */
+    RepeatStyle repeatStyle() default RepeatStyle.None;
+
+    /**
+     * If <code>true</code>, the image will be flipped along the y-axis when
+     * {@link com.google.gwt.i18n.client.LocaleInfo#isRTL()} returns
+     * <code>true</code>. This is intended to be used by graphics that are
+     * sensitive to layout direction, such as arrows and disclosure indicators.
+     */
+    boolean flipRtl() default false;
+  }
+
+  /**
+   * Indicates that an ImageResource should be bundled in such a way as to
+   * support horizontal or vertical repetition.
+   */
+  public enum RepeatStyle {
+    /**
+     * The image is not intended to be tiled.
+     */
+    None,
+
+    /**
+     * The image is intended to be tiled horizontally.
+     */
+    Horizontal,
+
+    /**
+     * The image is intended to be tiled vertically.
+     */
+    Vertical,
+
+    /**
+     * The image is intended to be tiled both horizontally and vertically. Note
+     * that this will prevent compositing of the particular image in most cases.
+     */
+    Both
+  }
+
+  /**
+   * Returns the height of the image.
+   */
+  int getHeight();
+
+  /**
+   * Returns the horizontal position of the image within the composite image.
+   */
+  int getLeft();
+
+  /**
+   * Returns the vertical position of the image within the composite image.
+   */
+  int getTop();
+
+  /**
+   * Returns the URL for the composite image that contains the ImageResource.
+   */
+  String getURL();
+
+  /**
+   * Returns the width of the image.
+   */
+  int getWidth();
+}
diff --git a/user/src/com/google/gwt/resources/client/ResourceCallback.java b/user/src/com/google/gwt/resources/client/ResourceCallback.java
new file mode 100644
index 0000000..a08e5fc
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ResourceCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+/**
+ * A callback interface for asynchronous operations on resources.
+ * 
+ * @param <R> the type of resource
+ */
+public interface ResourceCallback<R extends ResourcePrototype> {
+  /**
+   * Invoked if the asynchronous operation failed.
+   * @param e an exception describing the failure
+   */
+  void onError(ResourceException e);
+
+  /**
+   * Invoked if the asynchronous operation was successfully completed.
+   * @param resource the resource on which the operation was performed
+   */
+  void onSuccess(R resource);
+}
diff --git a/user/src/com/google/gwt/resources/client/ResourceException.java b/user/src/com/google/gwt/resources/client/ResourceException.java
new file mode 100644
index 0000000..797632a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ResourceException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+/**
+ * Associates a {@link ResourcePrototype} with a program error.
+ */
+public class ResourceException extends Exception {
+  private final ResourcePrototype resource;
+
+  public ResourceException(ResourcePrototype resource) {
+    this.resource = resource;
+  }
+
+  public ResourceException(ResourcePrototype resource, String msg) {
+    super(msg);
+    this.resource = resource;
+  }
+
+  public ResourceException(ResourcePrototype resource, String msg, Throwable t) {
+    super(msg, t);
+    this.resource = resource;
+  }
+
+  public ResourcePrototype getResource() {
+    return resource;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/client/ResourcePrototype.java b/user/src/com/google/gwt/resources/client/ResourcePrototype.java
new file mode 100644
index 0000000..81cc6e4
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/ResourcePrototype.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+/**
+ * The base interface all bundle resource types must extend.
+ */
+public interface ResourcePrototype {
+  /**
+   * Returns the name of the function within the ClientBundle used to create the
+   * ResourcePrototype.
+   * 
+   * @return the name of the function within the ClientBundle used to create the
+   *         ResourcePrototype
+   */
+  String getName();
+}
diff --git a/user/src/com/google/gwt/resources/client/TextResource.java b/user/src/com/google/gwt/resources/client/TextResource.java
new file mode 100644
index 0000000..69ee17a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/TextResource.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.TextResourceGenerator;
+
+/**
+ * A resource that contains text that should be incorporated into the compiled
+ * output.
+ */
+@ResourceGeneratorType(TextResourceGenerator.class)
+public interface TextResource extends ResourcePrototype {
+  String getText();
+}
diff --git a/user/src/com/google/gwt/resources/client/TextResourceCallback.java b/user/src/com/google/gwt/resources/client/TextResourceCallback.java
new file mode 100644
index 0000000..beac520
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/TextResourceCallback.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+/**
+ * A callback used when loading external text resources.
+ * @deprecated use {@link ResourceCallback} directly
+ */
+@Deprecated
+public interface TextResourceCallback extends ResourceCallback<TextResource> {
+}
diff --git a/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
new file mode 100644
index 0000000..c76ae01
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client.impl;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.resources.client.ExternalTextResource;
+import com.google.gwt.resources.client.ResourceCallback;
+import com.google.gwt.resources.client.ResourceException;
+import com.google.gwt.resources.client.TextResource;
+
+/**
+ * Implements external resource fetching of TextResources.
+ */
+public class ExternalTextResourcePrototype implements ExternalTextResource {
+
+  /**
+   * Maps the HTTP callback onto the ResourceCallback.
+   */
+  private class ETRCallback implements RequestCallback {
+    final ResourceCallback<TextResource> callback;
+
+    public ETRCallback(ResourceCallback<TextResource> callback) {
+      this.callback = callback;
+    }
+
+    public void onError(Request request, Throwable exception) {
+      callback.onError(new ResourceException(
+          ExternalTextResourcePrototype.this,
+          "Unable to retrieve external resource", exception));
+    }
+
+    public void onResponseReceived(Request request, final Response response) {
+      // Get the contents of the JSON bundle
+      String responseText = response.getText();
+
+      // Call eval() on the object.
+      JavaScriptObject jso = evalObject(responseText);
+      if (jso == null) {
+        callback.onError(new ResourceException(
+            ExternalTextResourcePrototype.this, "eval() returned null"));
+        return;
+      }
+
+      // Populate the TextResponse cache array
+      for (int i = 0; i < cache.length; i++) {
+        final String resourceText = extractString(jso, i);
+        cache[i] = new TextResource() {
+
+          public String getName() {
+            return name;
+          }
+
+          public String getText() {
+            return resourceText;
+          }
+
+        };
+      }
+
+      // Finish by invoking the callback
+      callback.onSuccess(cache[index]);
+    }
+  }
+
+  /**
+   * Evaluate the JSON payload. The regular expression to validate the safety of
+   * the payload is taken from RFC 4627 (D. Crockford).
+   * 
+   * @param data the raw JSON-encapsulated string bundle
+   * @return the evaluated JSON object, or <code>null</code> if there is an
+   *         error.
+   */
+  private static native JavaScriptObject evalObject(String data) /*-{
+    var safe = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
+      data.replace(/"(\\.|[^"\\])*"/g, '')));
+
+    if (!safe) {
+      return null;
+    }
+
+    return eval('(' + data + ')') || null;
+  }-*/;
+
+  /**
+   * Extract the specified String from a JavaScriptObject that is array-like.
+   * 
+   * @param jso the JavaScriptObject returned from {@link #evalObject(String)}
+   * @param index the index of the string to extract
+   * @return the requested string, or <code>null</code> if it does not exist.
+   */
+  private static native String extractString(JavaScriptObject jso, int index) /*-{
+    return (jso.length > index) && jso[index] || null;
+  }-*/;
+
+  /**
+   * This is a reference to an array nominally created in the IRB that contains
+   * the ExternalTextResource. It is intended to be shared between all instances
+   * of the ETR that have a common parent IRB.
+   */
+  private final TextResource[] cache;
+  private final int index;
+  private final String name;
+  private final String url;
+
+  public ExternalTextResourcePrototype(String name, String url,
+      TextResource[] cache, int index) {
+    this.name = name;
+    this.url = url;
+    this.cache = cache;
+    this.index = index;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Possibly fire off an HTTPRequest for the text resource.
+   */
+  public void getText(ResourceCallback<TextResource> callback)
+      throws ResourceException {
+
+    // If we've already parsed the JSON bundle, short-circuit.
+    if (cache[index] != null) {
+      callback.onSuccess(cache[index]);
+      return;
+    }
+
+    // Otherwise, fire an HTTP request.
+    RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);
+    try {
+      rb.sendRequest("", new ETRCallback(callback));
+    } catch (RequestException e) {
+      throw new ResourceException(this,
+          "Unable to initiate request for external resource", e);
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
new file mode 100644
index 0000000..11bcd64
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client.impl;
+
+import com.google.gwt.resources.client.ImageResource;
+
+/**
+ * This is part of an implementation of the ImageBundle optimization implemented
+ * with ClientBundle.
+ */
+public class ImageResourcePrototype implements ImageResource {
+
+  private final String name;
+  private final String url;
+  private final int left;
+  private final int top;
+  private final int width;
+  private final int height;
+
+  public ImageResourcePrototype(String name, String url, int left, int top,
+      int width, int height) {
+    this.name = name;
+    this.left = left;
+    this.top = top;
+    this.height = height;
+    this.width = width;
+    this.url = url;
+  }
+
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
+  public int getHeight() {
+    return height;
+  }
+
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
+  public int getLeft() {
+    return left;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
+  public int getTop() {
+    return top;
+  }
+
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
+  public String getURL() {
+    return url;
+  }
+
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
+  public int getWidth() {
+    return width;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/CssGenerationVisitor.java b/user/src/com/google/gwt/resources/css/CssGenerationVisitor.java
new file mode 100644
index 0000000..7fcfa95
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/CssGenerationVisitor.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css;
+
+import com.google.gwt.dev.util.TextOutput;
+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.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssNoFlip;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssPageRule;
+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.CssUrl;
+import com.google.gwt.resources.css.ast.CssVisitor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Generates a static CSS template string and provides information on where to
+ * inject dynamic expressions.
+ */
+public class CssGenerationVisitor extends CssVisitor {
+  private final TextOutput out;
+
+  private boolean needsOpenBrace;
+
+  private boolean needsComma;
+  private final boolean substituteDots;
+  private final SortedMap<Integer, List<CssNode>> substitutionPositions = new TreeMap<Integer, List<CssNode>>();
+
+  /**
+   * Constructor.
+   * 
+   * @param out the output hondler
+   */
+  public CssGenerationVisitor(TextOutput out) {
+    this(out, false);
+  }
+
+  /**
+   * Constructor for producing an abbreviated form of the template for use with
+   * {@link CssNode#toString()}.
+   * 
+   * @param out the output handler
+   * @param substituteDots if <code>true</code> locations in the text output
+   *          where expression substitutions would normally occur are replaced
+   *          with a textual placeholder
+   */
+  public CssGenerationVisitor(TextOutput out, boolean substituteDots) {
+    this.out = out;
+    this.substituteDots = substituteDots;
+  }
+
+  @Override
+  public void endVisit(CssIf x, Context ctx) {
+    // Match up an explanatory comment
+    out.indentOut();
+    out.printOpt("/* } */");
+    out.newlineOpt();
+  }
+
+  @Override
+  public void endVisit(CssMediaRule x, Context ctx) {
+    out.indentOut();
+    out.print("}");
+    out.newline();
+  }
+
+  @Override
+  public void endVisit(CssNoFlip x, Context ctx) {
+    out.printOpt("/*} @noflip */");
+    out.newlineOpt();
+  }
+
+  @Override
+  public void endVisit(CssPageRule x, Context ctx) {
+    out.indentOut();
+    out.print("}");
+    out.newline();
+  }
+
+  @Override
+  public void endVisit(CssRule x, Context ctx) {
+    if (!x.getProperties().isEmpty()) {
+      // Don't print empty rule blocks
+      closeBrace();
+    }
+  }
+
+  public SortedMap<Integer, List<CssNode>> getSubstitutionPositions() {
+    return substitutionPositions;
+  }
+
+  @Override
+  public boolean visit(CssDef x, Context ctx) {
+    // These are not valid CSS
+    out.printOpt("/* CssDef */");
+    out.newlineOpt();
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssEval x, Context ctx) {
+    // These are not valid CSS
+    out.printOpt("/* CssEval */");
+    out.newlineOpt();
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    // Record where the contents of the if block should be inserted
+    StringBuilder expr = new StringBuilder("/* @if ");
+    if (x.getExpression() != null) {
+      expr.append(x.getExpression()).append(" ");
+    } else {
+      expr.append(x.getPropertyName()).append(" ");
+      for (String v : x.getPropertyValues()) {
+        expr.append(v).append(" ");
+      }
+    }
+    expr.append("{ */");
+    out.printOpt(expr.toString());
+    out.newlineOpt();
+    out.indentIn();
+    addSubstitition(x);
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    out.print("@MEDIA");
+    for (String m : x.getMedias()) {
+      out.print(" " + m);
+    }
+    spaceOpt();
+    out.print("{");
+    out.newlineOpt();
+    out.indentIn();
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssNoFlip x, Context ctx) {
+    out.printOpt("/*@noflip { */)");
+    out.newlineOpt();
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssPageRule x, Context ctx) {
+    out.print("@page");
+    if (x.getPseudoPage() != null) {
+      out.print(" :");
+      out.print(x.getPseudoPage());
+    }
+    spaceOpt();
+    out.print("{");
+    out.newlineOpt();
+    out.indentIn();
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssProperty x, Context ctx) {
+    if (needsOpenBrace) {
+      openBrace();
+      needsOpenBrace = false;
+    }
+
+    out.print(x.getName());
+    colon();
+    addSubstitition(x);
+
+    if (x.isImportant()) {
+      important();
+    }
+
+    semi();
+
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    if (x.getProperties().isEmpty()) {
+      // Don't print empty rule blocks
+      return false;
+    }
+
+    needsOpenBrace = true;
+    needsComma = false;
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssSelector x, Context ctx) {
+    if (needsComma) {
+      comma();
+    }
+    needsComma = true;
+    out.print(x.getSelector());
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssSprite x, Context ctx) {
+    // These are not valid CSS
+    out.printOpt("/* CssSprite */");
+    out.newlineOpt();
+    addSubstitition(x);
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssUrl x, Context ctx) {
+    // These are not valid CSS
+    out.printOpt("/* CssUrl */");
+    out.newlineOpt();
+    return false;
+  }
+
+  private void addSubstitition(CssNode node) {
+    if (substituteDots) {
+      out.printOpt(".....");
+      out.newlineOpt();
+    } else {
+      int position = out.toString().length();
+      if (substitutionPositions.containsKey(position)) {
+        substitutionPositions.get(position).add(node);
+      } else {
+        List<CssNode> nodes = new ArrayList<CssNode>();
+        nodes.add(node);
+        substitutionPositions.put(position, nodes);
+      }
+    }
+  }
+
+  private void closeBrace() {
+    out.indentOut();
+    out.print('}');
+    out.newline();
+  }
+
+  private void colon() {
+    spaceOpt();
+    out.print(':');
+    spaceOpt();
+  }
+
+  private void comma() {
+    out.print(',');
+    spaceOpt();
+  }
+
+  private void important() {
+    out.print(" !important");
+  }
+
+  private void openBrace() {
+    spaceOpt();
+    out.print('{');
+    out.newlineOpt();
+    out.indentIn();
+  }
+
+  private void semi() {
+    out.print(';');
+    out.newlineOpt();
+  }
+
+  private void spaceOpt() {
+    out.printOpt(' ');
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/GenerateCssAst.java b/user/src/com/google/gwt/resources/css/GenerateCssAst.java
new file mode 100644
index 0000000..c1a9f8a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/GenerateCssAst.java
@@ -0,0 +1,1006 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.resources.css.ast.CssDef;
+import com.google.gwt.resources.css.ast.CssEval;
+import com.google.gwt.resources.css.ast.CssIf;
+import com.google.gwt.resources.css.ast.CssMediaRule;
+import com.google.gwt.resources.css.ast.CssNoFlip;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssPageRule;
+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.HasNodes;
+import com.google.gwt.resources.css.ast.HasProperties;
+import com.google.gwt.resources.css.ast.CssProperty.DotPathValue;
+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.StringValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import org.w3c.css.sac.AttributeCondition;
+import org.w3c.css.sac.CSSException;
+import org.w3c.css.sac.CSSParseException;
+import org.w3c.css.sac.CharacterDataSelector;
+import org.w3c.css.sac.CombinatorCondition;
+import org.w3c.css.sac.Condition;
+import org.w3c.css.sac.ConditionalSelector;
+import org.w3c.css.sac.ContentCondition;
+import org.w3c.css.sac.DescendantSelector;
+import org.w3c.css.sac.DocumentHandler;
+import org.w3c.css.sac.ElementSelector;
+import org.w3c.css.sac.ErrorHandler;
+import org.w3c.css.sac.InputSource;
+import org.w3c.css.sac.LangCondition;
+import org.w3c.css.sac.LexicalUnit;
+import org.w3c.css.sac.NegativeCondition;
+import org.w3c.css.sac.NegativeSelector;
+import org.w3c.css.sac.PositionalCondition;
+import org.w3c.css.sac.ProcessingInstructionSelector;
+import org.w3c.css.sac.SACMediaList;
+import org.w3c.css.sac.Selector;
+import org.w3c.css.sac.SelectorList;
+import org.w3c.css.sac.SiblingSelector;
+import org.w3c.flute.parser.Parser;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Generates a CssStylesheet from the contents of a URL.
+ */
+public class GenerateCssAst {
+
+  /**
+   * Maps SAC CSSParseExceptions into a TreeLogger. All parsing errors will be
+   * recorded in a single TreeLogger branch, which will be created only if a
+   * loggable error message is emitted.
+   */
+  private static class Errors implements ErrorHandler {
+    /**
+     * A flag that controls whether or not the exec method will fail.
+     */
+    private boolean fatalErrorEncountered;
+    private TreeLogger logger;
+    private final TreeLogger parentLogger;
+
+    /**
+     * Constructor.
+     * 
+     * @param parentLogger the TreeLogger that should be branched to produce the
+     *          CSS parsing messages.
+     */
+    public Errors(TreeLogger parentLogger) {
+      this.parentLogger = parentLogger;
+    }
+
+    public void error(CSSParseException exception) throws CSSException {
+      // TODO Since this indicates a loss of data, should this be a fatal error?
+      log(TreeLogger.WARN, exception);
+    }
+
+    public void fatalError(CSSParseException exception) throws CSSException {
+      log(TreeLogger.ERROR, exception);
+    }
+
+    public void log(TreeLogger.Type type, String message) {
+      log(type, message, null);
+    }
+
+    public void log(TreeLogger.Type type, String message, Throwable t) {
+      fatalErrorEncountered |= type == TreeLogger.Type.ERROR;
+      if (parentLogger.isLoggable(type)) {
+        maybeBranch();
+        logger.log(type, message, t);
+      }
+    }
+
+    public void warning(CSSParseException exception) throws CSSException {
+      log(TreeLogger.DEBUG, exception);
+    }
+
+    private void log(TreeLogger.Type type, CSSParseException e) {
+      log(type, "Line " + e.getLineNumber() + " column " + e.getColumnNumber()
+          + ": " + e.getMessage());
+    }
+
+    private void maybeBranch() {
+      if (logger == null) {
+        logger = parentLogger.branch(TreeLogger.INFO,
+            "The following problems were detected");
+      }
+    }
+  }
+
+  /**
+   * Maps the SAC model into our own CSS AST nodes.
+   */
+  private static class GenerationHandler implements DocumentHandler {
+    /**
+     * The stylesheet that is being composed.
+     */
+    private final CssStylesheet css = new CssStylesheet();
+
+    /**
+     * Accumulates CSS nodes as they are created.
+     */
+    private final Stack<HasNodes> currentParent = new Stack<HasNodes>();
+
+    /**
+     * Accumulates CSS properties as they are seen.
+     */
+    private HasProperties currentRule;
+
+    /**
+     * Records references to {@code @def} rules.
+     */
+    private final Map<String, CssDef> defs = new HashMap<String, CssDef>();
+
+    /**
+     * Used when parsing the contents of meta-styles.
+     */
+    private final Errors errors;
+
+    /**
+     * Used by {@link #startSelector(SelectorList)} to suppress the creation of
+     * new CssRules in favor of retaining {@link #currentRule}.
+     */
+    private boolean nextSelectorCreatesRule = true;
+
+    public GenerationHandler(Errors errors) {
+      this.errors = errors;
+      currentParent.push(css);
+    }
+
+    public void comment(String text) throws CSSException {
+      // Ignore comments
+      // TODO Should comments be retained but not generally printed?
+    }
+
+    public void endDocument(InputSource source) throws CSSException {
+    }
+
+    public void endFontFace() throws CSSException {
+    }
+
+    public void endMedia(SACMediaList media) throws CSSException {
+      currentParent.pop();
+    }
+
+    public void endPage(String name, String pseudoPage) throws CSSException {
+    }
+
+    public void endSelector(SelectorList selectors) throws CSSException {
+    }
+
+    /**
+     * Reflectively invoke a method named parseRule on this instance.
+     */
+    public void ignorableAtRule(String atRule) throws CSSException {
+      String ruleName = atRule.substring(1, atRule.indexOf(" "));
+      String methodName = "parse" + (Character.toUpperCase(ruleName.charAt(0)))
+          + ruleName.substring(1);
+      try {
+        Method parseMethod = getClass().getDeclaredMethod(methodName,
+            String.class);
+        parseMethod.invoke(this, atRule);
+      } catch (NoSuchMethodException e) {
+        errors.log(TreeLogger.WARN, "Ignoring @" + ruleName);
+      } catch (IllegalAccessException e) {
+        errors.log(TreeLogger.ERROR, "Unable to invoke parse method ", e);
+      } catch (InvocationTargetException e) {
+        if (e.getCause() instanceof CSSException) {
+          throw (CSSException) e.getCause();
+        }
+
+        errors.log(TreeLogger.ERROR, "Unable to invoke parse method ", e);
+      }
+    }
+
+    public void importStyle(String uri, SACMediaList media,
+        String defaultNamespaceURI) throws CSSException {
+    }
+
+    public void namespaceDeclaration(String prefix, String uri)
+        throws CSSException {
+    }
+
+    public void property(String name, LexicalUnit value, boolean important)
+        throws CSSException {
+      List<Value> values = new ArrayList<Value>();
+      if (value != null) {
+        extractValueOf(values, value);
+      }
+      currentRule.getProperties().add(
+          new CssProperty(escapeIdent(name), new ListValue(values), important));
+    }
+
+    public void startDocument(InputSource source) throws CSSException {
+    }
+
+    public void startFontFace() throws CSSException {
+    }
+
+    public void startMedia(SACMediaList media) throws CSSException {
+      CssMediaRule r = new CssMediaRule();
+      for (int i = 0; i < media.getLength(); i++) {
+        r.getMedias().add(media.item(i));
+      }
+
+      pushParent(r);
+    }
+
+    public void startPage(String name, String pseudoPage) throws CSSException {
+      CssPageRule r = new CssPageRule();
+      // name appears to be unused in CSS2
+      r.setPseudoPage(pseudoPage);
+      addNode(r);
+      currentRule = r;
+    }
+
+    public void startSelector(SelectorList selectors) throws CSSException {
+      CssRule r;
+
+      if (nextSelectorCreatesRule) {
+        r = new CssRule();
+        addNode(r);
+        currentRule = r;
+      } else {
+        r = (CssRule) currentRule;
+        nextSelectorCreatesRule = true;
+      }
+
+      for (int i = 0; i < selectors.getLength(); i++) {
+        r.getSelectors().add(new CssSelector(valueOf(selectors.item(i))));
+      }
+    }
+
+    void parseDef(String atRule) {
+      String value = atRule.substring(4, atRule.length()).trim();
+
+      InputSource s = new InputSource();
+      s.setCharacterStream(new StringReader(value));
+      Parser parser = new Parser();
+      parser.setErrorHandler(errors);
+
+      final List<Value> values = new ArrayList<Value>();
+      parser.setDocumentHandler(new PropertyExtractor(values));
+
+      try {
+        String dummy = "* { prop : " + value + "}";
+        parser.parseStyleSheet(new InputSource(new StringReader(dummy)));
+      } catch (IOException e) {
+        assert false : "Should never happen";
+      }
+
+      if (values.size() < 2) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "@def rules must specify an identifier and one or more values",
+            null);
+      }
+
+      IdentValue defName = values.get(0).isIdentValue();
+
+      if (defName == null) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "First lexical unit must be an identifier", null);
+      }
+
+      /*
+       * Replace any references to previously-seen @def constructs. We do
+       * expansion up-front to prevent the need for cycle-detection later.
+       */
+      for (ListIterator<Value> it = values.listIterator(1); it.hasNext();) {
+        IdentValue maybeDefReference = it.next().isIdentValue();
+        if (maybeDefReference != null) {
+          CssDef previousDef = defs.get(maybeDefReference.getIdent());
+          if (previousDef != null) {
+            it.remove();
+            for (Value previousValue : previousDef.getValues()) {
+              it.add(previousValue);
+            }
+          }
+        }
+      }
+
+      CssDef def = new CssDef(defName.getIdent());
+      def.getValues().addAll(values.subList(1, values.size()));
+      addNode(def);
+
+      defs.put(defName.getIdent(), def);
+    }
+
+    /**
+     * The elif nodes are processed as though they were {@code @if} nodes. The
+     * newly-generated CssIf node will be attached to the last CssIf in the
+     * if/else chain.
+     */
+    void parseElif(String atRule) throws CSSException {
+      List<CssNode> nodes = currentParent.peek().getNodes();
+      CssIf lastIf = findLastIfInChain(nodes);
+
+      if (lastIf == null) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "@elif must immediately follow an @if or @elif", null);
+      }
+
+      assert lastIf.getElseNodes().isEmpty();
+
+      // @elif -> lif (because parseIf strips the first three chars)
+      parseIf(atRule.substring(2));
+
+      // Fix up the structure by remove the newly-created node from the parent
+      // context and moving it to the end of the @if chain
+      lastIf.getElseNodes().add(nodes.remove(nodes.size() - 1));
+    }
+
+    /**
+     * The else nodes are processed as though they were written as {@code @elif
+     * true} rules.
+     */
+    void parseElse(String atRule) throws CSSException {
+      // The last CssIf in the if/else chain
+      CssIf lastIf = findLastIfInChain(currentParent.peek().getNodes());
+
+      if (lastIf == null) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "@else must immediately follow an @if or @elif", null);
+      }
+
+      // Create the CssIf to hold the @else rules
+      String fakeElif = "@elif (true) " + atRule.substring(atRule.indexOf("{"));
+      parseElif(fakeElif);
+      CssIf elseIf = findLastIfInChain(currentParent.peek().getNodes());
+
+      assert lastIf.getElseNodes().size() == 1
+          && lastIf.getElseNodes().get(0) == elseIf;
+      assert elseIf.getElseNodes().isEmpty();
+
+      // Merge the rules into the last CssIf to break the chain and prevent
+      // @else followed by @else
+      lastIf.getElseNodes().clear();
+      lastIf.getElseNodes().addAll(elseIf.getNodes());
+    }
+
+    void parseEval(String atRule) throws CSSException {
+      // @eval key com.google.Type.staticFunction
+      String[] parts = atRule.substring(0, atRule.length() - 1).split("\\s");
+
+      if (parts.length != 3) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "Incorrect number of parts for @eval", null);
+      }
+
+      CssEval eval = new CssEval(parts[1], parts[2]);
+      addNode(eval);
+    }
+
+    void parseIf(String atRule) throws CSSException {
+      String predicate = atRule.substring(3, atRule.indexOf('{') - 1).trim();
+      String blockContents = atRule.substring(atRule.indexOf('{') + 1,
+          atRule.length() - 1);
+
+      CssIf cssIf = new CssIf();
+
+      if (predicate.startsWith("(") && predicate.endsWith(")")) {
+        cssIf.setExpression(predicate);
+      } else {
+
+        String[] predicateParts = predicate.split("\\s");
+
+        switch (predicateParts.length) {
+          case 0:
+            throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+                "Incorrect format for @if predicate", null);
+          case 1:
+            if (predicateParts[0].length() == 0) {
+              throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+                  "Incorrect format for @if predicate", null);
+            }
+            errors.log(
+                TreeLogger.WARN,
+                "Deprecated syntax for Java expression detected. Enclose the expression in parentheses");
+            cssIf.setExpression(predicateParts[0]);
+            break;
+          default:
+            if (predicateParts[0].startsWith("!")) {
+              cssIf.setNegated(true);
+              cssIf.setProperty(predicateParts[0].substring(1));
+            } else {
+              cssIf.setProperty(predicateParts[0]);
+            }
+            String[] values = new String[predicateParts.length - 1];
+            System.arraycopy(predicateParts, 1, values, 0, values.length);
+            cssIf.setPropertyValues(values);
+        }
+      }
+
+      parseInnerStylesheet("@if", cssIf, blockContents);
+    }
+
+    void parseNoflip(String atRule) throws CSSException {
+      String blockContents = atRule.substring(atRule.indexOf('{') + 1,
+          atRule.length() - 1);
+
+      parseInnerStylesheet("@noflip", new CssNoFlip(), blockContents);
+    }
+
+    void parseSprite(String atRule) throws CSSException {
+      CssSprite sprite = new CssSprite();
+      currentRule = sprite;
+      addNode(sprite);
+
+      // Flag to tell startSelector() to use the CssSprite instead of creating
+      // its own CssRule.
+      nextSelectorCreatesRule = false;
+
+      // parse the inner text
+      InputSource s = new InputSource();
+      s.setCharacterStream(new StringReader(atRule.substring(7)));
+      Parser parser = new Parser();
+      parser.setDocumentHandler(this);
+      parser.setErrorHandler(errors);
+
+      try {
+        parser.parseRule(s);
+      } catch (IOException e) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "Unable to parse @sprite", e);
+      }
+    }
+
+    void parseUrl(String atRule) throws CSSException {
+      // @url key dataResourceFunction
+      String[] parts = atRule.substring(0, atRule.length() - 1).split("\\s");
+
+      if (parts.length != 3) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+            "Incorrect number of parts for @url", null);
+      }
+
+      CssUrl url = new CssUrl(parts[1], parts[2]);
+      addNode(url);
+    }
+
+    /**
+     * Add a node to the current parent.
+     */
+    private void addNode(CssNode node) {
+      currentParent.peek().getNodes().add(node);
+    }
+
+    private <T extends CssNode & HasNodes> void parseInnerStylesheet(
+        String tagName, T parent, String blockContents) {
+      pushParent(parent);
+
+      // parse the inner text
+      InputSource s = new InputSource();
+      s.setCharacterStream(new StringReader(blockContents));
+      Parser parser = new Parser();
+      parser.setDocumentHandler(this);
+      parser.setErrorHandler(errors);
+
+      try {
+        parser.parseStyleSheet(s);
+      } catch (IOException e) {
+        throw new CSSException(CSSException.SAC_SYNTAX_ERR, "Unable to parse "
+            + tagName, e);
+      }
+
+      if (currentParent.pop() != parent) {
+        // This is a coding error
+        throw new RuntimeException("Incorrect element popped");
+      }
+    }
+
+    /**
+     * Adds a node to the current parent and then makes the node the current
+     * parent node.
+     */
+    private <T extends CssNode & HasNodes> void pushParent(T newParent) {
+      addNode(newParent);
+      currentParent.push(newParent);
+    }
+  }
+
+  /**
+   * Extracts all properties in a document into a List.
+   */
+  private static class PropertyExtractor implements DocumentHandler {
+    private final List<Value> values;
+
+    private PropertyExtractor(List<Value> values) {
+      this.values = values;
+    }
+
+    public void comment(String text) throws CSSException {
+    }
+
+    public void endDocument(InputSource source) throws CSSException {
+    }
+
+    public void endFontFace() throws CSSException {
+    }
+
+    public void endMedia(SACMediaList media) throws CSSException {
+    }
+
+    public void endPage(String name, String pseudoPage) throws CSSException {
+    }
+
+    public void endSelector(SelectorList selectors) throws CSSException {
+    }
+
+    public void ignorableAtRule(String atRule) throws CSSException {
+    }
+
+    public void importStyle(String uri, SACMediaList media,
+        String defaultNamespaceURI) throws CSSException {
+    }
+
+    public void namespaceDeclaration(String prefix, String uri)
+        throws CSSException {
+    }
+
+    public void property(String name, LexicalUnit value, boolean important)
+        throws CSSException {
+      extractValueOf(values, value);
+    }
+
+    public void startDocument(InputSource source) throws CSSException {
+    }
+
+    public void startFontFace() throws CSSException {
+    }
+
+    public void startMedia(SACMediaList media) throws CSSException {
+    }
+
+    public void startPage(String name, String pseudoPage) throws CSSException {
+    }
+
+    public void startSelector(SelectorList selectors) throws CSSException {
+    }
+  }
+
+  private static final String LITERAL_FUNCTION_NAME = "literal";
+
+  private static final String VALUE_FUNCTION_NAME = "value";
+
+  /**
+   * Create a CssStylesheet from the contents of a URL.
+   */
+  public static CssStylesheet exec(TreeLogger logger, URL[] stylesheets)
+      throws UnableToCompleteException {
+    Parser p = new Parser();
+    Errors errors = new Errors(logger);
+    GenerationHandler g = new GenerationHandler(errors);
+    p.setDocumentHandler(g);
+    p.setErrorHandler(errors);
+
+    for (URL stylesheet : stylesheets) {
+      TreeLogger branchLogger = logger.branch(TreeLogger.DEBUG,
+          "Parsing CSS stylesheet " + stylesheet.toExternalForm());
+      try {
+        p.parseStyleSheet(stylesheet.toURI().toString());
+        continue;
+      } catch (CSSException e) {
+        branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
+      } catch (IOException e) {
+        branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
+      } catch (URISyntaxException e) {
+        branchLogger.log(TreeLogger.ERROR, "Unable to parse CSS", e);
+      }
+      throw new UnableToCompleteException();
+    }
+
+    if (errors.fatalErrorEncountered) {
+      // Logging will have been performed by the Errors instance, just exit
+      throw new UnableToCompleteException();
+    }
+
+    return g.css;
+  }
+
+  /**
+   * Expresses an rgb function as a hex expression.
+   * 
+   * @param colors a sequence of LexicalUnits, assumed to be
+   *          <code>(INT COMMA INT COMMA INT)</code>
+   * @return the minimal hex expression for the RGB color values
+   */
+  private static Value colorValue(LexicalUnit colors) {
+    LexicalUnit red = colors;
+    assert red.getLexicalUnitType() == LexicalUnit.SAC_INTEGER;
+    LexicalUnit green = red.getNextLexicalUnit().getNextLexicalUnit();
+    assert green.getLexicalUnitType() == LexicalUnit.SAC_INTEGER;
+    LexicalUnit blue = green.getNextLexicalUnit().getNextLexicalUnit();
+    assert blue.getLexicalUnitType() == LexicalUnit.SAC_INTEGER;
+
+    int r = Math.min(red.getIntegerValue(), 255);
+    int g = Math.min(green.getIntegerValue(), 255);
+    int b = Math.min(blue.getIntegerValue(), 255);
+
+    String sr = Integer.toHexString(r);
+    if (sr.length() == 1) {
+      sr = "0" + sr;
+    }
+
+    String sg = Integer.toHexString(g);
+    if (sg.length() == 1) {
+      sg = "0" + sg;
+    }
+
+    String sb = Integer.toHexString(b);
+    if (sb.length() == 1) {
+      sb = "0" + sb;
+    }
+
+    // #AABBCC --> #ABC
+    if (sr.charAt(0) == sr.charAt(1) && sg.charAt(0) == sg.charAt(1)
+        && sb.charAt(0) == sb.charAt(1)) {
+      sr = sr.substring(1);
+      sg = sg.substring(1);
+      sb = sb.substring(1);
+    }
+
+    return new IdentValue("#" + sr + sg + sb);
+  }
+
+  private static String escapeIdent(String selector) {
+    assert selector.length() > 0;
+
+    StringBuilder toReturn = new StringBuilder();
+    if (!isIdentStart(selector.charAt(0))) {
+      toReturn.append('\\');
+    }
+    toReturn.append(selector.charAt(0));
+
+    if (selector.length() > 1) {
+      for (char c : selector.substring(1).toCharArray()) {
+        if (!isIdentPart(c)) {
+          toReturn.append('\\');
+        }
+        toReturn.append(c);
+      }
+    }
+    return toReturn.toString();
+  }
+
+  /**
+   * Convert a LexicalUnit list into a List of Values.
+   */
+  private static void extractValueOf(List<Value> accumulator, LexicalUnit value) {
+    do {
+      accumulator.add(valueOf(value));
+      value = value.getNextLexicalUnit();
+    } while (value != null);
+  }
+
+  /**
+   * The elif and else constructs are modeled as nested if statements in the
+   * CssIf's elseNodes field. This method will search a list of CssNodes and
+   * remove the last chained CssIf from the last element in the list of nodes.
+   */
+  private static CssIf findLastIfInChain(List<CssNode> nodes) {
+    if (nodes.isEmpty()) {
+      return null;
+    }
+
+    CssNode lastNode = nodes.get(nodes.size() - 1);
+    if (lastNode instanceof CssIf) {
+      CssIf asIf = (CssIf) lastNode;
+      if (asIf.getElseNodes().isEmpty()) {
+        return asIf;
+      } else {
+        return findLastIfInChain(asIf.getElseNodes());
+      }
+    }
+    return null;
+  }
+
+  private static boolean isIdentPart(char c) {
+    return Character.isLetterOrDigit(c) || (c == '\\') || (c == '-');
+  }
+
+  private static boolean isIdentStart(char c) {
+    return Character.isLetter(c) || (c == '\\');
+  }
+
+  /**
+   * Utility method to concatenate strings.
+   */
+  private static String join(Iterable<Value> elements, String separator) {
+    StringBuilder b = new StringBuilder();
+    for (Iterator<Value> i = elements.iterator(); i.hasNext();) {
+      b.append(i.next().toCss());
+      if (i.hasNext()) {
+        b.append(separator);
+      }
+    }
+    return b.toString();
+  }
+
+  private static String maybeUnquote(String s) {
+    if (s.startsWith("\"") && s.endsWith("\"")) {
+      return s.substring(1, s.length() - 1);
+    }
+    return s;
+  }
+
+  /**
+   * Used when evaluating literal() rules.
+   */
+  private static String unescapeLiteral(String s) {
+    s = s.replaceAll(Pattern.quote("\\\""), "\"");
+    s = s.replaceAll(Pattern.quote("\\\\"), Matcher.quoteReplacement("\\"));
+    return s;
+  }
+
+  private static String valueOf(Condition condition) {
+    if (condition instanceof AttributeCondition) {
+      AttributeCondition c = (AttributeCondition) condition;
+      switch (c.getConditionType()) {
+        case Condition.SAC_ATTRIBUTE_CONDITION:
+          return "[" + c.getLocalName()
+              + (c.getValue() != null ? "=\"" + c.getValue() + '"' : "") + "]";
+        case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION:
+          return "[" + c.getLocalName() + "~=\"" + c.getValue() + "\"]";
+        case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION:
+          return "[" + c.getLocalName() + "|=\"" + c.getValue() + "\"]";
+        case Condition.SAC_ID_CONDITION:
+          return "#" + c.getValue();
+        case Condition.SAC_CLASS_CONDITION:
+          return "." + c.getValue();
+        case Condition.SAC_PSEUDO_CLASS_CONDITION:
+          return ":" + c.getValue();
+      }
+
+    } else if (condition instanceof CombinatorCondition) {
+      CombinatorCondition c = (CombinatorCondition) condition;
+      switch (condition.getConditionType()) {
+        case Condition.SAC_AND_CONDITION:
+          return valueOf(c.getFirstCondition())
+              + valueOf(c.getSecondCondition());
+        case Condition.SAC_OR_CONDITION:
+          // Unimplemented in CSS2?
+      }
+
+    } else if (condition instanceof ContentCondition) {
+      // Unimplemented in CSS2?
+
+    } else if (condition instanceof LangCondition) {
+      LangCondition c = (LangCondition) condition;
+      return ":lang(" + c.getLang() + ")";
+
+    } else if (condition instanceof NegativeCondition) {
+      // Unimplemented in CSS2?
+    } else if (condition instanceof PositionalCondition) {
+      // Unimplemented in CSS2?
+    }
+
+    throw new RuntimeException("Unhandled condition of type "
+        + condition.getConditionType() + " " + condition.getClass().getName());
+  }
+
+  private static Value valueOf(LexicalUnit value) {
+    switch (value.getLexicalUnitType()) {
+      case LexicalUnit.SAC_ATTR:
+        return new IdentValue("attr(" + value.getStringValue() + ")");
+      case LexicalUnit.SAC_IDENT:
+        return new IdentValue(escapeIdent(value.getStringValue()));
+      case LexicalUnit.SAC_STRING_VALUE:
+        return new StringValue(value.getStringValue());
+      case LexicalUnit.SAC_RGBCOLOR:
+        // flute models the commas as operators so no separator needed
+        return colorValue(value.getParameters());
+      case LexicalUnit.SAC_INTEGER:
+        return new NumberValue(value.getIntegerValue());
+      case LexicalUnit.SAC_REAL:
+        return new NumberValue(value.getFloatValue());
+      case LexicalUnit.SAC_CENTIMETER:
+      case LexicalUnit.SAC_DEGREE:
+      case LexicalUnit.SAC_DIMENSION:
+      case LexicalUnit.SAC_EM:
+      case LexicalUnit.SAC_EX:
+      case LexicalUnit.SAC_GRADIAN:
+      case LexicalUnit.SAC_HERTZ:
+      case LexicalUnit.SAC_KILOHERTZ:
+      case LexicalUnit.SAC_MILLIMETER:
+      case LexicalUnit.SAC_MILLISECOND:
+      case LexicalUnit.SAC_PERCENTAGE:
+      case LexicalUnit.SAC_PICA:
+      case LexicalUnit.SAC_PIXEL:
+      case LexicalUnit.SAC_POINT:
+      case LexicalUnit.SAC_RADIAN:
+      case LexicalUnit.SAC_SECOND:
+        return new NumberValue(value.getFloatValue(),
+            value.getDimensionUnitText());
+      case LexicalUnit.SAC_URI:
+        return new IdentValue("url(" + value.getStringValue() + ")");
+      case LexicalUnit.SAC_OPERATOR_COMMA:
+        return new IdentValue(",");
+      case LexicalUnit.SAC_COUNTER_FUNCTION:
+      case LexicalUnit.SAC_COUNTERS_FUNCTION:
+      case LexicalUnit.SAC_FUNCTION: {
+        if (value.getFunctionName().equals(VALUE_FUNCTION_NAME)) {
+          // This is a call to value()
+          List<Value> params = new ArrayList<Value>();
+          extractValueOf(params, value.getParameters());
+
+          if (params.size() != 1 && params.size() != 3) {
+            throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+                "Incorrect number of parameters to " + VALUE_FUNCTION_NAME,
+                null);
+          }
+
+          Value dotPathValue = params.get(0);
+          String dotPath = maybeUnquote(((StringValue) dotPathValue).getValue());
+          String suffix = params.size() == 3
+              ? maybeUnquote(((StringValue) params.get(2)).getValue()) : "";
+
+          return new DotPathValue(dotPath, suffix);
+        } else if (value.getFunctionName().equals(LITERAL_FUNCTION_NAME)) {
+          // This is a call to value()
+          List<Value> params = new ArrayList<Value>();
+          extractValueOf(params, value.getParameters());
+
+          if (params.size() != 1) {
+            throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+                "Incorrect number of parameters to " + LITERAL_FUNCTION_NAME,
+                null);
+          }
+
+          Value expression = params.get(0);
+          if (!(expression instanceof StringValue)) {
+            throw new CSSException(CSSException.SAC_SYNTAX_ERR,
+                "The single argument to " + LITERAL_FUNCTION_NAME
+                    + " must be a string value", null);
+          }
+
+          String s = maybeUnquote(((StringValue) expression).getValue());
+          s = unescapeLiteral(s);
+
+          return new IdentValue(s);
+
+        } else {
+          // Just return a String representation of the original value
+          List<Value> parameters = new ArrayList<Value>();
+          extractValueOf(parameters, value.getParameters());
+          return new IdentValue(value.getFunctionName() + "("
+              + join(parameters, "") + ")");
+        }
+      }
+      case LexicalUnit.SAC_INHERIT:
+        return new IdentValue("inherit");
+      case LexicalUnit.SAC_OPERATOR_EXP:
+        return new IdentValue("^");
+      case LexicalUnit.SAC_OPERATOR_GE:
+        return new IdentValue(">=");
+      case LexicalUnit.SAC_OPERATOR_GT:
+        return new IdentValue(">");
+      case LexicalUnit.SAC_OPERATOR_LE:
+        return new IdentValue("<=");
+      case LexicalUnit.SAC_OPERATOR_LT:
+        return new IdentValue("<");
+      case LexicalUnit.SAC_OPERATOR_MINUS:
+        return new IdentValue("-");
+      case LexicalUnit.SAC_OPERATOR_MOD:
+        return new IdentValue("%");
+      case LexicalUnit.SAC_OPERATOR_MULTIPLY:
+        return new IdentValue("*");
+      case LexicalUnit.SAC_OPERATOR_PLUS:
+        return new IdentValue("+");
+      case LexicalUnit.SAC_OPERATOR_SLASH:
+        return new IdentValue("/");
+      case LexicalUnit.SAC_OPERATOR_TILDE:
+        return new IdentValue("~");
+      case LexicalUnit.SAC_RECT_FUNCTION: {
+        // Just return this as a String
+        List<Value> parameters = new ArrayList<Value>();
+        extractValueOf(parameters, value.getParameters());
+        return new IdentValue("rect(" + join(parameters, "") + ")");
+      }
+      case LexicalUnit.SAC_SUB_EXPRESSION:
+        // Should have been taken care of by our own traversal
+      case LexicalUnit.SAC_UNICODERANGE:
+        // Cannot be expressed in CSS2
+    }
+    throw new RuntimeException("Unhandled LexicalUnit type "
+        + value.getLexicalUnitType());
+  }
+
+  private static String valueOf(Selector selector) {
+    if (selector instanceof CharacterDataSelector) {
+      // Unimplemented in CSS2?
+
+    } else if (selector instanceof ConditionalSelector) {
+      ConditionalSelector s = (ConditionalSelector) selector;
+      String simpleSelector = valueOf(s.getSimpleSelector());
+
+      if ("*".equals(simpleSelector)) {
+        // Don't need the extra * for compound selectors
+        return valueOf(s.getCondition());
+      } else {
+        return simpleSelector + valueOf(s.getCondition());
+      }
+
+    } else if (selector instanceof DescendantSelector) {
+      DescendantSelector s = (DescendantSelector) selector;
+      switch (s.getSelectorType()) {
+        case Selector.SAC_CHILD_SELECTOR:
+          if (s.getSimpleSelector().getSelectorType() == Selector.SAC_PSEUDO_ELEMENT_SELECTOR) {
+            return valueOf(s.getAncestorSelector()) + ":"
+                + valueOf(s.getSimpleSelector());
+          } else {
+            return valueOf(s.getAncestorSelector()) + ">"
+                + valueOf(s.getSimpleSelector());
+          }
+        case Selector.SAC_DESCENDANT_SELECTOR:
+          return valueOf(s.getAncestorSelector()) + " "
+              + valueOf(s.getSimpleSelector());
+      }
+
+    } else if (selector instanceof ElementSelector) {
+      ElementSelector s = (ElementSelector) selector;
+      if (s.getLocalName() == null) {
+        return "*";
+      } else {
+        return escapeIdent(s.getLocalName());
+      }
+
+    } else if (selector instanceof NegativeSelector) {
+      // Unimplemented in CSS2?
+
+    } else if (selector instanceof ProcessingInstructionSelector) {
+      // Unimplemented in CSS2?
+
+    } else if (selector instanceof SiblingSelector) {
+      SiblingSelector s = (SiblingSelector) selector;
+      return valueOf(s.getSelector()) + "+" + valueOf(s.getSiblingSelector());
+    }
+
+    throw new RuntimeException("Unhandled selector of type "
+        + selector.getClass().getName());
+  }
+
+  /**
+   * Utility class.
+   */
+  private GenerateCssAst() {
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/Minify.java b/user/src/com/google/gwt/resources/css/Minify.java
new file mode 100644
index 0000000..1a75c5b
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/Minify.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.util.DefaultTextOutput;
+import com.google.gwt.dev.util.TextOutput;
+import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
+import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.util.tools.ArgHandlerExtra;
+import com.google.gwt.util.tools.ArgHandlerFlag;
+import com.google.gwt.util.tools.ToolBase;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * This is a command-line utility to minify a GWT CSS stylesheet.
+ */
+public class Minify extends ToolBase {
+
+  /**
+   * See {@link #printHelp()} for usage.
+   */
+  public static void main(String[] args) {
+    (new Minify()).exec(args);
+  }
+
+  private final PrintWriterTreeLogger logger = new PrintWriterTreeLogger(
+      new PrintWriter(System.err));
+  private URL source;
+  private boolean pretty;
+
+  private Minify() {
+    logger.setMaxDetail(TreeLogger.DEBUG);
+  }
+
+  @Override
+  protected String getDescription() {
+    return "Minify a CSS file";
+  }
+
+  private void exec(String[] args) {
+    registerHandler(new ArgHandlerExtra() {
+
+      @Override
+      public boolean addExtraArg(String arg) {
+        if (source != null) {
+          return false;
+        }
+
+        try {
+          source = (new File(arg)).toURL();
+          return true;
+        } catch (MalformedURLException e) {
+          e.printStackTrace();
+          return false;
+        }
+      }
+
+      @Override
+      public String getPurpose() {
+        return "The css file to process";
+      }
+
+      @Override
+      public String[] getTagArgs() {
+        return new String[] {"stylesheet.css"};
+      }
+    });
+
+    registerHandler(new ArgHandlerFlag() {
+      @Override
+      public String getPurpose() {
+        return "Enable human-parsable output";
+      }
+
+      @Override
+      public String getTag() {
+        return "-pretty";
+      }
+
+      @Override
+      public boolean setFlag() {
+        return pretty = true;
+      }
+    });
+
+    if (!processArgs(args)) {
+      System.exit(-1);
+    }
+
+    if (source == null) {
+      printHelp();
+      System.exit(-1);
+    }
+
+    try {
+      CssStylesheet sheet = GenerateCssAst.exec(logger, new URL[] {source});
+      TextOutput out = new DefaultTextOutput(!pretty);
+      (new CssGenerationVisitor(out)).accept(sheet);
+      System.out.println(out.toString());
+    } catch (UnableToCompleteException e) {
+      logger.log(TreeLogger.ERROR, "Unable to compile CSS");
+    }
+
+    System.exit(0);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CollapsedNode.java b/user/src/com/google/gwt/resources/css/ast/CollapsedNode.java
new file mode 100644
index 0000000..f3dae4d
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CollapsedNode.java
@@ -0,0 +1,44 @@
+/*
+ * 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.ast;
+
+import java.util.List;
+
+/**
+ * This delegate class bypasses traversal of a node, instead traversing the
+ * node's children. Any modifications made to the node list of the CollapsedNode
+ * will be reflected in the original node.
+ */
+public class CollapsedNode extends CssNode implements HasNodes {
+
+  private final List<CssNode> nodes;
+
+  public CollapsedNode(HasNodes parent) {
+    this(parent.getNodes());
+  }
+
+  public CollapsedNode(List<CssNode> nodes) {
+    this.nodes = nodes;
+  }
+
+  public List<CssNode> getNodes() {
+    return nodes;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.acceptWithInsertRemove(getNodes());
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/ast/Context.java b/user/src/com/google/gwt/resources/css/ast/Context.java
new file mode 100644
index 0000000..9ee2608
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/Context.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+/**
+ * The context in which a CSSNode visitation occurs. This represents the set of
+ * possible operations a CSSVisitor subclass can perform on the currently
+ * visited node.
+ */
+public interface Context {
+  boolean canInsert();
+
+  boolean canRemove();
+
+  void insertAfter(CssNode node);
+
+  void insertBefore(CssNode node);
+
+  void removeMe();
+
+  void replaceMe(CssNode node);
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssCompilerException.java b/user/src/com/google/gwt/resources/css/ast/CssCompilerException.java
new file mode 100644
index 0000000..044d306
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssCompilerException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+/**
+ * Used by {@link CssVisitor}.
+ */
+public class CssCompilerException extends RuntimeException {
+  public CssCompilerException(String message) {
+    super(message);
+  }
+  
+  public CssCompilerException(String message, Throwable cause) {
+    super(message, cause);
+  }
+  
+  void addNode(CssNode node) {
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssDef.java b/user/src/com/google/gwt/resources/css/ast/CssDef.java
new file mode 100644
index 0000000..2506989
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssDef.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A constant definition.
+ */
+public class CssDef extends CssNode {
+  private final String key;
+  private final List<Value> values = new ArrayList<Value>();
+
+  public CssDef(String key) {
+    this.key = key;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public List<Value> getValues() {
+    return values;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.visit(this, context);
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssEval.java b/user/src/com/google/gwt/resources/css/ast/CssEval.java
new file mode 100644
index 0000000..8a5ff98
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssEval.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.resources.css.ast.CssProperty.ExpressionValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A definition that is evaluated at runtime.
+ */
+public class CssEval extends CssDef {
+  private final List<Value> values;
+
+  public CssEval(String key, String expression) {
+    super(key);
+    Value value = new ExpressionValue(expression);
+    values = Collections.singletonList(value);
+  }
+
+  @Override
+  public List<Value> getValues() {
+    return values;
+  }
+
+  @Override
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.visit(this, context);
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssIf.java b/user/src/com/google/gwt/resources/css/ast/CssIf.java
new file mode 100644
index 0000000..7a535f7
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssIf.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A GWTCSS if statement. The elif and else constructs are modeled as nested if
+ * statement is the elseNodes.
+ */
+public class CssIf extends CssNode implements HasNodes {
+  private final List<CssNode> elseNodes = new ArrayList<CssNode>();
+  private final List<CssNode> nodes = new ArrayList<CssNode>();
+  private String expression;
+  private boolean isNegated;
+  private String property;
+  private String[] propertyValues;
+
+  public List<CssNode> getElseNodes() {
+    return elseNodes;
+  }
+
+  public String getExpression() {
+    return expression;
+  }
+
+  public List<CssNode> getNodes() {
+    return nodes;
+  }
+
+  public String getPropertyName() {
+    return property;
+  }
+
+  public String[] getPropertyValues() {
+    return propertyValues;
+  }
+
+  public boolean isNegated() {
+    return isNegated;
+  }
+
+  public void setExpression(String expression) {
+    this.expression = expression;
+  }
+
+  public void setNegated(boolean isNegated) {
+    this.isNegated = isNegated;
+  }
+
+  public void setProperty(String property) {
+    this.property = property;
+  }
+
+  public void setPropertyValues(String[] propertyValues) {
+    this.propertyValues = propertyValues;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(nodes);
+      visitor.acceptWithInsertRemove(elseNodes);
+    }
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssMediaRule.java b/user/src/com/google/gwt/resources/css/ast/CssMediaRule.java
new file mode 100644
index 0000000..28c9192
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssMediaRule.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents an {@literal @}media rule.
+ */
+public class CssMediaRule extends CssNode implements HasNodes {
+  private final List<String> selectors = new ArrayList<String>();
+  private final List<CssNode> nodes = new ArrayList<CssNode>();
+
+  public List<String> getMedias() {
+    return selectors;
+  }
+
+  public List<CssNode> getNodes() {
+    return nodes;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(nodes);
+    }
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssModVisitor.java b/user/src/com/google/gwt/resources/css/ast/CssModVisitor.java
new file mode 100644
index 0000000..e0244fc
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssModVisitor.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.List;
+
+/**
+ * A visitor that can make structural modifications to a CSS tree.
+ */
+@SuppressWarnings("unchecked")
+public class CssModVisitor extends CssVisitor {
+  private class ListContext<T extends CssNode> implements Context {
+    private int index;
+    private List list;
+    private boolean removed;
+    private boolean replaced;
+
+    public boolean canInsert() {
+      return true;
+    }
+
+    public boolean canRemove() {
+      return true;
+    }
+
+    public void insertAfter(CssNode node) {
+      checkRemoved();
+      list.add(index + 1, node);
+      didChange = true;
+    }
+
+    public void insertBefore(CssNode node) {
+      checkRemoved();
+      list.add(index++, node);
+      didChange = true;
+    }
+
+    public void removeMe() {
+      checkState();
+      list.remove(index--);
+      didChange = removed = true;
+    }
+
+    public void replaceMe(CssNode node) {
+      checkState();
+      checkReplacement((CssNode) list.get(index), node);
+      list.set(index, node);
+      didChange = replaced = true;
+    }
+
+    protected void traverse(List<? extends CssNode> list) {
+      this.list = list;
+      for (index = 0; index < list.size(); ++index) {
+        removed = replaced = false;
+        doTraverse(list.get(index), this);
+      }
+    }
+
+    private void checkRemoved() {
+      if (removed) {
+        throw new CssCompilerException("Node was already removed");
+      }
+    }
+
+    private void checkState() {
+      checkRemoved();
+      if (replaced) {
+        throw new CssCompilerException("Node was already replaced");
+      }
+    }
+  }
+
+  protected static void checkReplacement(CssNode origNode, CssNode newNode) {
+    if (newNode == null) {
+      throw new CssCompilerException("Cannot replace with null");
+    }
+    if (newNode == origNode) {
+      throw new CssCompilerException(
+          "The replacement is the same as the original");
+    }
+  }
+
+  private boolean didChange;
+
+  public boolean didChange() {
+    return didChange;
+  }
+
+  @Override
+  protected void doAcceptWithInsertRemove(List<? extends CssNode> list) {
+    ListContext context = new ListContext();
+    context.traverse(list);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssNoFlip.java b/user/src/com/google/gwt/resources/css/ast/CssNoFlip.java
new file mode 100644
index 0000000..35604cd
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssNoFlip.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.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is a container node to prevent the RTL visitor from affecting its child
+ * nodes.
+ */
+public class CssNoFlip extends CssNode implements HasNodes {
+  private List<CssNode> nodes = new ArrayList<CssNode>();
+
+  public List<CssNode> getNodes() {
+    return nodes;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(nodes);
+    }
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssNode.java b/user/src/com/google/gwt/resources/css/ast/CssNode.java
new file mode 100644
index 0000000..80d6fd5
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.dev.util.DefaultTextOutput;
+import com.google.gwt.resources.css.CssGenerationVisitor;
+
+/**
+ * The basic type that composes a CSS tree.
+ */
+public abstract class CssNode implements CssVisitable {
+  @Override
+  public String toString() {
+    DefaultTextOutput out = new DefaultTextOutput(false);
+    CssGenerationVisitor v = new CssGenerationVisitor(out, true);
+    v.accept(this);
+    return out.toString();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssNodeCloner.java b/user/src/com/google/gwt/resources/css/ast/CssNodeCloner.java
new file mode 100644
index 0000000..8e8d145
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssNodeCloner.java
@@ -0,0 +1,310 @@
+/*
+ * 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.ast;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Stack;
+
+/**
+ * Clones CssNodes.
+ */
+public class CssNodeCloner extends CssVisitor {
+
+  /**
+   * Clone a list of nodes.
+   */
+  public static <T extends CssNode> List<T> clone(Class<T> clazz, List<T> nodes) {
+
+    // Push a fake context that will contain the cloned node
+    List<CssNode> topContext = new ArrayList<CssNode>();
+    final List<CssProperty> topProperties = new ArrayList<CssProperty>();
+    final List<CssSelector> topSelectors = new ArrayList<CssSelector>();
+
+    CssNodeCloner cloner = new CssNodeCloner();
+    cloner.curentNodes.push(topContext);
+    cloner.currentHasProperties = new HasProperties() {
+      public List<CssProperty> getProperties() {
+        return topProperties;
+      }
+    };
+    cloner.currentHasSelectors = new HasSelectors() {
+      public List<CssSelector> getSelectors() {
+        return topSelectors;
+      }
+    };
+
+    // Process the nodes
+    cloner.accept(nodes);
+
+    /*
+     * Return the requested data. Different AST nodes will have been collected
+     * in different parts of the initial context.
+     */
+    List<?> toIterate;
+    if (CssProperty.class.isAssignableFrom(clazz)) {
+      toIterate = topProperties;
+    } else if (CssSelector.class.isAssignableFrom(clazz)) {
+      toIterate = topSelectors;
+    } else {
+      toIterate = topContext;
+    }
+
+    assert toIterate.size() == nodes.size() : "Wrong number of nodes in top "
+        + "context. Expecting: " + nodes.size() + " Found: " + toIterate.size();
+
+    // Create the final return value
+    List<T> toReturn = new ArrayList<T>(toIterate.size());
+    for (Object node : toIterate) {
+      assert clazz.isInstance(node) : "Return type mismatch. Expecting: "
+          + clazz.getName() + " Found: " + node.getClass().getName();
+
+      // Cast to the correct type to avoid an unchecked generic cast
+      toReturn.add(clazz.cast(node));
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Clone a single node.
+   */
+  public static <T extends CssNode> T clone(Class<T> clazz, T node) {
+    return clone(clazz, Collections.singletonList(node)).get(0);
+  }
+
+  private HasProperties currentHasProperties;
+
+  private HasSelectors currentHasSelectors;
+
+  private final Stack<List<CssNode>> curentNodes = new Stack<List<CssNode>>();
+
+  private CssNodeCloner() {
+  }
+
+  @Override
+  public void endVisit(CssMediaRule x, Context ctx) {
+    popNodes(x);
+  }
+
+  @Override
+  public void endVisit(CssNoFlip x, Context ctx) {
+    popNodes(x);
+  }
+
+  @Override
+  public void endVisit(CssStylesheet x, Context ctx) {
+    popNodes(x);
+  }
+
+  @Override
+  public boolean visit(CssDef x, Context ctx) {
+    CssDef newDef = new CssDef(x.getKey());
+    newDef.getValues().addAll(x.getValues());
+
+    addToNodes(newDef);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssEval x, Context ctx) {
+    assert x.getValues().size() == 1;
+    assert x.getValues().get(0).isExpressionValue() != null;
+
+    String value = x.getValues().get(0).isExpressionValue().getExpression();
+    CssEval newEval = new CssEval(x.getKey(), value);
+    addToNodes(newEval);
+    return true;
+  }
+
+  /**
+   * A CssIf has two lists of nodes, so we want to handle traversal in this
+   * visitor.
+   */
+  @Override
+  public boolean visit(CssIf x, Context ctx) {
+    CssIf newIf = new CssIf();
+
+    if (x.getExpression() != null) {
+      newIf.setExpression(x.getExpression());
+    } else {
+      newIf.setProperty(x.getPropertyName());
+
+      String[] newValues = new String[x.getPropertyValues().length];
+      System.arraycopy(x.getPropertyValues(), 0, newValues, 0, newValues.length);
+      newIf.setPropertyValues(newValues);
+
+      newIf.setNegated(x.isNegated());
+    }
+
+    // Handle the "then" part
+    pushNodes(newIf);
+    accept(x.getNodes());
+    popNodes(x, newIf);
+
+    /*
+     * Push the "else" part as though it were its own node, but don't add it as
+     * its own top-level node.
+     */
+    CollapsedNode oldElseNodes = new CollapsedNode(x.getElseNodes());
+    CollapsedNode newElseNodes = new CollapsedNode(newIf.getElseNodes());
+    pushNodes(newElseNodes, false);
+    accept(oldElseNodes);
+    popNodes(oldElseNodes, newElseNodes);
+
+    return false;
+  }
+
+  @Override
+  public boolean visit(CssMediaRule x, Context ctx) {
+    CssMediaRule newRule = new CssMediaRule();
+    newRule.getMedias().addAll(newRule.getMedias());
+
+    pushNodes(newRule);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssNoFlip x, Context ctx) {
+    pushNodes(new CssNoFlip());
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssPageRule x, Context ctx) {
+    CssPageRule newRule = new CssPageRule();
+    newRule.setPseudoPage(x.getPseudoPage());
+    addToNodes(newRule);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssProperty x, Context ctx) {
+    CssProperty newProperty = new CssProperty(x.getName(), x.getValues(),
+        x.isImportant());
+    currentHasProperties.getProperties().add(newProperty);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssRule x, Context ctx) {
+    CssRule newRule = new CssRule();
+    addToNodes(newRule);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssSelector x, Context ctx) {
+    CssSelector newSelector = new CssSelector(x.getSelector());
+    currentHasSelectors.getSelectors().add(newSelector);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssSprite x, Context ctx) {
+    CssSprite newSprite = new CssSprite();
+    newSprite.setResourceFunction(x.getResourceFunction());
+    addToNodes(newSprite);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssStylesheet x, Context ctx) {
+    CssStylesheet newSheet = new CssStylesheet();
+    pushNodes(newSheet);
+    return true;
+  }
+
+  @Override
+  public boolean visit(CssUrl x, Context ctx) {
+    assert x.getValues().size() == 1;
+    assert x.getValues().get(0).isIdentValue() != null;
+    String ident = x.getValues().get(0).isIdentValue().getIdent();
+    CssUrl newUrl = new CssUrl(x.getKey(), ident);
+    addToNodes(newUrl);
+    return true;
+  }
+
+  /**
+   * Add a cloned node instance to the output.
+   */
+  private void addToNodes(CssNode node) {
+    curentNodes.peek().add(node);
+
+    currentHasProperties = node instanceof HasProperties ? (HasProperties) node
+        : null;
+    currentHasSelectors = node instanceof HasSelectors ? (HasSelectors) node
+        : null;
+  }
+
+  /**
+   * Remove a frame.
+   * 
+   * @param original the node that was being cloned so that validity checks may
+   *          be performed
+   */
+  private List<CssNode> popNodes(HasNodes original) {
+    List<CssNode> toReturn = curentNodes.pop();
+
+    if (toReturn.size() != original.getNodes().size()) {
+      throw new RuntimeException("Insufficient number of nodes for a "
+          + original.getClass().getName() + " Expected: "
+          + original.getNodes().size() + " Found: " + toReturn.size());
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Remove a frame.
+   * 
+   * @param original the node that was being cloned so that validity checks may
+   *          be performed
+   * @param expected the HasNodes whose nodes were being populated by the frame
+   *          being removed.
+   */
+  private List<CssNode> popNodes(HasNodes original, HasNodes expected) {
+    List<CssNode> toReturn = popNodes(original);
+
+    if (toReturn != expected.getNodes()) {
+      throw new RuntimeException("Incorrect parent node list popped");
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Push a new frame, adding the new parent as a child of the current parent.
+   */
+  private <T extends CssNode & HasNodes> void pushNodes(T parent) {
+    pushNodes(parent, true);
+  }
+
+  /**
+   * Push a new frame.
+   * 
+   * @param addToNodes if <code>true</code> add the new parent node as a child
+   *          of the current parent.
+   */
+  private <T extends CssNode & HasNodes> void pushNodes(T parent,
+      boolean addToNodes) {
+    if (addToNodes) {
+      addToNodes(parent);
+    }
+    this.curentNodes.push(parent.getNodes());
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssPageRule.java b/user/src/com/google/gwt/resources/css/ast/CssPageRule.java
new file mode 100644
index 0000000..36423ce
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssPageRule.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A page rule in CSS.
+ */
+public class CssPageRule extends CssNode implements HasProperties {
+  private final List<CssProperty> nodes = new ArrayList<CssProperty>();
+  private String pseudoPage;
+
+  public List<CssProperty> getProperties() {
+    return nodes;
+  }
+
+  public String getPseudoPage() {
+    return pseudoPage;
+  }
+
+  public void setPseudoPage(String pseudoPage) {
+    this.pseudoPage = pseudoPage;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(nodes);
+    }
+    visitor.endVisit(this, context);
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/css/ast/CssProperty.java b/user/src/com/google/gwt/resources/css/ast/CssProperty.java
new file mode 100644
index 0000000..be4e91d
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssProperty.java
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.core.ext.Generator;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Maps a named property to a Value.
+ */
+public class CssProperty extends CssNode {
+
+  /**
+   * Represents a sequence of no-arg method invocations.
+   */
+  public static class DotPathValue extends Value {
+    private final String path;
+    private final String suffix;
+
+    public DotPathValue(String path, String suffix) {
+      this.path = path;
+      this.suffix = suffix;
+    }
+
+    public String getExpression() {
+      return path.replace(".", "().") + "() + \"" + Generator.escape(suffix)
+          + "\"";
+    }
+
+    public String getPath() {
+      return path;
+    }
+
+    public String getSuffix() {
+      return suffix;
+    }
+
+    @Override
+    public DotPathValue isDotPathValue() {
+      return this;
+    }
+
+    public String toCss() {
+      return "value(\"" + path + "\""
+          + (suffix == null ? "" : (", \"" + suffix + "\"")) + ")";
+    }
+  }
+
+  /**
+   * Represents a literal Java expression.
+   */
+  public static class ExpressionValue extends Value {
+    private final String expression;
+
+    public ExpressionValue(String expression) {
+      this.expression = expression;
+    }
+
+    public String getExpression() {
+      return expression;
+    }
+
+    @Override
+    public ExpressionValue isExpressionValue() {
+      return this;
+    }
+
+    public String toCss() {
+      return "/* Java expression */";
+    }
+
+    /**
+     * For debugging only.
+     */
+    @Override
+    public String toString() {
+      return expression;
+    }
+  }
+
+  /**
+   * Represents an identifier in the CSS source.
+   */
+  public static class IdentValue extends Value {
+    private final String ident;
+
+    public IdentValue(String ident) {
+      this.ident = ident;
+    }
+
+    public String getExpression() {
+      return '"' + Generator.escape(ident) + '"';
+    }
+
+    public String getIdent() {
+      return ident;
+    }
+
+    @Override
+    public IdentValue isIdentValue() {
+      return this;
+    }
+
+    public String toCss() {
+      return ident;
+    }
+  }
+
+  /**
+   * Represents a space-separated list of Values.
+   */
+  public static class ListValue extends Value {
+    private final List<Value> values;
+
+    public ListValue(List<Value> values) {
+      this.values = Collections.unmodifiableList(new ArrayList<Value>(values));
+    }
+
+    public ListValue(Value... values) {
+      this(Arrays.asList(values));
+    }
+
+    public String getExpression() {
+      StringBuilder toReturn = new StringBuilder();
+      for (Iterator<Value> i = values.iterator(); i.hasNext();) {
+        toReturn.append(i.next().getExpression());
+        if (i.hasNext()) {
+          toReturn.append("+ \" \" +");
+        }
+      }
+      return toReturn.toString();
+    }
+
+    public List<Value> getValues() {
+      return values;
+    }
+
+    @Override
+    public ListValue isListValue() {
+      return this;
+    }
+
+    public String toCss() {
+      StringBuilder sb = new StringBuilder();
+      for (Value v : values) {
+        sb.append(" ").append(v.toCss());
+      }
+      return sb.substring(1);
+    }
+  }
+
+  /**
+   * Represents a numeric value, possibly with attached units.
+   */
+  public static class NumberValue extends Value {
+    private final String css;
+    private final String expression;
+    private final String units;
+    private final float value;
+
+    public NumberValue(float value) {
+      this(value, null);
+    }
+
+    public NumberValue(float value, String units) {
+      this.value = value;
+      this.units = units;
+
+      String s;
+      int i = (int) value;
+      if (i == value) {
+        s = String.valueOf(i);
+      } else {
+        s = String.valueOf(value);
+      }
+
+      if (units != null && value != 0) {
+        css = s + units;
+        expression = '"' + s + Generator.escape(units) + '"';
+      } else if (value == 0) {
+        css = "0";
+        expression = "\"0\"";
+      } else {
+        css = s;
+        expression = '"' + s + '"';
+      }
+    }
+
+    public String getExpression() {
+      return expression;
+    }
+
+    public String getUnits() {
+      return units;
+    }
+
+    public float getValue() {
+      return value;
+    }
+
+    @Override
+    public NumberValue isNumberValue() {
+      return this;
+    }
+
+    public String toCss() {
+      return css;
+    }
+  }
+
+  /**
+   * Represents one or more quoted string literals.
+   */
+  public static class StringValue extends Value {
+    private static String escapeValue(String s, boolean inDoubleQuotes) {
+      StringBuilder b = new StringBuilder();
+      for (char c : s.toCharArray()) {
+        if (Character.isISOControl(c)) {
+          b.append('\\').append(Integer.toHexString(c).toUpperCase()).append(
+              " ");
+        } else {
+          switch (c) {
+            case '\'':
+              // Special case a single quote in a pair of double quotes
+              if (inDoubleQuotes) {
+                b.append(c);
+              } else {
+                b.append("\\'");
+              }
+              break;
+
+            case '"':
+              // Special case a single quote in a pair of single quotes
+              if (inDoubleQuotes) {
+                b.append("\\\"");
+              } else {
+                b.append(c);
+              }
+              break;
+
+            case '\\':
+              b.append("\\\\");
+              break;
+
+            default:
+              b.append(c);
+          }
+        }
+      }
+
+      return b.toString();
+    }
+
+    private final String value;
+
+    public StringValue(String value) {
+      this.value = value;
+    }
+
+    public String getExpression() {
+      // The escaped CSS content has to be escaped to be a valid Java literal
+      return "\"" + Generator.escape(toCss()) + "\"";
+    }
+
+    public String getValue() {
+      return value;
+    }
+
+    @Override
+    public StringValue isStringValue() {
+      return this;
+    }
+
+    /**
+     * Returns a escaped, quoted representation of the underlying value.
+     */
+    public String toCss() {
+      return '"' + escapeValue(value, true) + '"';
+    }
+  }
+
+  /**
+   * An abstract encapsulation of property values in GWT CSS.
+   */
+  public abstract static class Value {
+    /**
+     * Generate a Java expression whose execution results in the value.
+     */
+    public abstract String getExpression();
+
+    public DotPathValue isDotPathValue() {
+      return null;
+    }
+
+    public ExpressionValue isExpressionValue() {
+      return null;
+    }
+
+    public IdentValue isIdentValue() {
+      return null;
+    }
+
+    public ListValue isListValue() {
+      return null;
+    }
+
+    public NumberValue isNumberValue() {
+      return null;
+    }
+
+    public StringValue isStringValue() {
+      return null;
+    }
+
+    /**
+     * Generate a CSS expression that represents the Value.
+     */
+    public abstract String toCss();
+
+    /**
+     * For debugging only.
+     */
+    @Override
+    public String toString() {
+      return toCss();
+    }
+  }
+
+  private boolean important;
+
+  private String name;
+  private ListValue value;
+
+  public CssProperty(String name, Value value, boolean important) {
+    this.name = name;
+    setValue(value);
+    this.important = important;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public ListValue getValues() {
+    return value;
+  }
+
+  public boolean isImportant() {
+    return important;
+  }
+
+  public void setImportant(boolean important) {
+    this.important = important;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public void setValue(Value value) {
+    this.value = value.isListValue();
+    if (this.value == null) {
+      this.value = new ListValue(value);
+    }
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.visit(this, context);
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssRule.java b/user/src/com/google/gwt/resources/css/ast/CssRule.java
new file mode 100644
index 0000000..6cb06ff
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssRule.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 
+ */
+public class CssRule extends CssNode implements HasProperties, HasSelectors {
+  protected final List<CssProperty> properties = new ArrayList<CssProperty>();
+  protected final List<CssSelector> selectors = new ArrayList<CssSelector>();
+
+  public List<CssProperty> getProperties() {
+    return properties;
+  }
+
+  public List<CssSelector> getSelectors() {
+    return selectors;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(selectors);
+      visitor.acceptWithInsertRemove(properties);
+    }
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssSelector.java b/user/src/com/google/gwt/resources/css/ast/CssSelector.java
new file mode 100644
index 0000000..5d52ed1
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssSelector.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+/**
+ * 
+ */
+public class CssSelector extends CssNode {
+  /*
+   * TODO: Evaluate whether or not we need to have a type hierarchy of
+   * selectors.
+   */
+  private String selector;
+
+  public CssSelector(String selector) {
+    this.selector = selector;
+  }
+
+  public String getSelector() {
+    return selector;
+  }
+
+  public void setSelector(String selector) {
+    this.selector = selector;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.visit(this, context);
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssSprite.java b/user/src/com/google/gwt/resources/css/ast/CssSprite.java
new file mode 100644
index 0000000..0d563f9
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssSprite.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.resources.css.ast.CssProperty.ListValue;
+import com.google.gwt.resources.css.ast.CssProperty.StringValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Represents a sprited image. This is basically a normal CssRule, except for
+ * one well-known property {@value CssSprite#IMAGE_PROPERTY_NAME}, which
+ * specifies the name of an ImageResource accessor.
+ */
+public class CssSprite extends CssRule {
+
+  public static final String IMAGE_PROPERTY_NAME = "gwt-image";
+
+  /**
+   * A facade for the underlying CssProperty list maintained by CssRule. We
+   * override the add and set methods to intercept the
+   * {@value CssSprite#IMAGE_PROPERTY_NAME} property.
+   */
+  private class SpritePropertyList implements List<CssProperty> {
+    private final List<CssProperty> source;
+
+    public SpritePropertyList(List<CssProperty> source) {
+      this.source = source;
+    }
+
+    public boolean add(CssProperty o) {
+      if (!processProperty(o)) {
+        return source.add(o);
+      } else {
+        return false;
+      }
+    }
+
+    public void add(int index, CssProperty element) {
+      if (!processProperty(element)) {
+        source.add(index, element);
+      }
+    }
+
+    public boolean addAll(Collection<? extends CssProperty> c) {
+      return source.addAll(c);
+    }
+
+    public boolean addAll(int index, Collection<? extends CssProperty> c) {
+      return source.addAll(index, c);
+    }
+
+    public void clear() {
+      source.clear();
+    }
+
+    public boolean contains(Object o) {
+      return source.contains(o);
+    }
+
+    public boolean containsAll(Collection<?> c) {
+      return source.containsAll(c);
+    }
+
+    public boolean equals(Object o) {
+      return source.equals(o);
+    }
+
+    public CssProperty get(int index) {
+      return source.get(index);
+    }
+
+    public int hashCode() {
+      return source.hashCode();
+    }
+
+    public int indexOf(Object o) {
+      return source.indexOf(o);
+    }
+
+    public boolean isEmpty() {
+      return source.isEmpty();
+    }
+
+    public Iterator<CssProperty> iterator() {
+      return source.iterator();
+    }
+
+    public int lastIndexOf(Object o) {
+      return source.lastIndexOf(o);
+    }
+
+    public ListIterator<CssProperty> listIterator() {
+      return source.listIterator();
+    }
+
+    public ListIterator<CssProperty> listIterator(int index) {
+      return source.listIterator(index);
+    }
+
+    public CssProperty remove(int index) {
+      return source.remove(index);
+    }
+
+    public boolean remove(Object o) {
+      return source.remove(o);
+    }
+
+    public boolean removeAll(Collection<?> c) {
+      return source.removeAll(c);
+    }
+
+    public boolean retainAll(Collection<?> c) {
+      return source.retainAll(c);
+    }
+
+    public CssProperty set(int index, CssProperty element) {
+      if (!processProperty(element)) {
+        return source.set(index, element);
+      } else {
+        return source.remove(index);
+      }
+    }
+
+    public int size() {
+      return source.size();
+    }
+
+    public List<CssProperty> subList(int fromIndex, int toIndex) {
+      return source.subList(fromIndex, toIndex);
+    }
+
+    public Object[] toArray() {
+      return source.toArray();
+    }
+
+    public <T> T[] toArray(T[] a) {
+      return source.toArray(a);
+    }
+  }
+
+  private String resourceFunction;
+
+  @Override
+  public List<CssProperty> getProperties() {
+    return new SpritePropertyList(super.getProperties());
+  }
+
+  public String getResourceFunction() {
+    return resourceFunction;
+  }
+
+  public void setResourceFunction(String resourceFunction) {
+    this.resourceFunction = resourceFunction;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(selectors);
+      visitor.acceptWithInsertRemove(getProperties());
+    }
+    visitor.endVisit(this, context);
+  }
+
+  private boolean processProperty(CssProperty property) {
+    if (IMAGE_PROPERTY_NAME.equals(property.getName())) {
+      setImageProperty(property.getValues());
+      return true;
+    }
+    return false;
+  }
+
+  private void setImageProperty(Value value) {
+    StringValue stringValue;
+    ListValue listValue;
+
+    if ((stringValue = value.isStringValue()) != null) {
+      resourceFunction = stringValue.getValue();
+
+      // Allow the user to use both raw idents and quoted strings
+      if (resourceFunction.startsWith("\"")) {
+        resourceFunction = resourceFunction.substring(1,
+            resourceFunction.length() - 1);
+      }
+
+    } else if ((listValue = value.isListValue()) != null) {
+      List<Value> values = listValue.getValues();
+      if (values.size() == 1) {
+        setImageProperty(values.get(0));
+      }
+
+    } else {
+      throw new IllegalArgumentException("The " + IMAGE_PROPERTY_NAME
+          + " property of @sprite must have exactly one value");
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssStylesheet.java b/user/src/com/google/gwt/resources/css/ast/CssStylesheet.java
new file mode 100644
index 0000000..743d54e
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssStylesheet.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 
+ */
+public class CssStylesheet extends CssNode implements HasNodes {
+  private List<CssNode> rules = new ArrayList<CssNode>();
+
+  public List<CssNode> getNodes() {
+    return rules;
+  }
+
+  public void traverse(CssVisitor visitor, Context context) {
+    if (visitor.visit(this, context)) {
+      visitor.acceptWithInsertRemove(rules);
+    }
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssUrl.java b/user/src/com/google/gwt/resources/css/ast/CssUrl.java
new file mode 100644
index 0000000..17f175c
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssUrl.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import com.google.gwt.resources.css.ast.CssProperty.IdentValue;
+import com.google.gwt.resources.css.ast.CssProperty.Value;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A reference to a DataResource that results in a url expression being inserted
+ * into the generated CSS.
+ */
+public class CssUrl extends CssDef {
+  private final List<Value> values;
+
+  public CssUrl(String key, String functionPath) {
+    super(key);
+    Value functionValue = new IdentValue(functionPath);
+    values = Collections.singletonList(functionValue);
+  }
+
+  @Override
+  public List<Value> getValues() {
+    return values;
+  }
+
+  @Override
+  public void traverse(CssVisitor visitor, Context context) {
+    visitor.visit(this, context);
+    visitor.endVisit(this, context);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssVisitable.java b/user/src/com/google/gwt/resources/css/ast/CssVisitable.java
new file mode 100644
index 0000000..335638d
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssVisitable.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+/**
+ * Allows traversal of a CSS tree.
+ */
+public interface CssVisitable {
+  void traverse(CssVisitor visitor, Context context);
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/CssVisitor.java b/user/src/com/google/gwt/resources/css/ast/CssVisitor.java
new file mode 100644
index 0000000..9c4edc9
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/CssVisitor.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.List;
+
+/**
+ * The base class for visiting a CSS tree.
+ */
+public class CssVisitor {
+  protected static final Context UNMODIFIABLE_CONTEXT = new Context() {
+
+    public boolean canInsert() {
+      return false;
+    }
+
+    public boolean canRemove() {
+      return false;
+    }
+
+    public void insertAfter(CssNode node) {
+      throw new UnsupportedOperationException();
+    }
+
+    public void insertBefore(CssNode node) {
+      throw new UnsupportedOperationException();
+    }
+
+    public void removeMe() {
+      throw new UnsupportedOperationException();
+    }
+
+    public void replaceMe(CssNode node) {
+      throw new UnsupportedOperationException();
+    }
+  };
+
+  public final void accept(List<? extends CssNode> nodes) {
+    doAccept(nodes);
+  }
+
+  public final <T extends CssNode> T accept(T node) {
+    return doAccept(node);
+  }
+
+  public final void acceptWithInsertRemove(List<? extends CssNode> nodes) {
+    doAcceptWithInsertRemove(nodes);
+  }
+
+  public void endVisit(CssDef x, Context ctx) {
+  }
+
+  public void endVisit(CssEval x, Context ctx) {
+  }
+
+  public void endVisit(CssIf x, Context ctx) {
+  }
+
+  public void endVisit(CssMediaRule x, Context ctx) {
+  }
+
+  public void endVisit(CssNoFlip x, Context ctx) {
+  }
+
+  public void endVisit(CssPageRule x, Context ctx) {
+  }
+
+  public void endVisit(CssProperty x, Context ctx) {
+  }
+
+  public void endVisit(CssRule x, Context ctx) {
+  }
+
+  public void endVisit(CssSelector x, Context ctx) {
+  }
+
+  public void endVisit(CssSprite x, Context ctx) {
+  }
+
+  public void endVisit(CssStylesheet x, Context ctx) {
+  }
+
+  public void endVisit(CssUrl x, Context ctx) {
+  }
+
+  public boolean visit(CssDef x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssEval x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssIf x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssMediaRule x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssNoFlip x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssPageRule x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssProperty x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssRule x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssSelector x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssSprite x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssStylesheet x, Context ctx) {
+    return true;
+  }
+
+  public boolean visit(CssUrl x, Context ctx) {
+    return true;
+  }
+
+  protected void doAccept(List<? extends CssNode> list) {
+    for (CssNode node : list) {
+      doTraverse(node, UNMODIFIABLE_CONTEXT);
+    }
+  }
+
+  protected <T extends CssNode> T doAccept(T node) {
+    doTraverse(node, UNMODIFIABLE_CONTEXT);
+    return node;
+  }
+
+  protected void doAcceptWithInsertRemove(List<? extends CssNode> list) {
+    for (CssNode node : list) {
+      doTraverse(node, UNMODIFIABLE_CONTEXT);
+    }
+  }
+
+  protected final void doTraverse(CssNode node, Context ctx) {
+    try {
+      node.traverse(this, ctx);
+    } catch (Throwable e) {
+      throw translateException(node, e);
+    }
+  }
+
+  private CssCompilerException translateException(CssNode node, Throwable e) {
+    CssCompilerException ex;
+    if (e instanceof CssCompilerException) {
+      ex = (CssCompilerException) e;
+    } else {
+      ex = new CssCompilerException("Unexpected error during visit.", e);
+    }
+    ex.addNode(node);
+    return ex;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/HasNodes.java b/user/src/com/google/gwt/resources/css/ast/HasNodes.java
new file mode 100644
index 0000000..28d1c41
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/HasNodes.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.List;
+
+/**
+ * Indicates that the node contains other nodes.
+ */
+public interface HasNodes {
+  List<CssNode> getNodes();
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/HasProperties.java b/user/src/com/google/gwt/resources/css/ast/HasProperties.java
new file mode 100644
index 0000000..cb1eb47
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/HasProperties.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.css.ast;
+
+import java.util.List;
+
+/**
+ * Something that has CSS properties.
+ */
+public interface HasProperties {
+  List<CssProperty> getProperties();
+}
diff --git a/user/src/com/google/gwt/resources/css/ast/HasSelectors.java b/user/src/com/google/gwt/resources/css/ast/HasSelectors.java
new file mode 100644
index 0000000..bf2557f
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/ast/HasSelectors.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2009 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may 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.ast;
+
+import java.util.List;
+
+/**
+ * Something that has CSS selectors.
+ */
+public interface HasSelectors {
+  List<CssSelector> getSelectors();
+}
diff --git a/user/src/com/google/gwt/resources/css/package-info.java b/user/src/com/google/gwt/resources/css/package-info.java
new file mode 100644
index 0000000..a930495
--- /dev/null
+++ b/user/src/com/google/gwt/resources/css/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/**
+ * Support package for manipulating CSS resources.
+ */
+@com.google.gwt.util.PreventSpuriousRebuilds
+package com.google.gwt.resources.css;
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/ext/ClientBundleFields.java b/user/src/com/google/gwt/resources/ext/ClientBundleFields.java
new file mode 100644
index 0000000..205774e
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ClientBundleFields.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import com.google.gwt.core.ext.typeinfo.JType;
+
+/**
+ * Allows ResourceGenerators to define fields within the implementation class
+ * for a bundle type. An instance of this interface will be provided via the
+ * {@link ResourceGenerator#createFields} method.
+ * <p>
+ * Because multiple, unrelated ResourceGenerators may be generating method
+ * implementations within a single bundle implementation, it is necessary to
+ * ensure that they do not attempt to declare multiple fields with the same
+ * name. The methods in this interface will provide a guaranteed-unique
+ * identifier to use when generating method implementations.
+ * <p>
+ * Multiple invocations of the {@link #define} method with the same inputs will
+ * result in different identifiers being produced.
+ */
+public interface ClientBundleFields {
+  /**
+   * Adds a field to the bundle. Equivalent to
+   * <code>defineField(type, name, null, true, false)</code>.
+   * 
+   * @param type the declared type of the field
+   * @param name a Java identifier to be used as the basis for the name of the
+   *          field
+   * @return the identifier that must be used to access the field
+   */
+  String define(JType type, String name);
+
+  /**
+   * Adds a field to the bundle.
+   * 
+   * @param type the declared type of the field
+   * @param name a Java identifier to be used as the basis for the name of the
+   *          field
+   * @param initializer a Java expression that will be used as the field's
+   *          initializer, or <code>null</code> if no initialization expression
+   *          is desired
+   * @param isStatic if <code>true</code> the field will be declared to be
+   *          static
+   * @param isFinal if <code>true</code> the fields will be declared as final
+   * @return the identifier that must be used to access the field
+   */
+  String define(JType type, String name, String initializer, boolean isStatic,
+      boolean isFinal);
+}
diff --git a/user/src/com/google/gwt/resources/ext/ClientBundleRequirements.java b/user/src/com/google/gwt/resources/ext/ClientBundleRequirements.java
new file mode 100644
index 0000000..caea394
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ClientBundleRequirements.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+
+/**
+ * Allows ResourceGenerators to indicate how their generated resources may be
+ * affected by their execution environment. An instance of this type will be
+ * provided to the ResourceGenerator via the {@link ResourceGenerator#prepare}
+ * method.
+ */
+public interface ClientBundleRequirements {
+  /**
+   * Indicates that the ResourcePrototype implementation generated by a
+   * ResourceGenerator is sensitive to the value of the specified
+   * deferred-binding property. This method should be called when the behavior
+   * of the ResourcePrototype must differ between permutations of the compiled
+   * output. For example, some resource implementations may be sensitive to the
+   * <code>user.agent</code> deferred-binding property, and would call this
+   * method with the literal string <code>user.agent</code>.
+   * 
+   * @param propertyName the name of the deferred-binding property
+   * @throws BadPropertyValueException if <code>propertyName</code> is not a
+   *           valid deferred-binding property.
+   */
+  void addPermutationAxis(String propertyName) throws BadPropertyValueException;
+}
diff --git a/user/src/com/google/gwt/resources/ext/ResourceContext.java b/user/src/com/google/gwt/resources/ext/ResourceContext.java
new file mode 100644
index 0000000..409dc31
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ResourceContext.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+import java.net.URL;
+
+/**
+ * Context object for ResourceGenerators. An instance of this type will be
+ * provided by the resource generation framework to implementations of
+ * ResourceGenerator via {@link ResourceGenerator#init}. Because this interface
+ * is not intended to be implemented by end-users, the API provided by this
+ * interface may be extended in the future.
+ * <p>
+ * Depending on the optimizations made by the implementation of {@link #deploy},
+ * the resulting URL may or may not be compatible with standard
+ * {@link com.google.gwt.http.client.RequestBuilder} / XMLHttpRequest security
+ * semantics. If the resource is intended to be used with XHR, the
+ * <code>xhrCompatible</code> paramater should be set to <code>true</code> when
+ * invoking {@link #deploy}.
+ * </p>
+ */
+public interface ResourceContext {
+
+  /**
+   * Cause a specific collection of bytes to be available in the program's
+   * compiled output. The return value of this method is a Java expression which
+   * will evaluate to the location of the resource at runtime. The exact format
+   * should not be depended upon.
+   * 
+   * @param suggestedFileName an unobfuscated filename to possibly use for the
+   *          resource
+   * @param mimeType the MIME type of the data being provided
+   * @param data the bytes to add to the output
+   * @param xhrCompatible enforces compatibility with security restrictions if
+   *          the resource is intended to be accessed via an XMLHttpRequest.
+   * @return a Java expression which will evaluate to the location of the
+   *         provided resource at runtime.
+   */
+  String deploy(String suggestedFileName, String mimeType, byte[] data,
+      boolean xhrCompatible) throws UnableToCompleteException;
+
+  /**
+   * Cause a specific collection of bytes to be available in the program's
+   * compiled output. The return value of this method is a Java expression which
+   * will evaluate to the location of the resource at runtime. The exact format
+   * should not be depended upon.
+   * 
+   * @param resource the resource to add to the compiled output
+   * @param xhrCompatible enforces compatibility with security restrictions if
+   *          the resource is intended to be accessed via an XMLHttpRequest.
+   * @return a Java expression which will evaluate to the location of the
+   *         provided resource at runtime.
+   */
+  String deploy(URL resource, boolean xhrCompatible)
+      throws UnableToCompleteException;
+
+  /**
+   * Return the interface type of the resource bundle being generated.
+   */
+  JClassType getClientBundleType();
+
+  /**
+   * Return the GeneratorContext in which the overall resource generation
+   * framework is being run. Implementations of ResourceGenerator should prefer
+   * {@link #addToOutput} over {@link GeneratorContext#tryCreateResource} in
+   * order to take advantage of serving optimizations that can be performed by
+   * the bundle architecture.
+   */
+  GeneratorContext getGeneratorContext();
+
+  /**
+   * Returns the simple source name of the implementation of the bundle being
+   * generated. This can be used during code-generation to refer to the instance
+   * of the bundle (e.g. via <code>SimpleSourceName.this</code>).
+   * 
+   * @throws IllegalStateException if this method is called during
+   *           {@link ResourceGenerator#init} or
+   *           {@link ResourceGenerator#prepare} methods.
+   */
+  String getImplementationSimpleSourceName() throws IllegalStateException;
+
+  /**
+   * Indicates if the runtime context supports data: urls. When data URLs are
+   * supported by the context, aggregation of resource data into larger payloads
+   * is discouraged, as it offers reduced benefit to the application at runtime.
+   */
+  boolean supportsDataUrls();
+}
diff --git a/user/src/com/google/gwt/resources/ext/ResourceGenerator.java b/user/src/com/google/gwt/resources/ext/ResourceGenerator.java
new file mode 100644
index 0000000..2a61d56
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ResourceGenerator.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+
+/**
+ * Encapsulates per-type resource generation logic. This type is used only by
+ * developers who wish to add additional resource types to the resource
+ * generation system. Implementations are paired with one or more interfaces
+ * derived from ResourcePrototype that have been annotated with an
+ * {@link ResourceGeneratorType} annotation. Instances of ResourceGenerator must
+ * support default-instantiation (i.e. via {@link Class#newInstance()}).
+ * <p>
+ * The methods on an instance of ResourceGenerator will be called in the
+ * following order by the resource generation system:
+ * <ol>
+ * <li>{@link #init}</li>
+ * <li>
+ * {@link #prepare} once for each method</li>
+ * <li>Based on the requirements specified by the ResourceGenerator, the
+ * framework may decide that no further action is required. If this is the case,
+ * the framework will immediately invoke {@link #finish}.</li>
+ * <li>{@link #createFields}</li>
+ * <li>{@link #createAssignment} once for each method</li>
+ * <li>{@link #finish}</li>
+ * </ol>
+ * <p>
+ * The methods {@link #prepare} and {@link #createAssignment} will be called
+ * only with those methods whose ResourcePrototype-derived type specifies the
+ * particular type of ResourceGenerator as the implementor. The relative order
+ * in which ResourceGenerators are invoked and the specific order in which the
+ * bundle's methods are presented is undefined.
+ * <p>
+ * Direct access to the contents of the generated bundle implementation is
+ * intentionally limited to prevent unrelated ResourceGenerators from
+ * potentially creating namespace conflicts or generating invalid Java source.
+ * 
+ * @see ResourceGeneratorUtil
+ */
+public interface ResourceGenerator {
+
+  /**
+   * Produce the right-hand-side of a Java assignment expression to provide the
+   * singleton instance object for a particular resource.
+   * <p>
+   * Example:
+   * 
+   * <pre>
+   * new MySampleResource() { public Foo getFoo() { ... } }
+   * </pre>
+   */
+  String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException;
+
+  /**
+   * The ResourceGenerator can create fields within the implementation of the
+   * bundle type. The Fields object passed into this method is used to declare,
+   * and possibly initialize, Java fields within the final implementation of the
+   * resource bundle type.
+   * <p>
+   * The Fields instance should not be retained beyond the lifetime of this
+   * method as operation on this object is not defined after the implementation
+   * of this method returns.
+   */
+  void createFields(TreeLogger logger, ResourceContext context,
+      ClientBundleFields fields) throws UnableToCompleteException;
+
+  /**
+   * Called at the end of the resource generation phase and can be used to
+   * perform cleanup.
+   */
+  void finish(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException;
+
+  /**
+   * Initialize the ResourceGenerator with the generation context that will
+   * remain valid for the duration of the resource-generation phase. The logger
+   * instance should not be retained; use the per-method loggers instead.
+   */
+  void init(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException;
+
+  /**
+   * Called once for each method the ResourceGenerator is expected to handle.
+   * This allows cross-resource state to be accumulated, such as for data
+   * aggregation.
+   */
+  void prepare(TreeLogger logger, ResourceContext context,
+      ClientBundleRequirements requirements, JMethod method)
+      throws UnableToCompleteException;
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/ext/ResourceGeneratorType.java b/user/src/com/google/gwt/resources/ext/ResourceGeneratorType.java
new file mode 100644
index 0000000..7489198
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ResourceGeneratorType.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * Specifies the implementation of ResourceGenerator to use for a type of
+ * {@link com.google.gwt.resources.client.ResourcePrototype}.
+ */
+@Target(ElementType.TYPE)
+public @interface ResourceGeneratorType {
+  Class<? extends ResourceGenerator> value();
+}
diff --git a/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
new file mode 100644
index 0000000..9b4e4a4
--- /dev/null
+++ b/user/src/com/google/gwt/resources/ext/ResourceGeneratorUtil.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.ext;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JPackage;
+import com.google.gwt.resources.client.ClientBundle.Source;
+
+import java.lang.annotation.Annotation;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility methods for building ResourceGenerators.
+ */
+public final class ResourceGeneratorUtil {
+
+  /**
+   * These are type names from previous APIs or from APIs with similar
+   * functionality that might be confusing.
+   * 
+   * @see #checkForDeprecatedAnnotations
+   */
+  private static final String[] DEPRECATED_ANNOTATION_NAMES = {
+      "com.google.gwt.libideas.resources.client.ImmutableResourceBundle$Resource",
+      "com.google.gwt.user.client.ui.ImageBundle$Resource"};
+
+  private static final List<Class<? extends Annotation>> DEPRECATED_ANNOTATION_CLASSES;
+
+  static {
+    List<Class<? extends Annotation>> classes = new ArrayList<Class<? extends Annotation>>(
+        DEPRECATED_ANNOTATION_NAMES.length);
+
+    for (String name : DEPRECATED_ANNOTATION_NAMES) {
+      try {
+        Class<?> maybeAnnotation = Class.forName(name, false,
+            ResourceGeneratorUtil.class.getClassLoader());
+
+        // Possibly throws ClassCastException
+        Class<? extends Annotation> annotationClass = maybeAnnotation.asSubclass(Annotation.class);
+
+        classes.add(annotationClass);
+
+      } catch (ClassCastException e) {
+        // If it's not an Annotation type, we don't care about it
+      } catch (ClassNotFoundException e) {
+        // This is OK; the annotation doesn't exist.
+      }
+    }
+
+    if (classes.isEmpty()) {
+      DEPRECATED_ANNOTATION_CLASSES = Collections.emptyList();
+    } else {
+      DEPRECATED_ANNOTATION_CLASSES = Collections.unmodifiableList(classes);
+    }
+  }
+
+  /**
+   * Return the base filename of a resource. The behavior is similar to the unix
+   * command <code>basename</code>.
+   * 
+   * @param resource the URL of the resource
+   * @return the final name segment of the resource
+   */
+  public static String baseName(URL resource) {
+    String path = resource.getPath();
+    return path.substring(path.lastIndexOf('/') + 1);
+  }
+
+  /**
+   * Find all resources referenced by a method in a bundle. The method's
+   * {@link Source} annotation will be examined and the specified locations will
+   * be expanded into URLs by which they may be accessed on the local system.
+   * <p>
+   * This method is sensitive to the <code>locale</code> deferred-binding
+   * property and will attempt to use a best-match lookup by removing locale
+   * components.
+   * 
+   * @param logger a TreeLogger that will be used to report errors or warnings
+   * @param context the ResourceContext in which the ResourceGenerator is
+   *          operating
+   * @param classLoader the ClassLoader to use when locating resources
+   * @param method the method to examine for {@link Source} annotations
+   * @param defaultSuffixes if the supplied method does not have any
+   *          {@link Source} annotations, act as though a Source annotation was
+   *          specified, using the name of the method and each of supplied
+   *          extensions in the order in which they are specified
+   * @return URLs for each {@link Source} annotation value defined on the
+   *         method, or an empty array if no sources could be found.
+   * @throws UnableToCompleteException if ore or more of the sources could not
+   *           be found. The error will be reported via the <code>logger</code>
+   *           provided to this method
+   */
+  public static URL[] findResources(TreeLogger logger, ClassLoader classLoader,
+      ResourceContext context, JMethod method, String[] defaultSuffixes)
+      throws UnableToCompleteException {
+    logger = logger.branch(TreeLogger.DEBUG, "Finding resources");
+
+    String locale;
+    try {
+      PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
+      locale = oracle.getPropertyValue(logger, "locale");
+    } catch (BadPropertyValueException e) {
+      locale = null;
+    }
+
+    checkForDeprecatedAnnotations(logger, method);
+
+    Source resourceAnnotation = method.getAnnotation(Source.class);
+    if (resourceAnnotation == null) {
+      if (defaultSuffixes != null) {
+        for (String extension : defaultSuffixes) {
+          logger.log(TreeLogger.SPAM, "Trying default extension " + extension);
+          URL resourceUrl = tryFindResource(classLoader,
+              getPathRelativeToPackage(method.getEnclosingType().getPackage(),
+                  method.getName() + extension), locale);
+
+          if (resourceUrl != null) {
+            return new URL[] {resourceUrl};
+          }
+        }
+      }
+      logger.log(TreeLogger.SPAM,
+          "No annotation and no hits with default extensions");
+      return new URL[0];
+    }
+
+    String[] resources = resourceAnnotation.value();
+
+    URL[] toReturn = new URL[resources.length];
+
+    boolean error = false;
+    int tagIndex = 0;
+    for (String resource : resources) {
+      // Try to find the resource relative to the package.
+      URL resourceURL = tryFindResource(classLoader, getPathRelativeToPackage(
+          method.getEnclosingType().getPackage(), resource), locale);
+
+      // If we didn't find the resource relative to the package, assume it is
+      // absolute.
+      if (resourceURL == null) {
+        resourceURL = tryFindResource(classLoader, resource, locale);
+      }
+
+      if (resourceURL == null) {
+        logger.log(TreeLogger.ERROR, "Resource " + resource
+            + " not found on classpath. Is the name specified as "
+            + "Class.getResource() would expect?");
+        error = true;
+      }
+
+      toReturn[tagIndex++] = resourceURL;
+    }
+
+    if (error) {
+      throw new UnableToCompleteException();
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Find all resources referenced by a method in a bundle. The method's
+   * {@link Source} annotation will be examined and the specified locations will
+   * be expanded into URLs by which they may be accessed on the local system.
+   * <p>
+   * This method is sensitive to the <code>locale</code> deferred-binding
+   * property and will attempt to use a best-match lookup by removing locale
+   * components.
+   * <p>
+   * The current Thread's context ClassLoader will be used to resolve resource
+   * locations. If it is necessary to alter the manner in which resources are
+   * resolved, use the overload that accepts an arbitrary ClassLoader.
+   * 
+   * @param logger a TreeLogger that will be used to report errors or warnings
+   * @param context the ResourceContext in which the ResourceGenerator is
+   *          operating
+   * @param method the method to examine for {@link Source} annotations
+   * @return URLs for each {@link Source} annotation value defined on the
+   *         method, or an empty array if no sources could be found.
+   * @throws UnableToCompleteException if ore or more of the sources could not
+   *           be found. The error will be reported via the <code>logger</code>
+   *           provided to this method
+   */
+  public static URL[] findResources(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    return findResources(logger, context, method, new String[0]);
+  }
+
+  /**
+   * Find all resources referenced by a method in a bundle. The method's
+   * {@link Source} annotation will be examined and the specified locations will
+   * be expanded into URLs by which they may be accessed on the local system.
+   * <p>
+   * This method is sensitive to the <code>locale</code> deferred-binding
+   * property and will attempt to use a best-match lookup by removing locale
+   * components.
+   * <p>
+   * The current Thread's context ClassLoader will be used to resolve resource
+   * locations. If it is necessary to alter the manner in which resources are
+   * resolved, use the overload that accepts an arbitrary ClassLoader.
+   * 
+   * @param logger a TreeLogger that will be used to report errors or warnings
+   * @param context the ResourceContext in which the ResourceGenerator is
+   *          operating
+   * @param method the method to examine for {@link Source} annotations
+   * @param defaultSuffixes if the supplied method does not have any
+   *          {@link Source} annotations, act as though a Source annotation was
+   *          specified, using the name of the method and each of supplied
+   *          extensions in the order in which they are specified
+   * @return URLs for each {@link Source} annotation value defined on the
+   *         method, or an empty array if no sources could be found.
+   * @throws UnableToCompleteException if ore or more of the sources could not
+   *           be found. The error will be reported via the <code>logger</code>
+   *           provided to this method
+   */
+  public static URL[] findResources(TreeLogger logger, ResourceContext context,
+      JMethod method, String[] defaultSuffixes)
+      throws UnableToCompleteException {
+    return findResources(logger,
+        Thread.currentThread().getContextClassLoader(), context, method,
+        defaultSuffixes);
+  }
+
+  /**
+   * We want to warn the user about any annotations from ImageBundle or the old
+   * incubator code.
+   */
+  private static void checkForDeprecatedAnnotations(TreeLogger logger,
+      JMethod method) {
+
+    for (Class<? extends Annotation> annotationClass : DEPRECATED_ANNOTATION_CLASSES) {
+      if (method.isAnnotationPresent(annotationClass)) {
+        logger.log(TreeLogger.WARN, "Deprecated annotation used; expecting "
+            + Source.class.getCanonicalName() + " but found "
+            + annotationClass.getName() + " instead.  It is likely "
+            + "that undesired operation will occur.");
+      }
+    }
+  }
+
+  /**
+   * Converts a package relative path into an absolute path.
+   * 
+   * @param pkg the package
+   * @param path a path relative to the package
+   * @return an absolute path
+   */
+  private static String getPathRelativeToPackage(JPackage pkg, String path) {
+    return pkg.getName().replace('.', '/') + '/' + path;
+  }
+
+  /**
+   * This performs the locale lookup function for a given resource name.
+   * 
+   * @param classLoader the ClassLoader to use to load the resources
+   * @param resourceName the string name of the desired resource
+   * @param locale the locale of the current rebind permutation
+   * @return a URL by which the resource can be loaded, <code>null</code> if one
+   *         cannot be found
+   */
+  private static URL tryFindResource(ClassLoader classLoader,
+      String resourceName, String locale) {
+    URL toReturn = null;
+
+    // Look for locale-specific variants of individual resources
+    if (locale != null) {
+      // Convert language_country_variant to independent pieces
+      String[] localeSegments = locale.split("_");
+      int lastDot = resourceName.lastIndexOf(".");
+      String prefix = lastDot == -1 ? resourceName : resourceName.substring(0,
+          lastDot);
+      String extension = lastDot == -1 ? "" : resourceName.substring(lastDot);
+
+      for (int i = localeSegments.length - 1; i >= -1; i--) {
+        String localeInsert = "";
+        for (int j = 0; j <= i; j++) {
+          localeInsert += "_" + localeSegments[j];
+        }
+
+        toReturn = classLoader.getResource(prefix + localeInsert + extension);
+        if (toReturn != null) {
+          break;
+        }
+      }
+    } else {
+      toReturn = classLoader.getResource(resourceName);
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Utility class.
+   */
+  private ResourceGeneratorUtil() {
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
new file mode 100644
index 0000000..65d51cd
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractClientBundleGenerator.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.generator.NameFactory;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ClientBundleWithLookup;
+import com.google.gwt.resources.client.ResourcePrototype;
+import com.google.gwt.resources.ext.ClientBundleFields;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGenerator;
+import com.google.gwt.resources.ext.ResourceGeneratorType;
+import com.google.gwt.resources.rg.BundleResourceGenerator;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The base class for creating new ClientBundle implementations.
+ */
+public abstract class AbstractClientBundleGenerator extends Generator {
+
+  /**
+   * An implementation of FieldAccumulator that immediately composes its output
+   * into a StringWriter.
+   */
+  private static class FieldsImpl implements ClientBundleFields {
+    private final NameFactory factory = new NameFactory();
+    private final StringWriter buffer = new StringWriter();
+    private final PrintWriter pw = new PrintWriter(buffer);
+
+    public void addName(String name) {
+      factory.addName(name);
+    }
+
+    public String define(JType type, String name) {
+      return define(type, name, null, true, false);
+    }
+
+    public String define(JType type, String name, String initializer,
+        boolean isStatic, boolean isFinal) {
+
+      assert Util.isValidJavaIdent(name) : name
+          + " is not a valid Java identifier";
+
+      String ident = factory.createName(name);
+
+      pw.print("private ");
+
+      if (isStatic) {
+        pw.print("static ");
+      }
+
+      if (isFinal) {
+        pw.print("final ");
+      }
+
+      pw.print(type.getQualifiedSourceName());
+      pw.print(" ");
+      pw.print(ident);
+
+      if (initializer != null) {
+        pw.print(" = ");
+        pw.print(initializer);
+      }
+
+      pw.println(";");
+
+      return ident;
+    }
+
+    public String toString() {
+      pw.flush();
+      return buffer.getBuffer().toString();
+    }
+  }
+
+  private static class RequirementsImpl implements ClientBundleRequirements {
+    private final Set<String> axes = new HashSet<String>();
+    private final PropertyOracle oracle;
+
+    public RequirementsImpl(PropertyOracle oracle) {
+      this.oracle = oracle;
+    }
+
+    public void addPermutationAxis(String propertyName)
+        throws BadPropertyValueException {
+      oracle.getPropertyValue(TreeLogger.NULL, propertyName);
+      axes.add(propertyName);
+    }
+  }
+
+  public final String generate(TreeLogger logger,
+      GeneratorContext generatorContext, String typeName)
+      throws UnableToCompleteException {
+
+    // The TypeOracle knows about all types in the type system
+    TypeOracle typeOracle = generatorContext.getTypeOracle();
+
+    // Get a reference to the type that the generator should implement
+    JClassType sourceType = typeOracle.findType(typeName);
+
+    // Ensure that the requested type exists
+    if (sourceType == null) {
+      logger.log(TreeLogger.ERROR, "Could not find requested typeName");
+      throw new UnableToCompleteException();
+    } else if (sourceType.isInterface() == null) {
+      // The incoming type wasn't a plain interface, we don't support
+      // abstract base classes
+      logger.log(TreeLogger.ERROR, sourceType.getQualifiedSourceName()
+          + " is not an interface.", null);
+      throw new UnableToCompleteException();
+    }
+
+    /*
+     * This associates the methods to implement with the ResourceGenerator class
+     * that will generate the implementations of those methods.
+     */
+    Map<Class<? extends ResourceGenerator>, List<JMethod>> taskList = createTaskList(
+        logger, typeOracle, sourceType);
+
+    /*
+     * Additional objects that hold state during the generation process.
+     */
+    AbstractResourceContext resourceContext = createResourceContext(logger,
+        generatorContext, sourceType);
+    FieldsImpl fields = new FieldsImpl();
+    RequirementsImpl requirements = new RequirementsImpl(
+        generatorContext.getPropertyOracle());
+
+    /*
+     * Initialize the ResourceGenerators and prepare them for subsequent code
+     * generation.
+     */
+    Map<ResourceGenerator, List<JMethod>> generators = initAndPrepare(logger,
+        taskList, resourceContext, requirements);
+
+    /*
+     * Now that the ResourceGenerators have been initialized and prepared, we
+     * can compute the actual name of the implementation class in order to
+     * ensure that we use a distinct name between permutations.
+     */
+    String generatedSimpleSourceName = generateSimpleSourceName(logger,
+        resourceContext, requirements);
+
+    // Begin writing the generated source.
+    ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
+        sourceType.getPackage().getName(), generatedSimpleSourceName);
+
+    // The generated class needs to be able to determine the module base URL
+    f.addImport(GWT.class.getName());
+
+    // Used by the map methods
+    f.addImport(ResourcePrototype.class.getName());
+
+    // The whole point of this exercise
+    f.addImplementedInterface(sourceType.getQualifiedSourceName());
+
+    String createdClassName = f.getCreatedClassName();
+
+    // All source gets written through this Writer
+    PrintWriter out = generatorContext.tryCreate(logger,
+        sourceType.getPackage().getName(), generatedSimpleSourceName);
+
+    // If an implementation already exists, we don't need to do any work
+    if (out != null) {
+      SourceWriter sw = f.createSourceWriter(generatorContext, out);
+
+      // Set the now-calculated simple source name
+      resourceContext.setSimpleSourceName(generatedSimpleSourceName);
+
+      // Write the generated code to disk
+      createFieldsAndAssignments(logger, sw, generators, resourceContext,
+          fields);
+
+      // Print the accumulated field definitions
+      sw.println(fields.toString());
+
+      /*
+       * The map-accessor methods use JSNI and need a fully-qualified class
+       * name, but should not include any sub-bundles.
+       */
+      taskList.remove(BundleResourceGenerator.class);
+      writeMapMethods(logger, createdClassName, sw, taskList);
+
+      sw.commit(logger);
+    }
+
+    finish(logger, resourceContext, generators.keySet());
+
+    // Return the name of the concrete class
+    return createdClassName;
+  }
+
+  /**
+   * Create the ResourceContext object that will be used by
+   * {@link ResourceGenerator} subclasses. This is the primary way to implement
+   * custom logic in the resource generation pass.
+   */
+  protected abstract AbstractResourceContext createResourceContext(
+      TreeLogger logger, GeneratorContext context, JClassType resourceBundleType);
+
+  /**
+   * Create fields and assignments for a single ResourceGenerator.
+   */
+  private boolean createFieldsAndAssignments(TreeLogger logger,
+      ResourceContext resourceContext, ResourceGenerator rg,
+      List<JMethod> generatorMethods, SourceWriter sw, ClientBundleFields fields) {
+
+    // Defer failure until this phase has ended
+    boolean fail = false;
+
+    // Write all field values
+    try {
+      rg.createFields(logger.branch(TreeLogger.DEBUG, "Creating fields"),
+          resourceContext, fields);
+    } catch (UnableToCompleteException e) {
+      return false;
+    }
+
+    // Create the instance variables in the IRB subclass by calling
+    // writeAssignment() on the ResourceGenerator
+    for (JMethod m : generatorMethods) {
+      String rhs;
+
+      try {
+        rhs = rg.createAssignment(logger.branch(TreeLogger.DEBUG,
+            "Creating assignment for " + m.getName()), resourceContext, m);
+      } catch (UnableToCompleteException e) {
+        fail = true;
+        continue;
+      }
+
+      String ident = fields.define(m.getReturnType().isClassOrInterface(),
+          m.getName(), null, true, false);
+
+      // Strip off all but the access modifiers
+      sw.print(m.getReadableDeclaration(false, true, true, true, true));
+      sw.println(" {");
+      sw.indent();
+      sw.println("if (" + ident + " == null) {");
+      sw.indent();
+      sw.println(ident + " = " + rhs + ";");
+      sw.outdent();
+      sw.println("}");
+      sw.println("return " + ident + ";");
+      sw.outdent();
+      sw.println("}");
+    }
+
+    if (fail) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Create fields and assignments for multiple ResourceGenerators.
+   */
+  private void createFieldsAndAssignments(TreeLogger logger, SourceWriter sw,
+      Map<ResourceGenerator, List<JMethod>> generators,
+      ResourceContext resourceContext, ClientBundleFields fields)
+      throws UnableToCompleteException {
+    // Try to provide as many errors as possible before failing.
+    boolean success = true;
+
+    // Run the ResourceGenerators to generate implementations of the methods
+    for (Map.Entry<ResourceGenerator, List<JMethod>> entry : generators.entrySet()) {
+
+      success &= createFieldsAndAssignments(logger, resourceContext,
+          entry.getKey(), entry.getValue(), sw, fields);
+    }
+
+    if (!success) {
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Given a ClientBundle subtype, compute which ResourceGenerators should
+   * implement which methods.
+   */
+  private Map<Class<? extends ResourceGenerator>, List<JMethod>> createTaskList(
+      TreeLogger logger, TypeOracle typeOracle, JClassType sourceType)
+      throws UnableToCompleteException {
+
+    Map<Class<? extends ResourceGenerator>, List<JMethod>> toReturn = new HashMap<Class<? extends ResourceGenerator>, List<JMethod>>();
+
+    JClassType bundleType = typeOracle.findType(ClientBundle.class.getName());
+    assert bundleType != null;
+
+    JClassType bundleWithLookupType = typeOracle.findType(ClientBundleWithLookup.class.getName());
+    assert bundleWithLookupType != null;
+
+    JClassType resourcePrototypeType = typeOracle.findType(ResourcePrototype.class.getName());
+    assert resourcePrototypeType != null;
+
+    // Accumulate as many errors as possible before failing
+    boolean throwException = false;
+
+    // Using overridable methods allows composition of interface types
+    for (JMethod m : sourceType.getOverridableMethods()) {
+      JClassType returnType = m.getReturnType().isClassOrInterface();
+
+      if (m.getEnclosingType().equals(bundleType)
+          || m.getEnclosingType().equals(bundleWithLookupType)) {
+        // Methods that we must generate, but that are not resources
+        continue;
+
+      } else if (!m.isAbstract()) {
+        // Covers the case of an abstract class base type
+        continue;
+
+      } else if (returnType == null
+          || !(returnType.isAssignableTo(resourcePrototypeType) || returnType.isAssignableTo(bundleType))) {
+        // Primitives and random other abstract methods
+        logger.log(TreeLogger.ERROR, "Unable to implement " + m.getName()
+            + " because it does not derive from "
+            + resourcePrototypeType.getQualifiedSourceName() + " or "
+            + bundleType.getQualifiedSourceName());
+        throwException = true;
+        continue;
+      }
+
+      try {
+        Class<? extends ResourceGenerator> clazz = findResourceGenerator(
+            logger, typeOracle, m);
+        List<JMethod> generatorMethods;
+        if (toReturn.containsKey(clazz)) {
+          generatorMethods = toReturn.get(clazz);
+        } else {
+          generatorMethods = new ArrayList<JMethod>();
+          toReturn.put(clazz, generatorMethods);
+        }
+
+        generatorMethods.add(m);
+      } catch (UnableToCompleteException e) {
+        throwException = true;
+      }
+    }
+
+    if (throwException) {
+      throw new UnableToCompleteException();
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Given a JMethod, find the a ResourceGenerator class that will be able to
+   * provide an implementation of the method.
+   */
+  private Class<? extends ResourceGenerator> findResourceGenerator(
+      TreeLogger logger, TypeOracle typeOracle, JMethod method)
+      throws UnableToCompleteException {
+
+    JClassType resourceType = method.getReturnType().isClassOrInterface();
+    assert resourceType != null;
+
+    List<JClassType> searchTypes = new ArrayList<JClassType>();
+    searchTypes.add(resourceType);
+
+    ResourceGeneratorType generatorType = null;
+
+    while (!searchTypes.isEmpty()) {
+      JClassType current = searchTypes.remove(0);
+      generatorType = current.getAnnotation(ResourceGeneratorType.class);
+      if (generatorType != null) {
+        break;
+      }
+
+      if (current.getSuperclass() != null) {
+        searchTypes.add(current.getSuperclass());
+      }
+
+      for (JClassType t : current.getImplementedInterfaces()) {
+        searchTypes.add(t);
+      }
+    }
+
+    if (generatorType == null) {
+      logger.log(TreeLogger.ERROR, "No @"
+          + ResourceGeneratorType.class.getName()
+          + " was specifed for resource type "
+          + resourceType.getQualifiedSourceName() + " or its supertypes");
+      throw new UnableToCompleteException();
+    }
+
+    return generatorType.value();
+  }
+
+  /**
+   * Call finish() on several ResourceGenerators.
+   */
+  private void finish(TreeLogger logger, ResourceContext context,
+      Collection<ResourceGenerator> generators)
+      throws UnableToCompleteException {
+    boolean fail = false;
+    // Finalize the ResourceGenerator
+    for (ResourceGenerator rg : generators) {
+      try {
+        rg.finish(
+            logger.branch(TreeLogger.DEBUG, "Finishing ResourceGenerator"),
+            context);
+      } catch (UnableToCompleteException e) {
+        fail = true;
+      }
+    }
+    if (fail) {
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Given a user-defined type name, determine the type name for the generated
+   * class based on accumulated requirements.
+   */
+  private String generateSimpleSourceName(TreeLogger logger,
+      ResourceContext context, RequirementsImpl requirements)
+      throws UnableToCompleteException {
+    StringBuilder toReturn = new StringBuilder(
+        context.getClientBundleType().getQualifiedSourceName().replaceAll(
+            "[.$]", "_"));
+    Set<String> permutationAxes = new HashSet<String>(requirements.axes);
+    permutationAxes.add("locale");
+
+    try {
+      PropertyOracle oracle = context.getGeneratorContext().getPropertyOracle();
+      for (String property : permutationAxes) {
+        String value = oracle.getPropertyValue(logger, property);
+        toReturn.append("_" + value);
+      }
+    } catch (BadPropertyValueException e) {
+    }
+
+    toReturn.append("_" + getClass().getSimpleName());
+
+    return toReturn.toString();
+  }
+
+  private Map<ResourceGenerator, List<JMethod>> initAndPrepare(
+      TreeLogger logger,
+      Map<Class<? extends ResourceGenerator>, List<JMethod>> taskList,
+      ResourceContext resourceContext, ClientBundleRequirements requirements)
+      throws UnableToCompleteException {
+    // Try to provide as many errors as possible before failing.
+    boolean success = true;
+    Map<ResourceGenerator, List<JMethod>> toReturn = new IdentityHashMap<ResourceGenerator, List<JMethod>>();
+
+    // Run the ResourceGenerators to generate implementations of the methods
+    for (Map.Entry<Class<? extends ResourceGenerator>, List<JMethod>> entry : taskList.entrySet()) {
+
+      ResourceGenerator rg = instantiateResourceGenerator(logger,
+          entry.getKey());
+      toReturn.put(rg, entry.getValue());
+
+      success &= initAndPrepare(logger, resourceContext, rg, entry.getValue(),
+          requirements);
+    }
+
+    if (!success) {
+      throw new UnableToCompleteException();
+    }
+
+    return toReturn;
+  }
+
+  private boolean initAndPrepare(TreeLogger logger,
+      ResourceContext resourceContext, ResourceGenerator rg,
+      List<JMethod> generatorMethods, ClientBundleRequirements requirements) {
+    try {
+      rg.init(
+          logger.branch(TreeLogger.DEBUG, "Initializing ResourceGenerator"),
+          resourceContext);
+    } catch (UnableToCompleteException e) {
+      return false;
+    }
+
+    boolean fail = false;
+
+    // Prepare the ResourceGenerator by telling it all methods that it is
+    // expected to produce.
+    for (JMethod m : generatorMethods) {
+      try {
+        rg.prepare(logger.branch(TreeLogger.DEBUG, "Preparing method "
+            + m.getName()), resourceContext, requirements, m);
+      } catch (UnableToCompleteException e) {
+        fail = true;
+      }
+    }
+
+    return !fail;
+  }
+
+  /**
+   * Utility method to construct a ResourceGenerator that logs errors.
+   */
+  private <T extends ResourceGenerator> T instantiateResourceGenerator(
+      TreeLogger logger, Class<T> generatorClass)
+      throws UnableToCompleteException {
+    try {
+      return generatorClass.newInstance();
+    } catch (InstantiationException e) {
+      logger.log(TreeLogger.ERROR, "Unable to initialize ResourceGenerator", e);
+    } catch (IllegalAccessException e) {
+      logger.log(TreeLogger.ERROR, "Unable to instantiate ResourceGenerator. "
+          + "Does it have a public default constructor?", e);
+    }
+    throw new UnableToCompleteException();
+  }
+
+  private void writeMapMethods(TreeLogger logger, String createdClassName,
+      SourceWriter sw,
+      Map<Class<? extends ResourceGenerator>, List<JMethod>> taskList)
+      throws UnableToCompleteException {
+
+    // Complete the IRB contract
+    sw.println("public ResourcePrototype[] getResources() {");
+    sw.indent();
+    sw.println("return new ResourcePrototype[] {");
+    sw.indent();
+    for (List<JMethod> methods : taskList.values()) {
+      for (JMethod m : methods) {
+        sw.println(m.getName() + "(), ");
+      }
+    }
+    sw.outdent();
+    sw.println("};");
+    sw.outdent();
+    sw.println("}");
+
+    // Use a switch statement as a fast map
+    sw.println("public native ResourcePrototype "
+        + "getResource(String name) /*-{");
+    sw.indent();
+    sw.println("switch (name) {");
+    sw.indent();
+    for (List<JMethod> list : taskList.values()) {
+      for (JMethod m : list) {
+        sw.println("case '" + m.getName() + "': return this.@"
+            + createdClassName + "::" + (m.getName() + "()()") + ";");
+      }
+    }
+    sw.outdent();
+    sw.println("}");
+    sw.println("return null;");
+    sw.outdent();
+    sw.println("}-*/;");
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
new file mode 100644
index 0000000..9e4e549
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/AbstractResourceContext.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.resources.ext.ResourceContext;
+
+/**
+ * Defines base methods for ResourceContext implementations.
+ */
+public abstract class AbstractResourceContext implements ResourceContext {
+  private final TreeLogger logger;
+  private final GeneratorContext context;
+  private final JClassType resourceBundleType;
+  private String simpleSourceName;
+
+  protected AbstractResourceContext(TreeLogger logger,
+      GeneratorContext context, JClassType resourceBundleType) {
+    this.logger = logger;
+    this.context = context;
+    this.resourceBundleType = resourceBundleType;
+  }
+
+  public JClassType getClientBundleType() {
+    return resourceBundleType;
+  }
+
+  public GeneratorContext getGeneratorContext() {
+    return context;
+  }
+
+  public String getImplementationSimpleSourceName() {
+    if (simpleSourceName == null) {
+      throw new IllegalStateException(
+          "Simple source name has not yet been set.");
+    }
+    return simpleSourceName;
+  }
+
+  protected TreeLogger getLogger() {
+    return logger;
+  }
+
+  void setSimpleSourceName(String name) {
+    simpleSourceName = name;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/InlineClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/InlineClientBundleGenerator.java
new file mode 100644
index 0000000..ecbd37d
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/InlineClientBundleGenerator.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+/**
+ * This is a refinement that will use data urls for browsers that support them.
+ * Only files whose size are smaller than MAX_INLINE_SIZE will be inlined.
+ * Larger files will use the standard CacheBundle behavior.
+ * 
+ * @see "RFC 2397"
+ */
+public final class InlineClientBundleGenerator extends
+    AbstractClientBundleGenerator {
+  protected AbstractResourceContext createResourceContext(TreeLogger logger,
+      GeneratorContext context, JClassType resourceBundleType) {
+    return new InlineResourceContext(logger.branch(TreeLogger.DEBUG,
+        "Using inline resources", null), context, resourceBundleType);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java
new file mode 100644
index 0000000..53aef65
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/InlineResourceContext.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+class InlineResourceContext extends StaticResourceContext {
+  /**
+   * The largest file size that will be inlined. Note that this value is taken
+   * before any encodings are applied.
+   */
+  // The JLS specifies a maximum size for any string to be 2^16 characters, so
+  // we'll leave some padding. Assuming a Base64 encoding, it is true that
+  // (2 ^ 15) * 4/3 < 2 ^ 16, so we can safely inline files up to 32k.
+  private static final int MAX_INLINE_SIZE = 2 << 15;
+
+  InlineResourceContext(TreeLogger logger, GeneratorContext context,
+      JClassType resourceBundleType) {
+    super(logger, context, resourceBundleType);
+  }
+
+  @Override
+  public String deploy(String suggestedFileName, String mimeType, byte[] data,
+      boolean xhrCompatible) throws UnableToCompleteException {
+    TreeLogger logger = getLogger();
+
+    // data: URLs are not compatible with XHRs on FF and Safari browsers
+    if ((!xhrCompatible) && (data.length < MAX_INLINE_SIZE)) {
+      logger.log(TreeLogger.DEBUG, "Inlining", null);
+
+      // This is bad, but I am lazy and don't want to write _another_ encoder
+      sun.misc.BASE64Encoder enc = new sun.misc.BASE64Encoder();
+      String base64Contents = enc.encode(data).replaceAll("\\s+", "");
+
+      return "\"data:" + mimeType + ";base64," + base64Contents + "\"";
+    } else {
+      return super.deploy(suggestedFileName, mimeType, data, true);
+    }
+  }
+
+  @Override
+  public boolean supportsDataUrls() {
+    return true;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/rebind/context/StaticClientBundleGenerator.java b/user/src/com/google/gwt/resources/rebind/context/StaticClientBundleGenerator.java
new file mode 100644
index 0000000..44f6534
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/StaticClientBundleGenerator.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+
+/**
+ * Copies selected files into module output with strong names and generates the
+ * ClientBundle mappings.
+ */
+public final class StaticClientBundleGenerator extends
+    AbstractClientBundleGenerator {
+
+  protected AbstractResourceContext createResourceContext(TreeLogger logger,
+      GeneratorContext context, JClassType resourceBundleType) {
+    return new StaticResourceContext(logger.branch(TreeLogger.DEBUG,
+        "Using static resources", null), context, resourceBundleType);
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java b/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java
new file mode 100644
index 0000000..a4beb90
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rebind/context/StaticResourceContext.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rebind.context;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+
+class StaticResourceContext extends AbstractResourceContext {
+  /**
+   * The name of a deferred binding property that determines whether or not this
+   * generator will rename the incoming resources to strong file names.
+   */
+  static final String ENABLE_RENAMING = "ClientBundle.enableRenaming";
+
+  StaticResourceContext(TreeLogger logger, GeneratorContext context,
+      JClassType resourceBundleType) {
+    super(logger, context, resourceBundleType);
+  }
+
+  public String deploy(String suggestedFileName, String mimeType, byte[] data,
+      boolean xhrCompatible) throws UnableToCompleteException {
+    TreeLogger logger = getLogger();
+    GeneratorContext context = getGeneratorContext();
+    PropertyOracle propertyOracle = context.getPropertyOracle();
+
+    // See if filename obfuscation should be enabled
+    String enableRenaming = null;
+    try {
+      enableRenaming = propertyOracle.getPropertyValue(logger, ENABLE_RENAMING);
+    } catch (BadPropertyValueException e) {
+      logger.log(TreeLogger.ERROR, "Bad value for " + ENABLE_RENAMING, e);
+      throw new UnableToCompleteException();
+    }
+
+    // Determine the final filename for the resource's file
+    String outputName;
+    if (Boolean.parseBoolean(enableRenaming)) {
+      String strongName = Util.computeStrongName(data);
+
+      // Determine the extension of the original file
+      String extension;
+      int lastIdx = suggestedFileName.lastIndexOf('.');
+      if (lastIdx != -1) {
+        extension = suggestedFileName.substring(lastIdx + 1);
+      } else {
+        extension = "noext";
+      }
+
+      // The name will be MD5.cache.ext
+      outputName = strongName + ".cache." + extension;
+
+    } else {
+      outputName = suggestedFileName.substring(suggestedFileName.lastIndexOf('/') + 1);
+    }
+
+    // Ask the context for an OutputStream into the named resource
+    OutputStream out = context.tryCreateResource(logger, outputName);
+
+    // This would be null if the resource has already been created in the
+    // output (because two or more resources had identical content).
+    if (out != null) {
+      try {
+        out.write(data);
+
+      } catch (IOException e) {
+        logger.log(TreeLogger.ERROR, "Unable to write data to output name "
+            + outputName, e);
+        throw new UnableToCompleteException();
+      }
+
+      // If there's an error, this won't be called and there will be nothing
+      // created in the output directory.
+      context.commitResource(logger, out);
+
+      logger.log(TreeLogger.DEBUG, "Copied " + data.length + " bytes to "
+          + outputName, null);
+    }
+
+    // Return a Java expression
+    return "GWT.getModuleBaseURL() + \"" + outputName + "\"";
+  }
+
+  public String deploy(URL resource, boolean xhrCompatible)
+      throws UnableToCompleteException {
+    String fileName = ResourceGeneratorUtil.baseName(resource);
+    byte[] bytes = Util.readURLAsBytes(resource);
+    try {
+      return deploy(fileName, resource.openConnection().getContentType(),
+          bytes, xhrCompatible);
+    } catch (IOException e) {
+      getLogger().log(TreeLogger.ERROR,
+          "Unable to determine mime type of resource", e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  public boolean supportsDataUrls() {
+    return false;
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/resources/rg/AbstractResourceGenerator.java b/user/src/com/google/gwt/resources/rg/AbstractResourceGenerator.java
new file mode 100644
index 0000000..bec4310
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/AbstractResourceGenerator.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.ext.ClientBundleFields;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGenerator;
+
+/**
+ * A base class providing common methods for ResourceGenerator implementations.
+ * 
+ * @see com.google.gwt.resources.ext.ResourceGeneratorUtil
+ */
+public abstract class AbstractResourceGenerator implements ResourceGenerator {
+  public abstract String createAssignment(TreeLogger logger,
+      ResourceContext context, JMethod method) throws UnableToCompleteException;
+
+  /**
+   * A no-op implementation.
+   */
+  public void createFields(TreeLogger logger, ResourceContext context,
+      ClientBundleFields fields) throws UnableToCompleteException {
+  }
+
+  /**
+   * A no-op implementation.
+   */
+  public void finish(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException {
+  }
+
+  /**
+   * A no-op implementation.
+   */
+  public void init(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException {
+  }
+
+  /**
+   * A no-op implementation.
+   */
+  public void prepare(TreeLogger logger, ResourceContext context,
+      ClientBundleRequirements requirements, JMethod method)
+      throws UnableToCompleteException {
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/BundleResourceGenerator.java b/user/src/com/google/gwt/resources/rg/BundleResourceGenerator.java
new file mode 100644
index 0000000..579677d
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/BundleResourceGenerator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.ext.ResourceContext;
+
+/**
+ * This is a special case of ResourceGenerator that handles nested bundles.
+ */
+public class BundleResourceGenerator extends AbstractResourceGenerator {
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    JClassType gwtType = context.getGeneratorContext().getTypeOracle().findType(
+        GWT.class.getName());
+    assert gwtType != null;
+
+    return gwtType.getQualifiedSourceName() + ".create("
+        + method.getReturnType().getQualifiedSourceName() + ".class)";
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
new file mode 100644
index 0000000..0a5c70b
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/CssResourceGenerator.java
@@ -0,0 +1,1614 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.BadPropertyValueException;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.PropertyOracle;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.NotFoundException;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+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.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.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.CssGenerationVisitor;
+import com.google.gwt.resources.css.GenerateCssAst;
+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.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;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+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.regex.Pattern;
+import java.util.zip.Adler32;
+
+/**
+ * Provides implementations of CSSResources.
+ */
+public class CssResourceGenerator extends AbstractResourceGenerator {
+  private static final String[] DEFAULT_EXTENSIONS = new String[] {".css"};
+
+  static class ClassRenamer extends CssVisitor {
+    private final Map<String, Map<JMethod, String>> classReplacementsWithPrefix;
+    private final Pattern classSelectorPattern = Pattern.compile("\\.([^ :>+#.]*)");
+    private final TreeLogger logger;
+    private final Set<JMethod> missingClasses;
+    private final Set<String> replacedClasses = new HashSet<String>();
+    private final boolean strict;
+    private final Set<String> unknownClasses = new HashSet<String>();
+
+    public ClassRenamer(TreeLogger logger,
+        Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+        boolean strict) {
+      this.logger = logger.branch(TreeLogger.DEBUG, "Replacing CSS class names");
+      this.classReplacementsWithPrefix = classReplacementsWithPrefix;
+      this.strict = strict;
+
+      // Require a definition for all classes in the default namespace
+      assert classReplacementsWithPrefix.containsKey("");
+      missingClasses = new HashSet<JMethod>(
+          classReplacementsWithPrefix.get("").keySet());
+    }
+
+    @Override
+    public void endVisit(CssSelector x, Context ctx) {
+      String sel = x.getSelector();
+
+      // TODO This would be simplified by having a class hierarchy for selectors
+      for (Map.Entry<String, Map<JMethod, String>> outerEntry : classReplacementsWithPrefix.entrySet()) {
+        String prefix = outerEntry.getKey();
+        for (Map.Entry<JMethod, String> entry : outerEntry.getValue().entrySet()) {
+          String name = entry.getKey().getName();
+
+          ClassName className = entry.getKey().getAnnotation(ClassName.class);
+          if (className != null) {
+            name = className.value();
+          }
+          name = prefix + name;
+
+          Pattern p = Pattern.compile("(.*)\\.(" + Pattern.quote(name)
+              + ")([ :>+#.].*|$)");
+          Matcher m = p.matcher(sel);
+          if (m.find()) {
+            sel = m.group(1) + "." + entry.getValue() + m.group(3);
+            missingClasses.remove(entry.getKey());
+            if (strict) {
+              replacedClasses.add(entry.getValue());
+            }
+          }
+        }
+      }
+
+      sel = sel.trim();
+
+      if (strict) {
+        Matcher m = classSelectorPattern.matcher(sel);
+        while (m.find()) {
+          String classSelector = m.group(1);
+          if (!replacedClasses.contains(classSelector)) {
+            unknownClasses.add(classSelector);
+          }
+        }
+      }
+
+      x.setSelector(sel);
+    }
+
+    @Override
+    public void endVisit(CssStylesheet x, Context ctx) {
+      boolean stop = false;
+      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);
+        }
+      }
+
+      if (stop) {
+        throw new CssCompilerException("Missing a CSS replacement");
+      }
+    }
+  }
+
+  /**
+   * 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 = oracle.getPropertyValue(logger, propertyName);
+
+          /*
+           * 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);
+        }
+      }
+    }
+  }
+
+  static class JClassOrderComparator implements Comparator<JClassType> {
+    public int compare(JClassType o1, JClassType o2) {
+      return o1.getQualifiedSourceName().compareTo(o2.getQualifiedSourceName());
+    }
+  }
+
+  /**
+   * 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.accept(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.accept(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 method = context.getClientBundleType().findMethod(
+              functionName, new JType[0]);
+
+          if (method == null) {
+            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.
+   */
+  private static final char[] BASE32_CHARS = new char[] {
+      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
+      'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '-', '0',
+      '1', '2', '3', '4'};
+
+  /**
+   * This value is used by {@link #concatOp} to help create a more balanced AST
+   * tree by producing parenthetical expressions.
+   */
+  private static final int CONCAT_EXPRESSION_LIMIT = 20;
+
+  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) {
+    if (a.getProperties().size() == 0 || b.getProperties().size() == 0) {
+      return false;
+    }
+
+    SortedSet<String> aProperties = new TreeSet<String>();
+    SortedSet<String> bProperties = new TreeSet<String>();
+
+    for (CssProperty p : a.getProperties()) {
+      aProperties.add(p.getName());
+    }
+    for (CssProperty p : b.getProperties()) {
+      bProperties.add(p.getName());
+    }
+
+    Iterator<String> ai = aProperties.iterator();
+    Iterator<String> bi = bProperties.iterator();
+
+    String aName = ai.next();
+    String bName = bi.next();
+    for (;;) {
+      int comp = aName.compareToIgnoreCase(bName);
+      if (comp == 0) {
+        return true;
+      } else if (comp > 0) {
+        if (aName.startsWith(bName + "-")) {
+          return true;
+        }
+
+        if (!bi.hasNext()) {
+          break;
+        }
+        bName = bi.next();
+      } else {
+        if (bName.startsWith(aName + "-")) {
+          return true;
+        }
+        if (!ai.hasNext()) {
+          break;
+        }
+        aName = ai.next();
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Create a Java expression that evaluates to a string representation of the
+   * given node. Visible only for testing.
+   */
+  static <T extends CssNode & HasNodes> String makeExpression(
+      TreeLogger logger, ResourceContext context, JClassType cssResourceType,
+      T node, boolean prettyOutput) throws UnableToCompleteException {
+    // Generate the CSS template
+    DefaultTextOutput out = new DefaultTextOutput(!prettyOutput);
+    CssGenerationVisitor v = new CssGenerationVisitor(out);
+    v.accept(node);
+
+    // Generate the final Java expression
+    String template = out.toString();
+    StringBuilder b = new StringBuilder();
+    int start = 0;
+
+    /*
+     * Very large concatenation expressions using '+' cause the GWT compiler to
+     * overflow the stack due to deep AST nesting. The workaround for now is to
+     * force it to be more balanced using intermediate concatenation groupings.
+     * 
+     * This variable is used to track the number of subexpressions within the
+     * current parenthetical expression.
+     */
+    int numExpressions = 0;
+
+    b.append('(');
+    for (Map.Entry<Integer, List<CssNode>> entry : v.getSubstitutionPositions().entrySet()) {
+      // Add the static section between start and the substitution point
+      b.append('"');
+      b.append(Generator.escape(template.substring(start, entry.getKey())));
+      b.append('\"');
+      numExpressions = concatOp(numExpressions, b);
+
+      // Add the nodes at the substitution point
+      for (CssNode x : entry.getValue()) {
+        TreeLogger loopLogger = logger.branch(TreeLogger.DEBUG,
+            "Performing substitution in node " + x.toString());
+
+        if (x instanceof CssIf) {
+          CssIf asIf = (CssIf) x;
+
+          // Generate the sub-expressions
+          String expression = makeExpression(loopLogger, context,
+              cssResourceType, new CollapsedNode(asIf), prettyOutput);
+
+          String elseExpression;
+          if (asIf.getElseNodes().isEmpty()) {
+            // We'll treat an empty else block as an empty string
+            elseExpression = "\"\"";
+          } else {
+            elseExpression = makeExpression(loopLogger, context,
+                cssResourceType, new CollapsedNode(asIf.getElseNodes()),
+                prettyOutput);
+          }
+
+          // ((expr) ? "CSS" : "elseCSS") +
+          b.append("((" + asIf.getExpression() + ") ? " + expression + " : "
+              + elseExpression + ") ");
+          numExpressions = concatOp(numExpressions, b);
+
+        } else if (x instanceof CssProperty) {
+          CssProperty property = (CssProperty) x;
+
+          validateValue(loopLogger, context.getClientBundleType(),
+              property.getValues());
+
+          // (expr) +
+          b.append("(" + property.getValues().getExpression() + ") ");
+          numExpressions = concatOp(numExpressions, b);
+
+        } else {
+          // This indicates that some magic node is slipping by our visitors
+          loopLogger.log(TreeLogger.ERROR, "Unhandled substitution "
+              + x.getClass());
+          throw new UnableToCompleteException();
+        }
+      }
+      start = entry.getKey();
+    }
+
+    // Add the remaining parts of the template
+    b.append('"');
+    b.append(Generator.escape(template.substring(start)));
+    b.append('"');
+    b.append(')');
+
+    return b.toString();
+  }
+
+  /**
+   * Check if number of concat expressions currently exceeds limit and either
+   * append '+' if the limit isn't reached or ') + (' if it is.
+   * 
+   * @return numExpressions + 1 or 0 if limit was exceeded.
+   */
+  private static int concatOp(int numExpressions, StringBuilder b) {
+    /*
+     * TODO: Fix the compiler to better handle arbitrarily long concatenation
+     * expressions.
+     */
+    if (numExpressions >= CONCAT_EXPRESSION_LIMIT) {
+      b.append(") + (");
+      return 0;
+    }
+
+    b.append(" + ");
+    return numExpressions + 1;
+  }
+
+  private static String makeIdent(long id) {
+    assert id >= 0;
+
+    StringBuilder b = new StringBuilder();
+
+    // Use only guaranteed-alpha characters for the first character
+    b.append(BASE32_CHARS[(int) (id & 0xf)]);
+    id >>= 4;
+
+    while (id != 0) {
+      b.append(BASE32_CHARS[(int) (id & 0x1f)]);
+      id >>= 5;
+    }
+
+    return b.toString();
+  }
+
+  /**
+   * This function validates any context-sensitive Values.
+   */
+  private static void validateValue(TreeLogger logger,
+      JClassType resourceBundleType, Value value)
+      throws UnableToCompleteException {
+
+    ListValue list = value.isListValue();
+    if (list != null) {
+      for (Value v : list.getValues()) {
+        validateValue(logger, resourceBundleType, v);
+      }
+      return;
+    }
+
+    DotPathValue dot = value.isDotPathValue();
+    if (dot != null) {
+      String[] elements = dot.getPath().split("\\.");
+      if (elements.length == 0) {
+        logger.log(TreeLogger.ERROR, "value() functions must specify a path");
+        throw new UnableToCompleteException();
+      }
+
+      JType currentType = resourceBundleType;
+      for (Iterator<String> i = Arrays.asList(elements).iterator(); i.hasNext();) {
+        String pathElement = i.next();
+
+        JClassType referenceType = currentType.isClassOrInterface();
+        if (referenceType == null) {
+          logger.log(TreeLogger.ERROR, "Cannot resolve member " + pathElement
+              + " on non-reference type "
+              + currentType.getQualifiedSourceName());
+          throw new UnableToCompleteException();
+        }
+
+        try {
+          JMethod m = referenceType.getMethod(pathElement, new JType[0]);
+          currentType = m.getReturnType();
+        } catch (NotFoundException e) {
+          logger.log(TreeLogger.ERROR, "Could not find no-arg method named "
+              + pathElement + " in type "
+              + currentType.getQualifiedSourceName());
+          throw new UnableToCompleteException();
+        }
+      }
+      return;
+    }
+  }
+
+  private String classPrefix;
+  private JClassType cssResourceType;
+  private JClassType elementType;
+  private boolean enableMerge;
+  private boolean prettyOutput;
+  private Map<JClassType, Map<JMethod, String>> replacementsByClassAndMethod;
+
+  private Map<JMethod, String> replacementsForSharedMethods;
+
+  private Map<JMethod, CssStylesheet> stylesheetMap;
+
+  private JClassType stringType;
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+
+    SourceWriter sw = new StringSourceWriter();
+    // Write the expression to create the subtype.
+    sw.println("new " + method.getReturnType().getQualifiedSourceName()
+        + "() {");
+    sw.indent();
+
+    JClassType cssResourceSubtype = method.getReturnType().isInterface();
+    assert cssResourceSubtype != null;
+    Map<String, Map<JMethod, String>> replacementsWithPrefix = new HashMap<String, Map<JMethod, String>>();
+
+    replacementsWithPrefix.put("",
+        computeReplacementsForType(cssResourceSubtype));
+    Import imp = method.getAnnotation(Import.class);
+    if (imp != null) {
+      boolean fail = false;
+      for (Class<? extends CssResource> clazz : imp.value()) {
+        JClassType importType = context.getGeneratorContext().getTypeOracle().findType(
+            clazz.getName().replace('$', '.'));
+        String prefix = importType.getSimpleSourceName();
+        ImportedWithPrefix exp = importType.getAnnotation(ImportedWithPrefix.class);
+        if (exp != null) {
+          prefix = exp.value();
+        }
+        assert importType != null;
+
+        if (replacementsWithPrefix.put(prefix + "-",
+            computeReplacementsForType(importType)) != null) {
+          logger.log(TreeLogger.ERROR,
+              "Multiple imports that would use the prefix " + prefix);
+          fail = true;
+        }
+      }
+      if (fail) {
+        throw new UnableToCompleteException();
+      }
+    }
+
+    /*
+     * getOverridableMethods is used to handle CssResources extending
+     * non-CssResource types. See the discussion in computeReplacementsForType.
+     */
+    for (JMethod toImplement : cssResourceSubtype.getOverridableMethods()) {
+      String name = toImplement.getName();
+      if ("getName".equals(name) || "getText".equals(name)) {
+        continue;
+      }
+
+      if (toImplement.getReturnType().equals(stringType)
+          && toImplement.getParameters().length == 0) {
+        writeClassAssignment(sw, toImplement, replacementsWithPrefix.get(""));
+
+      } else if (toImplement.getReturnType().isPrimitive() != null
+          && toImplement.getParameters().length == 0) {
+        writeDefAssignment(logger, sw, toImplement, stylesheetMap.get(method));
+
+      } else {
+        logger.log(TreeLogger.ERROR, "Don't know how to implement method "
+            + toImplement.getName());
+        throw new UnableToCompleteException();
+      }
+    }
+
+    sw.println("public String getText() {");
+    sw.indent();
+
+    boolean strict = method.getAnnotation(Strict.class) != null;
+    if (!strict) {
+      /*
+       * The developer may choose to force strict behavior onto the system. If
+       * the method does already have the @Strict annotation, print a warning.
+       */
+      try {
+        PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle();
+        String propertyValue = propertyOracle.getPropertyValue(logger,
+            "CssResource.strictAccessors");
+        if (Boolean.valueOf(propertyValue)) {
+          logger.log(TreeLogger.WARN, "CssResource.forceStrict is true, but "
+              + method.getName() + "() is missing the @Strict annotation.");
+          strict = true;
+        }
+      } catch (BadPropertyValueException e) {
+        // Ignore
+      }
+    }
+
+    String cssExpression = makeExpression(logger, context, cssResourceSubtype,
+        stylesheetMap.get(method), replacementsWithPrefix, strict);
+    sw.println("return " + cssExpression + ";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.println("public String getName() {");
+    sw.indent();
+    sw.println("return \"" + method.getName() + "\";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.outdent();
+    sw.println("}");
+
+    return sw.toString();
+  }
+
+  @Override
+  public void init(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException {
+    try {
+      PropertyOracle propertyOracle = context.getGeneratorContext().getPropertyOracle();
+      String style = propertyOracle.getPropertyValue(logger,
+          "CssResource.style").toLowerCase();
+      prettyOutput = style.equals("pretty");
+
+      String merge = propertyOracle.getPropertyValue(logger,
+          "CssResource.mergeEnabled").toLowerCase();
+      enableMerge = merge.equals("true");
+
+      classPrefix = propertyOracle.getPropertyValue(logger,
+          "CssResource.obfuscationPrefix");
+    } catch (BadPropertyValueException e) {
+      logger.log(TreeLogger.WARN, "Unable to query module property", e);
+      throw new UnableToCompleteException();
+    }
+
+    if ("default".equals(classPrefix)) {
+      // Compute it later in computeObfuscatedNames();
+      classPrefix = null;
+    } else if ("empty".equals(classPrefix)) {
+      classPrefix = "";
+    }
+
+    // Find all of the types that we care about in the type system
+    TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
+
+    cssResourceType = typeOracle.findType(CssResource.class.getName());
+    assert cssResourceType != null;
+
+    elementType = typeOracle.findType(Element.class.getName());
+    assert elementType != null;
+
+    stringType = typeOracle.findType(String.class.getName());
+    assert stringType != null;
+
+    replacementsByClassAndMethod = new IdentityHashMap<JClassType, Map<JMethod, String>>();
+    replacementsForSharedMethods = new IdentityHashMap<JMethod, String>();
+    stylesheetMap = new IdentityHashMap<JMethod, CssStylesheet>();
+
+    computeObfuscatedNames(logger);
+  }
+
+  @Override
+  public void prepare(TreeLogger logger, ResourceContext context,
+      ClientBundleRequirements requirements, JMethod method)
+      throws UnableToCompleteException {
+
+    if (method.getReturnType().isInterface() == null) {
+      logger.log(TreeLogger.ERROR, "Return type must be an interface");
+      throw new UnableToCompleteException();
+    }
+
+    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
+        method, DEFAULT_EXTENSIONS);
+
+    if (resources.length == 0) {
+      logger.log(TreeLogger.ERROR, "At least one source must be specified");
+      throw new UnableToCompleteException();
+    }
+
+    // Create the AST and do a quick scan for requirements
+    CssStylesheet sheet = GenerateCssAst.exec(logger, resources);
+    stylesheetMap.put(method, sheet);
+    (new RequirementsCollector(logger, requirements)).accept(sheet);
+  }
+
+  /**
+   * Each distinct type of CssResource has a unique collection of values that it
+   * will return, excepting for those methods that are defined within an
+   * interface that is tagged with {@code @Shared}.
+   */
+  private void computeObfuscatedNames(TreeLogger logger) {
+    logger = logger.branch(TreeLogger.DEBUG, "Computing CSS class replacements");
+
+    SortedSet<JClassType> cssResourceSubtypes = computeOperableTypes(logger);
+
+    if (classPrefix == null) {
+      Adler32 checksum = new Adler32();
+      for (JClassType type : cssResourceSubtypes) {
+        checksum.update(Util.getBytes(type.getQualifiedSourceName()));
+      }
+      classPrefix = "G"
+          + Long.toString(checksum.getValue(), Character.MAX_RADIX);
+    }
+
+    int count = 0;
+    for (JClassType type : cssResourceSubtypes) {
+      Map<JMethod, String> replacements = new IdentityHashMap<JMethod, String>();
+      replacementsByClassAndMethod.put(type, replacements);
+
+      for (JMethod method : type.getOverridableMethods()) {
+        String name = method.getName();
+        if ("getName".equals(name) || "getText".equals(name)
+            || !stringType.equals(method.getReturnType())) {
+          continue;
+        }
+
+        // The user provided the class name to use
+        ClassName classNameOverride = method.getAnnotation(ClassName.class);
+        if (classNameOverride != null) {
+          name = classNameOverride.value();
+        }
+
+        String obfuscatedClassName;
+        if (prettyOutput) {
+          obfuscatedClassName = classPrefix + "-"
+              + type.getQualifiedSourceName().replaceAll("[.$]", "-") + "-"
+              + name;
+        } else {
+          obfuscatedClassName = classPrefix + makeIdent(count++);
+        }
+
+        replacements.put(method, obfuscatedClassName);
+
+        if (method.getEnclosingType() == type) {
+          Shared shared = type.getAnnotation(Shared.class);
+          if (shared != null) {
+            replacementsForSharedMethods.put(method, obfuscatedClassName);
+          }
+        }
+
+        logger.log(TreeLogger.SPAM, "Mapped " + type.getQualifiedSourceName()
+            + "." + name + " to " + obfuscatedClassName);
+      }
+    }
+  }
+
+  /**
+   * Returns all interfaces derived from CssResource, sorted by qualified name.
+   * <p>
+   * We'll ignore concrete implementations of CssResource, which include types
+   * previously-generated by CssResourceGenerator and user-provided
+   * implementations of CssResource, which aren't valid for use with
+   * CssResourceGenerator anyway. By ignoring newly-generated CssResource types,
+   * we'll ensure a stable ordering, regardless of the actual execution order
+   * used by the Generator framework.
+   * <p>
+   * It is still possible that additional pure-interfaces could be introduced by
+   * other generators, which would change the result of this computation, but
+   * there is presently no way to determine when, or by what means, a type was
+   * added to the TypeOracle.
+   */
+  private SortedSet<JClassType> computeOperableTypes(TreeLogger logger) {
+    logger = logger.branch(TreeLogger.DEBUG,
+        "Finding operable CssResource subtypes");
+
+    SortedSet<JClassType> toReturn = new TreeSet<JClassType>(
+        new JClassOrderComparator());
+
+    JClassType[] cssResourceSubtypes = cssResourceType.getSubtypes();
+    for (JClassType type : cssResourceSubtypes) {
+      if (type.isInterface() != null) {
+        logger.log(TreeLogger.SPAM, "Added " + type.getQualifiedSourceName());
+        toReturn.add(type);
+
+      } else {
+        logger.log(TreeLogger.SPAM, "Ignored " + type.getQualifiedSourceName());
+      }
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Compute the mapping of original class names to obfuscated type names for a
+   * given subtype of CssResource. Mappings are inherited from the type's
+   * supertypes.
+   */
+  private Map<JMethod, String> computeReplacementsForType(JClassType type) {
+    Map<JMethod, String> toReturn = new IdentityHashMap<JMethod, String>();
+
+    /*
+     * We check to see if the type is derived from CssResource so that we can
+     * handle the case of a CssResource type being derived from a
+     * non-CssResource base type. This basically collapses the non-CssResource
+     * base types into their least-derived CssResource subtypes.
+     */
+    if (type == null || !derivedFromCssResource(type)) {
+      return toReturn;
+    }
+
+    if (replacementsByClassAndMethod.containsKey(type)) {
+      toReturn.putAll(replacementsByClassAndMethod.get(type));
+    }
+
+    /*
+     * Replacements for methods defined in shared types will override any
+     * locally-computed values.
+     */
+    for (JMethod method : type.getOverridableMethods()) {
+      if (replacementsForSharedMethods.containsKey(method)) {
+        assert toReturn.containsKey(method);
+        toReturn.put(method, replacementsForSharedMethods.get(method));
+      }
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * Determine if a type is derived from CssResource.
+   */
+  private boolean derivedFromCssResource(JClassType type) {
+    List<JClassType> superInterfaces = Arrays.asList(type.getImplementedInterfaces());
+    if (superInterfaces.contains(cssResourceType)) {
+      return true;
+    }
+
+    JClassType superClass = type.getSuperclass();
+    if (superClass != null) {
+      if (derivedFromCssResource(superClass)) {
+        return true;
+      }
+    }
+
+    for (JClassType superInterface : superInterfaces) {
+      if (derivedFromCssResource(superInterface)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Create a Java expression that evaluates to the string representation of the
+   * stylesheet resource.
+   */
+  private String makeExpression(TreeLogger logger, ResourceContext context,
+      JClassType cssResourceType, CssStylesheet sheet,
+      Map<String, Map<JMethod, String>> classReplacementsWithPrefix,
+      boolean strict) throws UnableToCompleteException {
+
+    try {
+
+      // Create CSS sprites
+      (new Spriter(logger, context)).accept(sheet);
+
+      // Perform @def and @eval substitutions
+      SubstitutionCollector collector = new SubstitutionCollector();
+      collector.accept(sheet);
+
+      (new SubstitutionReplacer(logger, context, collector.substitutions)).accept(sheet);
+
+      // Evaluate @if statements based on deferred binding properties
+      (new IfEvaluator(logger,
+          context.getGeneratorContext().getPropertyOracle())).accept(sheet);
+
+      // Rename css .class selectors
+      (new ClassRenamer(logger, classReplacementsWithPrefix, strict)).accept(sheet);
+
+      // Combine rules with identical selectors
+      if (enableMerge) {
+        // TODO This is an off-switch while this is being developed; remove
+        (new SplitRulesVisitor()).accept(sheet);
+        (new MergeIdenticalSelectorsVisitor()).accept(sheet);
+        (new MergeRulesByContentVisitor()).accept(sheet);
+      }
+
+      String standard = makeExpression(logger, context, cssResourceType, sheet,
+          prettyOutput);
+
+      (new RtlVisitor()).accept(sheet);
+
+      String reversed = makeExpression(logger, context, cssResourceType, sheet,
+          prettyOutput);
+
+      return "com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ? ("
+          + reversed + ") : (" + standard + ")";
+
+    } catch (CssCompilerException e) {
+      // Take this as a sign that one of the visitors was unhappy, but only
+      // log the stack trace if there's a causal (i.e. unknown) exception.
+      logger.log(TreeLogger.ERROR, "Unable to process CSS",
+          e.getCause() == null ? null : e);
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Write the CssResource accessor method for simple String return values.
+   */
+  private void writeClassAssignment(SourceWriter sw, JMethod toImplement,
+      Map<JMethod, String> classReplacements) {
+
+    String replacement = classReplacements.get(toImplement);
+    assert replacement != null;
+
+    sw.println(toImplement.getReadableDeclaration(false, true, true, true, true)
+        + "{");
+    sw.indent();
+    sw.println("return \"" + replacement + "\";");
+    sw.outdent();
+    sw.println("}");
+  }
+
+  private void writeDefAssignment(TreeLogger logger, SourceWriter sw,
+      JMethod toImplement, CssStylesheet cssStylesheet)
+      throws UnableToCompleteException {
+    SubstitutionCollector collector = new SubstitutionCollector();
+    collector.accept(cssStylesheet);
+
+    String name = toImplement.getName();
+    // TODO: Annotation for override
+
+    CssDef def = collector.substitutions.get(name);
+    if (def == null) {
+      logger.log(TreeLogger.ERROR, "No @def rule for name " + name);
+      throw new UnableToCompleteException();
+    }
+
+    // TODO: Allow returning an array of values
+    if (def.getValues().size() != 1) {
+      logger.log(TreeLogger.ERROR, "@def rule " + name
+          + " must define exactly one value");
+      throw new UnableToCompleteException();
+    }
+
+    NumberValue numberValue = def.getValues().get(0).isNumberValue();
+
+    if (numberValue == null) {
+      logger.log(TreeLogger.ERROR, "The define named " + name
+          + " does not define a numeric value");
+      throw new UnableToCompleteException();
+    }
+
+    JPrimitiveType returnType = toImplement.getReturnType().isPrimitive();
+    assert returnType != null;
+
+    sw.print(toImplement.getReadableDeclaration(false, false, false, false,
+        true));
+    sw.println(" {");
+    sw.indent();
+    if (returnType == JPrimitiveType.INT || returnType == JPrimitiveType.LONG) {
+      sw.println("return " + Math.round(numberValue.getValue()) + ";");
+    } else if (returnType == JPrimitiveType.FLOAT) {
+      sw.println("return " + numberValue.getValue() + "F;");
+    } else if (returnType == JPrimitiveType.DOUBLE) {
+      sw.println("return " + numberValue.getValue() + ";");
+    } else {
+      logger.log(TreeLogger.ERROR, returnType.getQualifiedSourceName()
+          + " is not a valid return type for @def accessors");
+      throw new UnableToCompleteException();
+    }
+    sw.outdent();
+    sw.println("}");
+
+    numberValue.getValue();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
new file mode 100644
index 0000000..f90257a
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+import java.net.URL;
+
+/**
+ * Provides implementations of DataResource.
+ */
+public final class DataResourceGenerator extends AbstractResourceGenerator {
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+
+    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
+        method);
+
+    if (resources.length != 1) {
+      logger.log(TreeLogger.ERROR, "Exactly one resource must be specified",
+          null);
+      throw new UnableToCompleteException();
+    }
+
+    URL resource = resources[0];
+    String outputUrlExpression = context.deploy(resource, false);
+
+    SourceWriter sw = new StringSourceWriter();
+    // Write the expression to create the subtype.
+    sw.println("new " + DataResource.class.getName() + "() {");
+    sw.indent();
+
+    // Convenience when examining the generated code.
+    sw.println("// " + resource.toExternalForm());
+
+    sw.println("public String getUrl() {");
+    sw.indent();
+    sw.println("return " + outputUrlExpression + ";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.println("public String getName() {");
+    sw.indent();
+    sw.println("return \"" + method.getName() + "\";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.outdent();
+    sw.println("}");
+
+    return sw.toString();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
new file mode 100644
index 0000000..c5f3d31
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.resources.client.impl.ExternalTextResourcePrototype;
+import com.google.gwt.resources.ext.ClientBundleFields;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Adds {@link ExternalTextResourcePrototype} objects to the bundle.
+ */
+public final class ExternalTextResourceGenerator extends
+    AbstractResourceGenerator {
+  private static final String[] DEFAULT_EXTENSIONS = new String[] {".txt"};
+  private StringBuffer data;
+  private boolean first;
+  private String urlExpression;
+  private Map<String, Integer> hashes;
+  private Map<String, Integer> offsets;
+  private int currentIndex;
+
+  private String externalTextUrlIdent;
+
+  private String externalTextCacheIdent;
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    String name = method.getName();
+
+    SourceWriter sw = new StringSourceWriter();
+    sw.println("new " + ExternalTextResourcePrototype.class.getName() + "(");
+    sw.indent();
+    sw.println('"' + name + "\",");
+    // These are field names
+    sw.println(externalTextUrlIdent + ", " + externalTextCacheIdent + ", ");
+    sw.println(offsets.get(method.getName()).toString());
+    sw.outdent();
+    sw.print(")");
+
+    return sw.toString();
+  }
+
+  @Override
+  public void createFields(TreeLogger logger, ResourceContext context,
+      ClientBundleFields fields) throws UnableToCompleteException {
+    data.append(']');
+
+    urlExpression = context.deploy(
+        context.getClientBundleType().getQualifiedSourceName().replace('.', '_')
+            + "_jsonbundle.txt", "text/plain", data.toString().getBytes(), true);
+
+    TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
+    JClassType stringType = typeOracle.findType(String.class.getName());
+    assert stringType != null;
+
+    externalTextUrlIdent = fields.define(stringType, "externalTextUrl",
+        urlExpression, true, true);
+
+    JClassType textResourceType = typeOracle.findType(TextResource.class.getName());
+    assert textResourceType != null;
+    JType textResourceArrayType = typeOracle.getArrayType(textResourceType);
+
+    externalTextCacheIdent = fields.define(textResourceArrayType,
+        "externalTextCache", "new " + TextResource.class.getName() + "["
+            + currentIndex + "]", true, true);
+  }
+
+  @Override
+  public void init(TreeLogger logger, ResourceContext context)
+      throws UnableToCompleteException {
+    data = new StringBuffer("[\n");
+    first = true;
+    urlExpression = null;
+    hashes = new HashMap<String, Integer>();
+    offsets = new HashMap<String, Integer>();
+    currentIndex = 0;
+  }
+
+  @Override
+  public void prepare(TreeLogger logger, ResourceContext context,
+      ClientBundleRequirements requirements, JMethod method)
+      throws UnableToCompleteException {
+
+    URL[] urls = ResourceGeneratorUtil.findResources(logger, context, method,
+        DEFAULT_EXTENSIONS);
+
+    if (urls.length != 1) {
+      logger.log(TreeLogger.ERROR, "Exactly one resource must be specified",
+          null);
+      throw new UnableToCompleteException();
+    }
+
+    URL resource = urls[0];
+
+    String toWrite = Util.readURLAsString(resource);
+
+    // This de-duplicates strings in the bundle.
+    if (!hashes.containsKey(toWrite)) {
+      hashes.put(toWrite, currentIndex++);
+
+      if (!first) {
+        data.append(",\n");
+      } else {
+        first = false;
+      }
+
+      data.append('"');
+      data.append(Generator.escape(toWrite));
+      data.append('"');
+    }
+
+    // Store the (possibly n:1) mapping of resource function to bundle index.
+    offsets.put(method.getName(), hashes.get(toWrite));
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/GwtCreateResourceGenerator.java b/user/src/com/google/gwt/resources/rg/GwtCreateResourceGenerator.java
new file mode 100644
index 0000000..8b981f4
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/GwtCreateResourceGenerator.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JParameterizedType;
+import com.google.gwt.resources.client.GwtCreateResource.ClassType;
+import com.google.gwt.resources.ext.ResourceContext;
+
+/**
+ * Provides implementations of GwtCreateResource.
+ */
+public class GwtCreateResourceGenerator extends AbstractResourceGenerator {
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    JParameterizedType returnType = method.getReturnType().isParameterized();
+    assert returnType != null;
+
+    JClassType args[] = returnType.getTypeArgs();
+    assert args.length == 1;
+
+    ClassType override = method.getAnnotation(ClassType.class);
+    JClassType toCreate;
+    if (override != null) {
+      toCreate = context.getGeneratorContext().getTypeOracle().findType(
+          override.value().getName().replace('$', '.'));
+      assert toCreate != null;
+    } else {
+      toCreate = args[0];
+    }
+
+    JClassType gwtType = context.getGeneratorContext().getTypeOracle().findType(
+        GWT.class.getName());
+    assert gwtType != null;
+
+    return "new " + returnType.getParameterizedQualifiedSourceName()
+        + "() {\n public " + toCreate.getQualifiedSourceName()
+        + " create() {\n return " + gwtType.getQualifiedSourceName()
+        + ".create(" + toCreate.getQualifiedSourceName() + ".class);}\n"
+        + "public String getName() { return \"" + method.getName() + "\";}}";
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
new file mode 100644
index 0000000..46ce8c2
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/ImageBundleBuilder.java
@@ -0,0 +1,662 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import java.awt.Graphics2D;
+import java.awt.geom.AffineTransform;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Accumulates state for the bundled image.
+ */
+class ImageBundleBuilder {
+  /**
+   * Abstracts the process of arranging a number of images into a composite
+   * image.
+   */
+  interface Arranger {
+    /**
+     * Determine the total area required to store a composite image.
+     */
+    Size arrangeImages(Collection<HasRect> rects);
+  }
+
+  /**
+   * Arranges the images to try to decrease the overall area of the resulting
+   * bundle. This uses a strategy that is basically Next-Fit Decreasing Height
+   * Decreasing Width (NFDHDW). The rectangles to be packed are sorted in
+   * decreasing order by height. The tallest rectangle is placed at the far
+   * left. We attempt to stack the remaining rectangles on top of one another to
+   * construct as many columns as necessary. After finishing each column, we
+   * also attempt to do some horizontal packing to fill up the space left due to
+   * widths of rectangles differing in the column.
+   */
+  static class BestFitArranger implements Arranger {
+    private static final Comparator<HasRect> decreasingHeightComparator = new Comparator<HasRect>() {
+      public int compare(HasRect a, HasRect b) {
+        final int c = b.getHeight() - a.getHeight();
+        // If we encounter equal heights, use the name to keep things
+        // deterministic.
+        return (c != 0) ? c : b.getName().compareTo(a.getName());
+      }
+    };
+
+    private static final Comparator<HasRect> decreasingWidthComparator = new Comparator<HasRect>() {
+      public int compare(HasRect a, HasRect b) {
+        final int c = b.getWidth() - a.getWidth();
+        // If we encounter equal heights, use the name to keep things
+        // deterministic.
+        return (c != 0) ? c : b.getName().compareTo(a.getName());
+      }
+    };
+
+    public Size arrangeImages(Collection<HasRect> rects) {
+      if (rects.size() == 0) {
+        return new Size(0, 0);
+      }
+
+      // Create a list of ImageRects ordered by decreasing height used for
+      // constructing columns.
+      final ArrayList<HasRect> rectsOrderedByHeight = new ArrayList<HasRect>(
+          rects);
+      Collections.sort(rectsOrderedByHeight, decreasingHeightComparator);
+
+      // Create a list of ImageRects ordered by decreasing width used for
+      // packing
+      // individual columns.
+      final ArrayList<HasRect> rectsOrderedByWidth = new ArrayList<HasRect>(
+          rects);
+      Collections.sort(rectsOrderedByWidth, decreasingWidthComparator);
+
+      // Place the first, tallest image as the first column.
+      final HasRect first = rectsOrderedByHeight.get(0);
+      first.setPosition(0, 0);
+
+      // Setup state for laying things cumulatively.
+      int curX = first.getWidth();
+      final int colH = first.getHeight();
+
+      for (int i = 1, n = rectsOrderedByHeight.size(); i < n; i++) {
+        // If this ImageRect has been positioned already, move on.
+        if (rectsOrderedByHeight.get(i).hasBeenPositioned()) {
+          continue;
+        }
+
+        int colW = 0;
+        int curY = 0;
+
+        final ArrayList<HasRect> rectsInColumn = new ArrayList<HasRect>();
+        for (int j = i; j < n; j++) {
+          final HasRect current = rectsOrderedByHeight.get(j);
+          // Look for rects that have not been positioned with a small enough
+          // height to go in this column.
+          if (!current.hasBeenPositioned()
+              && (curY + current.getHeight()) <= colH) {
+
+            // Set the horizontal position here, the top field will be set in
+            // arrangeColumn after we've collected a full set of ImageRects.
+            current.setPosition(curX, 0);
+            colW = Math.max(colW, current.getWidth());
+            curY += current.getHeight();
+
+            // Keep the ImageRects in this column in decreasing order by width.
+            final int pos = Collections.binarySearch(rectsInColumn, current,
+                decreasingWidthComparator);
+            assert pos < 0;
+            rectsInColumn.add(-1 - pos, current);
+          }
+        }
+
+        // Having selected a set of ImageRects that fill out this column
+        // vertical,
+        // now we'll scan the remaining ImageRects to try to fit some in the
+        // horizontal gaps.
+        if (!rectsInColumn.isEmpty()) {
+          arrangeColumn(rectsInColumn, rectsOrderedByWidth);
+        }
+
+        // We're done with that column, so move the horizontal accumulator by
+        // the
+        // width of the column we just finished.
+        curX += colW;
+      }
+
+      return new Size(curX, colH);
+    }
+
+    /**
+     * Companion method to {@link #arrangeImages()}. This method does a best
+     * effort horizontal packing of a column after it was packed vertically.
+     * This is the Decreasing Width part of Next-Fit Decreasing Height
+     * Decreasing Width. The basic strategy is to sort the remaining rectangles
+     * by decreasing width and try to fit them to the left of each of the
+     * rectangles we've already picked for this column.
+     * 
+     * @param rectsInColumn the ImageRects that were already selected for this
+     *          column
+     * @param remainingRectsOrderedByWidth the sub list of ImageRects that may
+     *          not have been positioned yet
+     */
+    private void arrangeColumn(List<HasRect> rectsInColumn,
+        List<HasRect> remainingRectsOrderedByWidth) {
+      final HasRect first = rectsInColumn.get(0);
+
+      final int columnWidth = first.getWidth();
+      int curY = first.getHeight();
+
+      // Skip this first ImageRect because it is guaranteed to consume the full
+      // width of the column.
+      for (int i = 1, m = rectsInColumn.size(); i < m; i++) {
+        final HasRect r = rectsInColumn.get(i);
+        // The ImageRect was previously positioned horizontally, now set the top
+        // field.
+        r.setPosition(r.getLeft(), curY);
+        int curX = r.getWidth();
+
+        // Search for ImageRects that are shorter than the left most ImageRect
+        // and
+        // narrow enough to fit in the column.
+        for (int j = 0, n = remainingRectsOrderedByWidth.size(); j < n; j++) {
+          final HasRect current = remainingRectsOrderedByWidth.get(j);
+          if (!current.hasBeenPositioned()
+              && (curX + current.getWidth()) <= columnWidth
+              && (current.getHeight() <= r.getHeight())) {
+            current.setPosition(r.getLeft() + curX, r.getTop());
+            curX += current.getWidth();
+          }
+        }
+
+        // Update the vertical accumulator so we'll know where to place the next
+        // ImageRect.
+        curY += r.getHeight();
+      }
+    }
+  }
+
+  /**
+   * A mockable interface to test the image arrangement algorithms.
+   */
+  interface HasRect {
+
+    int getHeight();
+
+    BufferedImage getImage();
+
+    int getLeft();
+
+    String getName();
+
+    int getTop();
+
+    AffineTransform getTransform();
+
+    int getWidth();
+
+    boolean hasBeenPositioned();
+
+    void setPosition(int left, int top);
+
+    AffineTransform transform();
+  }
+
+  /**
+   * Performs a simple horizontal arrangement of rectangles. Images will be
+   * tiled vertically to fill to fill the full height of the image.
+   */
+  static class HorizontalArranger implements Arranger {
+    public Size arrangeImages(Collection<HasRect> rects) {
+      int height = 1;
+      int width = 0;
+
+      for (HasRect rect : rects) {
+        rect.setPosition(width, 0);
+        width += rect.getWidth();
+        height = lcm(height, rect.getHeight());
+      }
+
+      List<HasRect> toAdd = new ArrayList<HasRect>();
+      for (HasRect rect : rects) {
+        int y = rect.getHeight();
+        while (y < height) {
+          ImageRect newRect = new ImageRect(rect);
+          newRect.setPosition(rect.getLeft(), y);
+          y += rect.getHeight();
+          toAdd.add(newRect);
+        }
+      }
+      rects.addAll(toAdd);
+
+      return new Size(width, height);
+    }
+  }
+
+  /**
+   * Does not rearrange the rectangles, but simply computes the size of the
+   * canvas needed to hold the images in their current positions.
+   */
+  static class IdentityArranger implements Arranger {
+    public Size arrangeImages(Collection<HasRect> rects) {
+      int height = 0;
+      int width = 0;
+
+      for (HasRect rect : rects) {
+        height = Math.max(height, rect.getTop() + rect.getHeight());
+        width = Math.max(width, rect.getLeft() + rect.getWidth());
+      }
+
+      return new Size(width, height);
+    }
+  }
+
+  /**
+   * The rectangle at which the original image is placed into the composite
+   * image.
+   */
+  static class ImageRect implements HasRect {
+
+    private boolean hasBeenPositioned;
+    private final int height, width;
+    private final BufferedImage image;
+    private int left, top;
+    private final String name;
+    private final AffineTransform transform = new AffineTransform();
+
+    /**
+     * Copy constructor.
+     */
+    public ImageRect(HasRect other) {
+      this.name = other.getName();
+      this.height = other.getHeight();
+      this.width = other.getWidth();
+      this.image = other.getImage();
+      this.left = other.getLeft();
+      this.top = other.getTop();
+      setTransform(other.getTransform());
+    }
+
+    public ImageRect(String name, BufferedImage image) {
+      this.name = name;
+      this.image = image;
+      this.width = image.getWidth();
+      this.height = image.getHeight();
+    }
+
+    public int getHeight() {
+      return height;
+    }
+
+    public BufferedImage getImage() {
+      return image;
+    }
+
+    public int getLeft() {
+      return left;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public int getTop() {
+      return top;
+    }
+
+    public AffineTransform getTransform() {
+      return new AffineTransform(transform);
+    }
+
+    public int getWidth() {
+      return width;
+    }
+
+    public boolean hasBeenPositioned() {
+      return hasBeenPositioned;
+    }
+
+    public void setPosition(int left, int top) {
+      hasBeenPositioned = true;
+      this.left = left;
+      this.top = top;
+    }
+
+    public void setTransform(AffineTransform transform) {
+      this.transform.setTransform(transform);
+    }
+
+    public AffineTransform transform() {
+      AffineTransform toReturn = new AffineTransform();
+      toReturn.translate(left, top);
+      toReturn.concatenate(transform);
+
+      assert checkTransform(toReturn);
+      return toReturn;
+    }
+
+    private boolean checkTransform(AffineTransform tx) {
+      double[] in = {0, 0, width, height};
+      double[] out = {0, 0, 0, 0};
+
+      tx.transform(in, 0, out, 0, 2);
+
+      assert width == Math.abs(out[0] - out[2]);
+      assert height == Math.abs(out[1] - out[3]);
+      assert out[0] >= 0;
+      assert out[1] >= 0;
+      assert out[2] >= 0;
+      assert out[3] >= 0;
+
+      return true;
+    }
+  }
+
+  /**
+   * Used to return the size of the resulting image from the method
+   * {@link ImageBundleBuilder#arrangeImages()}.
+   */
+  static class Size {
+    private final int width, height;
+
+    Size(int width, int height) {
+      this.width = width;
+      this.height = height;
+    }
+  }
+
+  /**
+   * Performs a simple vertical arrangement of rectangles. Images will be tiled
+   * horizontally to fill the full width of the image.
+   */
+  static class VerticalArranger implements Arranger {
+    public Size arrangeImages(Collection<HasRect> rects) {
+      int height = 0;
+      int width = 1;
+
+      for (HasRect rect : rects) {
+        rect.setPosition(0, height);
+        width = lcm(width, rect.getWidth());
+        height += rect.getHeight();
+      }
+
+      List<HasRect> toAdd = new ArrayList<HasRect>();
+      for (HasRect rect : rects) {
+        int x = rect.getWidth();
+        while (x < width) {
+          ImageRect newRect = new ImageRect(rect);
+          newRect.setPosition(x, rect.getTop());
+          x += rect.getWidth();
+          toAdd.add(newRect);
+        }
+      }
+      rects.addAll(toAdd);
+
+      return new Size(width, height);
+    }
+  }
+
+  /*
+   * Only PNG is supported right now. In the future, we may be able to infer the
+   * best output type, and get rid of this constant.
+   */
+  private static final String BUNDLE_FILE_TYPE = "png";
+  private static final String BUNDLE_MIME_TYPE = "image/png";
+  private static final int IMAGE_MAX_SIZE = 256;
+
+  public static byte[] toPng(TreeLogger logger, HasRect rect)
+      throws UnableToCompleteException {
+    // Create the bundled image.
+    BufferedImage bundledImage = new BufferedImage(rect.getWidth(),
+        rect.getHeight(), BufferedImage.TYPE_INT_ARGB_PRE);
+    Graphics2D g2d = bundledImage.createGraphics();
+
+    g2d.drawImage(rect.getImage(), rect.transform(), null);
+    g2d.dispose();
+
+    byte[] imageBytes;
+
+    try {
+      ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+      ImageIO.write(bundledImage, BUNDLE_FILE_TYPE, byteOutputStream);
+      imageBytes = byteOutputStream.toByteArray();
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR,
+          "Unable to generate file name for image bundle file", null);
+      throw new UnableToCompleteException();
+    }
+    return imageBytes;
+  }
+
+  /**
+   * Compute the greatest common denominator of two numbers.
+   */
+  private static int gcd(int a, int b) {
+    while (b != 0) {
+      int t = b;
+      b = a % b;
+      a = t;
+    }
+    return a;
+  }
+
+  /**
+   * Compute the least common multiple of two numbers. This is used by
+   * {@link HorizontalArranger} and {@link VerticalArranger} to determine how
+   * large the composite image should be to allow every image to line up when
+   * repeated.
+   */
+  private static int lcm(int a, int b) {
+    return b / gcd(a, b) * a;
+  }
+
+  private final Map<String, ImageRect> imageNameToImageRectMap = new HashMap<String, ImageRect>();
+
+  public ImageBundleBuilder() {
+  }
+
+  /**
+   * Copy constructor.
+   */
+  public ImageBundleBuilder(ImageBundleBuilder other) {
+    for (Map.Entry<String, ImageRect> entry : other.imageNameToImageRectMap.entrySet()) {
+      imageNameToImageRectMap.put(entry.getKey(), new ImageRect(
+          entry.getValue()));
+    }
+  }
+
+  /**
+   * Assimilates the image associated with a particular image method into the
+   * master composite. If the method names an image that has already been
+   * assimilated, the existing image rectangle is reused.
+   * 
+   * @param logger a hierarchical logger which logs to the hosted console
+   * @param imageName the name of an image that can be found on the classpath
+   * @param resource the URL from which the image data wil be loaded
+   * @throws UnableToCompleteException if the image with name
+   *           <code>imageName</code> cannot be added to the master composite
+   *           image
+   */
+  public ImageRect assimilate(TreeLogger logger, String imageName, URL resource)
+      throws UnableToCompleteException, UnsuitableForStripException {
+
+    /*
+     * Decide whether or not we need to add to the composite image. Either way,
+     * we associated it with the rectangle of the specified image as it exists
+     * within the composite image. Note that the coordinates of the rectangle
+     * aren't computed until the composite is written.
+     */
+    ImageRect rect = getMapping(imageName);
+    if (rect == null) {
+      // Assimilate the image into the composite.
+      rect = addImage(logger, imageName, resource);
+
+      imageNameToImageRectMap.put(imageName, rect);
+    }
+    return rect;
+  }
+
+  public int getImageCount() {
+    return imageNameToImageRectMap.size();
+  }
+
+  public ImageRect getMapping(String imageName) {
+    return imageNameToImageRectMap.get(imageName);
+  }
+
+  public ImageRect removeMapping(String imageName) {
+    return imageNameToImageRectMap.remove(imageName);
+  }
+
+  /**
+   * Write all images into the output.
+   * 
+   * @param logger a hierarchical logger which logs to the hosted console
+   * @param context the local ResourceContext that is being used to accumulate
+   *          output files
+   * @param arranger a provider of image layout logic
+   * @return a Java expression which will evaluate to the location of the
+   *         composite image at runtime
+   */
+  public String writeBundledImage(TreeLogger logger, ResourceContext context,
+      Arranger arranger) throws UnableToCompleteException {
+
+    if (imageNameToImageRectMap.isEmpty()) {
+      return null;
+    }
+
+    // Create the bundled image from all of the constituent images.
+    BufferedImage bundledImage = drawBundledImage(arranger);
+
+    // Write the bundled image into a byte array, so that we can compute
+    // its strong name.
+    byte[] imageBytes;
+
+    try {
+      ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+      ImageIO.write(bundledImage, BUNDLE_FILE_TYPE, byteOutputStream);
+      imageBytes = byteOutputStream.toByteArray();
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR,
+          "Unable to generate file name for image bundle file", null);
+      throw new UnableToCompleteException();
+    }
+
+    String bundleFileName = context.deploy(
+        context.getClientBundleType().getQualifiedSourceName() + ".cache."
+            + BUNDLE_FILE_TYPE, BUNDLE_MIME_TYPE, imageBytes, false);
+
+    return bundleFileName;
+  }
+
+  private ImageRect addImage(TreeLogger logger, String imageName, URL imageUrl)
+      throws UnableToCompleteException, UnsuitableForStripException {
+
+    logger = logger.branch(TreeLogger.TRACE,
+        "Adding image '" + imageName + "'", null);
+
+    BufferedImage image;
+    // Load the image
+    try {
+      image = ImageIO.read(imageUrl);
+    } catch (IllegalArgumentException iex) {
+      if (imageName.toLowerCase().endsWith("png")
+          && iex.getMessage() != null
+          && iex.getStackTrace()[0].getClassName().equals(
+              "javax.imageio.ImageTypeSpecifier$Indexed")) {
+        logger.log(TreeLogger.ERROR,
+            "Unable to read image. The image may not be in valid PNG format. "
+                + "This problem may also be due to a bug in versions of the "
+                + "JRE prior to 1.6. See "
+                + "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5098176 "
+                + "for more information. If this bug is the cause of the "
+                + "error, try resaving the image using a different image "
+                + "program, or upgrade to a newer JRE.", null);
+        throw new UnableToCompleteException();
+      } else {
+        throw iex;
+      }
+    } catch (IOException e) {
+      logger.log(TreeLogger.ERROR, "Unable to read image resource", null);
+      throw new UnableToCompleteException();
+    }
+
+    if (image == null) {
+      logger.log(TreeLogger.ERROR, "Unrecognized image file format", null);
+      throw new UnableToCompleteException();
+    }
+
+    ImageRect toReturn = new ImageRect(imageName, image);
+
+    if (toReturn.height > IMAGE_MAX_SIZE || toReturn.width > IMAGE_MAX_SIZE) {
+      throw new UnsuitableForStripException(toReturn);
+    }
+
+    return toReturn;
+  }
+
+  /**
+   * This method creates the bundled image through the composition of the other
+   * images.
+   * 
+   * In this particular implementation, we use NFDHDW (see
+   * {@link #arrangeImages()}) to get an approximate optimal image packing.
+   * 
+   * The most important aspect of drawing the bundled image is that it be drawn
+   * in a deterministic way. The drawing of the image should not rely on
+   * implementation details of the Generator system which may be subject to
+   * change.
+   */
+  private BufferedImage drawBundledImage(Arranger arranger) {
+
+    /*
+     * There is no need to impose any order here, because arrangeImages will
+     * position the ImageRects in a deterministic fashion, even though we might
+     * paint them in a non-deterministic order.
+     */
+    Collection<HasRect> imageRects = new LinkedList<HasRect>(
+        imageNameToImageRectMap.values());
+
+    // Arrange images and determine the size of the resulting bundle.
+    Size size = arranger.arrangeImages(imageRects);
+
+    // Create the bundled image.
+    BufferedImage bundledImage = new BufferedImage(size.width, size.height,
+        BufferedImage.TYPE_INT_ARGB_PRE);
+    Graphics2D g2d = bundledImage.createGraphics();
+
+    for (HasRect imageRect : imageRects) {
+      g2d.drawImage(imageRect.getImage(), imageRect.transform(), null);
+    }
+    g2d.dispose();
+
+    return bundledImage;
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
new file mode 100644
index 0000000..e9c38a5
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.resources.client.ImageResource.ImageOptions;
+import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.resources.client.impl.ImageResourcePrototype;
+import com.google.gwt.resources.ext.ClientBundleFields;
+import com.google.gwt.resources.ext.ClientBundleRequirements;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+import com.google.gwt.resources.rg.ImageBundleBuilder.Arranger;
+import com.google.gwt.resources.rg.ImageBundleBuilder.ImageRect;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+import java.awt.geom.AffineTransform;
+import java.net.URL;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Builds an image strip for all ImageResources defined within an ClientBundle.
+ */
+public final class ImageResourceGenerator extends AbstractResourceGenerator {
+  private static final String[] DEFAULT_EXTENSIONS = new String[] {
+      ".png", ".jpg", ".gif", ".bmp"};
+  private Map<String, ImageRect> imageRectsByName;
+  private Map<ImageRect, ImageBundleBuilder> buildersByImageRect;
+  private Map<RepeatStyle, ImageBundleBuilder> buildersByRepeatStyle;
+  private Map<ImageBundleBuilder, String[]> urlsByBuilder;
+  private Map<ImageRect, String[]> urlsByExternalImageRect;
+  private Map<ImageRect, ImageBundleBuilder> rtlImages;
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    String name = method.getName();
+
+    SourceWriter sw = new StringSourceWriter();
+    sw.println("new " + ImageResourcePrototype.class.getName() + "(");
+    sw.indent();
+    sw.println('"' + name + "\",");
+
+    ImageRect rect = imageRectsByName.get(name);
+    assert rect != null : "No ImageRect ever computed for " + name;
+
+    String[] urlExpressions;
+    {
+      ImageBundleBuilder builder = buildersByImageRect.get(rect);
+      if (builder == null) {
+        urlExpressions = urlsByExternalImageRect.get(rect);
+      } else {
+        urlExpressions = urlsByBuilder.get(builder);
+      }
+    }
+    assert urlExpressions != null : "No URL expression for " + name;
+    assert urlExpressions.length == 2;
+
+    if (urlExpressions[1] == null) {
+      sw.println(urlExpressions[0] + ",");
+    } else {
+      sw.println("com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?"
+          + urlExpressions[1] + " : " + urlExpressions[0] + ",");
+    }
+    sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth()
+        + ", " + rect.getHeight());
+
+    sw.outdent();
+    sw.print(")");
+
+    return sw.toString();
+  }
+
+  @Override
+  public void createFields(TreeLogger logger, ResourceContext context,
+      ClientBundleFields fields) throws UnableToCompleteException {
+
+    TypeOracle typeOracle = context.getGeneratorContext().getTypeOracle();
+    JClassType stringType = typeOracle.findType(String.class.getName());
+    assert stringType != null;
+
+    Map<ImageBundleBuilder, String> prettyNames = new IdentityHashMap<ImageBundleBuilder, String>();
+
+    for (Map.Entry<RepeatStyle, ImageBundleBuilder> entry : buildersByRepeatStyle.entrySet()) {
+      RepeatStyle repeatStyle = entry.getKey();
+      ImageBundleBuilder builder = entry.getValue();
+      Arranger arranger;
+
+      switch (repeatStyle) {
+        case None:
+          arranger = new ImageBundleBuilder.BestFitArranger();
+          break;
+        case Horizontal:
+          arranger = new ImageBundleBuilder.VerticalArranger();
+          break;
+        case Vertical:
+          arranger = new ImageBundleBuilder.HorizontalArranger();
+          break;
+        case Both:
+          // This is taken care of when writing the external images;
+          continue;
+        default:
+          logger.log(TreeLogger.ERROR, "Unknown RepeatStyle" + repeatStyle);
+          throw new UnableToCompleteException();
+      }
+
+      String bundleUrlExpression = builder.writeBundledImage(logger.branch(
+          TreeLogger.DEBUG, "Writing image strip", null), context, arranger);
+
+      if (bundleUrlExpression == null) {
+        continue;
+      }
+
+      String prettyName = "imageUrl" + repeatStyle;
+      prettyNames.put(builder, prettyName);
+      String fieldName = fields.define(stringType, prettyName,
+          bundleUrlExpression, true, true);
+      String[] strings = {fieldName, null};
+      urlsByBuilder.put(builder, strings);
+    }
+
+    if (rtlImages.size() > 0) {
+      Set<ImageBundleBuilder> rtlBuilders = new HashSet<ImageBundleBuilder>();
+
+      for (Map.Entry<ImageRect, ImageBundleBuilder> entry : rtlImages.entrySet()) {
+        ImageRect rtlImage = entry.getKey();
+
+        AffineTransform tx = new AffineTransform();
+        tx.setTransform(-1, 0, 0, 1, rtlImage.getWidth(), 0);
+
+        rtlImage.setTransform(tx);
+
+        if (buildersByImageRect.containsKey(rtlImage)) {
+          rtlBuilders.add(buildersByImageRect.get(rtlImage));
+        } else {
+          String[] strings = urlsByExternalImageRect.get(rtlImage);
+          assert strings != null;
+          byte[] imageBytes = ImageBundleBuilder.toPng(logger, rtlImage);
+          strings[1] = context.deploy(rtlImage.getName() + "_rtl.png",
+              "image/png", imageBytes, false);
+        }
+      }
+
+      for (ImageBundleBuilder builder : rtlBuilders) {
+        String bundleUrlExpression = builder.writeBundledImage(logger.branch(
+            TreeLogger.DEBUG, "Writing image strip", null), context,
+            new ImageBundleBuilder.IdentityArranger());
+
+        if (bundleUrlExpression == null) {
+          continue;
+        }
+
+        String prettyName = prettyNames.get(builder);
+        String[] strings = urlsByBuilder.get(builder);
+        assert strings != null;
+
+        strings[1] = fields.define(stringType, prettyName + "_rtl",
+            bundleUrlExpression, true, true);
+      }
+    }
+  }
+
+  @Override
+  public void init(TreeLogger logger, ResourceContext context) {
+    imageRectsByName = new HashMap<String, ImageRect>();
+    buildersByImageRect = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
+    buildersByRepeatStyle = new EnumMap<RepeatStyle, ImageBundleBuilder>(
+        RepeatStyle.class);
+    rtlImages = new IdentityHashMap<ImageRect, ImageBundleBuilder>();
+    urlsByBuilder = new IdentityHashMap<ImageBundleBuilder, String[]>();
+    urlsByExternalImageRect = new IdentityHashMap<ImageRect, String[]>();
+  }
+
+  @Override
+  public void prepare(TreeLogger logger, ResourceContext context,
+      ClientBundleRequirements requirements, JMethod method)
+      throws UnableToCompleteException {
+    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
+        method, DEFAULT_EXTENSIONS);
+
+    if (resources.length != 1) {
+      logger.log(TreeLogger.ERROR, "Exactly one image may be specified", null);
+      throw new UnableToCompleteException();
+    }
+
+    ImageBundleBuilder builder = getBuilder(method);
+    URL resource = resources[0];
+    String name = method.getName();
+
+    ImageRect rect;
+    try {
+      rect = builder.assimilate(logger, name, resource);
+      if (context.supportsDataUrls()
+          || getRepeatStyle(method) == RepeatStyle.Both) {
+        // Just use the calculated meta-data
+        builder.removeMapping(name);
+        rect.setPosition(0, 0);
+        throw new UnsuitableForStripException(rect);
+      }
+      buildersByImageRect.put(rect, builder);
+    } catch (UnsuitableForStripException e) {
+      // Add the image to the output as a separate resource
+      rect = e.getImageRect();
+      byte[] imageBytes = ImageBundleBuilder.toPng(logger, rect);
+      String urlExpression = context.deploy(rect.getName() + ".png",
+          "image/png", imageBytes, false);
+      urlsByExternalImageRect.put(rect, new String[] {urlExpression, null});
+    }
+
+    imageRectsByName.put(name, rect);
+
+    if (getFlipRtl(method)) {
+      rtlImages.put(rect, null);
+    }
+  }
+
+  private ImageBundleBuilder getBuilder(JMethod method) {
+    RepeatStyle repeatStyle = getRepeatStyle(method);
+    ImageBundleBuilder builder = buildersByRepeatStyle.get(repeatStyle);
+    if (builder == null) {
+      builder = new ImageBundleBuilder();
+      buildersByRepeatStyle.put(repeatStyle, builder);
+    }
+    return builder;
+  }
+
+  private boolean getFlipRtl(JMethod method) {
+    ImageOptions options = method.getAnnotation(ImageOptions.class);
+    if (options == null) {
+      return false;
+    } else {
+      return options.flipRtl();
+    }
+  }
+
+  private RepeatStyle getRepeatStyle(JMethod method) {
+    ImageOptions options = method.getAnnotation(ImageOptions.class);
+    if (options == null) {
+      return RepeatStyle.None;
+    } else {
+      return options.repeatStyle();
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/TextResourceGenerator.java b/user/src/com/google/gwt/resources/rg/TextResourceGenerator.java
new file mode 100644
index 0000000..88b3d33
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/TextResourceGenerator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.dev.util.Util;
+import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.resources.ext.ResourceContext;
+import com.google.gwt.resources.ext.ResourceGeneratorUtil;
+import com.google.gwt.user.rebind.SourceWriter;
+import com.google.gwt.user.rebind.StringSourceWriter;
+
+import java.net.URL;
+
+/**
+ * Provides implementations of TextResource.
+ */
+public final class TextResourceGenerator extends AbstractResourceGenerator {
+
+  private static final String[] DEFAULT_EXTENSIONS = new String[] {".txt"};
+
+  @Override
+  public String createAssignment(TreeLogger logger, ResourceContext context,
+      JMethod method) throws UnableToCompleteException {
+    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
+        method, DEFAULT_EXTENSIONS);
+
+    if (resources.length != 1) {
+      logger.log(TreeLogger.ERROR, "Exactly one resource must be specified",
+          null);
+      throw new UnableToCompleteException();
+    }
+
+    URL resource = resources[0];
+
+    SourceWriter sw = new StringSourceWriter();
+    // Write the expression to create the subtype.
+    sw.println("new " + TextResource.class.getName() + "() {");
+    sw.indent();
+
+    // Convenience when examining the generated code.
+    sw.println("// " + resource.toExternalForm());
+
+    sw.println("public String getText() {");
+    sw.indent();
+
+    String toWrite = Util.readURLAsString(resource);
+
+    sw.println("return \"" + Generator.escape(toWrite) + "\";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.println("public String getName() {");
+    sw.indent();
+    sw.println("return \"" + method.getName() + "\";");
+    sw.outdent();
+    sw.println("}");
+
+    sw.outdent();
+    sw.println("}");
+
+    return sw.toString();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/rg/UnsuitableForStripException.java b/user/src/com/google/gwt/resources/rg/UnsuitableForStripException.java
new file mode 100644
index 0000000..038dfa8
--- /dev/null
+++ b/user/src/com/google/gwt/resources/rg/UnsuitableForStripException.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.rg;
+
+/**
+ * Indicates that an image is not suitable for being added to an image strip.
+ */
+class UnsuitableForStripException extends Exception {
+  
+  private static final long serialVersionUID = -1;
+  private final ImageBundleBuilder.ImageRect rect;
+  
+  public UnsuitableForStripException(ImageBundleBuilder.ImageRect rect) {
+    this.rect = rect;
+  }
+  
+  public UnsuitableForStripException(ImageBundleBuilder.ImageRect rect, String msg) {
+    super(msg);
+    this.rect = rect;
+  }
+  
+  public UnsuitableForStripException(String msg, Throwable cause) {
+    super(msg, cause);
+    this.rect = null;
+  }
+  
+  public ImageBundleBuilder.ImageRect getImageRect() {
+    return rect;
+  }
+}
diff --git a/user/src/com/google/gwt/user/rebind/StringSourceWriter.java b/user/src/com/google/gwt/user/rebind/StringSourceWriter.java
new file mode 100644
index 0000000..78a2cd5
--- /dev/null
+++ b/user/src/com/google/gwt/user/rebind/StringSourceWriter.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.rebind;
+
+import com.google.gwt.core.ext.TreeLogger;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * A SourceWriter that accumulates source and returns it in the
+ * {@link #toString()} method.
+ */
+public class StringSourceWriter implements SourceWriter {
+
+  private final StringWriter buffer = new StringWriter();
+  private int indentLevel = 0;
+  private String indentPrefix = "";
+  private boolean needsIndent;
+  private final PrintWriter out = new PrintWriter(buffer);
+
+  public void beginJavaDocComment() {
+    out.println("/**");
+    indent();
+    indentPrefix = " * ";
+  }
+
+  /**
+   * This is a no-op.
+   */
+  public void commit(TreeLogger logger) {
+    out.flush();
+  }
+
+  public void endJavaDocComment() {
+    out.println("*/");
+    outdent();
+    indentPrefix = "";
+  }
+
+  public void indent() {
+    indentLevel++;
+  }
+
+  public void indentln(String s) {
+    indent();
+    println(s);
+    outdent();
+  }
+
+  public void outdent() {
+    indentLevel = Math.max(indentLevel - 1, 0);
+  }
+
+  public void print(String s) {
+    maybeIndent();
+    out.print(s);
+  }
+
+  public void println() {
+    maybeIndent();
+    out.println();
+    needsIndent = true;
+  }
+
+  public void println(String s) {
+    print(s);
+    println();
+  }
+
+  @Override
+  public String toString() {
+    out.flush();
+    return buffer.getBuffer().toString();
+  }
+
+  private void maybeIndent() {
+    if (needsIndent) {
+      needsIndent = false;
+      for (int i = 0; i < indentLevel; i++) {
+        out.print("  ");
+        out.print(indentPrefix);
+      }
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/resources/ResourcesSuite.java b/user/test/com/google/gwt/resources/ResourcesSuite.java
new file mode 100644
index 0000000..79d9385
--- /dev/null
+++ b/user/test/com/google/gwt/resources/ResourcesSuite.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.resources.client.CSSResourceTest;
+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.rg.CssNodeClonerTest;
+import com.google.gwt.resources.rg.CssReorderTest;
+import com.google.gwt.resources.rg.CssRtlTest;
+
+import junit.framework.Test;
+
+/**
+ * Tests the ClientBundle framework.
+ */
+public class ResourcesSuite {
+  public static Test suite() {
+
+    GWTTestSuite suite = new GWTTestSuite("Test for com.google.gwt.resources");
+    suite.addTestSuite(CSSResourceTest.class);
+    suite.addTestSuite(CssReorderTest.class);
+    suite.addTestSuite(CssRtlTest.class);
+    suite.addTestSuite(CssNodeClonerTest.class);
+    suite.addTestSuite(ImageResourceTest.class);
+    suite.addTestSuite(NestedBundleTest.class);
+    suite.addTestSuite(TextResourceTest.class);
+
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/resources/client/16x16.png b/user/test/com/google/gwt/resources/client/16x16.png
new file mode 100644
index 0000000..77e06ba
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/16x16.png
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/32x32.png b/user/test/com/google/gwt/resources/client/32x32.png
new file mode 100644
index 0000000..55af260
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/32x32.png
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/64x64-dup.png b/user/test/com/google/gwt/resources/client/64x64-dup.png
new file mode 100644
index 0000000..e437adc
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/64x64-dup.png
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/64x64.png b/user/test/com/google/gwt/resources/client/64x64.png
new file mode 100644
index 0000000..e437adc
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/64x64.png
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/CSSResourceTest.java b/user/test/com/google/gwt/resources/client/CSSResourceTest.java
new file mode 100644
index 0000000..dca1c2d
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/CSSResourceTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.resources.client.CssResource.Import;
+import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
+import com.google.gwt.resources.client.CssResource.Shared;
+import com.google.gwt.resources.client.CssResource.Strict;
+
+/**
+ * 
+ */
+public class CSSResourceTest extends GWTTestCase {
+
+  interface ConcatenatedResources extends ClientBundle {
+    @Source(value = {"concatenatedA.css", "concatenatedB.css"})
+    CssResource css();
+  }
+
+  interface CssWithDefines extends CssResource {
+    double lengthFloat();
+
+    int lengthInt();
+
+    double percentFloat();
+
+    int percentInt();
+
+    double rawDouble();
+
+    float rawFloat();
+
+    int rawInt();
+  }
+
+  interface MyCssResource extends CssResource, MyNonCssResource {
+    @ClassName("replacement-not-java-ident")
+    String nameOverride();
+  }
+
+  interface MyCssResourceA extends MyCssResource, SharedClasses {
+    String local();
+
+    // This shouldn't make a difference
+    String replacement();
+  }
+
+  @ImportedWithPrefix("gwt-MyCssResourceB")
+  interface MyCssResourceB extends MyCssResource, SharedClasses {
+    String local();
+
+    String sharedOverrideClass();
+  }
+
+  /*
+   * Check type inheritance.
+   */
+  interface MyCssResourceWithSprite extends MyCssResource {
+    String extraSpriteClass();
+
+    String multiClassA();
+
+    String multiClassB();
+  }
+
+  interface MyNonCssResource {
+    String nameOverride();
+
+    String replacement();
+  }
+
+  interface Resources extends ClientBundle {
+    Resources INSTANCE = GWT.create(Resources.class);
+
+    @Source("siblingTestA.css")
+    MyCssResourceA a();
+
+    @Source("siblingTestB.css")
+    MyCssResourceB b();
+
+    @Source("test.css")
+    MyCssResourceWithSprite css();
+
+    @Source("32x32.png")
+    DataResource dataMethod();
+
+    // Test default extensions
+    CssWithDefines deftest();
+
+    @Source("unrelatedDescendants.css")
+    @Import(value = {MyCssResourceA.class, MyCssResourceB.class})
+    @Strict
+    CssResource descendants();
+
+    @Source("16x16.png")
+    ImageResource spriteMethod();
+  }
+
+  interface SharedBase extends CssResource {
+    String unsharedClass();
+  }
+
+  @ImportedWithPrefix("gwt-Shared")
+  @Shared
+  interface SharedClasses extends SharedBase {
+    String sharedClass();
+
+    String sharedOverrideClass();
+  }
+
+  interface SiblingResources extends ClientBundle {
+    @Source("siblingTestA.css")
+    MyCssResourceA a();
+
+    @Source("siblingTestB.css")
+    MyCssResourceB b();
+  }
+
+  public static String red() {
+    return "orange";
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.resources.Resources";
+  }
+
+  public void report(String s) {
+    // Can be filled in if debugging.
+    System.out.println(s);
+  }
+
+  public void testConcatenatedResource() {
+    ConcatenatedResources r = GWT.create(ConcatenatedResources.class);
+    String text = r.css().getText();
+    assertTrue(text.contains(".partA"));
+    assertTrue(text.contains(".partB"));
+  }
+
+  public void testCss() {
+    MyCssResourceWithSprite css = Resources.INSTANCE.css();
+    String text = css.getText();
+    report(text);
+
+    // Check the sprite
+    assertTrue(text.contains("height:16px"));
+    assertTrue(text.contains("width:16px"));
+
+    // Check the value() expansion
+    assertTrue(text.contains("offset-left:\"guard\" 16px !important;"));
+    assertTrue(text.contains("offset:16px 16px;"));
+
+    // Make sure renaming works
+    assertFalse("replacement".equals(css.replacement()));
+    assertTrue(text.contains("." + css.replacement()));
+    assertTrue(text.contains("." + css.replacement() + ":after"));
+    assertTrue(text.contains("." + css.nameOverride()));
+
+    // Make sure renaming for multi-class selectors (.foo.bar) works
+    assertFalse("multiClassA".equals(css.multiClassA()));
+    assertFalse("multiClassB".equals(css.multiClassB()));
+    assertTrue(text.contains("." + css.multiClassA() + "." + css.multiClassB()));
+
+    // Check static if evaluation
+    assertTrue(text.contains("static:PASSED;"));
+    assertFalse(text.contains("FAIL"));
+
+    // Check runtime if evaluation
+    assertTrue(text.contains("runtime:PASSED;"));
+
+    // Check interestingly-named idents
+    assertTrue(text.contains("\\-some-wacky-extension"));
+    assertTrue(text.contains(".ns\\:tag"));
+    assertTrue(text.contains(".ns\\:tag:pseudo"));
+
+    // Check escaped string values
+    assertTrue(text.contains("\"Hello\\\\\\\" world\""));
+
+    // Check values
+    assertFalse(text.contains("0.0;"));
+    assertFalse(text.contains("0.0px;"));
+    assertFalse(text.contains("0px;"));
+    assertTrue(text.contains("background-color:#fff;"));
+    assertTrue(text.contains("content:\"bar\";"));
+
+    // Check invalid CSS values
+    assertTrue(text.contains("top:expression(document.compatMode==\"CSS1Compat\" ? documentElement.scrollTop:document.body.scrollTop \\ 2);"));
+
+    // Check data URL expansion
+    assertTrue(text.contains(Resources.INSTANCE.dataMethod().getUrl()));
+
+    // Check @eval expansion
+    assertTrue(text.contains(red() + ";"));
+
+    // Check @def substitution
+    assertTrue(text.contains("50px"));
+
+    // Check merging semantics
+    assertTrue(text.indexOf("static:PASSED") < text.indexOf("runtime:PASSED"));
+    assertTrue(text.indexOf("before:merge") != -1);
+    assertTrue(text.indexOf("before:merge") < text.indexOf("after:merge"));
+    assertTrue(text.indexOf(".may-combine,.may-combine2") != -1);
+    assertTrue(text.indexOf("merge:merge") != -1);
+    assertTrue(text.indexOf("merge:merge") < text.indexOf("may-not-combine"));
+    assertTrue(text.indexOf("may-not-combine") < text.indexOf("prevent:true"));
+    assertTrue(text.indexOf("prevent:true") < text.indexOf("prevent-merge:true"));
+    assertTrue(text.indexOf("prevent:true") < text.indexOf("may-not-combine2"));
+
+    // Check commonly-used CSS3 constructs
+    assertTrue(text.contains("background-color:rgba(0,0,0,0.5);"));
+  }
+
+  public void testDefines() {
+    Resources r = GWT.create(Resources.class);
+    CssWithDefines defines = r.deftest();
+
+    assertEquals(1, defines.rawInt());
+    assertEquals(1.5F, defines.rawFloat());
+    assertEquals(1.5, defines.rawDouble());
+
+    assertEquals(50, defines.lengthInt());
+    assertEquals(1.5, defines.lengthFloat());
+
+    assertEquals(50, defines.percentInt());
+    assertEquals(50.5, defines.percentFloat());
+  }
+
+  public void testMultipleBundles() {
+    Resources r1 = GWT.create(Resources.class);
+    SiblingResources r2 = GWT.create(SiblingResources.class);
+
+    assertEquals(r1.a().replacement(), r2.a().replacement());
+    assertEquals(r1.b().replacement(), r2.b().replacement());
+
+    assertEquals(r1.a().sharedClass(), r2.b().sharedClass());
+    assertFalse(r1.a().sharedOverrideClass().equals(
+        r2.b().sharedOverrideClass()));
+    assertFalse(r1.a().unsharedClass().equals(r2.b().unsharedClass()));
+
+    String text = r1.descendants().getText();
+    report(text);
+    assertTrue(text.contains("." + r1.a().local() + " ." + r1.b().local()));
+  }
+
+  public void testSiblingCSS() {
+    SiblingResources r = GWT.create(SiblingResources.class);
+
+    assertFalse(r.a().replacement().equals(r.b().replacement()));
+    assertFalse(r.a().local().equals(r.b().local()));
+
+    String a = r.a().getText();
+    String b = r.b().getText();
+
+    report(a);
+    report(b);
+
+    assertTrue(a.contains(".other"));
+    assertTrue(b.contains(".other"));
+    assertTrue(a.contains(r.a().local()));
+    assertTrue(b.contains(r.b().local()));
+    assertFalse(a.contains(r.b().local()));
+    assertFalse(b.contains(r.a().local()));
+  }
+}
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
new file mode 100644
index 0000000..982af95
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ErrorEvent;
+import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.resources.client.ImageResource.ImageOptions;
+import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.RootPanel;
+
+/**
+ * Tests ImageResource generation.
+ */
+public class ImageResourceTest extends GWTTestCase {
+  static interface Resources extends ClientBundle {
+    @Source("16x16.png")
+    ImageResource i16x16();
+
+    @Source("16x16.png")
+    @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
+    ImageResource i16x16Horizontal();
+
+    @Source("16x16.png")
+    @ImageOptions(repeatStyle = RepeatStyle.Vertical)
+    ImageResource i16x16Vertical();
+
+    @Source("32x32.png")
+    ImageResource i32x32();
+
+    @Source("32x32.png")
+    @ImageOptions(repeatStyle = RepeatStyle.Horizontal)
+    ImageResource i32x32Horizontal();
+
+    @Source("32x32.png")
+    @ImageOptions(repeatStyle = RepeatStyle.Vertical)
+    ImageResource i32x32Vertical();
+
+    @Source("64x64.png")
+    ImageResource i64x64();
+
+    @Source("64x64.png")
+    ImageResource i64x64Dup();
+
+    @Source("64x64-dup.png")
+    ImageResource i64x64Dup2();
+
+    // Test default filename lookup while we're at it
+    ImageResource largeLossless();
+
+    // Test default filename lookup while we're at it
+    ImageResource largeLossy();
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.resources.Resources";
+  }
+
+  public void testDedup() {
+    Resources r = GWT.create(Resources.class);
+
+    ImageResource a = r.i64x64();
+    ImageResource b = r.i64x64Dup();
+    ImageResource c = r.i64x64Dup2();
+    assertEquals(64, a.getHeight());
+    assertEquals(64, a.getWidth());
+
+    assertEquals(a.getLeft(), b.getLeft());
+    assertEquals(a.getLeft(), c.getLeft());
+
+    assertEquals(a.getLeft(), b.getTop());
+    assertEquals(a.getLeft(), c.getTop());
+
+    // See if the size of the image strip is what we expect
+    Image i = new Image(a.getURL());
+    i.addLoadHandler(new LoadHandler() {
+      public void onLoad(LoadEvent event) {
+        finishTest();
+      }
+    });
+    i.addErrorHandler(new ErrorHandler() {
+      public void onError(ErrorEvent event) {
+        fail("ErrorEvent");
+      }
+    });
+
+    RootPanel.get().add(i);
+    delayTestFinish(500);
+  }
+
+  public void testPacking() {
+    Resources r = GWT.create(Resources.class);
+
+    ImageResource i64 = r.i64x64();
+    ImageResource lossy = r.largeLossy();
+    ImageResource lossless = r.largeLossless();
+
+    // The large, lossless image should be bundled
+    if (!i64.getURL().startsWith("data:")) {
+      assertEquals(i64.getURL(), lossless.getURL());
+    }
+
+    // Make sure that the large, lossy image isn't bundled with the rest
+    assertTrue(!i64.getURL().equals(lossy.getURL()));
+
+    assertEquals(16, r.i16x16Vertical().getWidth());
+    assertEquals(16, r.i16x16Vertical().getHeight());
+
+    assertEquals(16, r.i16x16Horizontal().getWidth());
+    assertEquals(16, r.i16x16Horizontal().getHeight());
+  }
+}
diff --git a/user/test/com/google/gwt/resources/client/NestedBundleTest.java b/user/test/com/google/gwt/resources/client/NestedBundleTest.java
new file mode 100644
index 0000000..09efa3d
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/NestedBundleTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.resources.client.GwtCreateResource.ClassType;
+
+/**
+ * Verify that nested bundles work correctly.
+ */
+public class NestedBundleTest extends GWTTestCase {
+
+  interface NestedBundle extends ClientBundle {
+    @Source("hello.txt")
+    TextResource hello();
+
+    NestedBundle nested();
+
+    GwtCreateResource<NestedBundle> nestedCreate();
+
+    @ClassType(NestedBundle.class)
+    GwtCreateResource<Object> nestedCreateWithOverride();
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.resources.Resources";
+  }
+
+  public void testNestedBundle() {
+    NestedBundle b = GWT.create(NestedBundle.class);
+    assertSame(b.hello(), b.nested().hello());
+  }
+
+  public void testNestedCreate() {
+    NestedBundle b = GWT.create(NestedBundle.class);
+    NestedBundle q = b.nestedCreate().create();
+    NestedBundle r = b.nestedCreate().create();
+    assertNotSame(q, r);
+
+    assertSame(q.hello(), r.hello());
+  }
+
+  public void testNestedCreateOverride() {
+    NestedBundle b = GWT.create(NestedBundle.class);
+    Object o = b.nestedCreateWithOverride().create();
+    assertTrue(o instanceof NestedBundle);
+  }
+}
diff --git a/user/test/com/google/gwt/resources/client/TextResourceTest.java b/user/test/com/google/gwt/resources/client/TextResourceTest.java
new file mode 100644
index 0000000..ffeaca8
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/TextResourceTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Tests for TextResource assembly and use.
+ */
+public class TextResourceTest extends GWTTestCase {
+
+  private static final String HELLO = "Hello World!";
+
+  static interface Resources extends ClientBundleWithLookup {
+    @Source("com/google/gwt/resources/client/hello.txt")
+    TextResource helloWorldAbsolute();
+
+    @Source("hello.txt")
+    ExternalTextResource helloWorldExternal();
+
+    @Source("hello.txt")
+    TextResource helloWorldRelative();
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.resources.Resources";
+  }
+
+  public void testExternal() throws ResourceException {
+    final Resources r = GWT.create(Resources.class);
+
+    ResourceCallback<TextResource> c = new ResourceCallback<TextResource>() {
+
+      public void onError(ResourceException e) {
+        e.printStackTrace();
+        fail("Unable to fetch " + e.getResource().getName());
+      }
+
+      public void onSuccess(TextResource resource) {
+        assertEquals(r.helloWorldExternal().getName(), resource.getName());
+        assertEquals(HELLO, resource.getText());
+        finishTest();
+      }
+    };
+
+    r.helloWorldExternal().getText(c);
+    delayTestFinish(2000);
+  }
+
+  public void testInline() {
+    Resources r = GWT.create(Resources.class);
+    assertEquals(HELLO, r.helloWorldRelative().getText());
+    assertEquals(HELLO, r.helloWorldAbsolute().getText());
+  }
+
+  public void testMeta() {
+    Resources r = GWT.create(Resources.class);
+    assertEquals("helloWorldAbsolute", r.helloWorldAbsolute().getName());
+    assertEquals("helloWorldRelative", r.helloWorldRelative().getName());
+    assertEquals("helloWorldExternal", r.helloWorldExternal().getName());
+
+    ResourcePrototype[] resources = r.getResources();
+    assertEquals(3, resources.length);
+  }
+}
diff --git a/user/test/com/google/gwt/resources/client/concatenatedA.css b/user/test/com/google/gwt/resources/client/concatenatedA.css
new file mode 100644
index 0000000..6b1c69e
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/concatenatedA.css
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+.partA {
+  background: blue;
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/resources/client/concatenatedB.css b/user/test/com/google/gwt/resources/client/concatenatedB.css
new file mode 100644
index 0000000..f0b53db
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/concatenatedB.css
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+.partB {
+  background: blue;
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/resources/client/deftest.css b/user/test/com/google/gwt/resources/client/deftest.css
new file mode 100644
index 0000000..0db8a81
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/deftest.css
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+ 
+ @def rawInt 1;
+ @def rawFloat 1.5;
+ @def rawDouble 1.5;
+ 
+ @def percentInt 50%;
+ @def percentFloat 50.5%;
+ 
+ @def lengthInt 50px;
+ @def lengthFloat 1.5px;
\ No newline at end of file
diff --git a/user/test/com/google/gwt/resources/client/hello.txt b/user/test/com/google/gwt/resources/client/hello.txt
new file mode 100644
index 0000000..c57eff5
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/hello.txt
@@ -0,0 +1 @@
+Hello World!
\ No newline at end of file
diff --git a/user/test/com/google/gwt/resources/client/largeLossless.png b/user/test/com/google/gwt/resources/client/largeLossless.png
new file mode 100644
index 0000000..ca911e1
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/largeLossless.png
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/largeLossy.jpg b/user/test/com/google/gwt/resources/client/largeLossy.jpg
new file mode 100644
index 0000000..398e08e
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/largeLossy.jpg
Binary files differ
diff --git a/user/test/com/google/gwt/resources/client/siblingTestA.css b/user/test/com/google/gwt/resources/client/siblingTestA.css
new file mode 100644
index 0000000..9a6711e
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/siblingTestA.css
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+ 
+/* This is the first half of the sibling test. */
+.replacement { background:red; }
+.replacement-not-java-ident {color: blue;}
+.local {background: green; }
+.other {background: blue; }
+.unsharedClass {}
+.sharedClass {}
+.sharedOverrideClass {}
diff --git a/user/test/com/google/gwt/resources/client/siblingTestB.css b/user/test/com/google/gwt/resources/client/siblingTestB.css
new file mode 100644
index 0000000..15f05f1
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/siblingTestB.css
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+ 
+/* This is the other half of the sibling test. */
+.other {background: blue; }
+.replacement-not-java-ident {color: blue;}
+.replacement { background:red; }
+.local {background:green;}
+.unsharedClass {}
+.sharedClass {}
+.sharedOverrideClass {}
diff --git a/user/test/com/google/gwt/resources/client/test.css b/user/test/com/google/gwt/resources/client/test.css
new file mode 100644
index 0000000..ad420a5
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/test.css
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+@def BIG 50px;
+@def DIRECTION ltr;
+@eval RED com.google.gwt.resources.client.CSSResourceTest.red();
+@url BACKGROUND dataMethod;
+
+/* Check @def expansion */
+@def SPRITEWIDTH value("spriteMethod.getWidth", "px");
+@def SPRITESIZE SPRITEWIDTH value("spriteMethod.getHeight", "px");
+
+/*
+ * We'll use the token FAIL to indicate text that should never be seen in the output.
+ */
+
+/* Test @sprite expansion */
+@sprite .spriteClass, .extraSpriteClass {
+  gwt-image: "spriteMethod";
+  more: properties;
+}
+
+.affectedBySprite {
+    offset-left: "guard" SPRITEWIDTH !important;
+    offset: SPRITESIZE;
+}
+
+.blahA {
+	background-color: #000;
+}
+
+.blahA[someAttribute="foo"][beer="baz"] {
+	background-color: #fff;
+}
+
+/* Test value substitution */
+div {
+    border: BIG solid RED;
+    direction: DIRECTION;
+    background: BACKGROUND;
+}
+
+div[foo="bar"] {
+    content: 'bar';
+}
+
+/* Test structural modifications */
+@if (false) {
+  div {
+    runtime: FAIL;
+  }
+} @elif (true) {
+  div {
+    runtime: PASSED;
+  }
+} @else {
+  div {
+    runtime: FAIL;
+  }
+}
+
+@if CssResource.style obf OBF {
+  div {
+    static: PASSED;
+  }
+} @elif CssResource.style pretty PRETTY {
+  div {
+    static: PASSED;
+  }
+} @else {
+  div {
+    static: FAIL;
+  }
+}
+
+/* Test named classes */
+.replacement {
+  color: red;
+}
+
+.replacement:after {
+  content: "Hello\\\" world";
+}
+
+.replacement-not-java-ident {
+  color: blue;
+}
+
+/* Test unusual tag, class, and property names and values */
+div {
+  \-some-wacky-extension : boo;
+  another-extension: \-bar;
+}
+
+div {
+  filter: "alpha(opacity = 30)";
+}
+
+div-with-literal {
+  top: literal("expression(document.compatMode==\"CSS1Compat\" ? documentElement.scrollTop:document.body.scrollTop \\ 2)");
+}
+
+.ns\:tag {
+  border: red;
+}
+
+.ns\:tag:pseudo {
+  content : "blah";
+}
+
+/* Test merging / promotion functions*/
+
+.may-merge {
+  border: 0;
+}
+
+.may-combine {
+  border: 0;
+}
+
+/* Test merging around empty rules */
+.FAIL {}
+
+/* Make sure rules in @if and @media are respected */
+@media blah {
+  @if (true) {
+    .this-does-not-matter {
+      after: merge;
+    }
+  }
+}
+
+.may-merge {
+  before: merge;
+}
+
+.may-combine2 {
+  border: 0;
+}
+
+.may-not-merge {
+  merge: merge;
+}
+
+.may-not-combine {
+  combine: combine;
+  prevent-combine: true;
+}
+
+/* Make sure rules in @if and @media are respected */
+@media blah {
+  @if (true) {
+    .may-not-merge-or-combine-because-of-this {
+      prevent: true;
+    }
+  }
+}
+.may-not-merge {
+  prevent-merge: true;
+}
+
+.may-not-combine2 {
+  combine: combine;
+  prevent-combine: true;
+}
+
+/* Ensure preservation of standard @rules */
+
+@MEDIA print {
+  div {
+    media: no-merge;
+  }
+}
+
+@page {
+  margin: 3cm;
+}
+
+@page :left {
+  margin: 4cm;
+}
+
+/* Ensure multi-class selectors fully obfuscate */
+.multiClassA {
+  border: 1px solid red;
+}
+
+.multiClassB {
+  border: 1px solid blue;
+}
+
+.multiClassA.multiClassB {
+  border: 1px solid blue;
+}
+
+.css3Color {
+  background-color: rgba(0, 0, 0, 0.5); 
+}
diff --git a/user/test/com/google/gwt/resources/client/unrelatedDescendants.css b/user/test/com/google/gwt/resources/client/unrelatedDescendants.css
new file mode 100644
index 0000000..637f3fc
--- /dev/null
+++ b/user/test/com/google/gwt/resources/client/unrelatedDescendants.css
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2008 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/* No @ImportWithPrefix followed by one with the annotation */
+.MyCssResourceA-local .gwt-MyCssResourceB-local {
+  background: green;
+}
diff --git a/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java b/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
new file mode 100644
index 0000000..24c5042
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/CssNodeClonerTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.rg;
+
+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 java.net.URL;
+import java.util.List;
+
+/**
+ * Tests the CssNodeCloner utility class.
+ */
+public class CssNodeClonerTest extends CssTestCase {
+
+  public void testClone() throws UnableToCompleteException {
+    CssStylesheet sheet = GenerateCssAst.exec(TreeLogger.NULL,
+        new URL[] {getClass().getClassLoader().getResource(
+            "com/google/gwt/resources/client/test.css")});
+
+    CssStylesheet cloned = CssNodeCloner.clone(CssStylesheet.class, sheet);
+
+    assertNotSame(sheet, cloned);
+    assertNoAliasing(cloned);
+  }
+
+  public void testCloneList() throws UnableToCompleteException {
+    CssStylesheet sheet = GenerateCssAst.exec(TreeLogger.NULL,
+        new URL[] {getClass().getClassLoader().getResource(
+            "com/google/gwt/resources/client/test.css")});
+
+    List<CssNode> cloned = CssNodeCloner.clone(CssNode.class, sheet.getNodes());
+
+    assertEquals(sheet.getNodes().size(), cloned.size());
+
+    for (CssNode node : cloned) {
+      assertNoAliasing(node);
+    }
+  }
+
+  public void testCloneProperty() {
+    CssProperty.IdentValue value = new CssProperty.IdentValue("value");
+    CssProperty p = new CssProperty("name", value, true);
+
+    CssProperty clone = CssNodeCloner.clone(CssProperty.class, p);
+
+    assertNotSame(p, clone);
+    assertEquals(p.getName(), clone.getName());
+    assertEquals(value.getIdent(),
+        clone.getValues().getValues().get(0).isIdentValue().getIdent());
+  }
+
+  public void testCloneSelector() {
+    CssSelector sel = new CssSelector("a , b");
+
+    CssSelector clone = CssNodeCloner.clone(CssSelector.class, sel);
+
+    assertNotSame(sel, clone);
+    assertEquals(sel.getSelector(), clone.getSelector());
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/CssReorderTest.java b/user/test/com/google/gwt/resources/rg/CssReorderTest.java
new file mode 100644
index 0000000..2777548
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/CssReorderTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.rg;
+
+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;
+
+/**
+ * Tests CSS reordering visitors.
+ */
+public class CssReorderTest extends CssTestCase {
+  public void testPropertyMerging() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "propertyMerging", false, makeVisitors());
+  }
+
+  public void testSelectorMerging() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "selectorMerging", false, makeVisitors());
+  }
+
+  private CssVisitor[] makeVisitors() {
+    return new CssVisitor[] {
+        new SplitRulesVisitor(), new AliasDetector(),
+        new MergeIdenticalSelectorsVisitor(), new AliasDetector(),
+        new MergeRulesByContentVisitor(), new AliasDetector()};
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/CssRtlTest.java b/user/test/com/google/gwt/resources/rg/CssRtlTest.java
new file mode 100644
index 0000000..ac0bc8e
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/CssRtlTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.rg;
+
+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;
+
+/**
+ * This is a static test of the automatic RTL support.
+ */
+public class CssRtlTest extends CssTestCase {
+  public void testBackgroundProperties() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "backgroundProperties", true, makeVisitors());
+  }
+
+  public void testCursorProperties() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "cursorProperties", true, makeVisitors());
+  }
+
+  public void testDirectionUpdatedInBodyOnly() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "directionProperty", true, makeVisitors());
+  }
+
+  public void testFourValuedProperties() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "fourValuedProperties", true, makeVisitors());
+  }
+
+  public void testLeftRightProperties() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "leftRightProperties", true, makeVisitors());
+  }
+
+  public void testNoFlip() throws UnableToCompleteException {
+    test(TreeLogger.NULL, "noflip", true, makeVisitors());
+  }
+
+  private CssVisitor[] makeVisitors() {
+    return new CssVisitor[] {new RtlVisitor(), new AliasDetector()};
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/CssTestCase.java b/user/test/com/google/gwt/resources/rg/CssTestCase.java
new file mode 100644
index 0000000..d66d66c
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/CssTestCase.java
@@ -0,0 +1,172 @@
+/*
+ * 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.rg;
+
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.resources.css.GenerateCssAst;
+import com.google.gwt.resources.css.ast.CssNode;
+import com.google.gwt.resources.css.ast.CssStylesheet;
+import com.google.gwt.resources.css.ast.CssVisitor;
+import com.google.gwt.resources.css.ast.HasNodes;
+import com.google.gwt.resources.ext.ResourceContext;
+
+import junit.framework.TestCase;
+
+import java.net.URL;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains functions for golden-output tests that are concerned with structural
+ * modifications to the CSS AST.
+ */
+class CssTestCase extends TestCase {
+
+  /**
+   * Triggers an assertion if a CssNode is traversed more than once.
+   * 
+   * @see CssTestCase#assertNoAliasing(CssNode)
+   */
+  protected static class AliasDetector extends CssVisitor {
+    private final Map<CssNode, Void> seen = new IdentityHashMap<CssNode, Void>();
+
+    @Override
+    protected void doAccept(List<? extends CssNode> list) {
+      for (CssNode node : list) {
+        doAccept(node);
+      }
+    }
+
+    @Override
+    protected void doAcceptWithInsertRemove(List<? extends CssNode> list) {
+      for (CssNode node : list) {
+        doAccept(node);
+      }
+    }
+
+    @Override
+    protected <T extends CssNode> T doAccept(T node) {
+      assertFalse("Found repeated node " + node.toString(),
+          seen.containsKey(node));
+      seen.put(node, null);
+      return super.doAccept(node);
+    }
+  }
+
+  /**
+   * Total fake, no implementations.
+   */
+  private static class FakeContext implements ResourceContext {
+    public String deploy(String suggestedFileName, String mimeType,
+        byte[] data, boolean xhrCompatible) throws UnableToCompleteException {
+      return null;
+    }
+
+    public String deploy(URL resource, boolean xhrCompatible)
+        throws UnableToCompleteException {
+      return null;
+    }
+
+    public GeneratorContext getGeneratorContext() {
+      return null;
+    }
+
+    public String getImplementationSimpleSourceName()
+        throws IllegalStateException {
+      return null;
+    }
+
+    public JClassType getClientBundleType() {
+      return null;
+    }
+
+    public boolean supportsDataUrls() {
+      return true;
+    }
+  }
+
+  /**
+   * Asserts that two CssNodes are identical.
+   */
+  protected static <T extends CssNode & HasNodes> void assertEquals(
+      TreeLogger logger, T expected, T test) throws UnableToCompleteException {
+    String expectedCss = CssResourceGenerator.makeExpression(logger,
+        new FakeContext(), null, expected, false);
+    String testCss = CssResourceGenerator.makeExpression(logger,
+        new FakeContext(), null, test, false);
+    assertEquals(expectedCss, testCss);
+  }
+
+  /**
+   * Ensure that no CssNode is traversed more than once due to AST errors.
+   */
+  protected static void assertNoAliasing(CssNode node) {
+    (new AliasDetector()).accept(node);
+  }
+
+  /**
+   * Compares the generated Java expressions for an input file, transformed in
+   * order by the specified visitors, and a golden-output file.
+   */
+  private static void test(TreeLogger logger, URL test, URL expected,
+      CssVisitor... visitors) throws UnableToCompleteException {
+
+    CssStylesheet expectedSheet = null;
+    CssStylesheet testSheet = null;
+
+    try {
+      expectedSheet = GenerateCssAst.exec(logger, new URL[] {expected});
+      testSheet = GenerateCssAst.exec(logger, new URL[] {test});
+    } catch (UnableToCompleteException e) {
+      fail("Unable to parse stylesheet");
+    }
+
+    for (CssVisitor v : visitors) {
+      v.accept(testSheet);
+    }
+
+    assertEquals(logger, expectedSheet, testSheet);
+  }
+
+  /**
+   * Runs a test.
+   * 
+   * @param testName is used to compute the test and expected resource paths.
+   * @param reversible if <code>true</code>, the test will attempt to transform
+   *          the expected css into the test css
+   */
+  protected void test(TreeLogger logger, String testName, boolean reversible,
+      CssVisitor... visitors) throws UnableToCompleteException {
+    String packagePath = getClass().getPackage().getName().replace('.', '/')
+        + "/";
+    URL testUrl = getClass().getClassLoader().getResource(
+        packagePath + testName + "_test.css");
+    assertNotNull("Cauld not find testUrl", testUrl);
+    URL expectedUrl = getClass().getClassLoader().getResource(
+        packagePath + testName + "_expected.css");
+    assertNotNull("Cauld not find testUrl", expectedUrl);
+
+    test(logger, testUrl, expectedUrl, visitors);
+
+    if (reversible) {
+      test(logger, expectedUrl, testUrl, visitors);
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css b/user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css
new file mode 100644
index 0000000..ed77a6f
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/backgroundProperties_expected.css
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+ 
+/* Tests swapping background positions */
+.selector1 {
+  background-position-x: 60%;
+}
+
+.selector2 {
+  background-position-x: right;
+}
+
+.selector3 {
+  background-position-x: left;
+}
+
+.selector4 {
+  background-position: 60% 40%;
+}
+
+.selector5 {
+  background-position: right top;
+}
+
+.selector6 {
+  background-position: top left;
+}
+
+.selector7 {
+  background: url("chess.png") gray 60% repeat fixed;
+}
+
+/* Reported NPE bug */
+.selector8 {
+    background-position: 0 0;
+}
+
+/* Tests below here should be unchanged */
+
+.selector10 {
+  background-position-x: auto;
+}
+
+.selector11 {
+  background-position-x: center;
+}
+
+.selector12 {
+  background-position-y: 0;
+}
+
+.selector13 {
+  background-position: 10px 40%;
+}
+
+.selector14 {
+  background: url("chess.png") gray left 40% repeat fixed;
+}
+
+/* This probably isn't valid, but try it anyway */
+.selector15 {
+  background: url("chess.png") gray center 40% left repeat fixed;
+}
+
diff --git a/user/test/com/google/gwt/resources/rg/backgroundProperties_test.css b/user/test/com/google/gwt/resources/rg/backgroundProperties_test.css
new file mode 100644
index 0000000..9d3dd68
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/backgroundProperties_test.css
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+ 
+/* Tests swapping background positions */
+.selector1 {
+  background-position-x: 40%;
+}
+
+.selector2 {
+  background-position-x: left;
+}
+
+/* This isn't a defined property, but it appears to be in common use */
+.selector3 {
+  background-position-x: right;
+}
+
+.selector4 {
+  background-position: 40% 40%;
+}
+
+.selector5 {
+  background-position: left top;
+}
+
+.selector6 {
+  background-position: top right;
+}
+
+.selector7 {
+  background: url("chess.png") gray 40% repeat fixed;
+}
+
+/* Reported NPE bug */
+.selector8 {
+    background-position: 0 0;
+}
+
+/* Tests below here should be unchanged */
+
+.selector10 {
+  background-position-x: auto;
+}
+
+.selector11 {
+  background-position-x: center;
+}
+
+.selector12 {
+  background-position-y: 0;
+}
+
+.selector13 {
+  background-position: 10px 40%;
+}
+
+.selector14 {
+  background: url("chess.png") gray right 40% repeat fixed;
+}
+
+/* This probably isn't valid, but try it anyway */
+.selector15 {
+  background: url("chess.png") gray center 40% right repeat fixed;
+}
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_expected.css b/user/test/com/google/gwt/resources/rg/cursorProperties_expected.css
new file mode 100644
index 0000000..1ccd0ec
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/cursorProperties_expected.css
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+ 
+/* Tests swapping cursor directions */
+.selector {
+  cursor: nw-resize;
+}
+
+.selector2 {
+  cursor: w-resize;
+}
+
+.selector3 {
+  cursor: sw-resize;
+}
+
+.selector4 {
+  cursor: se-resize;
+}
+.selector5 {
+  cursor: e-resize;
+}
+
+.selector6 {
+  cursor: ne-resize;
+}
+
+/* Below should be unchanged */
+.selector10 {
+  cursor: be-resize;
+}
+
+.selector11 {
+  cursor: z-resize;
+}
+
+.selector12 {
+  cursor: inherit;
+}
+
+.selector13 {
+  cursor: no-resize;
+}
diff --git a/user/test/com/google/gwt/resources/rg/cursorProperties_test.css b/user/test/com/google/gwt/resources/rg/cursorProperties_test.css
new file mode 100644
index 0000000..39ed82f
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/cursorProperties_test.css
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+ 
+/* Tests swapping cursor directions */
+.selector {
+  cursor: ne-resize;
+}
+
+.selector2 {
+  cursor: e-resize;
+}
+
+.selector3 {
+  cursor: se-resize;
+}
+
+.selector4 {
+  cursor: sw-resize;
+}
+.selector5 {
+  cursor: w-resize;
+}
+
+.selector6 {
+  cursor: nw-resize;
+}
+
+/* Below should be unchanged */
+.selector10 {
+  cursor: be-resize;
+}
+
+.selector11 {
+  cursor: z-resize;
+}
+
+.selector12 {
+  cursor: inherit;
+}
+
+.selector13 {
+  cursor: no-resize;
+}
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_expected.css b/user/test/com/google/gwt/resources/rg/directionProperty_expected.css
new file mode 100644
index 0000000..6be29cd
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/directionProperty_expected.css
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+ 
+/* Only the direction property in the body selector should be updated. */
+body {
+  direction: rtl;
+}
+
+/* These should be unchanged */
+
+.selector1 {
+  direction: ltr;
+}
+
+body .selector2 {
+  direction: rtl:
+}
+
+* {
+  direction: inherit;
+}
diff --git a/user/test/com/google/gwt/resources/rg/directionProperty_test.css b/user/test/com/google/gwt/resources/rg/directionProperty_test.css
new file mode 100644
index 0000000..b137e14
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/directionProperty_test.css
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+ 
+/* Only the direction property in the body selector should be updated. */
+body {
+  direction: ltr;
+}
+
+/* These should be unchanged */
+
+.selector1 {
+  direction: ltr;
+}
+
+body .selector2 {
+  direction: rtl:
+}
+
+* {
+  direction: inherit;
+}
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css b/user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css
new file mode 100644
index 0000000..0ab4b4e
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/fourValuedProperties_expected.css
@@ -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.
+ */
+ 
+/* Tests border and margin properties with four values */
+.selector1 {
+  padding: 1px 4px 3px 2px;
+  margin: 1px 4px 3px 2px;
+}
+
+.selector2 {
+  padding: 1px inherit 3px auto;
+  margin: 1px auto 3px inherit;
+}
+
+.selector3 {
+  padding: 1px 'string' 3px 2px;
+}
+
+.selector4 {
+  border-color: red yellow blue green;
+  border-style: solid double dashed dotted;
+  border-width: thin 0 thick medium;
+}
+
+/* Nothing below here should be changed */
+.selector10 {
+  padding: 1px 2px 3px;
+}
+
+.selector11 {
+  padding: 1px 2px;
+}
+
+.selector12 {
+  padding: 1px;
+}
+
+.selector13 {
+  border: thin solid red collapse;
+}
diff --git a/user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css b/user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css
new file mode 100644
index 0000000..ea7c2e7
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/fourValuedProperties_test.css
@@ -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.
+ */
+ 
+/* Tests border and margin properties with four values */
+.selector1 {
+  padding: 1px 2px 3px 4px;
+  margin: 1px 2px 3px 4px;
+}
+
+.selector2 {
+  padding: 1px auto 3px inherit;
+  margin: 1px inherit 3px auto;
+}
+
+.selector3 {
+  padding: 1px 2px 3px 'string';
+}
+
+.selector4 {
+  border-color: red green blue yellow;
+  border-style: solid dotted dashed double;
+  border-width: thin medium thick 0;
+}
+
+/* Nothing below here should be changed */
+.selector10 {
+  padding: 1px 2px 3px;
+}
+
+.selector11 {
+  padding: 1px 2px;
+}
+
+.selector12 {
+  padding: 1px;
+}
+
+.selector13 {
+  border: thin solid red collapse;
+}
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css b/user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css
new file mode 100644
index 0000000..f39badc
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/leftRightProperties_expected.css
@@ -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.
+ */
+ 
+/* A basic test for switching the left and right properties */
+.selector {
+  right: 1px;
+}
+
+.selector2 {
+  left: 3px;
+}
+
+.selector3 {
+  padding-left: 5px;
+  margin-right: 10px;
+}
+
+.selector4 {
+  border-left-color: 1px solid blue;
+  border-right-color: red;
+}
+
+.selector5 {
+  float: right;
+  clear: right;
+}
+
+.selector6 {
+  float: left;
+  clear: left;
+}
+
+.selector7 {
+  text-align: right;
+}
+
+.selector8 {
+  page-break-after: right;
+}
+
+.selector9 {
+  page-break-before: right;
+}
+
+/* This is CSS3 syntax */
+.selector10 {
+  background: right 5px top 10px;
+}
diff --git a/user/test/com/google/gwt/resources/rg/leftRightProperties_test.css b/user/test/com/google/gwt/resources/rg/leftRightProperties_test.css
new file mode 100644
index 0000000..fc343af
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/leftRightProperties_test.css
@@ -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.
+ */
+ 
+/* A basic test for switching the left and right properties */
+.selector {
+  left: 1px;
+}
+
+.selector2 {
+  right: 3px;
+}
+
+.selector3 {
+  padding-right: 5px;
+  margin-left: 10px;
+}
+
+.selector4 {
+  border-right-color: 1px solid blue;
+  border-left-color: red;
+}
+
+.selector5 {
+  float: left;
+  clear: left;
+}
+
+.selector6 {
+  float: right;
+  clear: right;
+}
+
+.selector7 {
+  text-align: left;
+}
+
+.selector8 {
+  page-break-after: left;
+}
+
+.selector9 {
+  page-break-before: left;
+}
+
+/* This is CSS3 syntax */
+.selector10 {
+  background: left 5px top 10px;
+}
diff --git a/user/test/com/google/gwt/resources/rg/noflip_expected.css b/user/test/com/google/gwt/resources/rg/noflip_expected.css
new file mode 100644
index 0000000..8f2fcd4
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/noflip_expected.css
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+.selector1 {
+  right: 1px;
+}
+
+/* The following should not be affected. */
+
+@noflip {
+  .selector2 {
+    left: 1px;
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/noflip_test.css b/user/test/com/google/gwt/resources/rg/noflip_test.css
new file mode 100644
index 0000000..059cf85
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/noflip_test.css
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+.selector1 {
+  left: 1px;
+}
+
+/* The following should not be affected. */
+
+@noflip {
+  .selector2 {
+    left: 1px;
+  }
+}
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_expected.css b/user/test/com/google/gwt/resources/rg/propertyMerging_expected.css
new file mode 100644
index 0000000..e94e068
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/propertyMerging_expected.css
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+ 
+/* Tests merging selectors with identical properties */
+.a , .b {
+  group: one;
+}
+.c {
+  group: two;
+  border: green;
+}
+
+.d{
+  border: green;
+  group: two;
+}
+
+.e , .e2 , .g {
+  group: five;
+}
+
+.f {
+  unrelated: six;
+}
+
+.noMergeExact1 {
+  group: three;
+  border: blue;
+}
+
+.noMergeExactBlocker {
+  group: three;
+  border: thin;
+}
+
+.noMergeExact2 {
+  group: three;
+  border: blue;
+}
+
+.noMergePrefix1 {
+  group: four;
+  border: blue;
+}
+
+.noMergePrefixBlocker {
+  group: four;
+  border-top: thin;
+}
+
+.noMergePrefix2 {
+  group: four;
+  border: blue;
+}
+
+
+/* Should not merge across @if or @media blocks */
+.if {
+  group: if;
+}
+
+.ifOk, .ifOk2 {
+  group2: ifOk;
+}
+
+@if (ignored) {
+  .if2 {
+    group: if;
+  }
+}
+
+.if3 {
+  group: if;
+}
+
+.media {
+  group: media;
+}
+
+.mediaOk , .mediaOk2 {
+  group2: mediaOk;
+}
+
+@media print {
+  .media2 {
+    group: media;
+  }
+}
+
+.media3 {
+  group: media;
+}
diff --git a/user/test/com/google/gwt/resources/rg/propertyMerging_test.css b/user/test/com/google/gwt/resources/rg/propertyMerging_test.css
new file mode 100644
index 0000000..0589ccf
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/propertyMerging_test.css
@@ -0,0 +1,122 @@
+/*
+ * 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.
+ */
+ 
+/* Tests merging selectors with identical properties */
+.a {
+  group: one;
+}
+
+.b {
+  group: one;
+}
+
+.c {
+  group: two;
+  border: green;
+}
+
+.d{
+  border: green;
+  group: two;
+}
+
+.e , .e2 {
+  group: five;
+}
+
+.f {
+  unrelated: six;
+}
+
+.g {
+  group: five;
+}
+
+.noMergeExact1 {
+  group: three;
+  border: blue;
+}
+
+.noMergeExactBlocker {
+  group: three;
+  border: thin;
+}
+
+.noMergeExact2 {
+  group: three;
+  border: blue;
+}
+
+.noMergePrefix1 {
+  group: four;
+  border: blue;
+}
+
+.noMergePrefixBlocker {
+  group: four;
+  border-top: thin;
+}
+
+.noMergePrefix2 {
+  group: four;
+  border: blue;
+}
+
+/* Should not merge across @if or @media blocks */
+.if {
+  group: if;
+}
+
+.ifOk {
+  group2: ifOk;
+}
+
+@if (ignored) {
+  .if2 {
+    group: if;
+  }
+}
+
+.ifOk2 {
+  group2: ifOk;
+}
+
+.if3 {
+  group: if;
+}
+
+.media {
+  group: media;
+}
+
+.mediaOk {
+  group2: mediaOk;
+}
+
+@media print {
+  .media2 {
+    group: media;
+  }
+}
+
+.mediaOk2 {
+  group2: mediaOk;
+}
+
+
+.media3 {
+  group: media;
+}
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_expected.css b/user/test/com/google/gwt/resources/rg/selectorMerging_expected.css
new file mode 100644
index 0000000..c48b61f
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/selectorMerging_expected.css
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+ 
+ /* Tests merging adjacent rules with identical selectors */
+a {
+  prop: value; foo: bar;
+}
+
+c {
+  prop: value;
+}
+
+aNonBlocker {
+  random: other;
+}
+
+b {
+  border-left: 1px;
+}
+
+bBlocker {
+  border: solid;
+}
+
+b {
+  border-right: 2px;
+}
+
+/* Test blocking across @if rules. */
+if {
+  if1 : true;
+  if5 : true;
+}
+
+@if (true) {
+  if {
+    if3 : true;
+  }
+}
+
+if {
+  if3-block: false;
+}
+
+/* Test blocking across @media rules. */
+media {
+  media1 : true;
+  media5 : true;
+}
+
+@if (true) {
+  media {
+    media3 : true;
+  }
+}
+
+media {
+  media3-block : false;
+}
diff --git a/user/test/com/google/gwt/resources/rg/selectorMerging_test.css b/user/test/com/google/gwt/resources/rg/selectorMerging_test.css
new file mode 100644
index 0000000..fd5225c
--- /dev/null
+++ b/user/test/com/google/gwt/resources/rg/selectorMerging_test.css
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+ 
+ /* Tests merging adjacent rules with identical selectors */
+a , c {
+  prop: value;
+}
+
+aNonBlocker {
+  random: other;
+}
+
+a {
+  foo: bar;
+}
+
+b {
+  border-left: 1px;
+}
+
+bBlocker {
+  border: solid;
+}
+
+b {
+  border-right: 2px;
+}
+
+/* Test blocking across @if rules. */
+if {
+  if1 : true;
+}
+
+@if (true) {
+  if {
+    if3 : true;
+  }
+}
+
+if {
+  if5 : true;
+}
+
+if {
+  if3-block : false;
+}
+
+/* Test blocking across @media rules. */
+media {
+  media1 : true;
+}
+
+@if (true) {
+  media {
+    media3 : true;
+  }
+}
+
+media {
+  media5 : true;
+}
+
+media {
+  media3-block : false;
+}