Change Key.toString() to be useful for URL encoding

This makes the key string format shorter, and more suitable for use
within a URL.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/src/main/java/com/google/gwtorm/client/CompoundKey.java b/src/main/java/com/google/gwtorm/client/CompoundKey.java
index d1e5341..214c96d 100644
--- a/src/main/java/com/google/gwtorm/client/CompoundKey.java
+++ b/src/main/java/com/google/gwtorm/client/CompoundKey.java
@@ -76,21 +76,18 @@
   @Override
   public String toString() {
     final StringBuffer r = new StringBuffer();
-    r.append(getClass().getName());
-    r.append('[');
     boolean first = true;
     if (getParentKey() != null) {
-      r.append(getParentKey().toString());
+      r.append(KeyUtil.encode(getParentKey().toString()));
       first = false;
     }
-    for (final Key<?> k : members()){
+    for (final Key<?> k : members()) {
       if (!first) {
-        r.append(", ");
+        r.append(',');
       }
-      r.append(k.toString());
+      r.append(KeyUtil.encode(k.toString()));
       first = false;
     }
-    r.append(']');
     return r.toString();
   }
 
diff --git a/src/main/java/com/google/gwtorm/client/IntKey.java b/src/main/java/com/google/gwtorm/client/IntKey.java
index cba56dd..2d70972 100644
--- a/src/main/java/com/google/gwtorm/client/IntKey.java
+++ b/src/main/java/com/google/gwtorm/client/IntKey.java
@@ -60,14 +60,11 @@
   @Override
   public String toString() {
     final StringBuffer r = new StringBuffer();
-    r.append(getClass().getName());
-    r.append('[');
     if (getParentKey() != null) {
       r.append(getParentKey().toString());
-      r.append(", ");
+      r.append(',');
     }
     r.append(get());
-    r.append(']');
     return r.toString();
   }
 
diff --git a/src/main/java/com/google/gwtorm/client/KeyUtil.java b/src/main/java/com/google/gwtorm/client/KeyUtil.java
index acecb10..4f64f84 100644
--- a/src/main/java/com/google/gwtorm/client/KeyUtil.java
+++ b/src/main/java/com/google/gwtorm/client/KeyUtil.java
@@ -14,8 +14,42 @@
 
 package com.google.gwtorm.client;
 
-class KeyUtil {
-  static <T extends Key<?>> boolean eq(final T a, final T b) {
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.http.client.URL;
+import com.google.gwtorm.server.StandardKeyEncoder;
+
+
+/** Common utility functions for {@link Key} implementors. */
+public class KeyUtil {
+  private static Encoder ENCODER_IMPL;
+
+  static {
+    if (GWT.isClient()) {
+      ENCODER_IMPL = new GwtEncoder();
+    }
+  }
+
+  /**
+   * Set the encoder implementation to a valid implementation.
+   * <p>
+   * Server-side code needs to set the encoder to a {@link StandardKeyEncoder}
+   * instance prior to invoking any methods in this class. Typically this is
+   * done by the {@link SchemaFactory} implementation's static initializer.
+   */
+  public static void setEncoderImpl(final Encoder e) {
+    ENCODER_IMPL = e;
+  }
+
+  /**
+   * Determine if two keys are equal, supporting null references.
+   * 
+   * @param <T> type of the key entity.
+   * @param a first key to test; may be null.
+   * @param b second key to test; may be null.
+   * @return true if both <code>a</code> and <code>b</code> are null, or if both
+   *         are not-null and <code>a.equals(b)</code> is true. Otherwise false.
+   */
+  public static <T extends Key<?>> boolean eq(final T a, final T b) {
     if (a == b) {
       return true;
     }
@@ -25,6 +59,51 @@
     return a.equals(b);
   }
 
+  /**
+   * Encode a string to be safe for use within a URL like string.
+   * <p>
+   * The returned encoded string has URL component characters escaped with hex
+   * escapes (e.g. ' ' is '+' and '%' is '%25'). The special character '/' is
+   * left literal. The comma character (',') is always encoded, permitting
+   * multiple encoded string values to be joined together safely.
+   * 
+   * @param e the string to encode, must not be null.
+   * @return the encoded string.
+   */
+  public static String encode(final String e) {
+    return ENCODER_IMPL.encode(e);
+  }
+
+  /**
+   * Decode a string previously encoded by {@link #encode(String)}.
+   * 
+   * @param e the string to decode, must not be null.
+   * @return the decoded string.
+   */
+  public static String decode(final String e) {
+    return ENCODER_IMPL.decode(e);
+  }
+
+  public static abstract class Encoder {
+    public abstract String encode(String e);
+
+    public abstract String decode(String e);
+  }
+
+  private static class GwtEncoder extends Encoder {
+    @Override
+    public String encode(final String e) {
+      return fixPathImpl(URL.encodeComponent(e));
+    }
+
+    @Override
+    public String decode(final String e) {
+      return URL.decodeComponent(e);
+    }
+
+    private static native String fixPathImpl(String path) /*-{ return path.replace(/%2F/g, "/"); }-*/;
+  }
+
   private KeyUtil() {
   }
 }
diff --git a/src/main/java/com/google/gwtorm/client/LongKey.java b/src/main/java/com/google/gwtorm/client/LongKey.java
index 3741e2c..4bd5bae 100644
--- a/src/main/java/com/google/gwtorm/client/LongKey.java
+++ b/src/main/java/com/google/gwtorm/client/LongKey.java
@@ -60,14 +60,11 @@
   @Override
   public String toString() {
     final StringBuffer r = new StringBuffer();
-    r.append(getClass().getName());
-    r.append('[');
     if (getParentKey() != null) {
       r.append(getParentKey().toString());
-      r.append(", ");
+      r.append(',');
     }
     r.append(get());
-    r.append(']');
     return r.toString();
   }
 
diff --git a/src/main/java/com/google/gwtorm/client/ShortKey.java b/src/main/java/com/google/gwtorm/client/ShortKey.java
index 937c3f3..30daf88 100644
--- a/src/main/java/com/google/gwtorm/client/ShortKey.java
+++ b/src/main/java/com/google/gwtorm/client/ShortKey.java
@@ -61,14 +61,11 @@
   @Override
   public String toString() {
     final StringBuffer r = new StringBuffer();
-    r.append(getClass().getName());
-    r.append('[');
     if (getParentKey() != null) {
       r.append(getParentKey().toString());
-      r.append(", ");
+      r.append(',');
     }
     r.append(get());
-    r.append(']');
     return r.toString();
   }
 
diff --git a/src/main/java/com/google/gwtorm/client/StringKey.java b/src/main/java/com/google/gwtorm/client/StringKey.java
index 20172c4..a906142 100644
--- a/src/main/java/com/google/gwtorm/client/StringKey.java
+++ b/src/main/java/com/google/gwtorm/client/StringKey.java
@@ -62,14 +62,11 @@
   @Override
   public String toString() {
     final StringBuffer r = new StringBuffer();
-    r.append(getClass().getName());
-    r.append('[');
     if (getParentKey() != null) {
       r.append(getParentKey().toString());
-      r.append(", ");
+      r.append(',');
     }
-    r.append(get());
-    r.append(']');
+    r.append(KeyUtil.encode(get()));
     return r.toString();
   }
 
diff --git a/src/main/java/com/google/gwtorm/jdbc/Database.java b/src/main/java/com/google/gwtorm/jdbc/Database.java
index 7f575f6..0b9a39f 100644
--- a/src/main/java/com/google/gwtorm/jdbc/Database.java
+++ b/src/main/java/com/google/gwtorm/jdbc/Database.java
@@ -14,6 +14,7 @@
 
 package com.google.gwtorm.jdbc;
 
+import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.Schema;
 import com.google.gwtorm.client.SchemaFactory;
@@ -25,6 +26,7 @@
 import com.google.gwtorm.schema.sql.DialectH2;
 import com.google.gwtorm.schema.sql.DialectPostgreSQL;
 import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.StandardKeyEncoder;
 
 import java.sql.Connection;
 import java.sql.SQLException;
@@ -53,6 +55,10 @@
   private static final Map<Class<?>, String> schemaFactoryNames =
       Collections.synchronizedMap(new WeakHashMap<Class<?>, String>());
 
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
   private final DataSource dataSource;
   private final JavaSchemaModel schemaModel;
   private final AbstractSchemaFactory<T> implFactory;
diff --git a/src/main/java/com/google/gwtorm/server/StandardKeyEncoder.java b/src/main/java/com/google/gwtorm/server/StandardKeyEncoder.java
new file mode 100644
index 0000000..c9076e8
--- /dev/null
+++ b/src/main/java/com/google/gwtorm/server/StandardKeyEncoder.java
@@ -0,0 +1,118 @@
+// 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.gwtorm.server;
+
+import com.google.gwtorm.client.KeyUtil.Encoder;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+
+public class StandardKeyEncoder extends Encoder {
+  private static final char[] hexc =
+      {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
+          'E', 'F'};
+  private static final char safe[];
+  private static final byte hexb[];
+
+  static {
+    safe = new char[256];
+    safe['-'] = '-';
+    safe['_'] = '_';
+    safe['.'] = '.';
+    safe['!'] = '!';
+    safe['~'] = '~';
+    safe['*'] = '*';
+    safe['\''] = '\'';
+    safe['('] = '(';
+    safe[')'] = ')';
+    safe['/'] = '/';
+    safe[' '] = '+';
+    for (char c = '0'; c <= '9'; c++)
+      safe[c] = c;
+    for (char c = 'A'; c <= 'Z'; c++)
+      safe[c] = c;
+    for (char c = 'a'; c <= 'z'; c++)
+      safe[c] = c;
+
+    hexb = new byte['f' + 1];
+    Arrays.fill(hexb, (byte) -1);
+    for (char i = '0'; i <= '9'; i++)
+      hexb[i] = (byte) (i - '0');
+    for (char i = 'A'; i <= 'F'; i++)
+      hexb[i] = (byte) ((i - 'A') + 10);
+    for (char i = 'a'; i <= 'f'; i++)
+      hexb[i] = (byte) ((i - 'a') + 10);
+  }
+
+  @Override
+  public String encode(final String e) {
+    final byte[] b;
+    try {
+      b = e.getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+
+    final StringBuilder r = new StringBuilder(b.length);
+    for (int i = 0; i < b.length; i++) {
+      final int c = b[i] & 0xff;
+      final char s = safe[c];
+      if (s == 0) {
+        r.append('%');
+        r.append(hexc[c >> 4]);
+        r.append(hexc[c & 15]);
+      } else {
+        r.append(s);
+      }
+    }
+    return r.toString();
+  }
+
+  @Override
+  public String decode(final String e) {
+    if (e.indexOf('%') < 0) {
+      return e.replace('+', ' ');
+    }
+
+    final byte[] b = new byte[e.length()];
+    int bPtr = 0;
+    try {
+      for (int i = 0; i < e.length();) {
+        final char c = e.charAt(i);
+        if (c == '%' && i + 2 < e.length()) {
+          final int v = (hexb[e.charAt(i + 1)] << 4) | hexb[e.charAt(i + 2)];
+          if (v < 0) {
+            throw new IllegalArgumentException(e.substring(i, i + 3));
+          }
+          b[bPtr++] = (byte) v;
+          i += 3;
+        } else if (c == '+') {
+          b[bPtr++] = ' ';
+          i++;
+        } else {
+          b[bPtr++] = (byte) c;
+          i++;
+        }
+      }
+    } catch (ArrayIndexOutOfBoundsException err) {
+      throw new IllegalArgumentException("Bad encoding: " + e);
+    }
+    try {
+      return new String(b, 0, bPtr, "UTF-8");
+    } catch (UnsupportedEncodingException e1) {
+      throw new RuntimeException("No UTF-8 support", e1);
+    }
+  }
+}
\ No newline at end of file