Added all safehtml packages.
Added new safehtml test suites.
Several changes for migration:
* Split EscapeUtils into SafeHtmlUtils and UriUtils
* Added .equals() and .hashCode() to SafeHtmlString
* Updated comments, formatted code
* Removed i18n references
* Removed templates
* Removed .fromPlainText() functionality
* Removed HtmlRenderer functionality
* Added check in SafeHtmlUtils.htmlEscape() to check if a string needs to escaped, benchmarked this test.

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8621 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
new file mode 100644
index 0000000..9721d70
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/SafeHtml.gwt.xml
@@ -0,0 +1,23 @@
+<!--
+  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
+  License for the specific language governing permissions and limitations under
+  the License.
+-->
+
+<!-- SafeHtml - facilities for avoiding XSS attacks                        -->
+<!--                                                                       -->
+<module>
+  <inherits name='com.google.gwt.user.User'/>
+  <source path="client"/>
+  <source path="shared"/>
+</module>
diff --git a/user/src/com/google/gwt/safehtml/client/HasSafeHtml.java b/user/src/com/google/gwt/safehtml/client/HasSafeHtml.java
new file mode 100644
index 0000000..f9854d5
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/client/HasSafeHtml.java
@@ -0,0 +1,43 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.google.gwt.safehtml.client;
+
+import com.google.gwt.safehtml.shared.SafeHtml;
+
+/**
+ * An object that implements this interface contains text with HTML markup,
+ * which can be set with the Cross-Site-Scripting-safe HTML markup encapsulated
+ * in a {@link com.google.gwt.safehtml.shared.SafeHtml} object.
+ */
+public interface HasSafeHtml {
+
+  /**
+   * Sets this object's contents via known-safe HTML.
+   *
+   * <p>
+   * The object will behave exactly the same as when a widget's
+   * {@link com.google.gwt.user.client.ui.HasHTML#setHTML(String)} method is
+   * invoked; however the {@link SafeHtml} passed to this method observes the
+   * contract that it can be used in an HTML context without causing unsafe
+   * script execution. Thus, unlike
+   * {@link com.google.gwt.user.client.ui.HasHTML#setHTML(String)}, using this
+   * method cannot result in Cross-Site Scripting security vulnerabilities.
+   *
+   * @param html the object's new HTML, represented as a {@link SafeHtml} object
+   */
+  void setSafeHtml(SafeHtml html);
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/HtmlSanitizer.java b/user/src/com/google/gwt/safehtml/shared/HtmlSanitizer.java
new file mode 100644
index 0000000..14b4990
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/HtmlSanitizer.java
@@ -0,0 +1,36 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+/**
+ * Sanitizes untrusted HTML.
+ * 
+ * Note on usage: SafeHtml should be used to ensure user input is not executed 
+ * in the browser. SafeHtml should not be used to sanitize input before sending 
+ * it to the server.
+ * 
+ */
+public interface HtmlSanitizer {
+
+  /**
+   * Sanitizes a string into {@code SafeHtml}.
+   *
+   * @param html String containing untrusted HTML.
+   * @return Contents of {@code html}, sanitized according to the
+   *     policy implemented by this sanitizer.
+   */
+  SafeHtml sanitize(String html);
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.java b/user/src/com/google/gwt/safehtml/shared/OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.java
new file mode 100644
index 0000000..301a3c2
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml.java
@@ -0,0 +1,60 @@
+/*
+ * 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
+ * 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 SafeHtml}.
+ * 
+ * <p>
+ * This class is intended only for use in generated code where the code 
+ * generator guarantees that instances of this type will adhere to the 
+ * {@link SafeHtml} contract (hence the purposely unwieldy class name).
+ */
+public class OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml
+    implements SafeHtml {
+  private String html;
+
+  public OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml(String html) {
+    if (html == null) {
+      throw new NullPointerException("html is null");
+    }
+    this.html = html;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String asString() {
+    return html;
+  }
+  
+  /**
+   * Compares this string to the specified object.
+   */
+  public boolean equals(Object obj) {
+    if (!(obj instanceof SafeHtml)) {
+      return false;
+    }
+    return html.equals(((SafeHtml) obj).asString());
+  }
+
+  /**
+   * Returns a hash code for this string.
+   */
+  public int hashCode() {
+    return html.hashCode();
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtml.java b/user/src/com/google/gwt/safehtml/shared/SafeHtml.java
new file mode 100644
index 0000000..145f3a3
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtml.java
@@ -0,0 +1,82 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import java.io.Serializable;
+
+/**
+ * An object that implements this interface encapsulates HTML that is guaranteed
+ * to be safe to use (with respect to potential Cross-Site-Scripting
+ * vulnerabilities) in an HTML context.
+ * 
+ * Note on usage: SafeHtml should be used to ensure user input is not executed 
+ * in the browser. SafeHtml should not be used to sanitize input before sending 
+ * it to the server.
+ *
+ * <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 the {@code
+ * .innerHTML} DOM property in a browser (or to use similarly in an "inner HTML"
+ * context), in the sense that doing so must not cause execution of script in
+ * the browser.
+ *
+ * All implementations must implement equals() and hashCode() to behave
+ * consistently with the result of asString().equals() and asString.hashCode().
+ * 
+ * The internal string must not be null.
+ *
+ * <p>
+ * Implementations of this interface must not implement
+ * {@link com.google.gwt.user.client.rpc.IsSerializable}, since deserialization
+ * can result in violation of the class invariant.
+ */
+public interface SafeHtml extends Serializable {
+  /*
+   * Notes regarding serialization: - It may be reasonable to allow
+   * deserialization on the client of objects serialized on the server (i.e. RPC
+   * responses), based on the assumption that server code is trusted and would
+   * not provide a malicious serialized form (if a MitM were able to modify
+   * server responses, the client would be fully compromised in any case).
+   * However, the GWT RPC framework currently does not seem to provide a
+   * facility for restricting deserialization on the Server only (thought this
+   * shouldn't be difficult to implement through a custom SerializationPolicy)
+   *
+   * - Some implementations of SafeHtml would in principle be able to enforce
+   * their class invariant on deserialization (e.g., SimpleHtmlSanitizer could
+   * apply HTML sanitization on deserialization). However, the GWT RPC framework
+   * does not provide for an equivalent of readResolve() to enforce the class
+   * invariant on deserialization.
+   */
+
+  /**
+   * Returns this object's contained HTML as a string. Based on this class'
+   * contract, the returned string will be safe to use in an HTML context.
+   */
+  String asString();
+
+  /**
+   * Compares this string to the specified object.
+   * Must be equal to asString().equals()
+   */
+  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/SafeHtmlBuilder.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java
new file mode 100644
index 0000000..f2f57dc
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlBuilder.java
@@ -0,0 +1,207 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+/**
+ * A builder that facilitates the building up of XSS-safe HTML from text
+ * snippets. It is used essentially like a {@link StringBuilder}; unlike a
+ * {@link StringBuilder}, it automatically HTML-escapes appended input where
+ * necessary.
+ *
+ * <p>
+ * In addition, it supports methods that allow strings with HTML markup to be
+ * appended without escaping: One can append other {@link SafeHtml} objects, and
+ * one can append constant strings. The method that appends constant strings (
+ * {@link #appendHtmlConstant(String)}) requires a convention of use to be 
+ * adhered to in order for this class to adhere to the contract required by 
+ * {@link SafeHtml}: The argument expression must be fully determined and known
+ * to be safe at compile time, and the value of the argument must not contain 
+ * incomplete HTML tags. See {@link #appendHtmlConstant(String)} for details.
+ *
+ * <p>
+ * The accumulated XSS-safe HTML can be obtained in the form of a
+ * {@link SafeHtml} via the {@link #toSafeHtml()} method.
+ *
+ * <p>
+ * This class is not thread-safe.
+ */
+public final class SafeHtmlBuilder {
+
+  private StringBuilder sb = new StringBuilder();
+
+  /**
+   * Constructs an empty SafeHtmlBuilder.
+   */
+  public SafeHtmlBuilder() {
+  }
+
+  /*
+   * Boolean and numeric types converted to String are always HTML safe -- no
+   * escaping necessary.
+   */
+  
+  /**
+   * Appends the string representation of a boolean.
+   *
+   * @param b the boolean whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(boolean b) {
+    sb.append(b);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a number.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(byte num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a char.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(char num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a number.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(double num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a number.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(float num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a number.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(int num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the string representation of a number.
+   *
+   * @param num the number whose string representation to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(long num) {
+    sb.append(num);
+    return this;
+  }
+
+  /**
+   * Appends the contents of another {@link SafeHtml} object, without applying
+   * HTML-escaping to it.
+   *
+   * @param html the {@link SafeHtml} to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder append(SafeHtml html) {
+    sb.append(html.asString());
+    return this;
+  }
+
+  /**
+   * Appends a string after HTML-escaping it.
+   *
+   * @param text the string to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder appendEscaped(String text) {
+    sb.append(SafeHtmlUtils.htmlEscape(text));
+    return this;
+  }
+
+  /**
+   * Appends a string consisting of several newline-separated lines after
+   * HTML-escaping it. Newlines in the original string are converted to {@code 
+   * <br>}.
+   *
+   * @param text the string to append
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder appendEscapedLines(String text) {
+    sb.append(SafeHtmlUtils.htmlEscape(text).replaceAll("\n", "<br>"));
+    return this;
+  }
+
+  /**
+   * Appends a compile-time-constant string, which will <em>not</em> be escaped.
+   *
+   * <p>
+   * <b>Important</b>: For this class to be able to honor its contract as
+   * required by {@link SafeHtml}, all uses of this method must satisfy the
+   * following requirements:
+   *
+   * <ul>
+   *
+   * <li>The argument expression must be fully determined and known to be safe
+   * at compile time.
+   *
+   * <li>The value of the argument must not contain incomplete HTML tags. I.e.,
+   * the following is not a correct use of this method, because the {@code <a>}
+   * tag is incomplete:
+   *
+   * <pre class="code">
+   * {@code shb.appendConstantHtml("<a href='").append(url)}</pre>
+   *
+   * </ul>
+   *
+   * @param html the HTML snippet to be appended
+   * @return a reference to this object
+   */
+  public SafeHtmlBuilder appendHtmlConstant(String html) {
+    // TODO(xtof): (hosted-mode only) assert that html satisfies the second
+    // constraint.
+    sb.append(html);
+    return this;
+  }
+
+  /**
+   * Returns the safe HTML accumulated in the builder as a {@link SafeHtml}.
+   */
+  public SafeHtml toSafeHtml() {
+    return new SafeHtmlString(sb.toString());
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlString.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlString.java
new file mode 100644
index 0000000..2d7762a
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlString.java
@@ -0,0 +1,76 @@
+/*
+ * 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
+ * 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 SafeHtml}.
+ *
+ * <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 SafeHtmlString implements SafeHtml {
+  private String html;
+
+  /**
+   * Constructs a {@link SafeHtmlString} 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 SafeHtml}
+   * interface.
+   *
+   * @param html the string to be wrapped as a {@link SafeHtml}
+   */
+  SafeHtmlString(String html) {
+    if (html == null) {
+      throw new NullPointerException("html is null");
+    }
+    this.html = html;
+  }
+
+  /**
+   * No-arg constructor for compatibility with GWT serialization.
+   */
+  @SuppressWarnings("unused")
+  private SafeHtmlString() {
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public String asString() {
+    return html;
+  }
+
+  /**
+   * Compares this string to the specified object.
+   */
+  public boolean equals(Object obj) {
+    if (!(obj instanceof SafeHtml)) {
+      return false;
+    }
+    return html.equals(((SafeHtml) obj).asString());
+  }
+
+  /**
+   * Returns a hash code for this string.
+   */
+  public int hashCode() {
+    return html.hashCode();
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
new file mode 100644
index 0000000..0502c7e
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SafeHtmlUtils.java
@@ -0,0 +1,151 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.regexp.shared.SplitResult;
+
+/**
+ * Utility class containing static methods for escaping and sanitizing strings.
+ */
+public final class SafeHtmlUtils {
+
+  private static final String HTML_ENTITY_REGEX =
+      "[a-z]+|#[0-9]+|#x[0-9a-fA-F]+";
+  
+  public static final SafeHtml EMPTY_SAFE_HTML = new SafeHtmlString("");
+  
+  private static final RegExp AMP_RE = RegExp.compile("&", "g");
+  private static final RegExp GT_RE = RegExp.compile(">", "g");
+  private static final RegExp LT_RE = RegExp.compile("<", "g");
+  private static final RegExp SQUOT_RE = RegExp.compile("\'", "g");
+  private static final RegExp QUOT_RE = RegExp.compile("\"", "g");
+
+  /**
+   * Returns a SafeHtml constructed from a safe string, i.e. without escaping
+   * the string.
+   *
+   * <p>
+   * <b>Important</b>: For this class to be able to honor its contract as
+   * required by {@link SafeHtml}, all uses of this method must satisfy the
+   * following requirements:
+   *
+   * <ul>
+   *
+   * <li>The argument expression must be fully determined and known to be safe
+   * at compile time.
+   *
+   * <li>The value of the argument must not contain incomplete HTML tags.
+   *
+   * </ul>
+   */
+  public static SafeHtml fromSafeConstant(String s) {
+    // TODO(pdr): (hosted-mode only) assert that html satisfies the second
+    // constraint.
+    return new SafeHtmlString(s);
+  }
+
+  /**
+   * Returns a SafeHtml containing the escaped string.
+   */
+  public static SafeHtml fromString(String s) {
+    return new SafeHtmlString(htmlEscape(s));
+  }
+
+  /**
+   * HTML-escapes a string.
+   *
+   *  Note: The following variants of this function were profiled on FF36,
+   * Chrome6, IE8:
+   * #1) for each case, check indexOf, then use s.replace(regex, string)
+   * #2) for each case, check indexOf, then use s.replaceAll()
+   * #3) check if any metachar is present using a regex, then use #1
+   * #4) for each case, use s.replace(regex, string)
+   * 
+   * #1 was found to be the fastest, and is used below.
+   *
+   * @param s the string to be escaped
+   * @return the input string, with all occurrences of HTML meta-characters
+   *         replaced with their corresponding HTML Entity References
+   */
+  public static String htmlEscape(String s) {
+    if (s.indexOf("&") != -1) {
+      s = AMP_RE.replace(s, "&amp;");
+    }
+    if (s.indexOf("<") != -1) {
+      s = LT_RE.replace(s, "&lt;");
+    }
+    if (s.indexOf(">") != -1) {
+      s = GT_RE.replace(s, "&gt;");
+    }
+    if (s.indexOf("\"") != -1) {
+      s = QUOT_RE.replace(s, "&quot;");
+    }
+    if (s.indexOf("\'") != -1) {
+      s = SQUOT_RE.replace(s, "&#39;");
+    }
+    return s;
+  }
+
+  /**
+   * HTML-escapes a string, but does not double-escape HTML-entities already
+   * present in the string.
+   *
+   * @param text the string to be escaped
+   * @return the input string, with all occurrences of HTML meta-characters
+   *         replaced with their corresponding HTML Entity References, with the
+   *         exception that ampersand characters are not double-escaped if they
+   *         form the start of an HTML Entity Reference
+   */
+  public static String htmlEscapeAllowEntities(String text) {
+    StringBuilder escaped = new StringBuilder();
+
+    SplitResult splitSegment = AMP_RE.split(text, -1);
+    for (int i = 0, len = splitSegment.length(); i < len; i++) {
+      String segment = splitSegment.get(i);
+      if (i == 0) {
+        /*
+         * The first segment is never part of an entity reference, so we always
+         * escape it.
+         * Note that if the input starts with an ampersand, we will get an empty
+         * segment before that.
+         */
+        escaped.append(htmlEscape(segment));
+        continue;
+      }
+
+      int entityEnd = segment.indexOf(';');
+      if (entityEnd > 0
+          && segment.substring(0, entityEnd).matches(HTML_ENTITY_REGEX)) {
+        // Append the entity without escaping.
+        escaped.append("&").append(segment.substring(0, entityEnd + 1));
+
+        // Append the rest of the segment, escaped.
+        escaped.append(htmlEscape(segment.substring(entityEnd + 1)));
+      } else {
+        // The segment did not start with an entity reference, so escape the
+        // whole segment.
+        escaped.append("&amp;").append(htmlEscape(segment));
+      }
+    }
+
+    return escaped.toString();
+  }
+
+  // prevent instantiation
+  private SafeHtmlUtils() {
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/SimpleHtmlSanitizer.java b/user/src/com/google/gwt/safehtml/shared/SimpleHtmlSanitizer.java
new file mode 100644
index 0000000..4f761b1
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/SimpleHtmlSanitizer.java
@@ -0,0 +1,153 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.regexp.shared.SplitResult;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A simple and relatively inexpensive HTML sanitizer.
+ *
+ * <p>
+ * This sanitizer accepts the subset of HTML consisting of attribute-free tags
+ * in a whitelist (including {@code <b>, <em>, <i>}, etc; for the exact list
+ * consult the source), as well as numeric HTML entities and HTML entity
+ * references. Any HTML metacharacters that do not appear as part of markup in
+ * this subset will be HTML-esacped.
+ */
+public final class SimpleHtmlSanitizer implements HtmlSanitizer {
+
+  private static final SimpleHtmlSanitizer INSTANCE = new SimpleHtmlSanitizer();
+
+  private static final Set<String> TAG_WHITELIST = new HashSet<String>(
+      Arrays.asList("b", "em", "i", "h1", "h2", "h3", "h4", "h5", "h6", "hr",
+          "ul", "ol", "li"));
+  
+  private static final RegExp LT_RE = RegExp.compile("<", "g");
+
+  public static SimpleHtmlSanitizer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * HTML-sanitizes a string.
+   *
+   * <p>
+   * The input string is processed as described above. The result of sanitizing
+   * the string is guaranteed to be safe to use (with respect to XSS
+   * vulnerabilities) in HTML contexts, and is returned as an instance of the
+   * {@link SafeHtml} type.
+   */
+  public static SafeHtml sanitizeHtml(String html) {
+    if (html == null) {
+      throw new NullPointerException("html is null");
+    }
+    return new SafeHtmlString(simpleSanitize(html));
+  }
+
+  /*
+   * Sanitize a string containing simple HTML markup as defined above. The
+   * approach is as follows: We split the string at each occurence of '<'. Each
+   * segment thus obtained is inspected to determine if the leading '<' was
+   * indeed the start of a whitelisted tag or not. If so, the tag is emitted
+   * unescaped, and the remainder of the segment (which cannot contain any
+   * additional tags) is emitted in escaped form. Otherwise, the entire segment
+   * is emitted in escaped form.
+   *
+   * In either case, EscapeUtils.htmlEscapeAllowEntities is used to escape,
+   * which escapes HTML but does not double escape existing syntactially valid
+   * HTML entities.
+   */
+  // TODO(xtof): should this be in a utils class?
+  private static String simpleSanitize(String text) {
+    StringBuilder sanitized = new StringBuilder();
+
+    SplitResult splitSegment = LT_RE.split(text, -1);
+    for (int i = 0, len = splitSegment.length(); i < len; i++) {
+      String segment = splitSegment.get(i);
+      if (i == 0) {
+        /*
+         *  the first segment is never part of a valid tag; note that if the
+         *  input string starts with a tag, we will get an empty segment at the 
+         *  beginning.
+         */
+        sanitized.append(SafeHtmlUtils.htmlEscapeAllowEntities(segment));
+        continue;
+      }
+
+      /*
+       *  determine if the current segment is the start of an attribute-free tag
+       *  or end-tag in our whitelist
+       */
+      int tagStart = 0; // will be 1 if this turns out to be an end tag.
+      int tagEnd = segment.indexOf('>');
+      String tag = null;
+      boolean isValidTag = false;
+      if (tagEnd > 0) {
+        if (segment.charAt(0) == '/') {
+          tagStart = 1;
+        }
+        tag = segment.substring(tagStart, tagEnd);
+        if (TAG_WHITELIST.contains(tag)) {
+          isValidTag = true;
+        }
+      }
+
+      if (isValidTag) {
+        // append the tag, not escaping it
+        if (tagStart == 0) {
+          sanitized.append('<');
+        } else {
+          // we had seen an end-tag
+          sanitized.append("</");
+        }
+        sanitized.append(tag).append('>');
+
+        // append the rest of the segment, escaping it
+        sanitized.append(SafeHtmlUtils.htmlEscapeAllowEntities(
+            segment.substring(tagEnd + 1)));
+      } else {
+        // just escape the whole segment
+        sanitized.append("&lt;").append(
+            SafeHtmlUtils.htmlEscapeAllowEntities(segment));
+      }
+    }
+    return sanitized.toString();
+  }
+
+  /*
+   * Note: We purposely do not provide a method to create a SafeHtml from
+   * another (arbitrary) SafeHtml via sanitization, as this would permit the
+   * construction of SafeHtml objects that are not stable in the sense that for
+   * a {@code SafeHtml s} it may not be true that {@code s.asString()} equals
+   * {@code SimpleHtmlSanitizer.sanitizeHtml(s.asString()).asString()}. While
+   * this is not currently an issue, it might become one and result in 
+   * unexpected behavior if this class were to become serializable and enforce 
+   * its class invariant upon deserialization.
+   */
+
+  // prevent external instantiation
+  private SimpleHtmlSanitizer() {
+  }
+
+  public SafeHtml sanitize(String html) {
+    return sanitizeHtml(html);
+  }
+}
diff --git a/user/src/com/google/gwt/safehtml/shared/UriUtils.java b/user/src/com/google/gwt/safehtml/shared/UriUtils.java
new file mode 100644
index 0000000..f477d04
--- /dev/null
+++ b/user/src/com/google/gwt/safehtml/shared/UriUtils.java
@@ -0,0 +1,105 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+/**
+ * Utility class containing static methods for validating and sanitizing URIs.
+ */
+public final class UriUtils {
+  
+  /**
+   * 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
+   */
+  public static String extractScheme(String uri) {
+    int colonPos = uri.indexOf(':');
+    if (colonPos < 0) {
+      return null;
+    }
+    String scheme = uri.substring(0, colonPos);
+    if (scheme.indexOf('/') >= 0 || scheme.indexOf('#') >= 0) {
+      /*
+       *  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.
+       */
+      return null;
+    }
+    return scheme;
+  }
+  
+  /**
+   * 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
+   */
+  public static boolean isSafeUri(String uri) {
+    String scheme = extractScheme(uri);
+    if (scheme == null) {
+      return true;
+    }
+    /*
+     * Special care is be taken with case-insensitive 'i' in the Turkish locale.
+     * i -> to upper in Turkish locale -> İ
+     * I -> to lower in Turkish locale -> ı
+     * For this reason there are two checks for mailto: "mailto" and "MAILTO"
+     * For details, see: http://www.i18nguy.com/unicode/turkish-i18n.html
+     */
+    String schemeLc = scheme.toLowerCase();
+    return ("http".equals(schemeLc)
+        || "https".equals(schemeLc)
+        || "ftp".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.
+   */
+  public static String sanitizeUri(String uri) {
+    if (isSafeUri(uri)) {
+      return uri;
+    } else {
+      return "#";
+    }
+  }
+
+  // prevent instantiation
+  private UriUtils() {
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
new file mode 100644
index 0000000..1b656e3
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/SafeHtmlGwtSuite.java
@@ -0,0 +1,40 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.safehtml.shared.GwtSafeHtmlUtilsTest;
+import com.google.gwt.safehtml.shared.SafeHtmlStringTest;
+
+import junit.framework.Test;
+
+/**
+ * Test suite for SafeHtml GWTTestCases.
+ */
+public class SafeHtmlGwtSuite {
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite(
+        "Test suite for SafeHtml GWTTestCases");
+    
+    suite.addTestSuite(GwtSafeHtmlUtilsTest.class);
+    suite.addTestSuite(SafeHtmlStringTest.class);
+
+    return suite;
+  }
+  
+  private SafeHtmlGwtSuite() {
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
new file mode 100644
index 0000000..3bf970e
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/SafeHtmlJreSuite.java
@@ -0,0 +1,46 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml;
+
+import com.google.gwt.safehtml.server.UriUtilsTest;
+import com.google.gwt.safehtml.shared.SafeHtmlUtilsTest;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilderTest;
+import com.google.gwt.safehtml.shared.SafeHtmlStringTest;
+import com.google.gwt.safehtml.shared.SimpleHtmlSanitizerTest;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * Test suite for SafeHtml tests that require the JRE.
+ */
+public class SafeHtmlJreSuite {
+  public static Test suite() {
+    TestSuite suite = new TestSuite(
+        "Test suite for SafeHtml tests that require the JRE");
+    
+    suite.addTestSuite(SafeHtmlUtilsTest.class);
+    suite.addTestSuite(SafeHtmlBuilderTest.class);
+    suite.addTestSuite(SimpleHtmlSanitizerTest.class);
+    suite.addTestSuite(SafeHtmlStringTest.class);
+    suite.addTestSuite(UriUtilsTest.class);
+    
+    return suite;
+  }
+  
+  private SafeHtmlJreSuite() {
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/SafeHtmlTestsModule.gwt.xml b/user/test/com/google/gwt/safehtml/SafeHtmlTestsModule.gwt.xml
new file mode 100644
index 0000000..3cb77d1
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/SafeHtmlTestsModule.gwt.xml
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- 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   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.safehtml.SafeHtml"/>
+</module>
diff --git a/user/test/com/google/gwt/safehtml/server/UriUtilsTest.java b/user/test/com/google/gwt/safehtml/server/UriUtilsTest.java
new file mode 100644
index 0000000..83e90d4
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/server/UriUtilsTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.server;
+
+import com.google.gwt.safehtml.shared.UriUtils;
+
+import junit.framework.TestCase;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Unit tests for UriUtils.
+ */
+public class UriUtilsTest extends TestCase {
+
+  /**
+   * Encapsulates a URI and relevant attributes for use in tests of
+   * {@link UriUtils#sanitizeUri(String)} and related methods.
+   */
+  private static class UriTestCaseSpec {
+    private String uri;
+    private String scheme;
+    private boolean expectUriParseException;
+    private URI parsedUri;
+
+    /**
+     * Creates a URI test case specification.
+     *
+     * @param uri the URI of this test vector
+     * @param scheme the scheme that is expected to be parsed from {@code uri}
+     *          by {@link UriUtils#extractScheme(String)}
+     * @param expectUriParseException true if parsing {@code uri} into a
+     *          {@link URI} object is expected to result in a
+     *          {@link URISyntaxException}
+     */
+    public UriTestCaseSpec(
+        String uri, String scheme, boolean expectUriParseException) {
+      this.uri = uri;
+      this.scheme = scheme;
+      this.expectUriParseException = expectUriParseException;
+      if (!expectUriParseException) {
+        try {
+          parsedUri = new URI(uri);
+        } catch (URISyntaxException e) {
+          throw new IllegalStateException(
+              "parsing \"" + uri + "\" resulted in " + "unexpected exception: "
+                  + e);
+        }
+      }
+    }
+
+    public UriTestCaseSpec(String uri, String scheme) {
+      this(uri, scheme, false);
+    }
+
+    public String getUri() {
+      return uri;
+    }
+
+    public URI getParsedUri() {
+      return parsedUri;
+    }
+
+    public String getScheme() {
+      return scheme;
+    }
+
+    public boolean getExpectUriParseException() {
+      return expectUriParseException;
+    }
+  }
+
+  private static final List<UriTestCaseSpec> GOOD_URIS;
+  static {
+    ArrayList<UriTestCaseSpec> goodUris = new ArrayList<UriTestCaseSpec>();
+
+    // URIs with no scheme.
+    goodUris.add(new UriTestCaseSpec("bar", null));
+    goodUris.add(new UriTestCaseSpec("/foo/bar", null));
+    goodUris.add(new UriTestCaseSpec("/foo/bar#baz", null));
+    goodUris.add(new UriTestCaseSpec("/foo/bar:baz", null));
+    goodUris.add(new UriTestCaseSpec("#baz", null));
+    goodUris.add(new UriTestCaseSpec("#baz:dooz", null));
+    goodUris.add(new UriTestCaseSpec("foo#baz:dooz", null));
+
+    // URIs with http scheme.
+    goodUris.add(new UriTestCaseSpec("http:foo", "http"));
+    goodUris.add(new UriTestCaseSpec("http://foo.com:80/blah", "http"));
+    goodUris.add(new UriTestCaseSpec("http://foo.com/bar", "http"));
+    goodUris.add(new UriTestCaseSpec("http://foo.com/bar#baz", "http"));
+
+    // URIs with https, ftp, mailto scheme.
+    goodUris.add(new UriTestCaseSpec("mailto:good@good.com", "mailto"));
+    goodUris.add(new UriTestCaseSpec("https://foo.com", "https"));
+    goodUris.add(new UriTestCaseSpec("ftp://foo.com", "ftp"));
+
+    GOOD_URIS = Collections.unmodifiableList(goodUris);
+  }
+
+  private static final List<UriTestCaseSpec> BAD_URIS;
+  static {
+    ArrayList<UriTestCaseSpec> badUris = new ArrayList<UriTestCaseSpec>();
+
+    // URIs with defined, bad schemes.
+    badUris.add(new UriTestCaseSpec("javascript:evil", "javascript"));
+    badUris.add(new UriTestCaseSpec("javascript://foo()", "javascript"));
+    badUris.add(new UriTestCaseSpec("javascript:evil#world", "javascript"));
+    badUris.add(new UriTestCaseSpec("javascript:evil/is", "javascript"));
+
+    // URIs with weird schemes, neither of which can be parsed as a URI.
+    badUris.add(
+        new UriTestCaseSpec("  mailto:good@good.com", "  mailto", true));
+    badUris.add(new UriTestCaseSpec("ma&ilto:good@good.com", "ma&ilto", true));
+
+    BAD_URIS = Collections.unmodifiableList(badUris);
+  }
+
+  public static void testExtractScheme() {
+    for (UriTestCaseSpec uriSpec : GOOD_URIS) {
+      assertEquals(
+          uriSpec.getScheme(), UriUtils.extractScheme(uriSpec.getUri()));
+      // Verify that the scheme parsed by extractScheme() is the same as
+      // obtained by {@link URI}'s parser.
+      assertEquals(uriSpec.getScheme(), uriSpec.getParsedUri().getScheme());
+    }
+    for (UriTestCaseSpec uriSpec : BAD_URIS) {
+      assertEquals(
+          uriSpec.getScheme(), UriUtils.extractScheme(uriSpec.getUri()));
+      if (!uriSpec.getExpectUriParseException()) {
+        // Verify that the scheme parsed by extractScheme() is the same as
+        // obtained by {@link URI}'s parser (for those URIs that can be parsed
+        // by the latter).
+        assertEquals(uriSpec.getScheme(), uriSpec.getParsedUri().getScheme());
+      }
+    }
+  }
+
+  public static void testIsSafeUri() {
+    for (UriTestCaseSpec uriSpec : GOOD_URIS) {
+      assertTrue(UriUtils.isSafeUri(uriSpec.getUri()));
+    }
+    for (UriTestCaseSpec uriSpec : BAD_URIS) {
+      assertFalse(UriUtils.isSafeUri(uriSpec.getUri()));
+    }
+  }
+
+  public static void testSanitizeUri() {
+    for (UriTestCaseSpec uriSpec : GOOD_URIS) {
+      assertEquals(uriSpec.getUri(), UriUtils.sanitizeUri(uriSpec.getUri()));
+    }
+    for (UriTestCaseSpec uriSpec : BAD_URIS) {
+      assertEquals("#", UriUtils.sanitizeUri(uriSpec.getUri()));
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlStringTest.java b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlStringTest.java
new file mode 100644
index 0000000..635545c
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlStringTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * GWT Unit tests for SafeHtmlString.
+ */
+public class GwtSafeHtmlStringTest extends GWTTestCase {
+
+  // Test SafeHtmlString.equals()
+  public void testEquals() {
+    SafeHtmlString safe1 = new SafeHtmlString("stringsame");
+    SafeHtmlString safe2 = new SafeHtmlString("stringsame");
+    SafeHtmlString safe3 = new SafeHtmlString("stringdiff");
+    assertEquals(safe1, safe2);
+    assertFalse(safe1.equals(safe3));
+  }
+  
+  // Test SafeHtmlString.hashCode()
+  public void testHashCode() {
+    SafeHtmlString safe1 = new SafeHtmlString("stringsame");
+    SafeHtmlString safe3 = new SafeHtmlString("stringdiff");
+    SafeHtmlString safe2 = new SafeHtmlString("stringsame");
+    assertEquals("stringsame".hashCode(), safe1.hashCode());
+    assertEquals(safe1.hashCode(), safe2.hashCode());
+    assertEquals("stringdiff".hashCode(), safe3.hashCode());
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.safehtml.SafeHtmlTestsModule";
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java
new file mode 100644
index 0000000..b5eb126
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/GwtSafeHtmlUtilsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import com.google.gwt.junit.client.GWTTestCase;
+
+/**
+ * Unit tests for GwtEscapeUtils.
+ */
+public class GwtSafeHtmlUtilsTest extends GWTTestCase {
+
+  public void testEscape_noEscape() {
+    String escaped = SafeHtmlUtils.htmlEscape("foobar");
+    assertEquals("foobar", escaped);
+  }
+
+  public void testEscape_ampersand() {
+    String escaped = SafeHtmlUtils.htmlEscape("foo&bar");
+    assertEquals("foo&amp;bar", escaped);
+  }
+
+  public void testEscape_ampersandAndBrackets() {
+    String escaped = SafeHtmlUtils.htmlEscape("fo<o&b<em>ar");
+    assertEquals("fo&lt;o&amp;b&lt;em&gt;ar", escaped);
+  }
+
+  public void testEscape_allMetaCharacters() {
+    String escaped = SafeHtmlUtils.htmlEscape("f\"bar \'<&em><e/m>oo&bar");
+    assertEquals(
+        "f&quot;bar &#39;&lt;&amp;em&gt;&lt;e/m&gt;oo&amp;bar", escaped);
+  }
+
+  public void testEscape_withEntities1() {
+    String escaped = SafeHtmlUtils.htmlEscapeAllowEntities(
+        "f\"bar \'<&em><e/m>oo&bar");
+    assertEquals(
+        "f&quot;bar &#39;&lt;&amp;em&gt;&lt;e/m&gt;oo&amp;bar", escaped);
+  }
+
+  public void testEscape_withEntities2() {
+    String escaped = SafeHtmlUtils.htmlEscapeAllowEntities("& foo &lt;");
+    assertEquals("&amp; foo &lt;", escaped);
+  }
+
+  public void testEscape_withEntities3() {
+    String escaped = SafeHtmlUtils.htmlEscapeAllowEntities(
+        "<foo> &amp; <em> bar &#39; baz");
+    assertEquals("&lt;foo&gt; &amp; &lt;em&gt; bar &#39; baz", escaped);
+  }
+
+  public void testEscape_withEntities4() {
+    String escaped = SafeHtmlUtils.htmlEscapeAllowEntities(
+        "&foo &&amp; bar &#39; baz&");
+    assertEquals("&amp;foo &amp;&amp; bar &#39; baz&amp;", escaped);
+  }
+
+  public void testEscape_withEntitiesInvalidEntities() {
+    String escaped = SafeHtmlUtils.htmlEscapeAllowEntities(
+        "&a mp;&;&x;&#;&#x;");
+    assertEquals("&amp;a mp;&amp;;&x;&amp;#;&amp;#x;", escaped);
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.safehtml.SafeHtmlTestsModule";
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/SafeHtmlBuilderTest.java b/user/test/com/google/gwt/safehtml/shared/SafeHtmlBuilderTest.java
new file mode 100644
index 0000000..b2f3b09
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/SafeHtmlBuilderTest.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for SafeHtmlBuilder
+ */
+public class SafeHtmlBuilderTest extends TestCase {
+
+  private static final String FOOBARBAZ_HTML = "foo<em>bar</em>baz";
+
+  public void testEmpty() {
+    SafeHtmlBuilder b = new SafeHtmlBuilder();
+    assertEquals("", b.toSafeHtml().asString());
+  }
+
+  public void testFromSafeHtml() {
+    SafeHtml html = new SafeHtmlString(FOOBARBAZ_HTML);
+    SafeHtmlBuilder b = new SafeHtmlBuilder().append(html);
+    assertEquals(FOOBARBAZ_HTML, b.toSafeHtml().asString());
+  }
+
+  public void testAppend() {
+    SafeHtml html = new SafeHtmlString(FOOBARBAZ_HTML);
+    SafeHtmlBuilder b = new SafeHtmlBuilder().appendHtmlConstant(
+        "Yabba dabba &amp; doo\n").appendEscaped("What's up so&so\n").append(
+        html);
+
+    String expected = "Yabba dabba &amp; doo\n" + "What&#39;s up so&amp;so\n"
+        + FOOBARBAZ_HTML;
+    assertEquals(expected, b.toSafeHtml().asString());
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/SafeHtmlStringTest.java b/user/test/com/google/gwt/safehtml/shared/SafeHtmlStringTest.java
new file mode 100644
index 0000000..78a2543
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/SafeHtmlStringTest.java
@@ -0,0 +1,28 @@
+/*
+ * 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+/**
+ * JUnit tests for SafeHtmlString.
+ */
+public class SafeHtmlStringTest extends GwtSafeHtmlStringTest {
+
+  // This forces a GWTTestCase to run as a vanilla JUnit TestCase.
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/SafeHtmlUtilsTest.java b/user/test/com/google/gwt/safehtml/shared/SafeHtmlUtilsTest.java
new file mode 100644
index 0000000..b7bf9a2
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/SafeHtmlUtilsTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+/**
+ * Unit tests for SafeHtmlUtils.
+ */
+public class SafeHtmlUtilsTest extends GwtSafeHtmlUtilsTest {
+  
+  // This forces a GWTTestCase to run as a vanilla JUnit TestCase.
+  @Override
+  public String getModuleName() {
+    return null;
+  }
+}
diff --git a/user/test/com/google/gwt/safehtml/shared/SimpleHtmlSanitizerTest.java b/user/test/com/google/gwt/safehtml/shared/SimpleHtmlSanitizerTest.java
new file mode 100644
index 0000000..32983e2
--- /dev/null
+++ b/user/test/com/google/gwt/safehtml/shared/SimpleHtmlSanitizerTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2008 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.safehtml.shared;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for SanitizedHtml
+ */
+public class SimpleHtmlSanitizerTest extends TestCase {
+
+  public void testSimple() {
+    // simple case
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("foobar");
+    assertEquals("foobar", html.asString());
+  }
+
+  public void testDontChangeWhiteSpace() {
+    // shouldn't change whitespace or newlines
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml(
+        "things are breezy\nand jolly\tgood");
+    assertEquals("things are breezy\nand jolly\tgood", html.asString());
+  }
+
+  public void testEscapeHtmlMetaCharacters() {
+    // need to escape HTML metacharacters appearing on their own
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("foo < bar & that's good");
+    assertEquals("foo &lt; bar &amp; that&#39;s good", html.asString());
+  }
+
+  public void testDontDoubleEscape() {
+    // but don't double-escape HTML entities
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml(
+        "foo < bar &amp; that's good");
+    assertEquals("foo &lt; bar &amp; that&#39;s good", html.asString());
+  }
+
+  public void testEscapeLoneMetacharacters() {
+    // need to escape HTML metacharacters appearing on their own
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml(
+        "\"foo < bar & that's good\"");
+    assertEquals(
+        "&quot;foo &lt; bar &amp; that&#39;s good&quot;", html.asString());
+  }
+
+  public void testDontEscapeValidTags() {
+    // leave simple tags alone
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("foo <em>bar</em>");
+    assertEquals("foo <em>bar</em>", html.asString());
+  }
+
+  public void testTagAtBeginning() {
+    // correctly deal with a tag at the beginnign
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("<em>bar</em>");
+    assertEquals("<em>bar</em>", html.asString());
+  }
+
+  public void testNonTagAtBeginning() {
+    // correctly deal with a non-tag at the beginnig
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("<yow <em>bar</em>");
+    assertEquals("&lt;yow <em>bar</em>", html.asString());
+  }
+
+  public void testNonTagAtEnd() {
+    // correctly deal with a non-tag at the end
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("<em>bar</em> foo <");
+    assertEquals("<em>bar</em> foo &lt;", html.asString());
+  }
+
+  public void testNullTag() {
+    // correctly deal with bogus empty tag
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("<>bar</em> foo<>");
+    assertEquals("&lt;&gt;bar</em> foo&lt;&gt;", html.asString());
+  }
+
+  public void testNullEndTag() {
+    // correctly deal with bogus empty end tag
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("</>bar</em> foo</>");
+    assertEquals("&lt;/&gt;bar</em> foo&lt;/&gt;", html.asString());
+  }
+
+  public void testSimpleTagsAndHtmlMetaChars() {
+    // mix of simple tags and HTML metacharacters appearing on their own
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml(
+        "foo < bar & that's <b>good</b>");
+    assertEquals("foo &lt; bar &amp; that&#39;s <b>good</b>", html.asString());
+  }
+
+  public void testEvilTags() {
+    // escape tags we don't know
+    SafeHtml html = SimpleHtmlSanitizer.sanitizeHtml("<script>evil()</script>");
+    assertEquals("&lt;script&gt;evil()&lt;/script&gt;", html.asString());
+  }
+}