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<ServiceAsync> 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;
+}