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