Add SafeUri type, similar to SafeHtml but for values in a URL attribute context.

Support SafeUri-typed arguments in SafeHtmlTemplates.

Also added a few overloads in c.g.g.user.client.

Note that this is a breaking change in the sense that DataResource and
ImageResource have a new getSafeUri method, as well as overloaded constructors
taking URIs in the form of SafeUri.

Patch by: tbroyer
Review by: xtof
Fixes issues: 6145

Review at http://gwt-code-reviews.appspot.com/1447812


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10282 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt23_24userApi.conf b/tools/api-checker/config/gwt23_24userApi.conf
index 4f061ad..595e1e1 100644
--- a/tools/api-checker/config/gwt23_24userApi.conf
+++ b/tools/api-checker/config/gwt23_24userApi.conf
@@ -100,6 +100,7 @@
 :user/src/com/google/gwt/rpc/client/impl/EscapeUtil.java\
 :user/src/com/google/gwt/rpc/client/impl/RpcCallbackAdapter.java\
 :user/src/com/google/gwt/safehtml/shared/SafeHtmlHostedModeUtils.java\
+:user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java\
 :user/src/com/google/gwt/user/client/rpc/core/**\
 :user/src/com/google/gwt/user/client/rpc/impl/**\
 :user/src/com/google/gwt/uibinder/attributeparsers/**\
@@ -120,6 +121,7 @@
 :com.google.gwt.editor.client.impl\
 :com.google.gwt.junit.client.impl\
 :com.google.gwt.benchmarks.client.impl\
+:com.google.gwt.user.client.ui.impl\
 
 ##############################################
 #Api  whitelist
@@ -147,3 +149,11 @@
 com.google.gwt.user.client.Event::ONCANPLAYTHROUGH MISSING
 com.google.gwt.user.client.Event::ONENDED MISSING
 com.google.gwt.user.client.Event::ONPROGRESS MISSING
+
+# Addition of SafeUri variants
+com.google.gwt.user.client.ui.FormPanel::setAction(Ljava/lang/String;) OVERLOADED_METHOD_CALL
+com.google.gwt.user.client.ui.Frame::setUrl(Ljava/lang/String;) OVERLOADED_METHOD_CALL
+com.google.gwt.user.client.ui.Image::Image(Ljava/lang/String;IIII) OVERLOADED_METHOD_CALL
+com.google.gwt.user.client.ui.Image::prefetch(Ljava/lang/String;) OVERLOADED_METHOD_CALL
+com.google.gwt.user.client.ui.Image::setUrl(Ljava/lang/String;) OVERLOADED_METHOD_CALL
+com.google.gwt.user.client.ui.Image::setUrlAndVisibleRect(Ljava/lang/String;IIII) OVERLOADED_METHOD_CALL
diff --git a/user/src/com/google/gwt/cell/client/ImageResourceCell.java b/user/src/com/google/gwt/cell/client/ImageResourceCell.java
index e368759..2b00182 100644
--- a/user/src/com/google/gwt/cell/client/ImageResourceCell.java
+++ b/user/src/com/google/gwt/cell/client/ImageResourceCell.java
@@ -21,13 +21,6 @@
 
 /**
  * An {@link AbstractCell} used to render an {@link ImageResource}.
- *
- * <p>
- * This class assumes that the URL returned from ImageResource is safe from
- * script attacks. If you do not generate the ImageResource from a
- * {@link com.google.gwt.resources.client.ClientBundle ClientBundle}, you should
- * use {@link com.google.gwt.safehtml.shared.UriUtils UriUtils} to sanitize the
- * URL before returning it from {@link ImageResource#getURL()}.
  */
 public class ImageResourceCell extends AbstractCell<ImageResource> {
   private static ImageResourceRenderer renderer;
diff --git a/user/src/com/google/gwt/resources/Resources.gwt.xml b/user/src/com/google/gwt/resources/Resources.gwt.xml
index f46be7b..689d147 100644
--- a/user/src/com/google/gwt/resources/Resources.gwt.xml
+++ b/user/src/com/google/gwt/resources/Resources.gwt.xml
@@ -16,6 +16,8 @@
 <module>
   <!-- Pull in the necessary base support, including user.agent detection -->
   <inherits name="com.google.gwt.core.Core" />
+  <!-- Pull in SafeUri and SafeUriUtils -->
+  <inherits name="com.google.gwt.safehtml.SafeHtml" />
   <!-- Pull in StyleInjector for CssResource -->
   <inherits name="com.google.gwt.dom.DOM" />
   <!-- Used by ExternalTextResource -->
diff --git a/user/src/com/google/gwt/resources/client/DataResource.java b/user/src/com/google/gwt/resources/client/DataResource.java
index e87d0c6..c27ff91 100644
--- a/user/src/com/google/gwt/resources/client/DataResource.java
+++ b/user/src/com/google/gwt/resources/client/DataResource.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -17,6 +17,7 @@
 
 import com.google.gwt.resources.ext.ResourceGeneratorType;
 import com.google.gwt.resources.rg.DataResourceGenerator;
+import com.google.gwt.safehtml.shared.SafeUri;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -58,5 +59,14 @@
    * Retrieves a URL by which the contents of the resource can be obtained. This
    * will be an absolute URL.
    */
+  SafeUri getSafeUri();
+
+  /**
+   * Retrieves a URL by which the contents of the resource can be obtained. This
+   * will be an absolute URL.
+   *
+   * @deprecated Use {@link #getSafeUri()} instead.
+   */
+  @Deprecated
   String getUrl();
 }
diff --git a/user/src/com/google/gwt/resources/client/ImageResource.java b/user/src/com/google/gwt/resources/client/ImageResource.java
index 0266048..df63fc1 100644
--- a/user/src/com/google/gwt/resources/client/ImageResource.java
+++ b/user/src/com/google/gwt/resources/client/ImageResource.java
@@ -18,6 +18,7 @@
 import com.google.gwt.resources.ext.DefaultExtensions;
 import com.google.gwt.resources.ext.ResourceGeneratorType;
 import com.google.gwt.resources.rg.ImageResourceGenerator;
+import com.google.gwt.safehtml.shared.SafeUri;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
@@ -119,13 +120,21 @@
   int getLeft();
 
   /**
+   * Returns the URL for the composite image that contains the ImageResource.
+   */
+  SafeUri getSafeUri();
+
+  /**
    * Returns the vertical position of the image within the composite image.
    */
   int getTop();
 
   /**
    * Returns the URL for the composite image that contains the ImageResource.
+   * 
+   * @deprecated Use {@link #getSafeUri()} instead.
    */
