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
  *    *&#47;
+ *   {@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
  *    *&#47;
+ *   {@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;
+      }
+    };
+  }
+}