Addresses issue 2171. Adds the @Resource annotation to ImageBundle and deprecates the old @gwt.resource javadoc pseudo-annotation. Includes updated javadoc, samples, and a bonus class "UnitTestTreeLogger" that makes it easier to unit test behavior with respect to logging.
Patch by: bruce
Review by: bobv
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2101 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java b/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java
new file mode 100644
index 0000000..c54e65e
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/util/UnitTestTreeLogger.java
@@ -0,0 +1,173 @@
+/*
+ * 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.dev.util;
+
+import com.google.gwt.core.ext.TreeLogger;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * A {@link TreeLogger} implementation that can be used during JUnit tests to
+ * check for a specified sequence of log events.
+ */
+public class UnitTestTreeLogger implements TreeLogger {
+
+ /**
+ * Simplifies the creation of a {@link UnitTestTreeLogger} by providing
+ * convenience methods for specifying the expected log events.
+ */
+ public static class Builder {
+
+ private final List<LogEntry> expected = new ArrayList<LogEntry>();
+
+ public Builder() {
+ }
+
+ public UnitTestTreeLogger createLogger() {
+ return new UnitTestTreeLogger(expected);
+ }
+
+ public void expect(TreeLogger.Type type, String msg, Throwable caught) {
+ expected.add(new LogEntry(type, msg, caught));
+ }
+
+ public void expectDebug(String msg, Throwable caught) {
+ expect(TreeLogger.DEBUG, msg, caught);
+ }
+
+ public void expectError(String msg, Throwable caught) {
+ expect(TreeLogger.ERROR, msg, caught);
+ }
+
+ public void expectInfo(String msg, Throwable caught) {
+ expect(TreeLogger.INFO, msg, caught);
+ }
+
+ public void expectSpam(String msg, Throwable caught) {
+ expect(TreeLogger.SPAM, msg, caught);
+ }
+
+ public void expectTrace(String msg, Throwable caught) {
+ expect(TreeLogger.TRACE, msg, caught);
+ }
+
+ public void expectWarn(String msg, Throwable caught) {
+ expect(TreeLogger.WARN, msg, caught);
+ }
+ }
+
+ /**
+ * Represents a log event to check for.
+ */
+ private static class LogEntry {
+ private final Type type;
+ private final String msg;
+ private final Throwable caught;
+
+ public LogEntry(TreeLogger.Type type, String msg, Throwable caught) {
+ assert (type != null);
+ assert (msg != null);
+ this.type = type;
+ this.msg = msg;
+ this.caught = caught;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof LogEntry) {
+ LogEntry other = (LogEntry) obj;
+ if (getType().equals(other.getType())) {
+ if (getMessage().equals(other.getMessage())) {
+ if (caught == null ? other.getCaught() == null : caught.equals(other.getCaught())) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public Throwable getCaught() {
+ return caught;
+ }
+
+ public String getMessage() {
+ return msg;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ @Override
+ public int hashCode() {
+ return getMessage().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ Throwable t = getCaught();
+ String caughtStr = (t != null ? ": " + t.getClass().getName() : "");
+ return type.getLabel() + ": " + getMessage() + caughtStr;
+ }
+ }
+
+ private final List<LogEntry> actualEntries = new ArrayList<LogEntry>();
+ private final List<LogEntry> expectedEntries = new ArrayList<LogEntry>();
+ private final EnumSet<TreeLogger.Type> loggableTypes = EnumSet.noneOf(TreeLogger.Type.class);
+
+ public UnitTestTreeLogger(List<LogEntry> expectedEntries) {
+ this.expectedEntries.addAll(expectedEntries);
+
+ // Infer the set of types that are loggable.
+ for (LogEntry entry : expectedEntries) {
+ loggableTypes.add(entry.getType());
+ }
+ }
+
+ public void assertCorrectLogEntries() {
+ LogEntry expectedEntry = expectedEntries.isEmpty() ? null : expectedEntries.get(0);
+ if (expectedEntry != null) {
+ Assert.fail("Never received log entry: " + expectedEntry);
+ }
+ }
+
+ public TreeLogger branch(Type type, String msg, Throwable caught) {
+ log(type, msg, caught);
+ return this;
+ }
+
+ public boolean isLoggable(Type type) {
+ return loggableTypes.contains(type);
+ }
+
+ public void log(Type type, String msg, Throwable caught) {
+ LogEntry actualEntry = new LogEntry(type, msg, caught);
+ actualEntries.add(actualEntry);
+
+ if (expectedEntries.isEmpty()) {
+ Assert.fail("Unexpected trailing log entry: " + actualEntry);
+ } else {
+ LogEntry expectedEntry = expectedEntries.get(0);
+ Assert.assertEquals(expectedEntry, actualEntry);
+ expectedEntries.remove(0);
+ }
+ }
+}
diff --git a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/client/RichTextToolbar.java b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/client/RichTextToolbar.java
index 6f929c3..c808083 100644
--- a/samples/kitchensink/src/com/google/gwt/sample/kitchensink/client/RichTextToolbar.java
+++ b/samples/kitchensink/src/com/google/gwt/sample/kitchensink/client/RichTextToolbar.java
@@ -46,94 +46,40 @@
*/
public interface Images extends ImageBundle {
- /**
- * @gwt.resource bold.gif
- */
AbstractImagePrototype bold();
- /**
- * @gwt.resource createLink.gif
- */
AbstractImagePrototype createLink();
- /**
- * @gwt.resource hr.gif
- */
AbstractImagePrototype hr();
- /**
- * @gwt.resource indent.gif
- */
AbstractImagePrototype indent();
- /**
- * @gwt.resource insertImage.gif
- */
AbstractImagePrototype insertImage();
- /**
- * @gwt.resource italic.gif
- */
AbstractImagePrototype italic();
- /**
- * @gwt.resource justifyCenter.gif
- */
AbstractImagePrototype justifyCenter();
- /**
- * @gwt.resource justifyLeft.gif
- */
AbstractImagePrototype justifyLeft();
- /**
- * @gwt.resource justifyRight.gif
- */
AbstractImagePrototype justifyRight();
- /**
- * @gwt.resource ol.gif
- */
AbstractImagePrototype ol();
- /**
- * @gwt.resource outdent.gif
- */
AbstractImagePrototype outdent();
- /**
- * @gwt.resource removeFormat.gif
- */
AbstractImagePrototype removeFormat();
- /**
- * @gwt.resource removeLink.gif
- */
AbstractImagePrototype removeLink();
- /**
- * @gwt.resource strikeThrough.gif
- */
AbstractImagePrototype strikeThrough();
- /**
- * @gwt.resource subscript.gif
- */
AbstractImagePrototype subscript();
- /**
- * @gwt.resource superscript.gif
- */
AbstractImagePrototype superscript();
- /**
- * @gwt.resource ul.gif
- */
AbstractImagePrototype ul();
- /**
- * @gwt.resource underline.gif
- */
AbstractImagePrototype underline();
}
diff --git a/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
index 5e7314f..fec1a78 100644
--- a/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
+++ b/samples/mail/src/com/google/gwt/sample/mail/client/Contacts.java
@@ -26,6 +26,7 @@
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.ImageBundle.Resource;
/**
* A component that displays a list of contacts.
@@ -33,12 +34,10 @@
public class Contacts extends Composite {
/**
- * An image bundle for this widget and an example of the use of @gwt.resource.
+ * An image bundle for this widget and an example of the use of @Resource.
*/
public interface Images extends ImageBundle {
- /**
- * @gwt.resource default_photo.jpg
- */
+ @Resource("default_photo.jpg")
AbstractImagePrototype defaultPhoto();
}
diff --git a/user/javadoc/com/google/gwt/examples/TreeImagesExample.java b/user/javadoc/com/google/gwt/examples/TreeImagesExample.java
index 1b711cc..193f99e 100644
--- a/user/javadoc/com/google/gwt/examples/TreeImagesExample.java
+++ b/user/javadoc/com/google/gwt/examples/TreeImagesExample.java
@@ -15,14 +15,10 @@
*/
interface MyTreeImages extends TreeImages {
- /**
- * @gwt.resource downArrow.png
- */
+ @Resource("downArrow.png")
AbstractImagePrototype treeOpen();
- /**
- * @gwt.resource rightArrow.png
- */
+ @Resource("rightArrow.png")
AbstractImagePrototype treeClosed();
}
diff --git a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java
index dd2b959..cf491c9 100644
--- a/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java
+++ b/user/src/com/google/gwt/user/client/ui/HorizontalSplitPanelImages.java
@@ -23,8 +23,7 @@
/**
* An image representing the drag thumb.
- *
- * @gwt.resource splitPanelThumb.png
*/
+ @Resource("splitPanelThumb.png")
AbstractImagePrototype horizontalSplitPanelThumb();
}
diff --git a/user/src/com/google/gwt/user/client/ui/ImageBundle.java b/user/src/com/google/gwt/user/client/ui/ImageBundle.java
index 7761296..e1a5379 100644
--- a/user/src/com/google/gwt/user/client/ui/ImageBundle.java
+++ b/user/src/com/google/gwt/user/client/ui/ImageBundle.java
@@ -15,6 +15,12 @@
*/
package com.google.gwt.user.client.ui;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
/**
* A tag interface that is used in the generation of image bundles. An image
* bundle is a composition of multiple images into a single large image, along
@@ -30,12 +36,13 @@
* the bundle. Each method must take no parameters and must have a return type
* of
* {@link com.google.gwt.user.client.ui.AbstractImagePrototype AbstractImagePrototype}.
- * The image name can optionally be specified using the
- * <code>gwt.resource</code> metadata tag. Valid image name extensions are
- * <code>png</code>, <code>gif</code>, or <code>jpg</code>. If the
- * image name contains '/' characters, it is assumed to be the name of a
- * resource on the classpath, formatted as would be expected by
- * <code>
+ * The image name can optionally be specified using the {@link @Resource}
+ * annotation. (Note that the <code>gwt.resource</code> javadoc metadata tag
+ * supporting in GWT 1.4 has been superceded by the <code>Resource</code>
+ * annotation.) Valid image name extensions are <code>png</code>,
+ * <code>gif</code>, or <code>jpg</code>. If the image name contains '/'
+ * characters, it is assumed to be the name of a resource on the classpath,
+ * formatted as would be expected by <code>
* <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/lang/ClassLoader.html#getResource(java.lang.String)">ClassLoader.getResource(String)</a>.
* </code>
* Otherwise, the image must be located in the same package as the user-defined
@@ -43,14 +50,14 @@
* </p>
*
* <p>
- * The easiest way to create an image bundle is to omit the
- * <code>gwt.resource</code> metadata tag, and name the method the same as the
- * image name, excluding the extension. When the image name is inferred in this
- * manner, the image name's extension is assumed to be either <code>png</code>,
- * <code>gif</code>, or <code>jpg</code>, and the image location must be
- * in the same package as the user-defined image bundle. In the event that there
- * are multiple image files that have the same name with different extensions,
- * the order of extension precedence is <code>png</code>, <code>gif</code>,
+ * The easiest way to create an image bundle is to omit the {@link Resource}
+ * annotation, and name the method the same as the image name, excluding the
+ * extension. When the image name is inferred in this manner, the image name's
+ * extension is assumed to be either <code>png</code>, <code>gif</code>,
+ * or <code>jpg</code>, and the image location must be in the same package as
+ * the user-defined image bundle. In the event that there are multiple image
+ * files that have the same name with different extensions, the order of
+ * extension precedence is <code>png</code>, <code>gif</code>,
* <code>jpg</code>.
*
* <h3>Example</h3>
@@ -59,7 +66,7 @@
* public interface MyImageBundle extends ImageBundle {
*
* /**
- * * Notice that the gwt.resource metadata tag is not present,
+ * * Notice that the Resource annotation is not present,
* * so the method name itself is assumed to match the associated
* * image filename.
* *
@@ -78,27 +85,25 @@
* </p>
*
* <p>
- * An image bundle that uses the <code>gwt.resource</code> metadata tag to
- * specify image names might look something like this:
+ * An image bundle that uses the <code>Resource</code> annotation to specify
+ * image names might look something like this:
*
* <pre class="code">
* public interface MyImageBundle extends ImageBundle {
*
* /**
- * * The metadata tag contains no '/' characters, so
+ * * The resource annotation contains no '/' characters, so
* * btn_submit_icon.gif must be located in the same
* * package as MyImageBundle.
- * *
- * * @gwt.resource btn_submit_icon.gif
* */
+ * {@code @Resource("btn_submit_icon.gif")}
* public AbstractImagePrototype submitButtonIcon();
*
* /**
* * btn_cancel_icon.png must be located in the package
* * com.mycompany.myapp.icons (which must be on the classpath).
- * *
- * * @gwt.resource com/mycompany/myapp/icons/btn_cancel_icon.png
* */
+ * {@code @Resource("com/mycompany/myapp/icons/btn_cancel_icon.png")}
* public AbstractImagePrototype cancelButtonIcon();
* }
* </pre>
@@ -128,112 +133,106 @@
* </pre>
*
* </p>
- *
+ *
* <h3>Security Warning: Image Bundle's use of the javax.image.imageio Classes</h3>
* Certain versions of the JVM are susceptible to a vulnerability in the
- * javax.image.imageio classes, which are generally used to parse images.
- * These classes are used by image bundle's implementation to combine all
- * of the images into a single composite image.
- *
+ * javax.image.imageio classes, which are generally used to parse images. These
+ * classes are used by image bundle's implementation to combine all of the
+ * images into a single composite image.
+ *
* <p>
- * It is possible that the vulnerability could be exploited by using a
- * specially crafted image as part of an image bundle. To prevent this
- * type of attack from occurring, use a version of the JVM that
- * includes a fix for this vulnerability. See the following link for more
- * information:
+ * It is possible that the vulnerability could be exploited by using a specially
+ * crafted image as part of an image bundle. To prevent this type of attack from
+ * occurring, use a version of the JVM that includes a fix for this
+ * vulnerability. See the following link for more information:
* </p>
*
* <pre>
* <a href="http://sunsolve.sun.com/search/document.do?assetkey=1-26-102934-1">http://sunsolve.sun.com/search/document.do?assetkey=1-26-102934-1</a>
* </pre>
- *
+ *
* <p>
- * Alternatively, if the images to be used in the bundle are trusted, then
- * it is not necessary to upgrade the JVM.
+ * Alternatively, if the images to be used in the bundle are trusted, then it is
+ * not necessary to upgrade the JVM.
* </p>
*
* <h3>Caching Recommendations for Image Bundle Files</h3>
* Since the filename for the image bundle's composite image is based on a hash
* of the file's contents, the server can tell the browser to cache the file
* permanently.
- *
+ *
* <p>
* To make all image bundle files permanently cacheable, set up a rule in your
* web server to emit the <code>Expires</code> response header for any files
* ending with <code>".cache.*"</code>. Such a rule would automatically match
- * generated image bundle filenames
- * (e.g. <code>320ADF600D31858000C612E939F0AD1A.cache.png</code>).
- * The HTTP/1.1 specification recommends specifying date of approximately one
- * year in the future for the <code>Expires</code> header to indicate that the
- * resource is permanently cacheable.
+ * generated image bundle filenames (e.g.
+ * <code>320ADF600D31858000C612E939F0AD1A.cache.png</code>). The HTTP/1.1
+ * specification recommends specifying date of approximately one year in the
+ * future for the <code>Expires</code> header to indicate that the resource is
+ * permanently cacheable.
* </p>
- *
+ *
* <h3>Using Security Constraints to Protect Image Bundle Files</h3>
- * When a web application has a security constraint set for the composite
- * image, web application servers may change the image's HTTP response headers
- * so that web browsers will not cache it. For example, Tomcat and Glassfish
- * set the HTTP response headers <code>Pragma: No-cache</code>,
- * <code>Cache-Control: None</code>, and <code>Expires: Thu, 1 Jan 1970 00:00:00</code>
- * (or some other date in the past).
- *
+ * When a web application has a security constraint set for the composite image,
+ * web application servers may change the image's HTTP response headers so that
+ * web browsers will not cache it. For example, Tomcat and Glassfish set the
+ * HTTP response headers <code>Pragma: No-cache</code>,
+ * <code>Cache-Control: None</code>, and
+ * <code>Expires: Thu, 1 Jan 1970 00:00:00</code> (or some other date in the
+ * past).
+ *
* <p>
* This can lead to performance problems when using image bundles, because the
* large composite image will be re-requested unecessarily. In addition,
- * <code>clear.cache.gif</code>, which is a blank image used by the image bundle
- * implementation, will be re-requested as well. While some browsers will only
- * re-request these images for each page load, others will re-request them for
- * each image on the page that is part of an image bundle.
+ * <code>clear.cache.gif</code>, which is a blank image used by the image
+ * bundle implementation, will be re-requested as well. While some browsers will
+ * only re-request these images for each page load, others will re-request them
+ * for each image on the page that is part of an image bundle.
* </p>
- *
+ *
* There are several ways to work around this issue:
- *
+ *
* <ol>
- * <li>
- * Modify the servlet which serves <code>png</code> and <code>gif</code> files
- * so that it explicitly sets the <code>Pragma</code>, <code>Cache-Control</code>,
- * and <code>Expires</code> headers. The <code>Pragma</code> and
- * <code>Cache-Control</code> headers should be removed. The
- * <code>Expires</code> header should be set according to the
- * caching recommendations mentioned in the previous section.
- * </li>
- * <li>
- * If using Tomcat, use the <code>disableProxyCaching</code>
- * property in your web application configuration file to prevent the
- * <code>Pragma</code>, <code>Cache-Control</code>, and <code>Expires</code>
- * headers from being changed by the server. Refer to your web application
- * server's documentation for more information.
- * </li>
- * <li>
- * Exclude the image bundle's composite image from the web application's
- * security constraint.
- * </li>
- * <li>
- * If there is sensitive data in any of the images in the image bundle,
- * exclude that image from the bundle and include it in the web application's
- * security constraint. Then, rebuild the image bundle, and exclude the updated
- * bundle's composite image from the security constraint.
- * </li>
+ * <li> Modify the servlet which serves <code>png</code> and <code>gif</code>
+ * files so that it explicitly sets the <code>Pragma</code>,
+ * <code>Cache-Control</code>, and <code>Expires</code> headers. The
+ * <code>Pragma</code> and <code>Cache-Control</code> headers should be
+ * removed. The <code>Expires</code> header should be set according to the
+ * caching recommendations mentioned in the previous section. </li>
+ * <li> If using Tomcat, use the <code>disableProxyCaching</code> property in
+ * your web application configuration file to prevent the <code>Pragma</code>,
+ * <code>Cache-Control</code>, and <code>Expires</code> headers from being
+ * changed by the server. Refer to your web application server's documentation
+ * for more information. </li>
+ * <li> Exclude the image bundle's composite image from the web application's
+ * security constraint. </li>
+ * <li> If there is sensitive data in any of the images in the image bundle,
+ * exclude that image from the bundle and include it in the web application's
+ * security constraint. Then, rebuild the image bundle, and exclude the updated
+ * bundle's composite image from the security constraint. </li>
* </ol>
- *
+ *
* <h3>Image Bundles and the HTTPS Protocol</h3>
* There is an issue with displaying image bundle images in Internet Explorer
* when:
- *
+ *
* <ul>
- * <li>The image bundle's composite image is requested using the HTTPS protocol, and</li>
- * <li>The web application has a security constraint set for the composite image</li>
+ * <li>The image bundle's composite image is requested using the HTTPS
+ * protocol, and</li>
+ * <li>The web application has a security constraint set for the composite
+ * image</li>
* </ul>
- *
+ *
* This issue is known to occur with the web application servers Tomcat and
* Glassfish.
- *
+ *
* <p>
* The native format for the composite image is <code>png</code>, and
* versions of Internet Explorer prior to 7 cannot render <code>png</code>
* transparerency. To get around this problem, we make use of a plugin built
* into the operating system.
* </p>
- *
+ *
* <p>
* Internet Explorer specifies that files which require a plugin for viewing
* must be cached by the browser. That way, the plugin can read the cached file
@@ -242,22 +241,22 @@
* to prevent the browser from caching the image (see the previous section for
* details).
* </p>
- *
+ *
* <p>
* When using the HTTP protocol, Internet Explorer will disregard the
- * <code>Pragma: No-cache</code> and <code>Cache-Control: None</code> headers,
- * and will cache the image. However, When using the HTTPS protocol, Internet
- * Explorer will enforce these headers, and will not cache the image.
- * Since the composite image is not stored on disk, the plugin is unable to render
- * it, and all of the images in the application which rely on the composite image
- * will not be displayed.
+ * <code>Pragma: No-cache</code> and <code>Cache-Control: None</code>
+ * headers, and will cache the image. However, When using the HTTPS protocol,
+ * Internet Explorer will enforce these headers, and will not cache the image.
+ * Since the composite image is not stored on disk, the plugin is unable to
+ * render it, and all of the images in the application which rely on the
+ * composite image will not be displayed.
* </p>
- *
+ *
* <p>
- * To work around this issue, follow the recommendations outlined in the previous
- * section.
+ * To work around this issue, follow the recommendations outlined in the
+ * previous section.
* </p>
- *
+ *
* <h3>For More Information</h3>
* See the GWT Developer Guide for an introduction to image bundles.
* @see com.google.gwt.user.client.ui.AbstractImagePrototype
@@ -267,4 +266,20 @@
* int, int, int)
*/
public interface ImageBundle {
+
+ /**
+ * Explicitly specifies a file name or path to the image resource to be
+ * associated with a method in an {@link ImageBundle}. If the path is
+ * unqualified (that is, if it contains no slashes), then it is sought in the
+ * package enclosing the image bundle to which the annotation is attached. If
+ * the path is qualified, then it is expected that the string can be passed
+ * verbatim to <code>ClassLoader.getResource()</code>.
+ */
+ @Target(ElementType.METHOD)
+ @Retention(RetentionPolicy.RUNTIME)
+ @Documented
+ public @interface Resource {
+ String value();
+ }
+
}
diff --git a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java
index 409b6d3..2bfdb84 100644
--- a/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java
+++ b/user/src/com/google/gwt/user/client/ui/VerticalSplitPanelImages.java
@@ -23,8 +23,7 @@
/**
* An image representing the drag thumb.
- *
- * @gwt.resource splitPanelThumb.png
*/
+ @Resource("splitPanelThumb.png")
AbstractImagePrototype verticalSplitPanelThumb();
}
diff --git a/user/src/com/google/gwt/user/rebind/ui/ImageBundleGenerator.java b/user/src/com/google/gwt/user/rebind/ui/ImageBundleGenerator.java
index 2d0a143..5627daf 100644
--- a/user/src/com/google/gwt/user/rebind/ui/ImageBundleGenerator.java
+++ b/user/src/com/google/gwt/user/rebind/ui/ImageBundleGenerator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * 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
@@ -19,17 +19,20 @@
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.CompilationUnitProvider;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.client.ui.ImageBundle;
+import com.google.gwt.user.client.ui.ImageBundle.Resource;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import java.io.PrintWriter;
import java.net.URL;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.ArrayList;
+import java.util.List;
/**
* Generates an implementation of a user-defined interface <code>T</code> that
@@ -45,6 +48,64 @@
*/
public class ImageBundleGenerator extends Generator {
+ /**
+ * Simple wrapper around JMethod that allows for unit test mocking.
+ */
+ /* private */interface JMethodOracle {
+
+ Resource getAnnotation(Class<Resource> clazz);
+
+ String[][] getMetaData(String metadataTag);
+
+ String getName();
+
+ String getPackageName();
+ }
+
+ /**
+ * Indirection around the act of looking up a resource that allows for unit
+ * test mocking.
+ */
+ /* private */interface ResourceLocator {
+ /**
+ *
+ * @param resName the resource name in a format that could be passed to
+ * <code>ClassLoader.getResource()</code>
+ * @return <code>true</code> if the resource is present
+ */
+ boolean isResourcePresent(String resName);
+ }
+
+ private class JMethodOracleImpl implements JMethodOracle {
+ private final JMethod delegate;
+
+ public JMethodOracleImpl(JMethod delegate) {
+ this.delegate = delegate;
+ }
+
+ public Resource getAnnotation(Class<Resource> clazz) {
+ return delegate.getAnnotation(clazz);
+ }
+
+ public String[][] getMetaData(String metadataTag) {
+ return delegate.getMetaData(metadataTag);
+ }
+
+ public String getName() {
+ return delegate.getName();
+ }
+
+ public String getPackageName() {
+ return delegate.getEnclosingType().getPackage().getName();
+ }
+ }
+
+ /* private */static final String MSG_NO_FILE_BASED_ON_METHOD_NAME = "No matching image resource was found; any of the following filenames would have matched had they been present:";
+
+ /* private */static final String MSG_JAVADOC_FORM_DEPRECATED = "Use of @gwt.resource in javadoc is deprecated; use the annotation ImageBundle.@Resource instead";
+
+ /* private */static final String MSG_MULTIPLE_ANNOTATIONS = "You are using both the @Resource annotation and the deprecated @gwt.resource in javadoc; @Resource will be used, and @gwt.resource will be ignored";
+
private static final String ABSTRACTIMAGEPROTOTYPE_QNAME = "com.google.gwt.user.client.ui.AbstractImagePrototype";
private static final String CLIPPEDIMAGEPROTOTYPE_QNAME = "com.google.gwt.user.client.ui.impl.ClippedImagePrototype";
@@ -57,38 +118,91 @@
private static final String METADATA_TAG = "gwt.resource";
+ /* private */static String msgCannotFindImageFromMetaData(String imgResName) {
+ return "Unable to find image resource '" + imgResName + "'";
+ }
+
+ private final ResourceLocator resLocator;
+
+ /**
+ * Default constructor for image bundle. Locates resources using this class's
+ * own class loader.
+ */
public ImageBundleGenerator() {
+ this(new ResourceLocator() {
+ public boolean isResourcePresent(String resName) {
+ URL url = getClass().getClassLoader().getResource(resName);
+ return url != null;
+ }
+ });
+ }
+
+ /**
+ * Default access so that it can be accessed by unit tests.
+ */
+ /* private */ImageBundleGenerator(ResourceLocator resourceLocator) {
+ assert (resourceLocator != null);
+ this.resLocator = resourceLocator;
}
@Override
- public String generate(TreeLogger logger, GeneratorContext context,
- String typeName) throws UnableToCompleteException {
+ public String generate(TreeLogger logger, GeneratorContext context, String typeName)
+ throws UnableToCompleteException {
TypeOracle typeOracle = context.getTypeOracle();
// Get metadata describing the user's class.
JClassType userType = getValidUserType(logger, typeName, typeOracle);
- // Get the methods that correspond to constituent images.
- JMethod[] imgMethods = getValidImageMethods(logger, userType);
-
// Write the new class.
- String resultName = generateImpl(logger, context, userType, imgMethods);
+ JMethod[] imgMethods = userType.getOverridableMethods();
+ String resultName = generateImplClass(logger, context, userType, imgMethods);
// Return the complete name of the generated class.
return resultName;
}
+ /**
+ * Gets the resource name of the image associated with the specified image
+ * bundle method in a form that can be passed to
+ * <code>ClassLoader.getResource()</code>.
+ *
+ * @param logger the main logger
+ * @param method the image bundle method whose image name is being sought
+ * @return a resource name that is suitable to be passed into
+ * <code>ClassLoader.getResource()</code>; never returns
+ * <code>null</code>
+ * @throws UnableToCompleteException thrown if a resource was specified but
+ * could not be found on the classpath
+ */
+ /* private */String getImageResourceName(TreeLogger logger, JMethodOracle method)
+ throws UnableToCompleteException {
+ String imgName = tryGetImageNameFromMetaData(logger, method);
+ if (imgName != null) {
+ return imgName;
+ } else {
+ return getImageNameFromMethodName(logger, method);
+ }
+ }
+
private String computeSubclassName(JClassType userType) {
String baseName = userType.getName().replace('.', '_');
return baseName + "_generatedBundle";
}
- private void generateImageMethod(TreeLogger logger,
- ImageBundleBuilder compositeImage, SourceWriter sw, JMethod method)
- throws UnableToCompleteException {
+ private int countLines(char[] source, int declStartIndex) {
+ int lineNum = 1;
+ for (int i = 0; i < declStartIndex; ++i) {
+ if (source[i] == '\n') {
+ ++lineNum;
+ }
+ }
+ return lineNum;
+ }
- String imageName = getImageUrlFromMetaDataOrMethodName(logger, method);
+ private void generateImageMethod(TreeLogger logger, ImageBundleBuilder compositeImage,
+ SourceWriter sw, JMethod method, String imgResName) throws UnableToCompleteException {
+
String decl = method.getReadableDeclaration(false, true, true, true, true);
{
@@ -98,7 +212,7 @@
// create a new instance every time this method is called, since
// ClippedImagePrototype is immutable
- ImageBundleBuilder.ImageRect imageRect = compositeImage.getMapping(imageName);
+ ImageBundleBuilder.ImageRect imageRect = compositeImage.getMapping(imgResName);
String singletonName = method.getName() + "_SINGLETON";
sw.print("private static final ClippedImagePrototype ");
@@ -127,16 +241,30 @@
}
}
- private String generateImpl(TreeLogger logger, GeneratorContext context,
- JClassType userType, JMethod[] imageMethods)
- throws UnableToCompleteException {
+ /**
+ * Generates the image bundle implementation class, checking each method for
+ * validity as it is encountered.
+ */
+ private String generateImplClass(TreeLogger logger, GeneratorContext context,
+ JClassType userType, JMethod[] imageMethods) throws UnableToCompleteException {
+ // Lookup the type info for AbstractImagePrototype so that we can check for
+ // the proper return type
+ // on image bundle methods.
+ final JClassType abstractImagePrototype;
+ try {
+ abstractImagePrototype = userType.getOracle().getType(ABSTRACTIMAGEPROTOTYPE_QNAME);
+ } catch (NotFoundException e) {
+ logger.log(TreeLogger.ERROR, "GWT " + ABSTRACTIMAGEPROTOTYPE_QNAME
+ + " class is not available", e);
+ throw new UnableToCompleteException();
+ }
+
// Compute the package and class names of the generated class.
String pkgName = userType.getPackage().getName();
String subName = computeSubclassName(userType);
// Begin writing the generated source.
- ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
- pkgName, subName);
+ ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(pkgName, subName);
f.addImport(ABSTRACTIMAGEPROTOTYPE_QNAME);
f.addImport(CLIPPEDIMAGEPROTOTYPE_QNAME);
f.addImport(GWT_QNAME);
@@ -149,10 +277,34 @@
// Build a compound image from each individual image.
ImageBundleBuilder bulder = new ImageBundleBuilder();
+ // Store the computed image names so that we don't have to lookup them up
+ // again.
+ List<String> imageResNames = new ArrayList<String>();
+
for (JMethod method : imageMethods) {
- String imageUrl = getImageUrlFromMetaDataOrMethodName(logger, method);
- assert (imageUrl != null);
- bulder.assimilate(logger, imageUrl);
+ CompilationUnitProvider unit = method.getEnclosingType().getCompilationUnit();
+ String sourceFile = unit.getLocation();
+ int lineNum = countLines(unit.getSource(), method.getDeclStart());
+ String branchMsg = "Analyzing method '" + method.getName() + "' beginning on line "
+ + lineNum + " of " + sourceFile;
+ TreeLogger branch = logger.branch(TreeLogger.DEBUG, branchMsg, null);
+
+ // Verify that this method is valid on an image bundle.
+ if (method.getReturnType() != abstractImagePrototype) {
+ branch.log(TreeLogger.ERROR, "Return type must be " + ABSTRACTIMAGEPROTOTYPE_QNAME, null);
+ throw new UnableToCompleteException();
+ }
+
+ if (method.getParameters().length > 0) {
+ branch.log(TreeLogger.ERROR, "Method must have zero parameters", null);
+ throw new UnableToCompleteException();
+ }
+
+ // Find the associated imaged resource.
+ String imageResName = getImageResourceName(branch, new JMethodOracleImpl(method));
+ assert (imageResName != null);
+ imageResNames.add(imageResName);
+ bulder.assimilate(logger, imageResName);
}
// Write the compound image into the output directory.
@@ -166,8 +318,9 @@
sw.println("\";");
// Generate an implementation of each method.
+ int imageResNameIndex = 0;
for (JMethod method : imageMethods) {
- generateImageMethod(logger, bulder, sw, method);
+ generateImageMethod(logger, bulder, sw, method, imageResNames.get(imageResNameIndex++));
}
// Finish.
@@ -177,146 +330,49 @@
return f.getCreatedClassName();
}
- // Assume this is only called for valid methods.
- private String getImageUrlFromMetaDataOrMethodName(TreeLogger logger,
- JMethod method) throws UnableToCompleteException {
-
- String[][] md = method.getMetaData(METADATA_TAG);
-
- if (md.length == 1) {
- // Metadata is available, so get the image url from the metadata
- int lastTagIndex = md.length - 1;
- int lastValueIndex = md[lastTagIndex].length - 1;
- String imageNameFromMetaData = md[lastTagIndex][lastValueIndex];
-
- // Make sure the name is either absolute or package-relative.
- if (imageNameFromMetaData.indexOf("/") == -1) {
- String pkgName = method.getEnclosingType().getPackage().getName();
- // This construction handles the default package correctly, too.
- imageNameFromMetaData = pkgName.replace('.', '/') + "/"
- + imageNameFromMetaData;
- }
-
- // Make sure that the resource exists on the classpath. In the future,
- // this code will have to be changed if images are loaded from the
- // source path or public path.
- URL imageResourceURL = getClass().getClassLoader().getResource(
- imageNameFromMetaData);
- if (imageResourceURL == null) {
- logger.log(
- TreeLogger.ERROR,
- "Resource "
- + imageNameFromMetaData
- + " not found on classpath (is the name specified as Class.getResource() would expect?)",
- null);
- throw new UnableToCompleteException();
- }
-
- return imageNameFromMetaData;
- }
-
- String imageNameFromMethod = null;
- String packageAndMethodName = method.getEnclosingType().getPackage().getName().replace(
- '.', '/')
- + '/' + method.getName();
- // There is no metadata available, so the image url will be generated from
- // the method name with an image file extension.
- for (int i = 0; i < IMAGE_FILE_EXTENSIONS.length; i++) {
- String possibleImageName = packageAndMethodName + '.'
- + IMAGE_FILE_EXTENSIONS[i];
- // Check to see if the resource exists on the classpath for each possible
- // image file extension. This code will have to be changed if images are
- // loaded from the source path or the public path.
- URL imageResourceURL = getClass().getClassLoader().getResource(
- possibleImageName);
- if (imageResourceURL != null) {
- imageNameFromMethod = possibleImageName;
- break;
- }
- }
-
- if (imageNameFromMethod == null) {
-
- StringBuffer errorStringBuf = new StringBuffer();
- for (int i = 0; i < IMAGE_FILE_EXTENSIONS.length; i++) {
-
- errorStringBuf.append(IMAGE_FILE_EXTENSIONS[i]);
-
- if (i != IMAGE_FILE_EXTENSIONS.length - 1) {
- errorStringBuf.append(", ");
- }
- }
-
- logger.log(
- TreeLogger.ERROR,
- "Resource "
- + packageAndMethodName
- + ".("
- + errorStringBuf.toString()
- + ") not found on classpath (is the name specified as Class.getResource() would expect?)",
- null);
- throw new UnableToCompleteException();
- }
-
- return imageNameFromMethod;
- }
-
- private JMethod[] getValidImageMethods(TreeLogger logger, JClassType userType)
+ /**
+ * Attempts to get the image name from the name of the method itself by
+ * speculatively appending various image-like file extensions in a prioritized
+ * order. The first image found, if any, is used.
+ *
+ * @param logger if no matching image resource is found, an explanatory
+ * message will be logged
+ * @param method the method whose name is being examined for matching image
+ * resources
+ * @return a resource name that is suitable to be passed into
+ * <code>ClassLoader.getResource()</code>; never returns
+ * <code>null</code>
+ * @throws UnableToCompleteException thrown when no image can be found based
+ * on the method name
+ */
+ private String getImageNameFromMethodName(TreeLogger logger, JMethodOracle method)
throws UnableToCompleteException {
-
- logger = logger.branch(TreeLogger.TRACE, "Analyzing methods on "
- + userType.getQualifiedSourceName(), null);
-
- final JClassType imageClass;
- try {
- imageClass = userType.getOracle().getType(ABSTRACTIMAGEPROTOTYPE_QNAME);
- } catch (NotFoundException e) {
- logger.log(TreeLogger.ERROR, "GWT " + ABSTRACTIMAGEPROTOTYPE_QNAME
- + "class is not available", e);
- throw new UnableToCompleteException();
+ String pkgName = method.getPackageName();
+ String pkgPrefix = pkgName.replace('.', '/');
+ if (pkgPrefix.length() > 0) {
+ pkgPrefix += "/";
+ }
+ String methodName = method.getName();
+ String pkgAndMethodName = pkgPrefix + methodName;
+ List<String> testImgNames = new ArrayList<String>();
+ for (int i = 0; i < IMAGE_FILE_EXTENSIONS.length; i++) {
+ String testImgName = pkgAndMethodName + '.' + IMAGE_FILE_EXTENSIONS[i];
+ if (resLocator.isResourcePresent(testImgName)) {
+ return testImgName;
+ }
+ testImgNames.add(testImgName);
}
- Map<JMethod, String> rejectedMethodsAndWhy = new HashMap<JMethod, String>();
- JMethod[] leafMethods = userType.getOverridableMethods();
- for (JMethod method : leafMethods) {
- if (method.getReturnType() != imageClass) {
- rejectedMethodsAndWhy.put(method, "Return type must be "
- + ABSTRACTIMAGEPROTOTYPE_QNAME);
- continue;
- }
-
- if (method.getParameters().length > 0) {
- rejectedMethodsAndWhy.put(method, "Method cannot take parameters");
- continue;
- }
-
- String[][] md = method.getMetaData(METADATA_TAG);
- if ((md.length > 1) || (md.length == 1 && md[0].length != 1)) {
- rejectedMethodsAndWhy.put(
- method,
- "Expecting either no metadata tags, or one metadata tag of the form '@gwt.resource <resource-name>'");
- }
+ TreeLogger branch = logger.branch(TreeLogger.ERROR, MSG_NO_FILE_BASED_ON_METHOD_NAME, null);
+ for (String testImgName : testImgNames) {
+ branch.log(TreeLogger.ERROR, testImgName, null);
}
- // Make sure there aren't any invalid methods.
- if (!rejectedMethodsAndWhy.isEmpty()) {
- logger = logger.branch(TreeLogger.ERROR,
- "The following methods are invalid on an image bundle:", null);
- for (Map.Entry<JMethod, String> entry : rejectedMethodsAndWhy.entrySet()) {
- JMethod badMethod = entry.getKey();
- String reason = entry.getValue();
- TreeLogger branch = logger.branch(TreeLogger.ERROR,
- badMethod.getReadableDeclaration(), null);
- branch.log(TreeLogger.ERROR, reason, null);
- }
- throw new UnableToCompleteException();
- }
-
- return leafMethods;
+ throw new UnableToCompleteException();
}
- private JClassType getValidUserType(TreeLogger logger, String typeName,
- TypeOracle typeOracle) throws UnableToCompleteException {
+ private JClassType getValidUserType(TreeLogger logger, String typeName, TypeOracle typeOracle)
+ throws UnableToCompleteException {
try {
// Get the type that the user is introducing.
JClassType userType = typeOracle.getType(typeName);
@@ -326,16 +382,15 @@
// Ensure it's an interface.
if (userType.isInterface() == null) {
- logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
- + " must be an interface", null);
+ logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName() + " must be an interface",
+ null);
throw new UnableToCompleteException();
}
// Ensure proper derivation.
if (!userType.isAssignableTo(magicType)) {
- logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
- + " must be assignable to " + magicType.getQualifiedSourceName(),
- null);
+ logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName() + " must be assignable to "
+ + magicType.getQualifiedSourceName(), null);
throw new UnableToCompleteException();
}
@@ -347,4 +402,98 @@
}
}
+ /**
+ * Attempts to get the image name (verbatim) from an annotation.
+ *
+ * @return the string specified in in the {@link ImageBundle.Resource}
+ * annotation, or <code>null</code>
+ */
+ private String tryGetImageNameFromAnnotation(JMethodOracle method) {
+ ImageBundle.Resource imgResAnn = method.getAnnotation(ImageBundle.Resource.class);
+ String imgName = null;
+ if (imgResAnn != null) {
+ imgName = imgResAnn.value();
+ }
+ return imgName;
+ }
+
+ /**
+ * Attempts to get the image name (verbatim) from old-school javadoc metadata.
+ *
+ * @return the string specified in "resource" javadoc metdata tag; never
+ * returns <code>null</code>
+ */
+ private String tryGetImageNameFromJavaDoc(JMethodOracle method) {
+ String imgName = null;
+ String[][] md = method.getMetaData(METADATA_TAG);
+ if (md.length == 1) {
+ int lastTagIndex = md.length - 1;
+ int lastValueIndex = md[lastTagIndex].length - 1;
+ imgName = md[lastTagIndex][lastValueIndex];
+ }
+ return imgName;
+ }
+
+ /**
+ * Attempts to get the image name from either an annotation or from pre-1.5
+ * javadoc metadata, logging warnings to educate the user about deprecation
+ * and behaviors in the face of conflicting metadata (for example, if both
+ * forms of metadata are present).
+ *
+ * @param logger if metadata is found but the specified resource isn't
+ * available, a warning is logged
+ * @param method the image bundle method whose associated image resource is
+ * being sought
+ * @return a resource name that is suitable to be passed into
+ * <code>ClassLoader.getResource()</code>, or <code>null</code>
+ * if metadata wasn't provided
+ * @throws UnableToCompleteException thrown when metadata is provided but the
+ * resource cannot be found
+ */
+ private String tryGetImageNameFromMetaData(TreeLogger logger, JMethodOracle method)
+ throws UnableToCompleteException {
+ String imgFileName = null;
+ String imgNameAnn = tryGetImageNameFromAnnotation(method);
+ String imgNameJavaDoc = tryGetImageNameFromJavaDoc(method);
+ if (imgNameJavaDoc != null) {
+ if (imgNameAnn == null) {
+ // There is JavaDoc metadata but no annotation.
+ imgFileName = imgNameJavaDoc;
+ logger.log(TreeLogger.WARN, MSG_JAVADOC_FORM_DEPRECATED, null);
+ } else {
+ // There is both JavaDoc metadata and an annotation.
+ logger.log(TreeLogger.WARN, MSG_MULTIPLE_ANNOTATIONS, null);
+ imgFileName = imgNameAnn;
+ }
+ } else if (imgNameAnn != null) {
+ // There is only an annotation.
+ imgFileName = imgNameAnn;
+ }
+ assert (imgFileName == null || (imgNameAnn != null || imgNameJavaDoc != null));
+
+ if (imgFileName == null) {
+ // Exit early because neither an annotation nor javadoc was found.
+ return null;
+ }
+
+ // If the name has no slashes (that is, it isn't a fully-qualified resource
+ // name), then prepend the enclosing package name automatically, being
+ // careful about the default package.
+ if (imgFileName.indexOf("/") == -1) {
+ String pkgName = method.getPackageName();
+ if (!"".equals(pkgName)) {
+ imgFileName = pkgName.replace('.', '/') + "/" + imgFileName;
+ }
+ }
+
+ if (!resLocator.isResourcePresent(imgFileName)) {
+ // Not found.
+ logger.log(TreeLogger.ERROR, msgCannotFindImageFromMetaData(imgFileName), null);
+ throw new UnableToCompleteException();
+ }
+
+ // Success.
+ return imgFileName;
+ }
+
}
diff --git a/user/test/com/google/gwt/user/UISuite.java b/user/test/com/google/gwt/user/UISuite.java
index 6185e41..3fc844d 100644
--- a/user/test/com/google/gwt/user/UISuite.java
+++ b/user/test/com/google/gwt/user/UISuite.java
@@ -61,6 +61,7 @@
import com.google.gwt.user.client.ui.WidgetIteratorsTest;
import com.google.gwt.user.client.ui.WidgetOnLoadTest;
import com.google.gwt.user.client.ui.impl.ClippedImagePrototypeTest;
+import com.google.gwt.user.rebind.ui.ImageBundleGeneratorTest;
import com.google.gwt.xml.client.XMLTest;
import junit.framework.Test;
@@ -96,6 +97,7 @@
suite.addTestSuite(HorizontalPanelTest.class);
suite.addTestSuite(HTMLPanelTest.class);
suite.addTestSuite(HyperlinkTest.class);
+ suite.addTestSuite(ImageBundleGeneratorTest.class);
suite.addTestSuite(ImageTest.class);
suite.addTestSuite(LinearPanelTest.class);
suite.addTestSuite(ListBoxTest.class);
diff --git a/user/test/com/google/gwt/user/rebind/ui/ImageBundleGeneratorTest.java b/user/test/com/google/gwt/user/rebind/ui/ImageBundleGeneratorTest.java
new file mode 100644
index 0000000..6fd10ef
--- /dev/null
+++ b/user/test/com/google/gwt/user/rebind/ui/ImageBundleGeneratorTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.ui;
+
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.util.UnitTestTreeLogger;
+import com.google.gwt.user.client.ui.ImageBundle.Resource;
+import com.google.gwt.user.rebind.ui.ImageBundleGenerator.JMethodOracle;
+
+import junit.framework.TestCase;
+
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link ImageBundleGenerator}. Note that, at present, only a
+ * small minority of the functionality is really covered. The present tests
+ * cover the correctness of compile-time diagnostics and the sequence and
+ * phrasing of error and warning log messages.
+ */
+public class ImageBundleGeneratorTest extends TestCase {
+
+ /**
+ * This method is to remind the reader that ClassLoader.getResource() doesn't
+ * want a leading "/" on resource names.
+ */
+ public void testClassLoaderGetResourceHatesLeadingSlash() {
+ String fqcn = getClass().getCanonicalName();
+ // This class has a name.
+ assertNotNull(fqcn);
+ String thisClassResName = fqcn.replace('.', '/') + ".class";
+ // It has some slashes.
+ assertTrue(thisClassResName.indexOf('/') >= 0);
+ // But it does not start with a slash.
+ assertFalse(thisClassResName.startsWith("/"));
+ // The resource is successfully found without a leading slash.
+ URL url = getClass().getClassLoader().getResource(thisClassResName);
+ assertNotNull(url);
+ // It fails to be found found with a leading slash.
+ url = getClass().getClassLoader().getResource("/" + thisClassResName);
+ assertNull(url);
+ }
+
+ /**
+ * Tests that a message is logged and an exception is thrown when a resource
+ * isn't found after being sought based on the method name.
+ */
+ public void testResourceNotFoundGivenNoMetaData() {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ b.expectError(ImageBundleGenerator.MSG_NO_FILE_BASED_ON_METHOD_NAME, null);
+ b.expectError("test/nonexistentImg.png", null);
+ b.expectError("test/nonexistentImg.gif", null);
+ b.expectError("test/nonexistentImg.jpg", null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ try {
+ getImageName(logger, new String[0], "nonexistentImg", "test", new String[0][], null);
+ fail("Should have thrown");
+ } catch (UnableToCompleteException e) {
+ }
+ logger.assertCorrectLogEntries();
+ }
+
+ /**
+ * Tests that a message is logged and an exception is thrown when a resource
+ * isn't found after being sought based on a javadoc "gwt.resource"
+ * pseudo-annotation.
+ */
+ public void testResourceNotFoundGivenLegacyJavaDoc() {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ b.expectError(
+ ImageBundleGenerator.msgCannotFindImageFromMetaData("from/metadata/notfound.png"), null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ try {
+ getImageName(logger, new String[0], "nonexistentImg", "from.metadata",
+ new String[][] {{"notfound.png"}}, null);
+ fail("Should have thrown");
+ } catch (UnableToCompleteException e) {
+ }
+ logger.assertCorrectLogEntries();
+ }
+
+ /**
+ * Tests that a message is logged and an exception is thrown when a resource
+ * isn't found after being sought based on the ImageBundle.Resource
+ * annotation.
+ */
+ @Resource("notfound.png")
+ public void testResourceNotFoundGivenAnnotation() {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ b.expectError(
+ ImageBundleGenerator.msgCannotFindImageFromMetaData("from/metadata/notfound.png"), null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ try {
+ getImageName(logger, new String[0], "nonexistentImg", "from.metadata", new String[0][],
+ getResourceAnnotation("testResourceNotFoundGivenAnnotation"));
+ fail("Should have thrown");
+ } catch (UnableToCompleteException e) {
+ }
+ logger.assertCorrectLogEntries();
+ }
+
+ @Resource("dunebuggy.gif")
+ public void duneBuggyInDefaultPackage() {
+ }
+
+ @Resource("a/b/c/dunebuggy.gif")
+ public void duneBuggyInNonDefaultPackage() {
+ }
+
+ @Resource("io.jpg")
+ public void ioInSamePackage() {
+ }
+
+ /**
+ * Tests that resources can be found in a variety of ways from an image bundle
+ * residing in the default package.
+ */
+ public void testResourcesFoundFromImageBundleInDefaultPackage() throws UnableToCompleteException {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ // Due to [2] below
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ // Due to [4] below
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ {
+ // [1] Find an image in the default package using the method name.
+ String imgName = getImageName(logger, new String[] {"cabbage.jpg", "lettuce.jpg",},
+ "cabbage", "", new String[0][], null);
+ assertEquals("cabbage.jpg", imgName);
+ }
+
+ {
+ // [2] Find an image in the default package using javadoc.
+ String imgName = getImageName(logger, new String[] {"car.png", "dunebuggy.gif",},
+ "vehicleImage", "", new String[][] {{"dunebuggy.gif"}}, null);
+ assertEquals("dunebuggy.gif", imgName);
+ }
+
+ {
+ // [3] Find an image in the default package using an annotation.
+ String imgName = getImageName(logger, new String[] {"car.png", "dunebuggy.gif",},
+ "vehicleImage", "", new String[0][], getResourceAnnotation("duneBuggyInDefaultPackage"));
+ assertEquals("dunebuggy.gif", imgName);
+ }
+
+ {
+ // [4] Find an image in a non-default package using javadoc.
+ String imgName = getImageName(logger, new String[] {
+ "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage", "",
+ new String[][] {{"a/b/c/dunebuggy.gif"}}, null);
+ assertEquals("a/b/c/dunebuggy.gif", imgName);
+ }
+
+ {
+ // [5] Find an image in a non-default package using an annotation.
+ String imgName = getImageName(logger, new String[] {
+ "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage", "", new String[0][],
+ getResourceAnnotation("duneBuggyInNonDefaultPackage"));
+ assertEquals("a/b/c/dunebuggy.gif", imgName);
+ }
+
+ logger.assertCorrectLogEntries();
+ }
+
+ /**
+ * Tests that resources can be found in a variety of ways from an image bundle
+ * residing in a non-default package.
+ */
+ public void testResourcesFoundFromImageBundleInNonDefaultPackage()
+ throws UnableToCompleteException {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ // Due to [2] below
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ // Due to [4] below
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ {
+ // [1] Find an image in the same package using the method name.
+ String imgName = getImageName(logger, new String[] {"x/y/z/europa.png", "x/y/z/io.jpg",},
+ "io", "x.y.z", new String[0][], null);
+ assertEquals("x/y/z/io.jpg", imgName);
+ }
+
+ {
+ // [2] Find an image in the same package using javadoc.
+ String imgName = getImageName(logger, new String[] {"x/y/z/europa.png", "x/y/z/io.jpg",},
+ "moonImage", "x.y.z", new String[][] {{"io.jpg"}}, null);
+ assertEquals("x/y/z/io.jpg", imgName);
+ }
+
+ {
+ // [3] Find an image in the same package using an annotation.
+ String imgName = getImageName(logger, new String[] {"x/y/z/europa.png", "x/y/z/io.jpg",},
+ "moonImage", "x.y.z", new String[0][], getResourceAnnotation("ioInSamePackage"));
+ assertEquals("x/y/z/io.jpg", imgName);
+ }
+
+ {
+ // [4] Find an image in a non-default package using javadoc.
+ String imgName = getImageName(logger, new String[] {
+ "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage", "x.y.z",
+ new String[][] {{"a/b/c/dunebuggy.gif"}}, null);
+ assertEquals("a/b/c/dunebuggy.gif", imgName);
+ }
+
+ {
+ // [5] Find an image in a non-default package using an annotation.
+ String imgName = getImageName(logger, new String[] {
+ "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage", "x.y.z",
+ new String[0][], getResourceAnnotation("duneBuggyInNonDefaultPackage"));
+ assertEquals("a/b/c/dunebuggy.gif", imgName);
+ }
+
+ logger.assertCorrectLogEntries();
+ }
+
+ /**
+ * Tests that a warning is logged if the old "gwt.resource" javadoc construct
+ * is used.
+ */
+ public void testWarnOnUseOfLegacyJavaDoc() throws UnableToCompleteException {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ b.expectWarn(ImageBundleGenerator.MSG_JAVADOC_FORM_DEPRECATED, null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ String imgName = getImageName(logger, new String[] {
+ "test/flypaper.gif", "test/horse shoes.jpg", "test/Horse Shoes.jpg"}, "horseshoes", "test",
+ new String[][] {{"Horse Shoes.jpg"}}, null);
+
+ assertEquals("test/Horse Shoes.jpg", imgName);
+ logger.assertCorrectLogEntries();
+ }
+
+ /**
+ * Tests that a warning is logged if both the old "gwt.resource" javadoc
+ * construct is used and a resource annotation is used.
+ */
+ @Resource("Horse Shoes.jpg")
+ public void testWarnOnUseOfAnnotationAndLegacyJavaDoc() throws UnableToCompleteException {
+ UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
+ b.expectWarn(ImageBundleGenerator.MSG_MULTIPLE_ANNOTATIONS, null);
+ UnitTestTreeLogger logger = b.createLogger();
+
+ Resource resAnn = getResourceAnnotation("testWarnOnUseOfAnnotationAndLegacyJavaDoc");
+ String imgName = getImageName(logger, new String[] {
+ "test/flypaper.gif", "test/horse shoes.jpg", "test/Horse Shoes.jpg"}, "horseshoes", "test",
+ new String[][] {{"Horse Shoes.jpg"}}, resAnn);
+
+ assertEquals("test/Horse Shoes.jpg", imgName);
+ logger.assertCorrectLogEntries();
+ }
+
+ private Resource getResourceAnnotation(String methodName) {
+ Throwable caught = null;
+ try {
+ Resource res = getClass().getMethod(methodName, new Class[0]).getAnnotation(Resource.class);
+ assertNotNull(res);
+ return res;
+ } catch (SecurityException e) {
+ caught = e;
+ } catch (NoSuchMethodException e) {
+ caught = e;
+ }
+ fail("Unable to get @Resource annotation from method '" + methodName
+ + "' during test due to exception: " + caught.getMessage());
+ return null;
+ }
+
+ private String getImageName(UnitTestTreeLogger logger, final String[] pretendResources,
+ String methodName, String pkgName, String[][] resMetadata, final Resource resAnn)
+ throws UnableToCompleteException {
+ ImageBundleGenerator ibg = new ImageBundleGenerator(new ImageBundleGenerator.ResourceLocator() {
+ private final List<String> resList = Arrays.asList(pretendResources);
+
+ public boolean isResourcePresent(String resName) {
+ return resList.contains(resName);
+ }
+ });
+ JMethodOracle methodOracle = createJMethodOracle(methodName, pkgName, resMetadata, resAnn);
+ return ibg.getImageResourceName(logger, methodOracle);
+ }
+
+ private JMethodOracle createJMethodOracle(final String methodName, final String packageName,
+ final String[][] resourceMetadata, final Resource resourceAnnotation) {
+ return new JMethodOracle() {
+ public Resource getAnnotation(Class<Resource> clazz) {
+ return resourceAnnotation;
+ }
+
+ public String[][] getMetaData(String metadataTag) {
+ return resourceMetadata;
+ }
+
+ public String getName() {
+ return methodName;
+ }
+
+ public String getPackageName() {
+ return packageName;
+ }
+ };
+ }
+}