+  @Deprecated
   String getURL();
 
   /**
diff --git a/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java
new file mode 100644
index 0000000..f75c900
--- /dev/null
+++ b/user/src/com/google/gwt/resources/client/impl/DataResourcePrototype.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2011 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.resources.client.impl;
+
+import com.google.gwt.resources.client.DataResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+
+/**
+ * Simple implementation of {@link DataResource}.
+ */
+public class DataResourcePrototype implements DataResource {
+  private final String name;
+  private final SafeUri uri;
+
+  /**
+   * Only called by generated code.
+   */
+  public DataResourcePrototype(String name, SafeUri uri) {
+    this.name = name;
+    this.uri = uri;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public SafeUri getSafeUri() {
+    return uri;
+  }
+
+  public String getUrl() {
+    return uri.asString();
+  }
+}
diff --git a/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
index ca8f381..4015391 100644
--- a/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
+++ b/user/src/com/google/gwt/resources/client/impl/ExternalTextResourcePrototype.java
@@ -26,6 +26,7 @@
 import com.google.gwt.resources.client.ResourceCallback;
 import com.google.gwt.resources.client.ResourceException;
 import com.google.gwt.resources.client.TextResource;
+import com.google.gwt.safehtml.shared.SafeUri;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /**
@@ -129,9 +130,9 @@
   private final int index;
   private final String md5Hash;
   private final String name;
-  private final String url;
+  private final SafeUri url;
 
-  public ExternalTextResourcePrototype(String name, String url,
+  public ExternalTextResourcePrototype(String name, SafeUri url,
       TextResource[] cache, int index) {
     this.name = name;
     this.url = url;
@@ -140,7 +141,7 @@
     this.md5Hash = null;
   }
 
-  public ExternalTextResourcePrototype(String name, String url,
+  public ExternalTextResourcePrototype(String name, SafeUri url,
       TextResource[] cache, int index, String md5Hash) {
     this.name = name;
     this.url = url;
@@ -169,14 +170,13 @@
       // If we have an md5Hash, we should be using JSONP
       JsonpRequestBuilder rb = new JsonpRequestBuilder();
       rb.setPredeterminedId(md5Hash);
-      rb.requestObject(url, new ETRCallback(callback));
+      rb.requestObject(url.asString(), new ETRCallback(callback));
     } else {
-      RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url);
+      RequestBuilder rb = new RequestBuilder(RequestBuilder.GET, url.asString());
       try {
         rb.sendRequest("", new ETRCallback(callback));
       } catch (RequestException e) {
-        throw new ResourceException(this,
-            "Unable to initiate request for external resource", e);
+        throw new ResourceException(this, "Unable to initiate request for external resource", e);
       }
     }
   }
diff --git a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
index 0438ced..7a422d4 100644
--- a/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
+++ b/user/src/com/google/gwt/resources/client/impl/ImageResourcePrototype.java
@@ -16,6 +16,7 @@
 package com.google.gwt.resources.client.impl;
 
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.safehtml.shared.SafeUri;
 
 /**
  * This is part of an implementation of the ImageBundle optimization implemented
@@ -26,7 +27,7 @@
   private final boolean animated;
   private final boolean lossy;
   private final String name;
-  private final String url;
+  private final SafeUri url;
   private final int left;
   private final int top;
   private final int width;
@@ -35,8 +36,8 @@
   /**
    * Only called by generated code.
    */
-  public ImageResourcePrototype(String name, String url, int left, int top,
-      int width, int height, boolean animated, boolean lossy) {
+  public ImageResourcePrototype(String name, SafeUri url, int left, int top, int width, int height,
+      boolean animated, boolean lossy) {
     this.name = name;
     this.left = left;
     this.top = top;
@@ -65,18 +66,16 @@
     return name;
   }
 
-  /**
-   * Exists for testing purposes, not part of the ImageResource interface.
-   */
+  public SafeUri getSafeUri() {
+    return url;
+  }
+
   public int getTop() {
     return top;
   }
 
-  /**
-   * Exists for testing purposes, not part of the ImageResource interface.
-   */
   public String getURL() {
-    return url;
+    return url.asString();
   }
 
   /**
@@ -90,6 +89,9 @@
     return animated;
   }
 
+  /**
+   * Exists for testing purposes, not part of the ImageResource interface.
+   */
   public boolean isLossy() {
     return lossy;
   }
diff --git a/user/src/com/google/gwt/resources/css/Spriter.java b/user/src/com/google/gwt/resources/css/Spriter.java
index 52231ab..d821fa1 100644
--- a/user/src/com/google/gwt/resources/css/Spriter.java
+++ b/user/src/com/google/gwt/resources/css/Spriter.java
@@ -125,7 +125,7 @@
     }
 
     String backgroundExpression = "\"url(\\\"\" + " + instance
-        + ".getURL() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + "
+        + ".getSafeUri().asString() + \"\\\") -\" + " + instance + ".getLeft() + \"px -\" + "
         + instance + ".getTop() + \"px " + repeatText + "\"";
     properties.add(new CssProperty("background", new ExpressionValue(
         backgroundExpression), false));
diff --git a/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
index 416ca28..c5392d7 100644
--- a/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/DataResourceGenerator.java
@@ -18,13 +18,14 @@
 import com.google.gwt.core.ext.TreeLogger;
 import com.google.gwt.core.ext.UnableToCompleteException;
 import com.google.gwt.core.ext.typeinfo.JMethod;
-import com.google.gwt.resources.client.DataResource.MimeType;
-import com.google.gwt.resources.client.DataResource;
 import com.google.gwt.resources.client.DataResource.DoNotEmbed;
+import com.google.gwt.resources.client.DataResource.MimeType;
+import com.google.gwt.resources.client.impl.DataResourcePrototype;
 import com.google.gwt.resources.ext.AbstractResourceGenerator;
 import com.google.gwt.resources.ext.ResourceContext;
 import com.google.gwt.resources.ext.ResourceGeneratorUtil;
 import com.google.gwt.resources.ext.SupportsGeneratorResultCaching;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.rebind.SourceWriter;
 import com.google.gwt.user.rebind.StringSourceWriter;
 
@@ -33,56 +34,39 @@
 /**
  * Provides implementations of DataResource.
  */
-public final class DataResourceGenerator extends AbstractResourceGenerator
-    implements SupportsGeneratorResultCaching {
+public final class DataResourceGenerator extends AbstractResourceGenerator implements
+    SupportsGeneratorResultCaching {
   @Override
-  public String createAssignment(TreeLogger logger, ResourceContext context,
-      JMethod method) throws UnableToCompleteException {
+  public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method)
+      throws UnableToCompleteException {
 
-    URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
-        method);
+    URL[] resources = ResourceGeneratorUtil.findResources(logger, context, method);
 
     if (resources.length != 1) {
-      logger.log(TreeLogger.ERROR, "Exactly one resource must be specified",
-          null);
+      logger.log(TreeLogger.ERROR, "Exactly one resource must be specified", null);
       throw new UnableToCompleteException();
     }
 
     // Determine if a MIME Type has been specified
     MimeType mimeTypeAnnotation = method.getAnnotation(MimeType.class);
-    String mimeType = mimeTypeAnnotation != null
-        ? mimeTypeAnnotation.value() : null;
+    String mimeType = mimeTypeAnnotation != null ? mimeTypeAnnotation.value() : null;
 
     // Determine if resource should not be embedded
     DoNotEmbed doNotEmbed = method.getAnnotation(DoNotEmbed.class);
     boolean forceExternal = (doNotEmbed != null);
 
     URL resource = resources[0];
-    String outputUrlExpression = context.deploy(
-        resource, mimeType, forceExternal);
+    String outputUrlExpression = context.deploy(resource, mimeType, forceExternal);
 
     SourceWriter sw = new StringSourceWriter();
-    // Write the expression to create the subtype.
-    sw.println("new " + DataResource.class.getName() + "() {");
-    sw.indent();
-
     // Convenience when examining the generated code.
     sw.println("// " + resource.toExternalForm());
-
-    sw.println("public String getUrl() {");
+    sw.println("new " + DataResourcePrototype.class.getName() + "(");
     sw.indent();
-    sw.println("return " + outputUrlExpression + ";");
+    sw.println('"' + method.getName() + "\",");
+    sw.println(UriUtils.class.getName() + ".fromTrustedString(" + outputUrlExpression + ")");
     sw.outdent();
-    sw.println("}");
-
-    sw.println("public String getName() {");
-    sw.indent();
-    sw.println("return \"" + method.getName() + "\";");
-    sw.outdent();
-    sw.println("}");
-
-    sw.outdent();
-    sw.println("}");
+    sw.print(")");
 
     return sw.toString();
   }
diff --git a/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
index 04bc77b..7bb445e 100644
--- a/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/ExternalTextResourceGenerator.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -33,6 +33,7 @@
 import com.google.gwt.resources.ext.ResourceContext;
 import com.google.gwt.resources.ext.ResourceGeneratorUtil;
 import com.google.gwt.resources.ext.SupportsGeneratorResultCaching;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.rebind.SourceWriter;
 import com.google.gwt.user.rebind.StringSourceWriter;
 
@@ -50,7 +51,7 @@
    * generator will use JSONP to fetch the files.
    */
   static final String USE_JSONP = "ExternalTextResource.useJsonp";
-  
+
   // This string must stay in sync with the values in JsonpRequest.java
   static final String JSONP_CALLBACK_PREFIX = "__gwt_jsonp__.P";
 
@@ -75,7 +76,8 @@
     sw.indent();
     sw.println('"' + name + "\",");
     // These are field names
-    sw.println(externalTextUrlIdent + ", " + externalTextCacheIdent + ", ");
+    sw.println(UriUtils.class.getName() + ".fromTrustedString(" + externalTextUrlIdent + "),");
+    sw.println(externalTextCacheIdent + ", ");
     sw.println(offsets.get(method.getName()).toString());
     if (shouldUseJsonp(context, logger)) {
       sw.println(", \"" + getMd5HashOfData() + "\"");
@@ -178,7 +180,7 @@
       ConfigurationProperty prop = context.getGeneratorContext()
         .getPropertyOracle().getConfigurationProperty(USE_JSONP);
       useJsonpProp = prop.getValues().get(0);
-      
+
       // add this configuration property to our requirements
       context.getRequirements().addConfigurationProperty(USE_JSONP);
     } catch (BadPropertyValueException e) {
diff --git a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
index 1d86ad1..6d38fb6 100644
--- a/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
+++ b/user/src/com/google/gwt/resources/rg/ImageResourceGenerator.java
@@ -35,6 +35,7 @@
 import com.google.gwt.resources.ext.SupportsGeneratorResultCaching;
 import com.google.gwt.resources.rg.ImageBundleBuilder.Arranger;
 import com.google.gwt.resources.rg.ImageBundleBuilder.ImageRect;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.rebind.SourceWriter;
 import com.google.gwt.user.rebind.StringSourceWriter;
 
@@ -75,8 +76,7 @@
         CannotBundleImageException {
 
       LocalizedImage localized = LocalizedImage.create(logger, context, image);
-      localizedByImageResource = Maps.put(localizedByImageResource, image,
-          localized);
+      localizedByImageResource = Maps.put(localizedByImageResource, image, localized);
       if (images.containsKey(localized)) {
         return localized;
       }
@@ -140,8 +140,7 @@
             throw new UnableToCompleteException();
         }
         URL normalContents = renderToTempPngFile(logger, builder, arranger);
-        normalContentsUrlExpression = context.deploy(
-            normalContents, MIME_TYPE_IMAGE_PNG, false);
+        normalContentsUrlExpression = context.deploy(normalContents, MIME_TYPE_IMAGE_PNG, false);
 
         if (!rtlImages.isEmpty()) {
           for (LocalizedImage rtlImage : rtlImages) {
@@ -151,22 +150,20 @@
             tx.setTransform(-1, 0, 0, 1, imageRect.getWidth(), 0);
             imageRect.setTransform(tx);
           }
-          URL rtlContents = renderToTempPngFile(logger, builder,
-              new ImageBundleBuilder.IdentityArranger());
+          URL rtlContents =
+              renderToTempPngFile(logger, builder, new ImageBundleBuilder.IdentityArranger());
           assert rtlContents != null;
-          rtlContentsUrlExpression = context.deploy(
-              rtlContents, MIME_TYPE_IMAGE_PNG, false);
+          rtlContentsUrlExpression = context.deploy(rtlContents, MIME_TYPE_IMAGE_PNG, false);
         }
 
         dirty = false;
         if (logger.isLoggable(TreeLogger.DEBUG)) {
-          logger.log(TreeLogger.DEBUG, "Composited " + builder.getImageCount()
-              + " images");
+          logger.log(TreeLogger.DEBUG, "Composited " + builder.getImageCount() + " images");
         }
       }
 
-      JClassType stringType = context.getGeneratorContext().getTypeOracle().findType(
-          String.class.getCanonicalName());
+      JClassType stringType =
+          context.getGeneratorContext().getTypeOracle().findType(String.class.getCanonicalName());
 
       // Create the field that holds the normal contents
       assert normalContentsUrlExpression != null;
@@ -222,8 +219,10 @@
    * ClientBundle .
    */
   static class CachedState {
-    public final Map<BundleKey, BundledImage> bundledImages = new LinkedHashMap<BundleKey, BundledImage>();
-    public final Map<BundleKey, ExternalImage> externalImages = new LinkedHashMap<BundleKey, ExternalImage>();
+    public final Map<BundleKey, BundledImage> bundledImages =
+        new LinkedHashMap<BundleKey, BundledImage>();
+    public final Map<BundleKey, ExternalImage> externalImages =
+        new LinkedHashMap<BundleKey, ExternalImage>();
   }
 
   /**
@@ -233,8 +232,7 @@
     private final ImageRect imageRect;
     private final LocalizedImage localized;
 
-    public CannotBundleImageException(LocalizedImage localized,
-        ImageRect imageRect) {
+    public CannotBundleImageException(LocalizedImage localized, ImageRect imageRect) {
       this.localized = localized;
       this.imageRect = imageRect;
     }
@@ -291,8 +289,7 @@
     /**
      * Create an unbundled image.
      */
-    public ExternalImage(ImageResourceDeclaration image,
-        LocalizedImage localized, ImageRect rect) {
+    public ExternalImage(ImageResourceDeclaration image, LocalizedImage localized, ImageRect rect) {
       this.image = image;
       this.localized = localized;
       this.rect = rect;
@@ -307,8 +304,8 @@
     public void render(TreeLogger logger, ResourceContext context,
         ClientBundleFields fields, RepeatStyle repeatStyle)
         throws UnableToCompleteException {
-      JClassType stringType = context.getGeneratorContext().getTypeOracle().findType(
-          String.class.getCanonicalName());
+      JClassType stringType =
+          context.getGeneratorContext().getTypeOracle().findType(String.class.getCanonicalName());
 
       String contentsExpression = context.deploy(
           localized.getUrl(), null, image.isPreventInlining());
@@ -343,8 +340,7 @@
    */
   static class ImageResourceDeclaration extends StringKey {
     private static String key(JMethod method) {
-      return method.getEnclosingType().getQualifiedSourceName() + "."
-          + method.getName();
+      return method.getEnclosingType().getQualifiedSourceName() + "." + method.getName();
     }
 
     private final String name;
@@ -398,8 +394,7 @@
         ResourceContext context, ImageResourceDeclaration image)
         throws UnableToCompleteException {
 
-      URL[] resources = ResourceGeneratorUtil.findResources(logger, context,
-          image.getMethod());
+      URL[] resources = ResourceGeneratorUtil.findResources(logger, context, image.getMethod());
 
       if (resources.length != 1) {
         logger.log(TreeLogger.ERROR, "Exactly one image may be specified", null);
@@ -447,8 +442,7 @@
         return null;
       }
 
-      File file = File.createTempFile(
-          ImageResourceGenerator.class.getSimpleName(), ".png");
+      File file = File.createTempFile(ImageResourceGenerator.class.getSimpleName(), ".png");
       file.deleteOnExit();
       Util.writeBytesToFile(logger, file, imageBytes);
       return file.toURI().toURL();
@@ -465,8 +459,8 @@
   private CachedState shared;
 
   @Override
-  public String createAssignment(TreeLogger logger, ResourceContext context,
-      JMethod method) throws UnableToCompleteException {
+  public String createAssignment(TreeLogger logger, ResourceContext context, JMethod method)
+      throws UnableToCompleteException {
     String name = method.getName();
 
     SourceWriter sw = new StringSourceWriter();
@@ -479,19 +473,19 @@
     ImageRect rect = bundle.getImageRect(image);
     assert rect != null : "No ImageRect ever computed for " + name;
 
-    String[] urlExpressions = new String[] {
-        bundle.getNormalContentsFieldName(), bundle.getRtlContentsFieldName()};
+    String[] urlExpressions =
+        new String[] {bundle.getNormalContentsFieldName(), bundle.getRtlContentsFieldName()};
     assert urlExpressions[0] != null : "No primary URL expression for " + name;
 
     if (urlExpressions[1] == null) {
-      sw.println(urlExpressions[0] + ",");
+      sw.println(UriUtils.class.getName() + ".fromTrustedString(" + urlExpressions[0] + "),");
     } else {
-      sw.println("com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?"
-          + urlExpressions[1] + " : " + urlExpressions[0] + ",");
+      sw.println(UriUtils.class.getName() + ".fromTrustedString("
+          + "com.google.gwt.i18n.client.LocaleInfo.getCurrentLocale().isRTL() ?"
+          + urlExpressions[1] + " : " + urlExpressions[0] + "),");
     }
-    sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth()
-        + ", " + rect.getHeight() + ", " + rect.isAnimated() + ", "
-        + rect.isLossy());
+    sw.println(rect.getLeft() + ", " + rect.getTop() + ", " + rect.getWidth() + ", "
+        + rect.getHeight() + ", " + rect.isAnimated() + ", " + rect.isLossy());
 
     sw.outdent();
     sw.print(")");
@@ -504,8 +498,8 @@
    * create the bundled images.
    */
   @Override
-  public void createFields(TreeLogger logger, ResourceContext context,
-      ClientBundleFields fields) throws UnableToCompleteException {
+  public void createFields(TreeLogger logger, ResourceContext context, ClientBundleFields fields)
+      throws UnableToCompleteException {
     renderImageMap(logger, context, fields, shared.bundledImages);
     renderImageMap(logger, context, fields, shared.externalImages);
   }
@@ -579,18 +573,15 @@
               logger.log(TreeLogger.SPAM, "Reencoded image and saved "
                   + (originalSize - newSize) + " bytes");
             }
-            localizedImage = new LocalizedImage(localizedImage,
-                reencodedContents);
+            localizedImage = new LocalizedImage(localizedImage, reencodedContents);
           }
         } catch (IOException e2) {
           // Non-fatal, but weird
           logger.log(TreeLogger.WARN,
-              "Unable to determine before/after size when re-encoding image "
-                  + "data", e2);
+              "Unable to determine before/after size when re-encoding image " + "data", e2);
         }
       }
-      ExternalImage externalImage = new ExternalImage(image, localizedImage,
-          rect);
+      ExternalImage externalImage = new ExternalImage(image, localizedImage, rect);
       shared.externalImages.put(new BundleKey(image, true), externalImage);
       displayed = externalImage;
     }
@@ -653,8 +644,7 @@
         return null;
       }
 
-      File file = File.createTempFile(
-          ImageResourceGenerator.class.getSimpleName(), ".png");
+      File file = File.createTempFile(ImageResourceGenerator.class.getSimpleName(), ".png");
       file.deleteOnExit();
       Util.writeBytesToFile(logger, file, imageBytes);
       return file.toURI().toURL();
diff --git a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
index 7bab92a..6b207f3 100644
--- a/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
+++ b/user/src/com/google/gwt/safehtml/rebind/SafeHtmlTemplatesImplMethodCreator.java
@@ -31,6 +31,7 @@
 import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.safehtml.shared.SafeUri;
 import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.rebind.AbstractGeneratorClassCreator;
 import com.google.gwt.user.rebind.AbstractMethodCreator;
@@ -47,6 +48,16 @@
   private static final String JAVA_LANG_STRING_FQCN = String.class.getName();
 
   /**
+   * Simple class name of the {@link SafeUri} interface.
+   */
+  private static final String SAFE_URI_CN = SafeUri.class.getSimpleName();
+
+  /**
+   * Fully-qualified class name of the {@link SafeUri} interface.
+   */
+  private static final String SAFE_URI_FQCN = SafeUri.class.getName();
+
+  /**
    * Simple class name of the {@link SafeStyles} interface.
    */
   private static final String SAFE_STYLES_CN = SafeStyles.class.getSimpleName();
@@ -75,15 +86,14 @@
   /**
    * Fully-qualified class name of the {@link SafeHtmlUtils} class.
    */
-  private static final String ESCAPE_UTILS_FQCN = SafeHtmlUtils.class.getName();
+  private static final String SAFE_HTML_UTILS_FQCN = SafeHtmlUtils.class.getName();
 
   /**
    * Fully-qualified class name of the {@link UriUtils} class.
    */
   private static final String URI_UTILS_FQCN = UriUtils.class.getName();
 
-  public SafeHtmlTemplatesImplMethodCreator(
-      AbstractGeneratorClassCreator classCreator) {
+  public SafeHtmlTemplatesImplMethodCreator(AbstractGeneratorClassCreator classCreator) {
     super(classCreator);
   }
 
@@ -91,9 +101,8 @@
    * {@inheritDoc}
    */
   @Override
-  public void createMethodFor(TreeLogger logger, JMethod targetMethod,
-      String key, ResourceList resourceList, GwtLocale locale)
-      throws UnableToCompleteException {
+  public void createMethodFor(TreeLogger logger, JMethod targetMethod, String key,
+      ResourceList resourceList, GwtLocale locale) throws UnableToCompleteException {
     if (!targetMethod.getReturnType().getQualifiedSourceName().equals(
         SafeHtmlTemplatesImplMethodCreator.SAFE_HTML_FQCN)) {
       throw error(logger, "All methods in interfaces extending "
@@ -102,8 +111,8 @@
     }
     Template templateAnnotation = targetMethod.getAnnotation(Template.class);
     if (templateAnnotation == null) {
-      throw error(logger, "Required annotation @Template not present "
-          + "on interface method " + targetMethod.toString());
+      throw error(logger, "Required annotation @Template not present on interface method "
+          + targetMethod.toString());
     }
 
     String template = templateAnnotation.value();
@@ -122,11 +131,14 @@
    * <ul>
    *   <li>If the parameter is of type {@link SafeStyles}, it is converted to a
    *       string using {@link SafeStyles#asString()}.
-   *   <li>Otherwise, if the parameter is not of type {@link String}, it is first
-   *       converted to {@link String}.
-   *   <li>If the template parameter occurs at the start of a URI-valued attribute
-   *       within the template, it is sanitized to ensure that it is safe in this
-   *       context. This is done by passing the value through
+   *   <li>Otherwise, if the parameter is of type {@link SafeUri}, it is converted to a
+   *       string using {@link SafeUri#asString()}.
+   *   <li>Otherwise, if the parameter is not of type {@link String}, it is
+   *       first converted to {@link String}.
+   *   <li>If the template parameter occurs at the start, or as the entire value,
+   *       of a URI-valued attribute within the template, and the parameter isn't
+   *       of type {@link SafeUri}, it is sanitized to ensure that it is safe in
+   *       this context.  This is done by passing the value through
    *       {@link UriUtils#sanitizeUri(String)}.
    *   <li>The result is then HTML-escaped by passing it through
    *       {@link SafeHtmlUtils#htmlEscape(String)}.
@@ -145,9 +157,8 @@
    * @param parameterType the Java type of the corresponding template method's
    *        parameter
    */
-  private void emitAttributeContextParameterExpression(TreeLogger logger,
-      HtmlContext htmlContext, String formalParameterName,
-      JType parameterType) {
+  private void emitAttributeContextParameterExpression(TreeLogger logger, HtmlContext htmlContext,
+      String formalParameterName, JType parameterType) {
 
     /*
      * Build up the expression from the "inside out", i.e. start with the formal
@@ -156,24 +167,26 @@
      */
     String expression = formalParameterName;
 
-    if (isSafeStyles(parameterType)) {
-      // SafeCss is safe in a CSS context, so we can use its string (but we still
-      // escape it).
+    if (isSafeUri(parameterType) || isSafeStyles(parameterType)) {
+      // SafeUri is safe in a URL context, and SafeStyles is safe in a CSS context,
+      // so we can use their string (but we still escape it).
       expression = expression + ".asString()";
-    } else if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) {
-      // The parameter's value must be explicitly converted to String unless it
-      // is already of that type.
-      expression = "String.valueOf(" + expression + ")";
-    }
+    } else {
+      if (!JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName())) {
+        // The parameter's value must be explicitly converted to String unless it
+        // is already of that type.
+        expression = "String.valueOf(" + expression + ")";
+      }
 
-    if ((htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_START) ||
-        (htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_ENTIRE)) {
-      expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")";
+      if ((htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_START) ||
+          (htmlContext.getType() == HtmlContext.Type.URL_ATTRIBUTE_ENTIRE)) {
+        expression = URI_UTILS_FQCN + ".sanitizeUri(" + expression + ")";
+      }
     }
 
     // TODO(xtof): Handle EscapedString subtype of SafeHtml, once it's been
     //     introduced.
-    expression = ESCAPE_UTILS_FQCN + ".htmlEscape(" + expression + ")";
+    expression = SAFE_HTML_UTILS_FQCN + ".htmlEscape(" + expression + ")";
 
     print(expression);
   }
@@ -199,8 +212,8 @@
    * @throws UnableToCompleteException if an error occurred that prevented
    *         code generation for the template
    */
-  private void emitMethodBodyFromTemplate(TreeLogger logger, String template,
-      JParameter[] params) throws UnableToCompleteException {
+  private void emitMethodBodyFromTemplate(TreeLogger logger, String template, JParameter[] params)
+      throws UnableToCompleteException {
     println("StringBuilder sb = new java.lang.StringBuilder();");
 
     HtmlTemplateParser parser = new HtmlTemplateParser(logger);
@@ -214,17 +227,16 @@
 
         int formalParameterIndex = parameterChunk.getParameterIndex();
         if (formalParameterIndex < 0 || formalParameterIndex >= params.length) {
-          throw error(logger, "Argument " + formalParameterIndex
-              + " beyond range of arguments: " + template);
+          throw error(logger, "Argument " + formalParameterIndex + " beyond range of arguments: "
+              + template);
         }
         String formalParameterName = "arg" + formalParameterIndex;
         JType paramType = params[formalParameterIndex].getType();
 
-        emitParameterExpression(logger, parameterChunk.getContext(),
-                                formalParameterName, paramType);
+        emitParameterExpression(
+            logger, parameterChunk.getContext(), formalParameterName, paramType);
       } else {
-        throw error(logger, "Unexpected chunk kind in parsed template "
-            + template);
+        throw error(logger, "Unexpected chunk kind in parsed template " + template);
       }
     }
     outdent();
@@ -282,6 +294,24 @@
             + " used in a non-CSS attribute context. Did you mean to use " + JAVA_LANG_STRING_FQCN
             + " or " + SAFE_HTML_CN + " instead?");
       }
+    } else if (isSafeUri(parameterType) && HtmlContext.Type.URL_ATTRIBUTE_ENTIRE != contextType) {
+      // TODO(xtof): refactor HtmlContext with isStart/isEnd/isEntire accessors and simplified type.
+      if (HtmlContext.Type.URL_ATTRIBUTE_START == contextType) {
+        // SafeUri can only be used as the entire value of an URL attribute.
+        throw error(logger, SAFE_URI_CN + " cannot be used in a URL attribute if it isn't the "
+            + "entire attribute value.");
+      } else {
+        /*
+         * SafeUri outside a URL-attribute context (or in a URL-attribute, but
+         * not at start). SafeUri is only safe if it comprises the entire URL
+         * attribute's value. We could treat it as a normal parameter and escape
+         * the string value of the parameter, but it almost definitely isn't
+         * what the developer intended to do.
+         */
+        throw error(logger, SAFE_URI_CN + " can only be used as the entire value of a URL "
+            + "attribute. Did you mean to use " + JAVA_LANG_STRING_FQCN + " or " + SAFE_HTML_CN
+            + " instead?");
+      }
     }
 
     print("sb.append(");
@@ -289,10 +319,10 @@
       case CSS:
         /*
          * TODO(jlabanca): Handle CSS in a text context.
-         * 
+         *
          * The stream parser does not parse CSS; we could however improve safety
          * via sub-formats that specify the in-css context.
-         * 
+         *
          * SafeStyles is safe in a CSS context when used inside of a CSS style
          * rule, but they are not always safe. We could implement SafeCssRules,
          * which would consist of SafeStyles inside of a CSS style rules found
@@ -315,20 +345,35 @@
          * used SafeStyles in the current context.
          */
         if (!isSafeStyles(parameterType)) {
-          // Warn against using unsafe parameters in a CSS attribute context.  
+          // Warn against using unsafe parameters in a CSS attribute context.
           logger.log(TreeLogger.WARN,
               "Template with variable in CSS attribute context: The template code generator cannot"
                   + " guarantee HTML-safety of the template -- please inspect manually or use "
                   + SAFE_STYLES_CN + " to specify arguments in a CSS attribute context");
         }
-        emitAttributeContextParameterExpression(logger, htmlContext,
-            formalParameterName, parameterType);
+        emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName,
+            parameterType);
         break;
       case URL_ATTRIBUTE_START:
       case URL_ATTRIBUTE_ENTIRE:
+        /*
+         * We already checked if the user tried to use SafeUri in an invalid
+         * (non-URL_ATTRIBUTE) context, but now we check if the user could have
+         * used SafeUri in the current context.
+         */
+        if (!isSafeUri(parameterType)) {
+          // Warn against using unsafe parameters in a URL attribute context.
+          logger.log(TreeLogger.WARN,
+              "Template with variable in URL attribute context: The template code generator cannot"
+                  + " guarantee HTML-safety of the template -- please inspect manually or use "
+                  + SAFE_URI_CN + " to specify arguments in a URL attribute context");
+        }
+        emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName,
+            parameterType);
+        break;
       case ATTRIBUTE_VALUE:
-        emitAttributeContextParameterExpression(logger, htmlContext,
-            formalParameterName, parameterType);
+        emitAttributeContextParameterExpression(logger, htmlContext, formalParameterName,
+            parameterType);
         break;
 
       default:
@@ -360,8 +405,10 @@
    * <ul>
    *   <li>If the parameter is of a primitive (e.g., numeric, boolean) type, or
    *       of type {@link SafeHtml}, it is emitted as is, without escaping.
-   *   <li>Otherwise, an expression that passes the paramter's value through
-   *       {@link SafeHtmlUtils#htmlEscape(String)} is emitted.
+   *   <li>Otherwise, an expression that passes the parameter's value through
+   *       {@link SafeHtmlUtils#htmlEscape(String)} is emitted. If the value is
+   *       of type {@link SafeUri}, it is converted to string using
+   *       {@link SafeUri#asString()}.
    * </ul>
    *
    * @param formalParameterName the name of the template method's formal
@@ -369,13 +416,12 @@
    * @param parameterType the Java type of the corresponding template method's
    *        parameter
    */
-  private void emitTextContextParameterExpression(String formalParameterName,
-      JType parameterType) {
+  private void emitTextContextParameterExpression(String formalParameterName, JType parameterType) {
     boolean parameterIsPrimitiveType = (parameterType.isPrimitive() != null);
-    boolean parameterIsNotStringTyped = !(JAVA_LANG_STRING_FQCN.equals(
-        parameterType.getQualifiedSourceName()));
+    boolean parameterIsNotStringTyped =
+        !(JAVA_LANG_STRING_FQCN.equals(parameterType.getQualifiedSourceName()));
 
-    if (SAFE_HTML_FQCN.equals(parameterType.getQualifiedSourceName())) {
+    if (isSafeHtml(parameterType)) {
       // The parameter is of type SafeHtml and its wrapped string can
       // therefore be emitted safely without escaping.
       print(formalParameterName + ".asString()");
@@ -387,21 +433,17 @@
       // The parameter is of some other type, and its value must be HTML
       // escaped. Furthermore, unless the parameter's type is {@link String},
       // it must be explicitly converted to {@link String}.
-      print(ESCAPE_UTILS_FQCN + ".htmlEscape(");
+      String expression = formalParameterName;
       if (parameterIsNotStringTyped) {
-        print("String.valueOf(");
+        expression = "String.valueOf(" + expression + ")";
       }
-      print(formalParameterName);
-      if (parameterIsNotStringTyped) {
-        print(")");
-      }
-      print(")");
+      print(SAFE_HTML_UTILS_FQCN + ".htmlEscape(" + expression + ")");
     }
   }
 
   /**
    * Check if the specified parameter type represents a {@link SafeHtml}.
-   * 
+   *
    * @param parameterType the Java parameter type
    * @return true if the type represents a {@link SafeHtml}
    */
@@ -411,11 +453,21 @@
 
   /**
    * Check if the specified parameter type represents a {@link SafeStyles}.
-   * 
+   *
    * @param parameterType the Java parameter type
    * @return true if the type represents a {@link SafeStyles}
    */
   private boolean isSafeStyles(JType parameterType) {
     return parameterType.getQualifiedSourceName().equals(SAFE_STYLES_FQCN);
   }
+
+  /**
+   * Check if the specified parameter type represents a {@link SafeUri}.
+   *
+   * @param parameterType the Java parameter type
+   * @return true if the type represents a {@link SafeUri}
+   */
+  private boolean isSafeUri(JType parameterType) {
+    return parameterType.getQualifiedSourceName().equals(SAFE_URI_FQCN);
+  }
 }
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
index 1ea9c38..e3a10f2 100644
--- a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
@@ -22,8 +22,7 @@
  */
 public final class SafeHtmlUtils {
 
-  private static final String HTML_ENTITY_REGEX =
-      "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
+  private static final String HTML_ENTITY_REGEX = "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
 
   /**
    * An empty String.
@@ -104,9 +103,8 @@
   }
 
   /**
-   * HTML-escapes a character.  HTML meta characters
-   * will be escaped as follows:
-   *
+   * HTML-escapes a character. HTML meta characters will be escaped as follows:
+   * 
    * <pre>
    * &amp; - &amp;amp;
    * &lt; - &amp;lt;
@@ -167,7 +165,7 @@
     if (s.indexOf("\"") != -1) {
       s = QUOT_RE.replace(s, "&quot;");
     }
-    if (s.indexOf("\'") != -1) {
+    if (s.indexOf("'") != -1) {
       s = SQUOT_RE.replace(s, "&#39;");
     }
     return s;
@@ -201,8 +199,7 @@
       }
 
       int entityEnd = segment.indexOf(';');
-      if (entityEnd > 0
-          && segment.substring(0, entityEnd).matches(HTML_ENTITY_REGEX)) {
+      if (entityEnd > 0 && segment.substring(0, entityEnd).matches(HTML_ENTITY_REGEX)) {
         // Append the entity without escaping.
         escaped.append("&").append(segment.substring(0, entityEnd + 1));
 
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUri.java b/user/src/com/google/gwt/safehtml/shared/SafeUri.java
new file mode 100644
index 0000000..e09aa35
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeUri.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+/**
+ * An object that implements this interface encapsulates a URI that is
+ * guaranteed to be safe to use (with respect to potential Cross-Site-Scripting
+ * vulnerabilities) in a URL context, for example in a URL-typed attribute in an
+ * HTML document.
+ *
+ * <p>
+ * Note on usage: SafeUri should be used to ensure user input is not executed in
+ * the browser. SafeUri should not be used to sanitize input before sending it
+ * to the server: The server cannot rely on the type contract of SafeUri values
+ * received from clients, because a malicious client could provide maliciously
+ * crafted serialized forms of implementations of this type that violate the
+ * type contract.
+ *
+ * <p>
+ * All implementing classes must maintain the class invariant (by design and
+ * implementation and/or convention of use), that invoking {@link #asString()}
+ * on any instance will return a string that is safe to assign to a URL-typed
+ * DOM or CSS property in a browser (or to use similarly in a "URL context"), in
+ * the sense that doing so must not cause unintended execution of script in the
+ * browser.
+ *
+ * <p>
+ * In determining safety of a URL both the value itself as well as its
+ * provenance matter. An arbitrary URI, including e.g. a
+ * <code>javascript:</code> URI, can be deemed safe in the sense of this type's
+ * contract if it is entirely under the program's control (e.g., a string
+ * literal, {@see UriUtils#fromSafeConstant}).
+ *
+ * <p>
+ * All implementations must implement equals() and hashCode() to behave
+ * consistently with the result of asString().equals() and asString.hashCode().
+ *
+ * <p>
+ * Implementations must not return {@code null} from {@link #asString()}.
+ */
+public interface SafeUri {
+
+  /**
+   * Returns this object's contained URI as a string.
+   *
+   * <p>
+   * Based on this class' contract, the returned value will be non-null and a
+   * string that is safe to use in a URL context.
+   *
+   * @return the contents as a String
+   */
+  String asString();
+
+  /**
+   * Compares this string to the specified object. Must be equal to
+   * asString().equals().
+   *
+   * @param anObject the object to compare to
+   */
+  boolean equals(Object anObject);
+
+  /**
+   * Returns a hash code for this string. Must be equal to
+   * asString().hashCode().
+   */
+  int hashCode();
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
new file mode 100644
index 0000000..597e63c
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.thirdparty.guava.common.base.Preconditions;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * SafeUri utilities whose implementation differs between Development and
+ * Production Mode.
+ *
+ * <p>
+ * This class has a super-source peer that provides the Production Mode
+ * implementation.
+ *
+ * <p>
+ * Do not use this class - it is used for implementation only, and its methods
+ * may change in the future.
+ */
+public class SafeUriHostedModeUtils {
+
+  /**
+   * All valid Web Addresses, i.e. the href-ucschar production from RFC 3987bis.
+   *
+   * @see <a href="http://tools.ietf.org/html/rfc3986#section-2">RFC 3986</a>
+   * @see <a href="http://tools.ietf.org/html/draft-ietf-iri-3987bis-05#section-7.2">RFC 3987bis Web Addresses</a>
+   */
+  static final String HREF_UCSCHAR = "("
+    + "["
+    + ":/?#\\[\\]@!$&'()*+,;=" // reserved
+    + "a-zA-Z0-9\\-._~" // iunreserved
+    + " <>\"{}|\\\\^`\u0000-\u001F\u001F-\uD7FF\uE000-\uFFFD" // href-ucschar
+    + "]"
+    + "|"
+    + "[\uD800-\uDBFF][\uDC00-\uDFFF]" // surrogate pairs
+    + ")*";
+
+  /**
+   * Name of system property that if set, enables checks in server-side code
+   * (even if assertions are disabled).
+   */
+  public static final String FORCE_CHECK_VALID_URI = "com.google.gwt.safehtml.ForceCheckValidUri";
+
+  private static boolean forceCheckValidUri;
+
+  static {
+    setForceCheckValidUriFromProperty();
+  }
+
+  /**
+   * Checks if the provided URI is a valid Web Address (per RFC 3987bis).
+   *
+   * @param uri the URL to check
+   */
+  public static void maybeCheckValidUri(String uri) {
+    if (GWT.isClient() || forceCheckValidUri) {
+      Preconditions.checkArgument(isValidUri(uri), "String is not a valid URI: %s", uri);
+    } else {
+      assert isValidUri(uri) : "String is not a valid URI: " + uri;
+    }
+  }
+
+  /**
+   * Sets a global flag that controls whether or not
+   * {@link #maybeCheckValidUri(String)} should perform its check in a
+   * server-side environment.
+   *
+   * @param check if true, perform server-side checks.
+   */
+  public static void setForceCheckValidUri(boolean check) {
+    forceCheckValidUri = check;
+  }
+
+  /**
+   * Sets a global flag that controls whether or not
+   * {@link #maybeCheckValidUri(String)} should perform its check in a
+   * server-side environment from the value of the {@value
+   * FORCE_CHECK_VALID_URI} property.
+   */
+  // The following annotation causes javadoc to crash on Mac OS X 10.5.8,
+  // using java 1.5.0_24.
+  //
+  // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6442982
+  //
+  // @VisibleForTesting
+  public static void setForceCheckValidUriFromProperty() {
+    forceCheckValidUri = System.getProperty(FORCE_CHECK_VALID_URI) != null;
+  }
+
+  private static boolean isValidUri(String uri) {
+    if (!uri.matches(HREF_UCSCHAR)) {
+      return false;
+    }
+    /*
+     * pre-process to turn href-ucschars into ucschars, and encode to URI.
+     *
+     * This is done by encoding everything, and decoding back "%25" to "%".
+     */
+    uri = UriUtils.encode(uri).replace("%25", "%");
+    try {
+      new URI(uri);
+      return true;
+    } catch (URISyntaxException e) {
+      return false;
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeUriString.java b/user/src/com/google/gwt/safehtml/shared/SafeUriString.java
new file mode 100644
index 0000000..92fb99c
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeUriString.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+/**
+ * A string wrapped as an object of type {@link SafeUri}.
+ * 
+ * <p>
+ * This class is package-private and intended for internal use by the
+ * {@link com.google.gwt.safehtml} package.
+ * 
+ * All implementors must implement .equals and .hashCode so that they operate
+ * just like String.equals() and String.hashCode().
+ */
+class SafeUriString implements SafeUri {
+  private String uri;
+
+  /**
+   * Constructs a {@link SafeUriString} from a string. Callers are responsible
+   * for ensuring that the string passed as the argument to this constructor
+   * satisfies the constraints of the contract imposed by the {@link SafeUri}
+   * interface.
+   * 
+   * @param uri the string to be wrapped as a {@link SafeUri}
+   */
+  SafeUriString(String uri) {
+    if (uri == null) {
+      throw new NullPointerException("uri is null");
+    }
+    this.uri = uri;
+  }
+
+  /**
+   * No-arg constructor for compatibility with GWT serialization.
+   */
+  @SuppressWarnings("unused")
+  private SafeUriString() {
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String asString() {
+    return uri;
+  }
+
+  /**
+   * Compares this string to the specified object.
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof SafeUri)) {
+      return false;
+    }
+    return uri.equals(((SafeUri) obj).asString());
+  }
+
+  /**
+   * Returns a hash code for this string.
+   */
+  @Override
+  public int hashCode() {
+    return uri.hashCode();
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/UriUtils.java b/user/src/com/google/gwt/safehtml/shared/UriUtils.java
index c177518..896d1d9 100644
--- a/user/src/com/google/gwt/safehtml/shared/UriUtils.java
+++ b/user/src/com/google/gwt/safehtml/shared/UriUtils.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2010 Google Inc.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,14 +15,120 @@
  */
 package com.google.gwt.safehtml.shared;
 
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.regexp.shared.RegExp;
+
+import java.io.UnsupportedEncodingException;
+
 /**
  * Utility class containing static methods for validating and sanitizing URIs.
  */
 public final class UriUtils {
-  
+
+  /**
+   * Characters that don't need %-escaping (minus letters and digits), according
+   * to ECMAScript 5th edition for the {@code encodeURI} function.
+   */
+  static final String DONT_NEED_ENCODING = ";/?:@&=+$," // uriReserved
+      + "-_.!~*'()" // uriMark
+      + "#"
+      + "[]"; // could be used in IPv6 addresses
+
+  // used in conditional code in encode()
+  private static final RegExp ESCAPED_LBRACKET_RE =
+    GWT.isScript() ? RegExp.compile("%5B", "g") : null;
+  private static final RegExp ESCAPED_RBRACKET_RE =
+    GWT.isScript() ? RegExp.compile("%5D", "g") : null;
+
+  /**
+   * Encodes the URL.
+   * <p>
+   * In client code, this method delegates to {@link URL#encode(String)} and
+   * then unescapes brackets, as they might be used for IPv6 addresses.
+   *
+   * @param uri the URL to encode
+   * @return the %-escaped URL
+   */
+  public static String encode(String uri) {
+    if (GWT.isScript()) {
+      uri = URL.encode(uri);
+      // Follow the same approach as SafeHtmlUtils.htmlEscape
+      if (uri.indexOf("%5B") != -1) {
+        uri = ESCAPED_LBRACKET_RE.replace(uri, "[");
+      }
+      if (uri.indexOf("%5D") != -1) {
+        uri = ESCAPED_RBRACKET_RE.replace(uri, "]");
+      }
+      return uri;
+    } else {
+      StringBuilder sb = new StringBuilder();
+      byte[] utf8bytes;
+      try {
+        utf8bytes = uri.getBytes("UTF-8");
+      } catch (UnsupportedEncodingException e) {
+        // UTF-8 is guaranteed to be implemented, this code won't ever run.
+        return null;
+      }
+      for (byte b : utf8bytes) {
+        int c = b & 0xFF;
+        // This works because characters that don't need encoding are all
+        // expressed as a single UTF-8 byte
+        if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')
+            || DONT_NEED_ENCODING.indexOf(c) != -1) {
+          sb.append((char) c);
+        } else {
+          String hexByte = Integer.toHexString(c).toUpperCase();
+          if (hexByte.length() == 1) {
+            hexByte = "0" + hexByte;
+          }
+          sb.append('%').append(hexByte);
+        }
+      }
+      return sb.toString();
+    }
+  }
+
+  /**
+   * Encodes the URL, preserving existing %-escapes.
+   *
+   * @param uri the URL to encode
+   * @return the %-escaped URL
+   */
+  public static String encodeAllowEscapes(String uri) {
+    StringBuilder escaped = new StringBuilder();
+
+    boolean firstSegment = true;
+    for (String segment : uri.split("%", -1)) {
+      if (firstSegment) {
+        /*
+         * The first segment is never part of a percent-escape, so we always
+         * escape it. Note that if the input starts with a percent, we will get
+         * an empty segment before that.
+         */
+        firstSegment = false;
+        escaped.append(encode(segment));
+        continue;
+      }
+
+      if (segment.length() >= 2 && segment.substring(0, 2).matches("[0-9a-fA-F]{2}")) {
+        // Append the escape without encoding.
+        escaped.append("%").append(segment.substring(0, 2));
+
+        // Append the rest of the segment, escaped.
+        escaped.append(encode(segment.substring(2)));
+      } else {
+        // The segment did not start with an escape, so encode the whole
+        // segment.
+        escaped.append("%25").append(encode(segment));
+      }
+    }
+    return escaped.toString();
+  }
+
   /**
    * Extracts the scheme of a URI.
-   * 
+   *
    * @param uri the URI to extract the scheme from
    * @return the URI's scheme, or {@code null} if the URI does not have one
    */
@@ -36,7 +142,7 @@
       /*
        *  The URI's prefix up to the first ':' contains other URI special
        *  chars, and won't be interpreted as a scheme.
-       *  
+       *
        *  TODO(xtof): Consider basing this on URL#isValidProtocol or similar;
        *  however I'm worried that being too strict here will effectively
        *   allow dangerous schemes accepted in loosely parsing browsers.
@@ -45,18 +151,66 @@
     }
     return scheme;
   }
-  
+
+  /**
+   * Returns a {@link SafeUri} constructed from a value that is fully under
+   * the control of the program, e.g., a constant.
+   *
+   * <p>
+   * The string is not sanitized and no checks are performed.  The assumption
+   * that the resulting value adheres to the {@link SafeUri} type contract
+   * is entirely based on the argument being fully under program control and
+   * not being derived from a program input.
+   *
+   * <p>
+   * <strong>Convention of use:</strong> This method must only be invoked on
+   * values that are fully under the program's control, such as string literals.
+   *
+   * @param s the input String
+   * @return a SafeUri instance
+   */
+  public static SafeUri fromSafeConstant(String s) {
+    SafeUriHostedModeUtils.maybeCheckValidUri(s);
+    return new SafeUriString(s);
+  }
+
+  /**
+   * Returns a {@link SafeUri} obtained by sanitizing the provided string.
+   *
+   * <p>
+   * The input string is sanitized using {@link #sanitizeUri(String)}.
+   *
+   * @param s the input String
+   * @return a SafeUri instance
+   */
+  public static SafeUri fromString(String s) {
+    return new SafeUriString(sanitizeUri(s));
+  }
+
+  /**
+   * Returns a {@link SafeUri} constructed from a trusted string, i.e., without
+   * sanitizing the string. No checks are performed. The calling code should be
+   * carefully reviewed to ensure the argument meets the SafeUri contract.
+   *
+   * @param s the input String
+   * @return a SafeUri instance
+   */
+  public static SafeUri fromTrustedString(String s) {
+    SafeUriHostedModeUtils.maybeCheckValidUri(s);
+    return new SafeUriString(s);
+  }
+
   /**
    * Determines if a {@link String} is safe to use as the value of a URI-valued
    * HTML attribute such as {@code src} or {@code href}.
-   * 
+   *
    * <p>
    * In this context, a URI is safe if it can be established that using it as
    * the value of a URI-valued HTML attribute such as {@code src} or {@code
    * href} cannot result in script execution. Specifically, this method deems a
    * URI safe if it either does not have a scheme, or its scheme is one of
    * {@code http, https, ftp, mailto}.
-   * 
+   *
    * @param uri the URI to validate
    * @return {@code true} if {@code uri} is safe in the above sense; {@code
    *         false} otherwise
@@ -77,29 +231,49 @@
     return ("http".equals(schemeLc)
         || "https".equals(schemeLc)
         || "ftp".equals(schemeLc)
-        || "mailto".equals(schemeLc) 
+        || "mailto".equals(schemeLc)
         || "MAILTO".equals(scheme.toUpperCase()));
   }
 
   /**
    * Sanitizes a URI.
-   * 
+   *
    * <p>
    * This method returns the URI provided if it is safe to use as the the value
    * of a URI-valued HTML attribute according to {@link #isSafeUri}, or the URI
    * "{@code #}" otherwise.
-   * 
+   *
    * @param uri the URI to sanitize
    * @return a sanitized String
    */
   public static String sanitizeUri(String uri) {
     if (isSafeUri(uri)) {
-      return uri;
+      return encodeAllowEscapes(uri);
     } else {
       return "#";
     }
   }
 
+  /**
+   * Returns a {@link SafeUri} constructed from an untrusted string but without
+   * sanitizing it.
+   *
+   * <strong>Despite this method creating a SafeUri instance, no checks are
+   * performed, so the returned SafeUri is absolutely NOT guaranteed to be
+   * safe!</strong>
+   *
+   * @param s the input String
+   * @return a SafeUri instance
+   * @deprecated This method is intended only for use in APIs that use
+   *             {@link SafeUri} to represent URIs, but for backwards
+   *             compatibility have methods that accept URI parameters as plain
+   *             strings.
+   */
+  @Deprecated
+  public static SafeUri unsafeCastFromUntrustedString(String s) {
+    return new SafeUriString(s);
+  }
+
   // prevent instantiation
   private UriUtils() {
   }
diff --git a/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java b/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java
index ee80f69..b40d913 100644
--- a/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java
+++ b/user/src/com/google/gwt/user/client/ui/AbstractImagePrototype.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.user.client.ui.impl.ClippedImagePrototype;
 
 /**
@@ -70,8 +71,8 @@
    *         ImageResource
    */
   public static AbstractImagePrototype create(ImageResource resource) {
-    return new ClippedImagePrototype(resource.getURL(), resource.getLeft(),
-        resource.getTop(), resource.getWidth(), resource.getHeight());
+    return new ClippedImagePrototype(resource.getSafeUri(), resource.getLeft(), resource.getTop(),
+        resource.getWidth(), resource.getHeight());
   }
 
   /**
@@ -123,8 +124,28 @@
    * prototype. The HTML returned is not necessarily a simple
    * <code>&lt;img&gt;</code> element. It may be a more complex structure that
    * should be treated opaquely.
+   * <p>
+   * The default implementation calls {@link #getSafeHtml()}.
    * 
    * @return the HTML representation of this prototype
    */
-  public abstract String getHTML();
+  public String getHTML() {
+    return getSafeHtml().asString();
+  }
+
+  /**
+   * Gets an HTML fragment that displays the image represented by this
+   * prototype. The HTML returned is not necessarily a simple
+   * <code>&lt;img&gt;</code> element. It may be a more complex structure that
+   * should be treated opaquely.
+   * <p>
+   * The default implementation throws an {@link UnsupportedOperationException}.
+   * 
+   * @return the HTML representation of this prototype
+   */
+  public SafeHtml getSafeHtml() {
+    // Because this is a new method on an existing base class, we need to throw
+    // UnsupportedOperationException to avoid static errors.
+    throw new UnsupportedOperationException();
+  }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/FormPanel.java b/user/src/com/google/gwt/user/client/ui/FormPanel.java
index 697b158..6143ff5 100644
--- a/user/src/com/google/gwt/user/client/ui/FormPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/FormPanel.java
@@ -22,6 +22,7 @@
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.safehtml.shared.SafeUri;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.Event;
@@ -59,13 +60,11 @@
  * </p>
  */
 @SuppressWarnings("deprecation")
-public class FormPanel extends SimplePanel implements FiresFormEvents,
-    FormPanelImplHost {
+public class FormPanel extends SimplePanel implements FiresFormEvents, FormPanelImplHost {
   /**
    * Fired when a form has been submitted successfully.
    */
-  public static class SubmitCompleteEvent extends
-      GwtEvent<SubmitCompleteHandler> {
+  public static class SubmitCompleteEvent extends GwtEvent<SubmitCompleteHandler> {
     /**
      * The event type.
      */
@@ -196,7 +195,7 @@
    */
   public interface SubmitHandler extends EventHandler {
     /**
-     *Fired when the form is submitted.
+     * Fired when the form is submitted.
      *
      * <p>
      * The FormPanel must <em>not</em> be detached (i.e. removed from its parent
@@ -418,8 +417,7 @@
    * @param handler the handler
    * @return the handler registration used to remove the handler
    */
-  public HandlerRegistration addSubmitCompleteHandler(
-      SubmitCompleteHandler handler) {
+  public HandlerRegistration addSubmitCompleteHandler(SubmitCompleteHandler handler) {
     return addHandler(handler, SubmitCompleteEvent.getType());
   }
 
@@ -514,6 +512,16 @@
   }
 
   /**
+   * Sets the 'action' associated with this form. This is the URL to which it
+   * will be submitted.
+   *
+   * @param url the form's action
+   */
+  public void setAction(SafeUri url) {
+    setAction(url.asString());
+  }
+
+  /**
    * Sets the encoding used for submitting this form. This should be either
    * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}.
    *
diff --git a/user/src/com/google/gwt/user/client/ui/Frame.java b/user/src/com/google/gwt/user/client/ui/Frame.java
index cdd8b48..0e1005c 100644
--- a/user/src/com/google/gwt/user/client/ui/Frame.java
+++ b/user/src/com/google/gwt/user/client/ui/Frame.java
@@ -23,6 +23,7 @@
 import com.google.gwt.event.dom.client.LoadEvent;
 import com.google.gwt.event.dom.client.LoadHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.safehtml.shared.SafeUri;
 
 /**
  * A widget that wraps an IFRAME element, which can contain an arbitrary web
@@ -125,6 +126,15 @@
     getFrameElement().setSrc(url);
   }
 
+  /**
+   * Sets the URL of the resource to be displayed within the frame.
+   * 
+   * @param url the frame's new URL
+   */
+  public void setUrl(SafeUri url) {
+    setUrl(url.asString());
+  }
+
   private FrameElement getFrameElement() {
     return getElement().cast();
   }
diff --git a/user/src/com/google/gwt/user/client/ui/Image.java b/user/src/com/google/gwt/user/client/ui/Image.java
index c8da399..a3aa6a4 100644
--- a/user/src/com/google/gwt/user/client/ui/Image.java
+++ b/user/src/com/google/gwt/user/client/ui/Image.java
@@ -80,6 +80,8 @@
 import com.google.gwt.event.dom.client.TouchStartHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.impl.ClippedImageImpl;
 
@@ -123,10 +125,9 @@
  * </p>
  */
 @SuppressWarnings("deprecation")
-public class Image extends Widget implements SourcesLoadEvents, HasLoadHandlers,
-    HasErrorHandlers, SourcesClickEvents, HasClickHandlers,
-    HasDoubleClickHandlers, HasAllDragAndDropHandlers, HasAllGestureHandlers,
-    HasAllMouseHandlers, HasAllTouchHandlers, SourcesMouseEvents {
+public class Image extends Widget implements SourcesLoadEvents, HasLoadHandlers, HasErrorHandlers,
+    SourcesClickEvents, HasClickHandlers, HasDoubleClickHandlers, HasAllDragAndDropHandlers,
+    HasAllGestureHandlers, HasAllMouseHandlers, HasAllTouchHandlers, SourcesMouseEvents {
 
   /**
    * The attribute that is set when an image fires a native load or error event
@@ -145,11 +146,10 @@
     private int left = 0;
     private boolean pendingNativeLoadEvent = true;
     private int top = 0;
-    private String url = null;
+    private SafeUri url = null;
     private int width = 0;
 
-    ClippedState(Image image, String url, int left, int top, int width,
-        int height) {
+    ClippedState(Image image, SafeUri url, int left, int top, int width, int height) {
       this.left = left;
       this.top = top;
       this.width = width;
@@ -183,7 +183,7 @@
     }
 
     @Override
-    public String getUrl(Image image) {
+    public SafeUri getUrl(Image image) {
       return url;
     }
 
@@ -200,7 +200,7 @@
     }
 
     @Override
-    public void setUrl(Image image, String url) {
+    public void setUrl(Image image, SafeUri url) {
       image.changeState(new UnclippedState(image));
       // Need to make sure we change the state before an onload event can fire,
       // or handlers will be fired while we are in the old state.
@@ -208,7 +208,7 @@
     }
 
     @Override
-    public void setUrlAndVisibleRect(Image image, String url, int left, int top, int width,
+    public void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top, int width,
         int height) {
       /*
        * In the event that the clipping rectangle has not changed, we want to
@@ -238,8 +238,7 @@
     }
 
     @Override
-    public void setVisibleRect(Image image, int left, int top, int width,
-        int height) {
+    public void setVisibleRect(Image image, int left, int top, int width, int height) {
       setUrlAndVisibleRect(image, url, left, top, width, height);
     }
 
@@ -269,7 +268,7 @@
 
     public abstract int getOriginTop();
 
-    public abstract String getUrl(Image image);
+    public abstract SafeUri getUrl(Image image);
 
     public abstract int getWidth(Image image);
 
@@ -297,14 +296,13 @@
       // Overridden by ClippedState.
     }
 
-    public abstract void setUrl(Image image, String url);
+    public abstract void setUrl(Image image, SafeUri url);
 
-    public abstract void setUrlAndVisibleRect(Image image, String url,
-        int left, int top, int width, int height);
-
-    public abstract void setVisibleRect(Image image, int left, int top,
+    public abstract void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top,
         int width, int height);
 
+    public abstract void setVisibleRect(Image image, int left, int top, int width, int height);
+
     /**
      * We need to synthesize a load event in case the image loads synchronously,
      * before our handlers can be attached.
@@ -360,9 +358,8 @@
     UnclippedState(Element element) {
       // This case is relatively unusual, in that we swapped a clipped image
       // out, so does not need to be efficient.
-      Event.sinkEvents(element, Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
-          | Event.ONLOAD | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS
-          | Event.GESTUREEVENTS);
+      Event.sinkEvents(element, Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS | Event.ONLOAD
+          | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS | Event.GESTUREEVENTS);
     }
 
     UnclippedState(Image image) {
@@ -377,7 +374,7 @@
           | Event.ONERROR | Event.ONMOUSEWHEEL | Event.TOUCHEVENTS | Event.GESTUREEVENTS);
     }
 
-    UnclippedState(Image image, String url) {
+    UnclippedState(Image image, SafeUri url) {
       this(image);
       setUrl(image, url);
     }
@@ -403,8 +400,8 @@
     }
 
     @Override
-    public String getUrl(Image image) {
-      return getImageElement(image).getSrc();
+    public SafeUri getUrl(Image image) {
+      return UriUtils.unsafeCastFromUntrustedString(getImageElement(image).getSrc());
     }
 
     @Override
@@ -413,22 +410,20 @@
     }
 
     @Override
-    public void setUrl(Image image, String url) {
+    public void setUrl(Image image, SafeUri url) {
       image.clearUnhandledEvent();
-      getImageElement(image).setSrc(url);
+      getImageElement(image).setSrc(url.asString());
     }
 
     @Override
-    public void setUrlAndVisibleRect(Image image, String url, int left,
-        int top, int width, int height) {
+    public void setUrlAndVisibleRect(Image image, SafeUri url, int left, int top, int width,
+        int height) {
       image.changeState(new ClippedState(image, url, left, top, width, height));
     }
 
     @Override
-    public void setVisibleRect(Image image, int left, int top, int width,
-        int height) {
-      image.changeState(new ClippedState(image, getUrl(image), left, top,
-          width, height));
+    public void setVisibleRect(Image image, int left, int top, int width, int height) {
+      image.changeState(new ClippedState(image, getUrl(image), left, top, width, height));
     }
 
     // This method is used only by unit tests.
@@ -457,6 +452,15 @@
   }
 
   /**
+   * Causes the browser to pre-fetch the image at a given URL.
+   * 
+   * @param url the URL of the image to be prefetched
+   */
+  public static void prefetch(SafeUri url) {
+    prefetch(url.asString());
+  }
+
+  /**
    * Creates a Image widget that wraps an existing &lt;img&gt; element.
    * 
    * This element must already be attached to the document. If the element is
@@ -494,8 +498,8 @@
    * @param resource the ImageResource to be displayed
    */
   public Image(ImageResource resource) {
-    this(resource.getURL(), resource.getLeft(), resource.getTop(),
-        resource.getWidth(), resource.getHeight());
+    this(resource.getURL(), resource.getLeft(), resource.getTop(), resource.getWidth(),
+        resource.getHeight());
   }
 
   /**
@@ -505,6 +509,16 @@
    * @param url the URL of the image to be displayed
    */
   public Image(String url) {
+    this(UriUtils.unsafeCastFromUntrustedString(url));
+  }
+
+  /**
+   * Creates an image with a specified URL. The load event will be fired once
+   * the image at the given URL has been retrieved by the browser.
+   * 
+   * @param url the URL of the image to be displayed
+   */
+  public Image(SafeUri url) {
     changeState(new UnclippedState(this, url));
     setStyleName("gwt-Image");
   }
@@ -528,6 +542,28 @@
    * @param height the height of the visibility rectangle
    */
   public Image(String url, int left, int top, int width, int height) {
+    this(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height);
+  }
+
+  /**
+   * Creates a clipped image with a specified URL and visibility rectangle. The
+   * visibility rectangle is declared relative to the the rectangle which
+   * encompasses the entire image, which has an upper-left vertex of (0,0). The
+   * load event will be fired immediately after the object has been constructed
+   * (i.e. potentially before the image has been loaded in the browser). Since
+   * the width and height are specified explicitly by the user, this behavior
+   * will not cause problems with retrieving the width and height of a clipped
+   * image in a load event handler.
+   * 
+   * @param url the URL of the image to be displayed
+   * @param left the horizontal co-ordinate of the upper-left vertex of the
+   *          visibility rectangle
+   * @param top the vertical co-ordinate of the upper-left vertex of the
+   *          visibility rectangle
+   * @param width the width of the visibility rectangle
+   * @param height the height of the visibility rectangle
+   */
+  public Image(SafeUri url, int left, int top, int width, int height) {
     changeState(new ClippedState(this, url, left, top, width, height));
     setStyleName("gwt-Image");
   }
@@ -729,7 +765,7 @@
    * @return the image URL
    */
   public String getUrl() {
-    return state.getUrl(this);
+    return state.getUrl(this).asString();
   }
 
   /**
@@ -812,8 +848,20 @@
    * @param resource the ImageResource to display
    */
   public void setResource(ImageResource resource) {
-    setUrlAndVisibleRect(resource.getURL(), resource.getLeft(),
-        resource.getTop(), resource.getWidth(), resource.getHeight());
+    setUrlAndVisibleRect(resource.getSafeUri(), resource.getLeft(), resource.getTop(),
+        resource.getWidth(), resource.getHeight());
+  }
+
+  /**
+   * Sets the URL of the image to be displayed. If the image is in the clipped
+   * state, a call to this method will cause a transition of the image to the
+   * unclipped state. Regardless of whether or not the image is in the clipped
+   * or unclipped state, a load event will be fired.
+   * 
+   * @param url the image URL
+   */
+  public void setUrl(SafeUri url) {
+    state.setUrl(this, url);
   }
 
   /**
@@ -825,7 +873,7 @@
    * @param url the image URL
    */
   public void setUrl(String url) {
-    state.setUrl(this, url);
+    setUrl(UriUtils.unsafeCastFromUntrustedString(url));
   }
 
   /**
@@ -844,12 +892,31 @@
    * @param width the width of the visibility rectangle
    * @param height the height of the visibility rectangle
    */
-  public void setUrlAndVisibleRect(String url, int left, int top, int width,
-      int height) {
+  public void setUrlAndVisibleRect(SafeUri url, int left, int top, int width, int height) {
     state.setUrlAndVisibleRect(this, url, left, top, width, height);
   }
 
   /**
+   * Sets the url and the visibility rectangle for the image at the same time. A
+   * single load event will be fired if either the incoming url or visiblity
+   * rectangle co-ordinates differ from the image's current url or current
+   * visibility rectangle co-ordinates. If the image is currently in the
+   * unclipped state, a call to this method will cause a transition to the
+   * clipped state.
+   * 
+   * @param url the image URL
+   * @param left the horizontal coordinate of the upper-left vertex of the
+   *          visibility rectangle
+   * @param top the vertical coordinate of the upper-left vertex of the
+   *          visibility rectangle
+   * @param width the width of the visibility rectangle
+   * @param height the height of the visibility rectangle
+   */
+  public void setUrlAndVisibleRect(String url, int left, int top, int width, int height) {
+    setUrlAndVisibleRect(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height);
+  }
+
+  /**
    * Sets the visibility rectangle of an image. The visibility rectangle is
    * declared relative to the the rectangle which encompasses the entire image,
    * which has an upper-left vertex of (0,0). Provided that any of the left,
diff --git a/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java b/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java
index a46aed5..962e04f 100644
--- a/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java
+++ b/user/src/com/google/gwt/user/client/ui/ImageResourceRenderer.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.safehtml.shared.SafeHtml;
-import com.google.gwt.safehtml.shared.SafeHtmlUtils;
 import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
 
 /**
@@ -26,6 +25,6 @@
 public class ImageResourceRenderer extends AbstractSafeHtmlRenderer<ImageResource> {
   @Override
   public SafeHtml render(ImageResource image) {
-    return SafeHtmlUtils.fromTrustedString(AbstractImagePrototype.create(image).getHTML());
+    return AbstractImagePrototype.create(image).getSafeHtml();
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
index 9d112af..afa0c7c 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImpl.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -18,47 +18,64 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.safecss.shared.SafeStyles;
+import com.google.gwt.safecss.shared.SafeStylesUtils;
+import com.google.gwt.safehtml.client.SafeHtmlTemplates;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.client.ui.Image;
 
 /**
  * Uses a combination of a clear image and a background image to clip all except
  * a desired portion of an underlying image.
- * 
+ *
  * Do not use this class - it is used for implementation only, and its methods
  * may change in the future.
  */
 public class ClippedImageImpl {
 
-  public void adjust(Element img, String url, int left, int top, int width,
-      int height) {
-    String style = "url(" + url + ") no-repeat " + (-left + "px ")
-        + (-top + "px");
+  interface Template extends SafeHtmlTemplates {
+    @SafeHtmlTemplates.Template("<img onload='this.__gwtLastUnhandledEvent=\"load\";' src='{0}' "
+        + "style='{1}' border='0'>")
+    SafeHtml image(SafeUri clearImage, SafeStyles style);
+  }
+
+  protected static final SafeUri clearImage =
+    UriUtils.fromTrustedString(GWT.getModuleBaseURL() + "clear.cache.gif");
+  private static Template template;
+
+  public void adjust(Element img, SafeUri url, int left, int top, int width, int height) {
+    String style = "url(" + url.asString() + ") no-repeat " + (-left + "px ") + (-top + "px");
     img.getStyle().setProperty("background", style);
     img.getStyle().setPropertyPx("width", width);
     img.getStyle().setPropertyPx("height", height);
   }
 
-  public Element createStructure(String url, int left, int top, int width,
-      int height) {
+  public Element createStructure(SafeUri url, int left, int top, int width, int height) {
     Element tmp = Document.get().createSpanElement();
-    tmp.setInnerHTML(getHTML(url, left, top, width, height));
+    tmp.setInnerHTML(getSafeHtml(url, left, top, width, height).asString());
     return tmp.getFirstChildElement();
   }
 
-  public String getHTML(String url, int left, int top, int width, int height) {
-    String style = "width: " + width + "px; height: " + height
-        + "px; background: url(" + url + ") no-repeat " + (-left + "px ")
-        + (-top + "px");
-
-    String clippedImgHtml = "<img "
-        + "onload='this.__gwtLastUnhandledEvent=\"load\";' src='"
-        + GWT.getModuleBaseURL() + "clear.cache.gif' style='" + style
-        + "' border='0'>";
-
-    return clippedImgHtml;
-  }
-
   public Element getImgElement(Image image) {
     return image.getElement();
   }
+
+  public SafeHtml getSafeHtml(SafeUri url, int left, int top, int width, int height) {
+    // TODO(t.broyer): use the context-specific SafeStyles API once it's been introduced.
+    String style = "width: " + width + "px; height: " + height + "px; background: url("
+        + url.asString() + ") " + "no-repeat " + (-left + "px ")
+        + (-top + "px;");
+
+    return getTemplate().image(clearImage, SafeStylesUtils.fromTrustedString(style));
+  }
+
+  private Template getTemplate() {
+    // no need to synchronize, JavaScript in the browser is single-threaded
+    if (template == null) {
+      template = GWT.create(Template.class);
+    }
+    return template;
+  }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
index 8895b68..d69fe3b 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImageImplIE6.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -17,6 +17,9 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.safehtml.shared.SafeUri;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Image;
 
@@ -31,7 +34,7 @@
 
   private static String moduleBaseUrlProtocol =
       GWT.getHostPageBaseURL().startsWith("https") ?  "https://" : "http://";
-  
+
   private static native void injectGlobalHandler() /*-{
     $wnd.__gwt_transparentImgHandler = function (elem) {
       elem.onerror = null;
@@ -48,20 +51,19 @@
   }
 
   @Override
-  public void adjust(Element clipper, String url, int left, int top, int width,
-      int height) {
+  public void adjust(Element clipper, SafeUri url, int left, int top, int width, int height) {
     if (!isIE6()) {
       super.adjust(clipper, url, left, top, width, height);
       return;
     }
-    
+
     clipper.getStyle().setPropertyPx("width", width);
     clipper.getStyle().setPropertyPx("height", height);
 
     // Update the nested image's url.
     Element img = clipper.getFirstChildElement();
     img.getStyle().setProperty("filter",
-        "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url
+        "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + url.asString()
             + "',sizingMethod='crop')");
     img.getStyle().setPropertyPx("marginLeft", -left);
     img.getStyle().setPropertyPx("marginTop", -top);
@@ -75,12 +77,11 @@
   }
 
   @Override
-  public Element createStructure(String url, int left, int top, int width,
-      int height) {
+  public Element createStructure(SafeUri url, int left, int top, int width, int height) {
     if (!isIE6()) {
       return super.createStructure(url, left, top, width, height);
     }
-    
+
     // We need to explicitly sink ONLOAD on the child image element, because it
     // can't be fired on the clipper.
     Element clipper = super.createStructure(url, left, top, width, height);
@@ -90,16 +91,24 @@
   }
 
   @Override
-  public String getHTML(String url, int left, int top, int width, int height) {
+  public Element getImgElement(Image image) {
     if (!isIE6()) {
-      return super.getHTML(url, left, top, width, height);
+      return super.getImgElement(image);
     }
-    
+    return image.getElement().getFirstChildElement();
+  }
+
+  @Override
+  public SafeHtml getSafeHtml(SafeUri url, int left, int top, int width, int height) {
+    if (!isIE6()) {
+      return super.getSafeHtml(url, left, top, width, height);
+    }
+    // We cannot use a SafeHtmlTemplates because there are parameters in javascript context.
     String clipperStyle = "overflow: hidden; width: " + width + "px; height: "
         + height + "px; padding: 0px; zoom: 1";
 
     String imgStyle = "filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"
-        + url
+        + url.asString()
         + "',sizingMethod='crop'); margin-left: "
         + -left
         + "px; margin-top: " + -top + "px; border: none";
@@ -121,14 +130,6 @@
         + imgStyle + "\" width=" + (left + width) + " height=" + (top + height)
         + " border='0'></gwt:clipper>";
 
-    return clippedImgHtml;
-  }
-
-  @Override
-  public Element getImgElement(Image image) {
-    if (!isIE6()) {
-      return super.getImgElement(image);
-    }
-    return image.getElement().getFirstChildElement();
+    return SafeHtmlUtils.fromTrustedString(clippedImgHtml);
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java b/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java
index 6bb5ab5..0b9b63c 100644
--- a/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java
+++ b/user/src/com/google/gwt/user/client/ui/impl/ClippedImagePrototype.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,6 +16,9 @@
 package com.google.gwt.user.client.ui.impl;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
 import com.google.gwt.user.client.ui.Image;
 
@@ -31,11 +34,10 @@
   private int height = 0;
   private int left = 0;
   private int top = 0;
-  private String url = null;
+  private SafeUri url = null;
   private int width = 0;
 
-  public ClippedImagePrototype(String url, int left, int top, int width,
-      int height) {
+  public ClippedImagePrototype(SafeUri url, int left, int top, int width, int height) {
     this.url = url;
     this.left = left;
     this.top = top;
@@ -43,6 +45,11 @@
     this.height = height;
   }
 
+  @Deprecated
+  public ClippedImagePrototype(String url, int left, int top, int width, int height) {
+    this(UriUtils.unsafeCastFromUntrustedString(url), left, top, width, height);
+  }
+
   @Override
   public void applyTo(Image image) {
     image.setUrlAndVisibleRect(url, left, top, width, height);
@@ -64,7 +71,7 @@
   }
 
   @Override
-  public String getHTML() {
-    return impl.getHTML(url, left, top, width, height);
+  public SafeHtml getSafeHtml() {
+    return impl.getSafeHtml(url, left, top, width, height);
   }
 }
diff --git a/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java b/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
new file mode 100644
index 0000000..849ac5b
--- /dev/null
+++ b/user/super/com/google/gwt/safehtml/super/com/google/gwt/safehtml/shared/SafeUriHostedModeUtils.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+import com.google.gwt.core.client.GwtScriptOnly;
+
+// This is the super-source peer of this class.
+@GwtScriptOnly
+public class SafeUriHostedModeUtils {
+
+  // Unused in super-source; only defined to avoid compiler warnings
+  public static final String FORCE_CHECK_VALID_URI = null;
+  static final String HREF_UCSCHAR = null;
+  
+  public static void maybeCheckValidUri(String uri) {
+    // This check is a noop in web mode.
+  }
+  
+  // Unused in super-source; only defined to avoid compiler warnings
+  public static void setForceCheckValidUri(boolean check) { }
+  static void setForceCheckValidUriFromProperty() { }
+}
diff --git a/user/test/com/google/gwt/resources/client/ImageResourceTest.java b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
index 07f1420..55141d6 100644
--- a/user/test/com/google/gwt/resources/client/ImageResourceTest.java
+++ b/user/test/com/google/gwt/resources/client/ImageResourceTest.java
@@ -116,7 +116,7 @@
     assertEquals(0, a.getTop());
 
     // Make sure the animated image is encoded separately
-    assertFalse(a.getURL().equals(r.i16x16().getURL()));
+    assertFalse(a.getSafeUri().equals(r.i16x16().getSafeUri()));
   }
 
   public void testDedup() {
@@ -136,7 +136,7 @@
 
     delayTestFinish(10000);
     // See if the size of the image strip is what we expect
-    Image i = new Image(a.getURL());
+    Image i = new Image(a.getSafeUri());
     i.addLoadHandler(new LoadHandler() {
       public void onLoad(LoadEvent event) {
         finishTest();
@@ -162,7 +162,7 @@
 
     // Make sure that the large, lossy image isn't bundled with the rest
     assertTrue(((ImageResourcePrototype) lossy).isLossy());
-    assertTrue(!i64.getURL().equals(lossy.getURL()));
+    assertTrue(!i64.getSafeUri().equals(lossy.getSafeUri()));
 
     assertEquals(16, r.i16x16Vertical().getWidth());
     assertEquals(16, r.i16x16Vertical().getHeight());
@@ -194,4 +194,11 @@
     assertEquals(0, b.getTop());
     assertEquals(0, b.getLeft());
   }
+  
+  @SuppressWarnings("deprecation")
+  public void testSafeUri() {
+    Resources r = GWT.create(Resources.class);
+
+    assertEquals(r.i64x64().getSafeUri().asString(), r.i64x64().getURL());
+  }
 }
diff --git a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
index 3a7e0a3..9b83055 100644
--- a/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
+++ b/user/test/com/google/gwt/safehtml/client/SafeHtmlTemplatesTest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2009 Google Inc.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -16,11 +16,13 @@
 package com.google.gwt.safehtml.client;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.safecss.shared.SafeStyles;
 import com.google.gwt.safecss.shared.SafeStylesUtils;
-import com.google.gwt.safehtml.shared.SafeHtmlUtils;
 import com.google.gwt.safehtml.shared.SafeHtml;
-import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.safehtml.shared.SafeUri;
+import com.google.gwt.safehtml.shared.UriUtils;
 
 import junit.framework.Assert;
 
@@ -31,8 +33,8 @@
 
   private static final String HTML_MARKUP = "woo <i>whee</i>";
 
-  private static final String GOOD_URL_ESCAPED =
-      "http://foo.com/foo&lt;bar&gt;&amp;baz=dootz";
+  private static final String GOOD_URL_ESCAPED = "http://foo.com/foo&lt;bar&gt;&amp;baz=dootz";
+  private static final String GOOD_URL_ENCODED = "http://foo.com/foo%3Cbar%3E&amp;baz=dootz";
   private static final String GOOD_URL = "http://foo.com/foo<bar>&baz=dootz";
   private static final String BAD_URL = "javascript:evil(1<2)";
   private static final String BAD_URL_ESCAPED = "javascript:evil(1&lt;2)";
@@ -54,11 +56,14 @@
    */
   public interface TestTemplates extends SafeHtmlTemplates {
     @Template("<span><b>{0}</b><span>{1}</span></span>")
-    SafeHtml simpleTemplate(String foo, SafeHtml bar);  
+    SafeHtml simpleTemplate(String foo, SafeHtml bar);
 
     @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>")
     SafeHtml templateWithUriAttribute(String url, SafeHtml html);
 
+    @Template("<span><a href=\"{0}\"><b>{1}</b></a></span>")
+    SafeHtml templateWithUriAttribute(SafeUri url, SafeHtml html);
+
     @Template("<div id=\"{0}\">{1}</div>")
     SafeHtml templateWithRegularAttribute(String id, SafeHtml html);
 
@@ -84,8 +89,9 @@
   }
 
   public void testTemplateWithUriAttribute() {
+    // as String: sanitized by the template
     Assert.assertEquals(
-        "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP
+        "<span><a href=\"" + GOOD_URL_ENCODED + "\"><b>" + HTML_MARKUP
             + "</b></a></span>",
         templates.templateWithUriAttribute(
             GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
@@ -93,14 +99,36 @@
         "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>",
         templates.templateWithUriAttribute(
             BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    // UriUtils.fromString: sanitized by fromString
+    Assert.assertEquals(
+        "<span><a href=\"" + GOOD_URL_ENCODED + "\"><b>" + HTML_MARKUP
+            + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            UriUtils.fromString(GOOD_URL), SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    Assert.assertEquals(
+        "<span><a href=\"#\"><b>" + HTML_MARKUP + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            UriUtils.fromString(BAD_URL), SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    // UriUtils.fromTrustedString: not sanitized
+    Assert.assertEquals(
+        "<span><a href=\"" + GOOD_URL_ESCAPED + "\"><b>" + HTML_MARKUP
+            + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            UriUtils.fromTrustedString(GOOD_URL),
+            SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
+    Assert.assertEquals(
+        "<span><a href=\"" + BAD_URL_ESCAPED + "\"><b>" + HTML_MARKUP + "</b></a></span>",
+        templates.templateWithUriAttribute(
+            UriUtils.fromTrustedString(BAD_URL),
+            SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
   }
-  
+
   public void testTemplateWithRegularAttribute() {
     Assert.assertEquals(
         "<div id=\"" + GOOD_URL_ESCAPED + "\">" + HTML_MARKUP + "</div>",
         templates.templateWithRegularAttribute(
             GOOD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
-    
+
     // Values inserted into non-URI-valued attributes should not be sanitized,
     // but still escaped.
     Assert.assertEquals(
@@ -108,7 +136,7 @@
         templates.templateWithRegularAttribute(
             BAD_URL, SafeHtmlUtils.fromSafeConstant(HTML_MARKUP)).asString());
   }
-  
+
   public void testTemplateWithSafeStyleAttributeComplete() {
     Assert.assertEquals("<div style=\"width:10px;\">" + HTML_MARKUP + "</div>",
         templates.templateWithSafeStyleAttributeComplete(
@@ -124,8 +152,9 @@
   }
 
   public void testTemplateWithTwoPartUriAttribute() {
+    // sanitized by the template
     Assert.assertEquals(
-        "<span><img src=\"" + GOOD_URL_ESCAPED + "/x&amp;y\"/></span>",
+        "<span><img src=\"" + GOOD_URL_ENCODED + "/x&amp;y\"/></span>",
         templates.templateWithTwoPartUriAttribute(
             GOOD_URL, "x&y").asString());
     Assert.assertEquals(
@@ -133,7 +162,7 @@
         templates.templateWithTwoPartUriAttribute(
             BAD_URL, "x&y").asString());
   }
-  
+
   public void testTemplateWithStyleAttribute() {
     Assert.assertEquals(
         "<span style='background: purple; color: green;'></span>",
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java
new file mode 100644
index 0000000..f76c66f
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/GwtUriUtilsTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Unit tests for {@link UriUtils}.
+ */
+public class GwtUriUtilsTest extends GWTTestCase {
+
+  private static final String JAVASCRIPT_URL = "javascript:alert('BOOM!');";
+  private static final String MAILTO_URL = "mailto:foo@example.com";
+  private static final String CONSTANT_URL =
+    "http://gwt.google.com/samples/Showcase/Showcase.html?locale=fr#!CwCheckBox";
+  private static final String EMPTY_GIF_DATA_URL =
+    "data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==";
+
+  public void testEncode_noEscape() {
+    StringBuilder sb = new StringBuilder(UriUtils.DONT_NEED_ENCODING);
+    final int upcaseOffset = 'A' - 'a';
+    for (char c = 'a'; c <= 'z'; c++) {
+      sb.append(c).append((char) (c + upcaseOffset));
+    }
+    for (char c = '0'; c <= '9'; c++) {
+      sb.append(c);
+    }
+    final String expected = sb.toString();
+
+    assertEquals(expected, UriUtils.encode(expected));
+  }
+
+  public void testEncode_percent() {
+    assertEquals("foo%25bar", UriUtils.encode("foo%bar"));
+  }
+
+  public void testEncode_percentAndOthers() {
+    assertEquals("fo%20o%25b%0Aa%22r", UriUtils.encode("fo o%b\na\"r"));
+  }
+
+  public void testEncode_withEscapes1() {
+    assertEquals("foo%bar", UriUtils.encodeAllowEscapes("foo%bar"));
+  }
+
+  public void testEncode_withEscapes2() {
+    assertEquals("foo%25bar", UriUtils.encodeAllowEscapes("foo%25bar"));
+  }
+
+  public void testEncode_withEscapes3() {
+    assertEquals("foo%E2%82%ACbar", UriUtils.encodeAllowEscapes("foo\u20ACbar"));
+  }
+
+  public void testEncode_withEscapes4() {
+    assertEquals("foo%E2%82%ACbar", UriUtils.encodeAllowEscapes("foo%E2%82%ACbar"));
+  }
+
+  public void testEncode_withEscapesIncompleteEscapes() {
+    assertEquals("foob%25ar%25a", UriUtils.encodeAllowEscapes("foob%ar%a"));
+  }
+
+  public void testEncode_withEscapesInvalidEscapes() {
+    assertEquals("f%25ooba%25r", UriUtils.encodeAllowEscapes("f%ooba%r"));
+  }
+
+  public void testFromTrustedString() {
+    assertEquals(CONSTANT_URL, UriUtils.fromTrustedString(CONSTANT_URL).asString());
+    assertEquals(MAILTO_URL, UriUtils.fromTrustedString(MAILTO_URL).asString());
+    assertEquals(EMPTY_GIF_DATA_URL, UriUtils.fromTrustedString(EMPTY_GIF_DATA_URL).asString());
+    assertEquals(JAVASCRIPT_URL, UriUtils.fromTrustedString(JAVASCRIPT_URL).asString());
+    if (GWT.isClient()) {
+      assertEquals(GWT.getModuleBaseURL(),
+          UriUtils.fromTrustedString(GWT.getModuleBaseURL()).asString());
+      assertEquals(GWT.getHostPageBaseURL(),
+          UriUtils.fromTrustedString(GWT.getHostPageBaseURL()).asString());
+    }
+  }
+
+  public void testFromTrustedString_withInvalidUrl() {
+    if (GWT.isProdMode()) {
+      // fromTrustedString does not parse/validate its argument in prod mode.
+      // Hence we short-circuit this test in prod mode.
+      return;
+    }
+    try {
+      SafeUri u = UriUtils.fromTrustedString("a\uD800b");
+      fail("Should have thrown IllegalArgumentException");
+    } catch (IllegalArgumentException e) {
+      // expected
+    }
+  }
+
+  @SuppressWarnings("deprecation")
+  public void testUnsafeCastFromUntrustedString() {
+    assertEquals(CONSTANT_URL, UriUtils.unsafeCastFromUntrustedString(CONSTANT_URL).asString());
+    assertEquals(MAILTO_URL, UriUtils.unsafeCastFromUntrustedString(MAILTO_URL).asString());
+    assertEquals(EMPTY_GIF_DATA_URL, UriUtils.unsafeCastFromUntrustedString(EMPTY_GIF_DATA_URL)
+        .asString());
+    assertEquals(JAVASCRIPT_URL, UriUtils.unsafeCastFromUntrustedString(JAVASCRIPT_URL).asString());
+    assertEquals("a\uD800b", UriUtils.unsafeCastFromUntrustedString("a\uD800b").asString());
+    if (GWT.isClient()) {
+      assertEquals(GWT.getModuleBaseURL(), UriUtils.unsafeCastFromUntrustedString(
+          GWT.getModuleBaseURL()).asString());
+      assertEquals(GWT.getHostPageBaseURL(), UriUtils.unsafeCastFromUntrustedString(
+          GWT.getHostPageBaseURL()).asString());
+    }
+  }
+
+  public void testFromString() {
+    assertEquals(CONSTANT_URL, UriUtils.fromString(CONSTANT_URL).asString());
+    assertEquals(MAILTO_URL, UriUtils.fromString(MAILTO_URL).asString());
+    assertEquals(UriUtils.sanitizeUri(EMPTY_GIF_DATA_URL),
+        UriUtils.fromString(EMPTY_GIF_DATA_URL).asString());
+    assertEquals(UriUtils.sanitizeUri(JAVASCRIPT_URL),
+        UriUtils.fromString(JAVASCRIPT_URL).asString());
+    if (GWT.isClient()) {
+      assertEquals(GWT.getModuleBaseURL(),
+          UriUtils.fromString(GWT.getModuleBaseURL()).asString());
+      assertEquals(GWT.getHostPageBaseURL(),
+          UriUtils.fromString(GWT.getHostPageBaseURL()).asString());
+    }
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.safehtml.SafeHtmlTestsModule";
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java
new file mode 100644
index 0000000..71d3746
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/UriUtilsTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * 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.safehtml.shared;
+
+/**
+ * Unit tests for {@link UriUtils}.
+ */
+public class UriUtilsTest extends GwtUriUtilsTest {
+
+  // This forces a GWTTestCase to run as a vanilla JUnit TestCase.
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    super.gwtSetUp();
+    // Since we can't assume assertions are enabled, force
+    // UriUtilsImpl#maybeCheckValidUri to perform its check
+    // when running in JRE.
+    SafeUriHostedModeUtils.setForceCheckValidUri(true);
+  }
+}
diff --git a/user/test/com/google/gwt/user/client/ui/ImageTest.java b/user/test/com/google/gwt/user/client/ui/ImageTest.java
index 0827501..5fa9bf1 100644
--- a/user/test/com/google/gwt/user/client/ui/ImageTest.java
+++ b/user/test/com/google/gwt/user/client/ui/ImageTest.java
@@ -782,7 +782,7 @@
   }
 
   private void assertResourceWorked(Image image, ImageResource prettyPiccy) {
-    assertEquals(prettyPiccy.getURL(), image.getUrl());
+    assertEquals(prettyPiccy.getSafeUri().asString(), image.getUrl());
     assertEquals(prettyPiccy.getTop(), image.getOriginTop());
     assertEquals(prettyPiccy.getHeight(), image.getHeight());
     assertEquals(prettyPiccy.getLeft(), image.getOriginLeft());
diff --git a/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java b/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java
index c9c54ba..15cadeb 100644
--- a/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java
+++ b/user/test/com/google/gwt/user/client/ui/impl/ClippedImagePrototypeTest.java
@@ -1,12 +1,12 @@
 /*
  * Copyright 2007 Google Inc.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -22,12 +22,11 @@
 import com.google.gwt.junit.DoNotRunWith;
 import com.google.gwt.junit.Platform;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.safehtml.shared.UriUtils;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ImageTest;
-import com.google.gwt.user.client.ui.LoadListener;
 import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.Widget;
 
 /**
  * Tests for the ClippedImagePrototype implementation. Tests are done to ensure
@@ -38,28 +37,6 @@
  * application of the prototype to the image.
  */
 public class ClippedImagePrototypeTest extends GWTTestCase {
-  @Deprecated
-  private static class TestLoadListener implements LoadListener {
-    private int onloadEventFireCount = 0;
-    private Image image;
-
-    public TestLoadListener(Image image) {
-      this.image = image;
-    }
-
-    public void onError(Widget sender) {
-      fail("The image " + image.getUrl() + " failed to load.");
-    }
-
-    public int getOnloadEventFireCount() {
-      return onloadEventFireCount;
-    }
-
-    public void onLoad(Widget sender) {
-      onloadEventFireCount++;
-    }
-  }
-  
   private static class TestLoadHandler implements LoadHandler {
     private int onloadEventFireCount = 0;
 
@@ -71,18 +48,18 @@
       onloadEventFireCount++;
     }
   }
-  
+
   @Override
   public String getModuleName() {
     return "com.google.gwt.user.UserTest";
   }
- 
+
   /**
    * Tests that a clipped image can be transformed to match a given prototype.
    * Also checks to make sure that a load event is fired on when
    * {@link com.google.gwt.user.client.ui.impl.ClippedImagePrototype#applyTo(com.google.gwt.user.client.ui.Image)}
    * is called.
-   * 
+   *
    * TODO(jlabanca): Enable this test when issue 863 is fixed
    */
   @DoNotRunWith({Platform.HtmlUnitBug})
@@ -102,7 +79,7 @@
 
         if (image.getOriginLeft() == 12 && image.getOriginTop() == 13) {
           ClippedImagePrototype clippedImagePrototype = new ClippedImagePrototype(
-              "counting-forwards.png", 16, 16, 16, 16);
+              UriUtils.fromString("counting-forwards.png"), 16, 16, 16, 16);
 
           clippedImagePrototype.applyTo(image);
 
@@ -141,43 +118,43 @@
    * <code>applyTo(Image)</code> is called.
    */
   /*
-   * This test has been commented out because of issue #863 
-   * 
-   * public void testApplyToUnclippedImage() { 
+   * This test has been commented out because of issue #863
+   *
+   * public void testApplyToUnclippedImage() {
    * final Image image = new Image("counting-backwards.png");
-   * 
+   *
    * assertEquals(0, image.getOriginLeft()); assertEquals(0,
    * image.getOriginTop()); assertEquals("unclipped",
    * ImageTest.getCurrentImageStateName(image));
-   * 
+   *
    * final ArrayList onloadEventFireCounter = new ArrayList();
-   * 
+   *
    * image.addLoadListener(new LoadListener() { public void onError(Widget
    * sender) { fail("The image " + ((Image) sender).getUrl() + " failed to
    * load."); }
-   * 
+   *
    * public void onLoad(Widget sender) { onloadEventFireCounter.add(new
    * Object());
-   * 
+   *
    * if (ImageTest.getCurrentImageStateName(image).equals("unclipped")) {
-   * 
+   *
    * assertEquals(32, image.getWidth()); assertEquals(32, image.getHeight());
-   * 
+   *
    * ClippedImagePrototype clippedImagePrototype = new
    * ClippedImagePrototype("counting-forwards.png", 16, 16, 16, 16);
-   * 
+   *
    * clippedImagePrototype.adjust(image);
-   * 
+   *
    * assertEquals(16, image.getOriginLeft()); assertEquals(16,
    * image.getOriginTop()); assertEquals(16, image.getWidth()); assertEquals(16,
    * image.getHeight()); assertEquals("counting-forwards.png", image.getUrl());
    * assertEquals("clipped", ImageTest.getCurrentImageStateName(image)); } } });
-   * 
+   *
    * RootPanel.get().add(image); delayTestFinish(2000);
-   * 
+   *
    * Timer t = new Timer() { public void run() { assertEquals(2,
    * onloadEventFireCounter.size()); finishTest(); } };
-   * 
+   *
    * t.schedule(1000); }
    */
 
@@ -187,7 +164,7 @@
    */
   public void testGenerateNewImage() {
     ClippedImagePrototype clippedImagePrototype = new ClippedImagePrototype(
-        "counting-forwards.png", 16, 16, 16, 16);
+        UriUtils.fromString("counting-forwards.png"), 16, 16, 16, 16);
 
     Image image = clippedImagePrototype.createImage();