Escape unsafe characters in urls.

Fixes #133
diff --git a/integration/volley/src/androidTest/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java b/integration/volley/src/androidTest/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java
index 8d50837..e70c616 100644
--- a/integration/volley/src/androidTest/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java
+++ b/integration/volley/src/androidTest/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java
@@ -8,6 +8,7 @@
 import com.android.volley.toolbox.Volley;
 import com.bumptech.glide.Priority;
 import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.GlideUrl;
 import com.squareup.okhttp.mockwebserver.MockResponse;
 import com.squareup.okhttp.mockwebserver.MockWebServer;
 import org.junit.After;
@@ -196,7 +197,7 @@
                 return super.get();
             }
         };
-        return new VolleyStreamFetcher(requestQueue, url.toString(), requestFuture);
+        return new VolleyStreamFetcher(requestQueue, new GlideUrl(url.toString()), requestFuture);
     }
 
     private static String isToString(InputStream is) throws IOException {
diff --git a/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyStreamFetcher.java b/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyStreamFetcher.java
index 8b9e5f9..d78723d 100644
--- a/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyStreamFetcher.java
+++ b/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyStreamFetcher.java
@@ -7,24 +7,27 @@
 import com.android.volley.toolbox.HttpHeaderParser;
 import com.bumptech.glide.Priority;
 import com.bumptech.glide.load.data.DataFetcher;
+import com.bumptech.glide.load.model.GlideUrl;
 
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
+import java.lang.reflect.Method;
 
 /**
  * A DataFetcher backed by volley for fetching images via http.
  */
 public class VolleyStreamFetcher implements DataFetcher<InputStream> {
     private final RequestQueue requestQueue;
-    private final String url;
+    private final GlideUrl url;
     private VolleyRequestFuture<InputStream> requestFuture;
 
     @SuppressWarnings("unused")
-    public VolleyStreamFetcher(RequestQueue requestQueue, String url) {
+    public VolleyStreamFetcher(RequestQueue requestQueue, GlideUrl url) {
         this(requestQueue, url,  null);
     }
 
-    public VolleyStreamFetcher(RequestQueue requestQueue, String url, VolleyRequestFuture<InputStream> requestFuture) {
+    public VolleyStreamFetcher(RequestQueue requestQueue, GlideUrl url,
+            VolleyRequestFuture<InputStream> requestFuture) {
         this.requestQueue = requestQueue;
         this.url = url;
         this.requestFuture = requestFuture;
@@ -35,7 +38,9 @@
 
     @Override
     public InputStream loadData(Priority priority) throws Exception {
-        GlideRequest request = new GlideRequest(url, requestFuture, glideToVolleyPriority(priority));
+        // Make sure the string url safely encodes non ascii characters.
+        String stringUrl = url.toURL().toString();
+        GlideRequest request = new GlideRequest(stringUrl, requestFuture, glideToVolleyPriority(priority));
 
         requestFuture.setRequest(requestQueue.add(request));
 
@@ -49,7 +54,7 @@
 
     @Override
     public String getId() {
-        return url;
+        return url.toString();
     }
 
     @Override
diff --git a/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyUrlLoader.java b/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyUrlLoader.java
index b08b022..b2093ef 100644
--- a/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyUrlLoader.java
+++ b/integration/volley/src/main/java/com/bumptech/glide/integration/volley/VolleyUrlLoader.java
@@ -67,6 +67,6 @@
 
     @Override
     public DataFetcher<InputStream> getResourceFetcher(GlideUrl url, int width, int height) {
-        return new VolleyStreamFetcher(requestQueue, url.toString(), new VolleyRequestFuture<InputStream>());
+        return new VolleyStreamFetcher(requestQueue, url, new VolleyRequestFuture<InputStream>());
     }
 }
diff --git a/library/src/androidTest/java/com/bumptech/glide/load/model/GlideUrlTest.java b/library/src/androidTest/java/com/bumptech/glide/load/model/GlideUrlTest.java
index ed8559a..5d0071b 100644
--- a/library/src/androidTest/java/com/bumptech/glide/load/model/GlideUrlTest.java
+++ b/library/src/androidTest/java/com/bumptech/glide/load/model/GlideUrlTest.java
@@ -60,4 +60,26 @@
 
         assertEquals(expected, glideUrl.toString());
     }
+
+    @Test
+    public void testIssue133() throws MalformedURLException {
+        // u00e0=à
+        final String original =  "http://www.commitstrip.com/wp-content/uploads/2014/07/"
+                + "Excel-\u00E0-toutes-les-sauces-650-finalenglish.jpg";
+
+        final String escaped = "http://www.commitstrip.com/wp-content/uploads/2014/07/"
+                + "Excel-%C3%A0-toutes-les-sauces-650-finalenglish.jpg";
+
+        GlideUrl glideUrlFromString = new GlideUrl(original);
+        assertEquals(escaped, glideUrlFromString.toURL().toString());
+
+        GlideUrl glideUrlFromEscapedString = new GlideUrl(escaped);
+        assertEquals(escaped, glideUrlFromEscapedString.toURL().toString());
+
+        GlideUrl glideUrlFromUrl = new GlideUrl(new URL(original));
+        assertEquals(escaped, glideUrlFromUrl.toURL().toString());
+
+        GlideUrl glideUrlFromEscapedUrl = new GlideUrl(new URL(escaped));
+        assertEquals(escaped, glideUrlFromEscapedUrl.toURL().toString());
+    }
 }
\ No newline at end of file
diff --git a/library/src/main/java/com/bumptech/glide/load/model/GlideUrl.java b/library/src/main/java/com/bumptech/glide/load/model/GlideUrl.java
index ddb8dcb..b87b3cf 100644
--- a/library/src/main/java/com/bumptech/glide/load/model/GlideUrl.java
+++ b/library/src/main/java/com/bumptech/glide/load/model/GlideUrl.java
@@ -1,19 +1,30 @@
 package com.bumptech.glide.load.model;
 
+import android.net.Uri;
 import android.text.TextUtils;
 
 import java.net.MalformedURLException;
 import java.net.URL;
 
 /**
- * This is a simple wrapper for strings representing http/https URLs. new URL() is an excessively expensive operation
- * that may be unnecessary if the class loading the image from the URL doesn't actually require a URL object.
+ * A wrapper for strings representing http/https URLs responsible for ensuring URLs are properly escaped and avoiding
+ * unnecessary URL instantiations for loaders that require only string urls rather than URL objects.
  *
- * Users wishing to replace the class for handling URLs must register a factory using GlideUrl.
+ * <p>
+ *  Users wishing to replace the class for handling URLs must register a factory using GlideUrl.
+ * </p>
+ *
+ * <p>
+ *     To obtain a properly escaped URL, call {@link #toURL()}. To obtain a properly escaped string URL, call
+ *     {@link #toURL()} and then {@link java.net.URL#toString()}.
+ * </p>
  */
 public class GlideUrl {
     private String stringUrl;
+    private static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
+
     private URL url;
+    private URL safeUrl;
 
     public GlideUrl(URL url) {
         if (url == null) {
@@ -32,10 +43,21 @@
     }
 
     public URL toURL() throws MalformedURLException {
-        if (url == null) {
-            url = new URL(stringUrl);
+        return getSafeUrl();
+    }
+
+    // See http://stackoverflow.com/questions/3286067/url-encoding-in-android. Although the answer using URI would work,
+    // using it would require both decoding and encoding each string which is more complicated, slower and generates
+    // more objects than the solution below. See also issue #133.
+    private URL getSafeUrl() throws MalformedURLException {
+        if (safeUrl != null) {
+            return safeUrl;
         }
-        return url;
+        String unsafe = toString();
+        String safe = Uri.encode(unsafe, ALLOWED_URI_CHARS);
+
+        safeUrl = new URL(safe);
+        return safeUrl;
     }
 
     @Override
diff --git a/library/src/main/java/com/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java b/library/src/main/java/com/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java
index 086a55b..573f709 100644
--- a/library/src/main/java/com/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java
+++ b/library/src/main/java/com/bumptech/glide/load/model/stream/HttpUrlGlideUrlLoader.java
@@ -5,6 +5,7 @@
 import com.bumptech.glide.load.data.HttpUrlFetcher;
 import com.bumptech.glide.load.model.GenericLoaderFactory;
 import com.bumptech.glide.load.model.GlideUrl;
+import com.bumptech.glide.load.model.ModelCache;
 import com.bumptech.glide.load.model.ModelLoader;
 import com.bumptech.glide.load.model.ModelLoaderFactory;
 
@@ -16,13 +17,17 @@
  */
 public class HttpUrlGlideUrlLoader implements ModelLoader<GlideUrl, InputStream> {
 
+    private ModelCache<GlideUrl, GlideUrl> modelCache;
+
     /**
      * The default factory for {@link com.bumptech.glide.load.model.stream.HttpUrlGlideUrlLoader}s.
      */
     public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
+        private ModelCache<GlideUrl, GlideUrl> modelCache = new ModelCache<GlideUrl, GlideUrl>(500);
+
         @Override
         public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
-            return new HttpUrlGlideUrlLoader();
+            return new HttpUrlGlideUrlLoader(modelCache);
         }
 
         @Override
@@ -31,8 +36,25 @@
         }
     }
 
+    public HttpUrlGlideUrlLoader() {
+        // Empty.
+    }
+
+    public HttpUrlGlideUrlLoader(ModelCache<GlideUrl, GlideUrl> modelCache) {
+        this.modelCache = modelCache;
+    }
+
     @Override
     public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) {
-        return new HttpUrlFetcher(model);
+        // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time spent parsing urls.
+        GlideUrl url = model;
+        if (modelCache != null) {
+            url = modelCache.get(model, 0, 0);
+            if (url == null) {
+                modelCache.put(model, 0, 0, model);
+                url = model;
+            }
+        }
+        return new HttpUrlFetcher(url);
     }
 }