You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by et...@apache.org on 2008/02/12 05:17:19 UTC

svn commit: r620700 - in /incubator/shindig/trunk: features/core/ java/gadgets/src/main/java/org/apache/shindig/gadgets/ java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ java/gadgets/src/main/java/org/apache/shindig/util/ java/gadgets/src/te...

Author: etnu
Date: Mon Feb 11 20:17:18 2008
New Revision: 620700

URL: http://svn.apache.org/viewvc?rev=620700&view=rev
Log:
Implemented SHINDIG-56, and SHINDIG-38. This change required a significant overhaul of RemoteContentFetcher and ProxyHandler, and touched many parts of the code base. 


Added:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentRequest.java
Modified:
    incubator/shindig/trunk/features/core/feature.xml
    incubator/shindig/trunk/features/core/io.js
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicRemoteContentFetcher.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetServer.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleSubstituter.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContent.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentFetcher.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CajaContentFilter.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingServlet.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcServlet.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/InputStreamConsumer.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetServerTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/RemoteContentTest.java

Modified: incubator/shindig/trunk/features/core/feature.xml
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/core/feature.xml?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/features/core/feature.xml (original)
+++ incubator/shindig/trunk/features/core/feature.xml Mon Feb 11 20:17:18 2008
@@ -27,7 +27,7 @@
     <script><![CDATA[
       gadgets.io.init({
         proxyUrl: "http://www.gmodules.com/ig/proxy?url=%url%",
-        jsonProxyUrl: "proxy?url=%url%&output=js"
+        jsonProxyUrl: "proxy?output=js"
       });
     ]]></script>
     <script src="legacy.js"/>

Modified: incubator/shindig/trunk/features/core/io.js
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/features/core/io.js?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/features/core/io.js (original)
+++ incubator/shindig/trunk/features/core/io.js Mon Feb 11 20:17:18 2008
@@ -106,7 +106,7 @@
         } else {
           var parser = new DOMParser();
           dom = parser.parseFromString(resp.text, "text/xml");
-          if ("parsererror" == dom.documentElement.nodeName) {
+          if ("parsererror" === dom.documentElement.nodeName) {
             resp.errors.push("failed to parse XML");
           } else {
             resp.data = dom;
@@ -146,34 +146,36 @@
       // gadgets.io.RequestParameters, and validate them.
       var xhr = makeXhr();
       var params = opt_params || {};
-      var newUrl = config.jsonProxyUrl.replace("%url%",
-          encodeURIComponent(url));
+
+      xhr.open("POST", config.jsonProxyUrl, true);
+      if (callback) {
+        xhr.onreadystatechange = gadgets.util.makeClosure(
+            null, processResponse, url, callback, params, xhr);
+      }
+      // We always send a POST request; we just hide the details.
+      xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 
       // Check if authorization is requested
+      var auth, st;
       if (params.AUTHORIZATION && params.AUTHORIZATION !== "NONE") {
-        newUrl += "&authz=" + params.AUTHORIZATION.toLowerCase();
-        // Add the security-token if available
-        if (gadgets.util.getUrlParameters()["st"]) {
-          newUrl += "&st=" + gadgets.util.getUrlParameters()["st"];
-        }
-      }
-      // TODO: Fetcher cannot distinguish between GET & POST yet.
-      xhr.open(params.METHOD || "GET", newUrl, true);
-      if (callback) {
-        xhr.onreadystatechange = gadgets.util.makeClosure(null,
-            processResponse, url, callback, params, xhr);
+        auth = params.AUTHORIZATION.toLowerCase();
+        st = gadgets.util.getUrlParameters().st;
       }
-      if (params.METHOD === "POST") {
-        xhr.setRequestHeader('Content-Type',
-            'application/x-www-form-urlencoded');
-        if (params.POST_DATA) {
-          xhr.send("postData=" + encodeURIComponent(params.POST_DATA));
-        } else {
-          xhr.send("postData=");
-        }
-      } else {
-        xhr.send(null);
+
+      var headers = params.HEADERS || {};
+      if (params.METHOD === "POST" && !headers["Content-Type"]) {
+        headers["Content-Type"] = "application/x-www-form-urlencoded";
       }
+
+      var postData = {
+        url: url,
+        httpMethod : params.METHOD || "GET",
+        headers: gadgets.io.encodeValues(headers),
+        postData : params.POST_DATA || "",
+        authz : auth || "",
+        st : st || "",
+      };
+      xhr.send(gadgets.io.encodeValues(postData));
     },
 
     /**
@@ -181,18 +183,22 @@
      * (key=value&amp;...)
      *
      * @param {Object} fields The post fields you wish to encode
-     * @return {String} The processed post data; this will include a trailing
-     *    ampersand (&)
+     * @return {String} The processed post data in www-form-urlencoded format.
      *
      * @member gadgets.io
      */
     encodeValues : function (fields) {
       var buf = [];
+      var first = false;
       for (var i in fields) {
+        if (!first) {
+          first = true;
+        } else {
+          buf.push("&");
+        }
         buf.push(encodeURIComponent(i));
         buf.push("=");
         buf.push(encodeURIComponent(fields[i]));
-        buf.push("&");
       }
       return buf.join("");
     },

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicGadgetToken.java Mon Feb 11 20:17:18 2008
@@ -19,6 +19,7 @@
 package org.apache.shindig.gadgets;
 
 import java.net.URL;
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -47,8 +48,8 @@
    * {@inheritDoc}
    * Signer that does not sign.
    */
-  public URL signUrl(URL uri, String httpMethod, Map parameters)
-      throws GadgetException {
+  public URL signUrl(URL uri, String httpMethod,
+      Map<String, Collection<String>> parameters) {
     return uri;
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicRemoteContentFetcher.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicRemoteContentFetcher.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicRemoteContentFetcher.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/BasicRemoteContentFetcher.java Mon Feb 11 20:17:18 2008
@@ -17,11 +17,10 @@
  */
 package org.apache.shindig.gadgets;
 
-import java.io.ByteArrayOutputStream;
+import org.apache.shindig.util.InputStreamConsumer;
+
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.HttpURLConnection;
-import java.net.URL;
 import java.util.List;
 import java.util.Map;
 
@@ -43,67 +42,82 @@
     this.maxObjSize = maxObjSize;
   }
 
-  /** {@inheritDoc} */
-  public RemoteContent fetch(URL url, ProcessingOptions options) {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-
-    int responseCode;
+  /**
+   * Initializes the connection.
+   *
+   * @param request
+   * @param options
+   * @return The opened connection
+   * @throws IOException
+   */
+  private HttpURLConnection getConnection(RemoteContentRequest request,
+      ProcessingOptions options) throws IOException {
     HttpURLConnection fetcher;
-    Map<String, List<String>> headers = null;
-
-    try {
-      fetcher = (HttpURLConnection) url.openConnection();
-      fetcher.setInstanceFollowRedirects(true);
-      fetcher.setConnectTimeout(CONNECT_TIMEOUT_MS);
-
-      responseCode = fetcher.getResponseCode();
-      headers = fetcher.getHeaderFields();
-
-      byte[] chunk = new byte[8192];
-      int chunkSize;
-      InputStream in = fetcher.getInputStream();
-      while (out.size() < maxObjSize && (chunkSize = in.read(chunk)) != -1) {
-        out.write(chunk, 0, chunkSize);
+    fetcher = (HttpURLConnection)request.getUri().toURL().openConnection();
+    fetcher.setInstanceFollowRedirects(true);
+    fetcher.setConnectTimeout(CONNECT_TIMEOUT_MS);
+    Map<String, List<String>> reqHeaders = request.getAllHeaders();
+    for (Map.Entry<String, List<String>> entry : reqHeaders.entrySet()) {
+      List<String> value = entry.getValue();
+      if (value.size() == 1) {
+        fetcher.setRequestProperty(entry.getKey(), value.get(0));
+      } else {
+        StringBuilder headerList = new StringBuilder();
+        boolean first = false;
+        for (String val : value) {
+          if (!first) {
+            first = true;
+          } else {
+            headerList.append(",");
+          }
+          headerList.append(val);
+        }
+        fetcher.setRequestProperty(entry.getKey(), headerList.toString());
       }
-    } catch (IOException e) {
-      responseCode = 500;
     }
+    fetcher.setDefaultUseCaches(!options.getIgnoreCache());
+    return fetcher;
+  }
 
-    return new RemoteContent(responseCode, out.toByteArray(), headers);
+  /**
+   * @param fetcher
+   * @return A RemoteContent object made by consuming the response of the
+   *     given HttpURLConnection.
+   */
+  private RemoteContent makeResponse(HttpURLConnection fetcher)
+      throws IOException {
+    Map<String, List<String>> headers = fetcher.getHeaderFields();
+    int responseCode = fetcher.getResponseCode();
+    byte[] body = InputStreamConsumer.readToByteArray(
+        fetcher.getInputStream(), maxObjSize);
+    return new RemoteContent(responseCode, body, headers);
   }
 
-  public RemoteContent fetchByPost(URL url, byte[] postData,
-      ProcessingOptions options) {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-    
-    int responseCode;
-    HttpURLConnection fetcher;
-    Map<String, List<String>> headers = null;
+  /** {@inheritDoc} */
+  public RemoteContent fetch(RemoteContentRequest request,
+                             ProcessingOptions options) {
+    try {
+      return makeResponse(getConnection(request, options));
+    } catch (IOException e) {
+      return RemoteContent.ERROR;
+    }
+  }
 
+  public RemoteContent fetchByPost(RemoteContentRequest request,
+                                   ProcessingOptions options) {
     try {
-      fetcher = (HttpURLConnection) url.openConnection();
+      HttpURLConnection fetcher = getConnection(request, options);
       fetcher.setRequestMethod("POST");
-      fetcher.setInstanceFollowRedirects(true);
-      fetcher.setConnectTimeout(CONNECT_TIMEOUT_MS);
-      fetcher.setRequestProperty("Content-Length", String.valueOf(postData.length));
+      fetcher.setRequestProperty("Content-Length",
+                                 String.valueOf(request.getPostBodyLength()));
       fetcher.setUseCaches(false);
       fetcher.setDoInput(true);
       fetcher.setDoOutput(true);
-      fetcher.getOutputStream().write(postData);
-
-      responseCode = fetcher.getResponseCode();
-      headers = fetcher.getHeaderFields();
-
-      byte[] chunk = new byte[8192];
-      int chunkSize;
-      InputStream in = fetcher.getInputStream();
-      while (out.size() < maxObjSize && (chunkSize = in.read(chunk)) != -1) {
-        out.write(chunk, 0, chunkSize);
-      }
+      InputStreamConsumer.pipe(request.getPostBody(),
+                               fetcher.getOutputStream());
+      return makeResponse(fetcher);
     } catch (IOException e) {
-      responseCode = 500;
+      return RemoteContent.ERROR;
     }
-
-    return new RemoteContent(responseCode, out.toByteArray(), headers);
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetServer.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetServer.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetServer.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetServer.java Mon Feb 11 20:17:18 2008
@@ -19,7 +19,6 @@
 
 import org.apache.shindig.util.Check;
 
-import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -322,10 +321,9 @@
         return;
       }
 
-      RemoteContent xml = null;
-      try {
-        xml = fetcher.fetch(gadgetId.getURI().toURL(), wc.context.getOptions());
-      } catch (MalformedURLException e) {
+      RemoteContentRequest req = new RemoteContentRequest(gadgetId.getURI());
+      RemoteContent xml = fetcher.fetch(req, wc.context.getOptions());
+      if (xml.getHttpStatusCode() != RemoteContent.SC_OK) {
         throw new GadgetException(
             GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
             "Malformed gadget spec URL: " + gadgetId.getURI().toString());

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetToken.java Mon Feb 11 20:17:18 2008
@@ -19,6 +19,7 @@
 package org.apache.shindig.gadgets;
 
 import java.net.URL;
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -45,6 +46,6 @@
    * @return The signed URL
    * @throws GadgetException
    */
-  public URL signUrl(URL uri, String httpMethod, Map parameters)
-      throws GadgetException;
+  public URL signUrl(URL uri, String httpMethod,
+      Map<String, Collection<String>> parameters) throws GadgetException;
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleSubstituter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleSubstituter.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleSubstituter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleSubstituter.java Mon Feb 11 20:17:18 2008
@@ -7,7 +7,7 @@
  * "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
+ * 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
@@ -20,7 +20,6 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.net.MalformedURLException;
 import java.net.URI;
 import java.util.List;
 import java.util.Locale;
@@ -87,10 +86,9 @@
         bundle = context.getMessageBundleCache().get(uri.toString());
         if (bundle == null) {
           RemoteContent data = null;
-          try {
-            data = context.getHttpFetcher().fetch(uri.toURL(),
-                                                  context.getOptions());
-          } catch (MalformedURLException e) {
+          data = context.getHttpFetcher().fetch(new RemoteContentRequest(uri),
+                                                context.getOptions());
+          if (data.getHttpStatusCode() != RemoteContent.SC_OK) {
             throw new GadgetException(
                 GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
                 String.format("Malformed message bundle URL: %s",

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContent.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContent.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContent.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContent.java Mon Feb 11 20:17:18 2008
@@ -17,10 +17,12 @@
  */
 package org.apache.shindig.gadgets;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
@@ -28,18 +30,34 @@
  * Represents the results of an HTTP content retrieval operation.
  */
 public class RemoteContent {
+  // Replicate HTTP status codes here.
+  public final static int SC_OK = 200;
+  public final static int SC_INTERNAL_SERVER_ERROR = 500;
+
   private final int httpStatusCode;
   private static final String DEFAULT_ENCODING = "UTF-8";
   private final String encoding;
 
+  public static final RemoteContent ERROR = new RemoteContent();
+
   // Used to lazily convert to a string representation of the input.
   private String responseString = null;
   private final byte[] responseBytes;
   private final Map<String, List<String>> headers;
 
   /**
+   * Create a dummy empty map. Access via RemoteContent.ERROR
+   */
+  private RemoteContent() {
+    this.httpStatusCode = SC_INTERNAL_SERVER_ERROR;
+    this.responseBytes = new byte[0];
+    this.encoding = DEFAULT_ENCODING;
+    this.headers = Collections.emptyMap();
+  }
+
+  /**
    * @param httpStatusCode
-   * @param resultBody
+   * @param responseBytes
    * @param headers May be null.
    */
   public RemoteContent(int httpStatusCode, byte[] responseBytes,
@@ -48,22 +66,22 @@
     if (responseBytes == null) {
       this.responseBytes = new byte[0];
     } else {
-      this.responseBytes = responseBytes;
+      this.responseBytes = new byte[responseBytes.length];
+      System.arraycopy(
+          responseBytes, 0, this.responseBytes, 0, responseBytes.length);
     }
 
-    Map<String, List<String>> tempHeaders = new HashMap<String, List<String>>();
-
-    if (headers != null) {
-      for (Map.Entry<String, List<String>> header : headers.entrySet()) {
-        List<String> values = new LinkedList<String>();
-        for (String value : header.getValue()) {
-          values.add(value);
-        }
-        tempHeaders.put(header.getKey(), values);
+    if (headers == null) {
+      this.headers = Collections.emptyMap();
+    } else {
+      Map<String, List<String>> tmpHeaders
+          = new HashMap<String, List<String>>();
+      for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+        List<String> newList = new ArrayList<String>(entry.getValue());
+        tmpHeaders.put(entry.getKey(), Collections.unmodifiableList(newList));
       }
+      this.headers = Collections.unmodifiableMap(tmpHeaders);
     }
-
-    this.headers = Collections.unmodifiableMap(tempHeaders);
     this.encoding = detectEncoding();
   }
 
@@ -90,15 +108,21 @@
     return httpStatusCode;
   }
 
-  public byte[] getByteArray() {
-    return responseBytes;
-  }
-
+  /**
+   * @return The encoding of the response body, if we're able to determine it.
+   */
   public String getEncoding() {
     return encoding;
   }
 
   /**
+   * @return An input stream suitable for reading the entirety of the response.
+   */
+  public InputStream getResponse() {
+    return new ByteArrayInputStream(responseBytes);
+  }
+
+  /**
    * Attempts to convert the response body to a string using the Content-Type
    * header. If no Content-Type header is specified (or it doesn't include an
    * encoding), we will assume it is UTF-8.
@@ -138,7 +162,7 @@
     if (ret == null) {
       return Collections.emptyList();
     } else {
-      return Collections.unmodifiableList(ret);
+      return ret;
     }
   }
 

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentFetcher.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentFetcher.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentFetcher.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentFetcher.java Mon Feb 11 20:17:18 2008
@@ -17,25 +17,24 @@
  */
 package org.apache.shindig.gadgets;
 
-import java.net.URL;
 
 public interface RemoteContentFetcher {
 
   /**
    * Fetch content using the HTTP GET method
-   * @param url Location of content to fetch
-   * @param options Additioanl options
+   * @param request The request to fetch.
+   * @param options Additional options
    * @return RemoteContent
    */
-  public RemoteContent fetch(URL url, ProcessingOptions options);
+  public RemoteContent fetch(RemoteContentRequest request,
+                             ProcessingOptions options);
 
   /**
    * Fetch content using the HTTP POST method
-   * @param url Location of content to fetch
-   * @param postData The data to post
-   * @param options Additioanl options
+   * @param request The request to fetch.
+   * @param options Additional options
    * @return RemoteContent
    */
-  public RemoteContent fetchByPost(URL url, byte[] postData,
-      ProcessingOptions options);
+  public RemoteContent fetchByPost(RemoteContentRequest request,
+                                   ProcessingOptions options);
 }

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentRequest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentRequest.java?rev=620700&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentRequest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RemoteContentRequest.java Mon Feb 11 20:17:18 2008
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.shindig.gadgets;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Holds request data for passing to a RemoteContentFetcher.
+ * Instances of this object are immutable.
+ *
+ * TODO: We should probably just stick the method in here. Having separate
+ * calls for POST vs. get seems unnecessary.
+ *
+ * TODO: This naming seems really ridiculous now. Why don't we just call it
+ * what it is -- an HTTP request?
+ */
+public class RemoteContentRequest {
+  private final byte[] postBody;
+
+  /**
+   * @return An input stream that can be used to read the post body.
+   */
+  public InputStream getPostBody() {
+    return new ByteArrayInputStream(postBody);
+  }
+
+  /**
+   * Retrieves the total length of the post body.
+   *
+   * @return The length of the post body.
+   */
+  public int getPostBodyLength() {
+    return postBody.length;
+  }
+
+  private final String contentType;
+  private final static String DEFAULT_CONTENT_TYPE
+      = "application/x-www-form-urlencoded; charset=utf-8";
+
+  /**
+   * @return The content type of the request (determined from request headers)
+   */
+  public String getContentType() {
+    return contentType;
+  }
+
+  private final Map<String, List<String>> headers;
+
+  /**
+   * @return All headers set in this request.
+   */
+  public Map<String, List<String>> getAllHeaders() {
+    return headers;
+  }
+
+  /**
+   * @param name The header to fetch
+   * @return A list of headers with that name (may be empty).
+   */
+  public List<String> getHeaders(String name) {
+    List<String> match = headers.get(name);
+    if (match == null) {
+      return Collections.emptyList();
+    } else {
+      return match;
+    }
+  }
+
+  /**
+   * @param name
+   * @return The first set header with the given name or null if not set. If
+   *         you need multiple values for the header, use getHeaders().
+   */
+  public String getHeader(String name) {
+    List<String> headerList = getHeaders(name);
+    if (headerList.size() == 0) {
+      return null;
+    } else {
+      return headerList.get(0);
+    }
+  }
+
+  private final URI uri;
+  public URI getUri() {
+    return uri;
+  }
+
+  /**
+   *
+   * @param uri
+   * @param headers
+   * @param postBody
+   */
+  public RemoteContentRequest(URI uri,
+                              Map<String, List<String>> headers,
+                              byte[] postBody) {
+    this.uri = uri;
+    // Copy the headers
+    if (headers == null) {
+      this.headers = Collections.emptyMap();
+    } else {
+      Map<String, List<String>> tmpHeaders
+          = new HashMap<String, List<String>>();
+      for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+        List<String> newList = new ArrayList<String>(entry.getValue());
+        tmpHeaders.put(entry.getKey(), Collections.unmodifiableList(newList));
+      }
+      this.headers = Collections.unmodifiableMap(tmpHeaders);
+    }
+    if (postBody == null) {
+      this.postBody = new byte[0];
+    } else {
+      this.postBody = new byte[postBody.length];
+      System.arraycopy(postBody, 0, this.postBody, 0, postBody.length);
+    }
+
+    // Calculate content type.
+    String type = getHeader("Content-Type");
+    if (type == null) {
+      contentType = DEFAULT_CONTENT_TYPE;
+    } else {
+      contentType = type;
+    }
+  }
+
+  public RemoteContentRequest(URI uri, Map<String, List<String>> headers) {
+    this(uri, headers, null);
+  }
+
+  public RemoteContentRequest(URI uri, byte[] postBody) {
+    this(uri, null, postBody);
+  }
+
+  public RemoteContentRequest(URI uri) {
+    this(uri, null, null);
+  }
+
+  @Override
+  public boolean equals(Object rhs) {
+    if (rhs == this) {return true;}
+    if (rhs instanceof RemoteContentRequest) {
+      RemoteContentRequest req = (RemoteContentRequest)rhs;
+      return uri.equals(req.uri) &&
+             Arrays.equals(postBody, req.postBody) &&
+             headers.equals(req.headers);
+    }
+    return false;
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CajaContentFilter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CajaContentFilter.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CajaContentFilter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CajaContentFilter.java Mon Feb 11 20:17:18 2008
@@ -59,8 +59,7 @@
         throw new UriCallbackException(externalReference);
       }
 
-      public URI rewrite(ExternalReference externalReference, String string)
-          throws UriCallbackException {
+      public URI rewrite(ExternalReference externalReference, String string) {
         return externalReference.getUri();
       }
     };

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingServlet.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/GadgetRenderingServlet.java Mon Feb 11 20:17:18 2008
@@ -38,6 +38,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -244,7 +245,8 @@
 
     // Preserve existing query string parameters.
     URI redirURI = gadget.getContentHref();
-    StringBuilder query = new StringBuilder(redirURI.getQuery());
+    String queryStr = redirURI.getQuery();
+    StringBuilder query = new StringBuilder(queryStr == null ? "" : queryStr);
 
     // TODO: userprefs on the fragment rather than query string
     query.append(getPrefsQueryString(gadget.getUserPrefValues()));
@@ -252,7 +254,8 @@
     String[] libs;
     String forcedLibs = options.getForcedJsLibs();
     if (forcedLibs == null) {
-      libs = (String[])gadget.getRequires().keySet().toArray();
+      Set<String> reqs = gadget.getRequires().keySet();
+      libs = reqs.toArray(new String[reqs.size()]);
     } else {
       libs = forcedLibs.split(":");
     }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java Mon Feb 11 20:17:18 2008
@@ -24,17 +24,22 @@
 import org.apache.shindig.gadgets.ProcessingOptions;
 import org.apache.shindig.gadgets.RemoteContent;
 import org.apache.shindig.gadgets.RemoteContentFetcher;
+import org.apache.shindig.gadgets.RemoteContentRequest;
+import org.apache.shindig.util.InputStreamConsumer;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLDecoder;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -44,9 +49,6 @@
 
 public class ProxyHandler {
   private static final String UNPARSEABLE_CRUFT = "throw 1; < don't be evil' >";
-  private static final int TWO_HOURS_IN_MS = 7200000;
-  private static final int ONE_HOUR_IN_SECS = 3600;
-  private static final int MAX_PROXY_SIZE = 1024 * 1024;
 
   private final RemoteContentFetcher fetcher;
 
@@ -58,15 +60,16 @@
                         HttpServletResponse response,
                         GadgetSigner signer)
       throws ServletException, IOException {
-
     GadgetToken token = extractAndValidateToken(request, signer);
-    URL originalUrl = extractAndValidateUrl(request);
+    String url = request.getParameter("url");
+    URL originalUrl = validateUrl(url);
     URL signedUrl = signUrl(originalUrl, token, request);
 
     // Fetch the content and convert it into JSON.
     // TODO: Fetcher needs to handle variety of HTTP methods.
-    RemoteContent results = fetchContent(signedUrl, request,
-        new ProcessingOptions());
+    RemoteContent results = fetchContent(signedUrl,
+                                         request,
+                                         new HttpProcessingOptions(request));
 
     response.setStatus(results.getHttpStatusCode());
     if (results.getHttpStatusCode() == HttpServletResponse.SC_OK) {
@@ -76,8 +79,7 @@
         JSONObject resp = new JSONObject()
             .put("body", results.getResponseAsString())
             .put("rc", results.getHttpStatusCode());
-        String json = new JSONObject()
-            .put(request.getParameter("url"), resp).toString();
+        String json = new JSONObject().put(url, resp).toString();
         output = UNPARSEABLE_CRUFT + json;
       } catch (JSONException e) {
         output = "";
@@ -86,8 +88,7 @@
       setCachingHeaders(response);
       response.setContentType("application/json; charset=utf-8");
       response.setHeader("Content-Disposition", "attachment;filename=p.txt");
-      PrintWriter pw = response.getWriter();
-      pw.write(output);
+      response.getWriter().write(output);
     }
   }
 
@@ -95,14 +96,14 @@
                     HttpServletResponse response,
                     GadgetSigner signer)
       throws ServletException, IOException {
-
     GadgetToken token = extractAndValidateToken(request, signer);
-    URL originalUrl = extractAndValidateUrl(request);
+    URL originalUrl = validateUrl(request.getParameter("url"));
     URL signedUrl = signUrl(originalUrl, token, request);
 
     // TODO: Fetcher needs to handle variety of HTTP methods.
-    RemoteContent results = fetchContent(signedUrl, request,
-        new ProcessingOptions());
+    RemoteContent results = fetchContent(signedUrl,
+                                         request,
+                                         new HttpProcessingOptions(request));
 
     int status = results.getHttpStatusCode();
     response.setStatus(status);
@@ -123,56 +124,114 @@
           }
         }
       }
-      response.getOutputStream().write(results.getByteArray());
+      response.getOutputStream().write(
+          InputStreamConsumer.readToByteArray(results.getResponse()));
     }
   }
 
   /**
    * Fetch the content for a request
    */
-  private RemoteContent fetchContent(URL signedUrl, HttpServletRequest request,
-      ProcessingOptions procOptions) throws ServletException {
+  @SuppressWarnings("unchecked")
+  private RemoteContent fetchContent(URL signedUrl,
+                                     HttpServletRequest request,
+                                     ProcessingOptions procOptions)
+      throws ServletException {
+    String encoding = request.getCharacterEncoding();
+    if (encoding == null) {
+      encoding = "UTF-8";
+    }
     try {
       if ("POST".equals(request.getMethod())) {
-        String data = request.getParameter("postData");
-        return fetcher.fetchByPost(signedUrl,
-            URLDecoder.decode(data, request.getCharacterEncoding()).getBytes(),
-            procOptions);
+        String method = getParameter(request, "httpMethod", "GET");
+        String postData = URLDecoder.decode(
+            getParameter(request, "postData", ""), encoding);
+
+        Map<String, List<String>> headers;
+        String headerData = request.getParameter("headers");
+        if (headerData == null) {
+          headers = Collections.emptyMap();
+        } else {
+          if (headerData.length() == 0) {
+            headers = Collections.emptyMap();
+          } else {
+            // We actually only accept single key value mappings now.
+            headers = new HashMap<String, List<String>>();
+            String[] headerList = headerData.split("&");
+            for (String header : headerList) {
+              String[] parts = header.split("=");
+              if (parts.length != 2) {
+                // Malformed headers
+                return RemoteContent.ERROR;
+              }
+              headers.put(URLDecoder.decode(parts[0], encoding),
+                  Arrays.asList(URLDecoder.decode(parts[1], encoding)));
+            }
+          }
+        }
+
+        removeUnsafeHeaders(headers);
+
+        RemoteContentRequest req = new RemoteContentRequest(
+            signedUrl.toURI(), headers, postData.getBytes());
+        if ("POST".equals(method)) {
+          return fetcher.fetchByPost(req, procOptions);
+        } else {
+          return fetcher.fetch(req, procOptions);
+        }
       } else {
-        return fetcher.fetch(signedUrl, new ProcessingOptions());
+        Map<String, List<String>> headers = new HashMap<String, List<String>>();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        while (headerNames.hasMoreElements()) {
+          String header = headerNames.nextElement();
+          headers.put(header, Collections.list(request.getHeaders(header)));
+        }
+        removeUnsafeHeaders(headers);
+        RemoteContentRequest req
+            = new RemoteContentRequest(signedUrl.toURI(), headers);
+        return fetcher.fetch(req, procOptions);
       }
-    } catch (UnsupportedEncodingException uee) {
-      throw new ServletException(uee);
+    } catch (UnsupportedEncodingException e) {
+      throw new ServletException(e);
+    } catch (URISyntaxException e) {
+      throw new ServletException(e);
     }
   }
 
   /**
-   * Gets the url= parameter from the request and applies some basic sanity
-   * checking.
+   * Removes unsafe headers from the header set.
+   * @param headers
+   */
+  private void removeUnsafeHeaders(Map<String, List<String>> headers) {
+    // Host must be removed.
+    final String[] badHeaders = new String[] {
+        // No legitimate reason to over ride these.
+        // TODO: We probably need to test variations as well.
+        "Host", "Accept-Encoding", "Accept"
+    };
+    for (String bad : badHeaders) {
+      headers.remove(bad);
+    }
+  }
+
+  /**
+   * Validates that the given url is valid for this reques.t
    *
-   * @param request The HTTP request from the browser.
+   * @param url
    * @return A URL object of the URL
    * @throws ServletException if the URL fails security checks or is malformed.
    */
-  private URL extractAndValidateUrl(HttpServletRequest request)
-      throws ServletException {
-    String url = request.getParameter("url");
+  private URL validateUrl(String url) throws ServletException {
     if (url == null) {
-      throw new ServletException("Missing url parameter");
+      throw new ServletException("url parameter is missing.");
     }
-
     try {
-      URI origin = new URI(request.getParameter("url"));
+      URI origin = new URI(url);
       if (origin.getScheme() == null) {
-        // No scheme, assume it was double-encoded.
-        origin = new URI(URLDecoder.decode(request.getParameter("url"),
-                         request.getCharacterEncoding()));
-        if (origin.getScheme() == null) {
-          throw new ServletException("Invalid URL " + origin.toString());
-        }
+        throw new ServletException("Invalid URL " + origin.toString());
       }
       if (!origin.getScheme().equals("http")) {
-        throw new ServletException("Unsupported protocol: " + origin.getScheme());
+        throw new ServletException("Unsupported scheme: " + origin.getScheme());
       }
       if (origin.getPath() == null || origin.getPath().length() == 0) {
         // Forcibly set the path to "/" if it is empty
@@ -187,8 +246,6 @@
       throw new ServletException("Malformed URL " + use.getMessage());
     } catch (MalformedURLException mfe) {
       throw new ServletException("Malformed URL " + mfe.getMessage());
-    } catch (UnsupportedEncodingException uee) {
-      throw new ServletException("Unsupported encoding " + uee.getMessage());
     }
   }
 
@@ -202,10 +259,7 @@
       if (signer == null) {
         return null;
       }
-      String token = request.getParameter("st");
-      if (token == null) {
-        token = "";
-      }
+      String token = getParameter(request, "st", "");
       return signer.createToken(token);
     } catch (GadgetException ge) {
       throw new ServletException(ge);
@@ -213,34 +267,43 @@
   }
 
   /**
-   * Sets HTTP headers that instruct the browser to cache for 2 hours.
+   * Sets HTTP caching headers
    *
    * @param response The HTTP response
    */
   private void setCachingHeaders(HttpServletResponse response) {
-    // TODO: figure out why we're not using the same amount of time for these
-    // headers.
-    response.setHeader("Cache-Control", "public,max-age=" + ONE_HOUR_IN_SECS);
-    response.setDateHeader("Expires", System.currentTimeMillis()
-                                     + TWO_HOURS_IN_MS);
+    // TODO: Re-implement caching behavior if appropriate.
+    response.setHeader("Cache-Control", "private; max-age=0");
+    response.setDateHeader("Expires", System.currentTimeMillis() - 30);
   }
 
   /**
    * Sign a URL with a GadgetToken if needed
-   * @return
+   * @return The signed url.
    */
+  @SuppressWarnings("unchecked")
   private URL signUrl(URL originalUrl, GadgetToken token,
       HttpServletRequest request) throws ServletException {
     try {
-      if (token == null ||
-          !"signed".equals(request.getParameter("authz"))) {
+      if (token == null || !"signed".equals(request.getParameter("authz"))) {
         return originalUrl;
       }
-      return token.signUrl(originalUrl, "GET", // TODO: request.getMethod()
-          request.getParameterMap());
+      String method = getParameter(request, "httpMethod", "GET");
+      return token.signUrl(originalUrl, method, request.getParameterMap());
     } catch (GadgetException ge) {
       throw new ServletException(ge);
     }
   }
 
+  /**
+   * Extracts the first parameter from the parameter map with the given name.
+   * @param request
+   * @param name
+   * @return The parameter, if found, or defaultValue
+   */
+  private static String getParameter(HttpServletRequest request,
+                                     String name, String defaultValue) {
+    String ret = request.getParameter(name);
+    return ret == null ? defaultValue : ret;
+  }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcServlet.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcServlet.java Mon Feb 11 20:17:18 2008
@@ -19,9 +19,10 @@
 
 package org.apache.shindig.gadgets.http;
 
+import org.apache.shindig.util.InputStreamConsumer;
+
 import org.json.JSONObject;
 
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.logging.Level;
@@ -58,19 +59,12 @@
       return;
     }
 
-    ServletInputStream reader = request.getInputStream();
-    ByteArrayOutputStream os = new ByteArrayOutputStream();
-    byte[] buf = new byte[1024 * 8];
-    int read = 0;
-
-    while ((read = reader.read(buf, 0, buf.length)) > 0) {
-      os.write(buf, 0, read);
-      if (os.size() > length) {
-        // Bad request, we're leaving now.
-        logger.info("Wrong size. Length: " + length + " real: " + os.size());
-        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-        return;
-      }
+    ServletInputStream is = request.getInputStream();
+    byte[] body = InputStreamConsumer.readToByteArray(is, length);
+    if (body.length != length) {
+      logger.info("Wrong size. Length: " + length + " real: " + body.length);
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      return;
     }
 
     try {
@@ -78,7 +72,7 @@
       if (encoding == null) {
         encoding = "UTF-8";
       }
-      String postBody = new String(os.toByteArray(), encoding);
+      String postBody = new String(body, encoding);
       JsonRpcRequest req = new JsonRpcRequest(postBody);
       JSONObject out = req.process(state);
       response.setStatus(HttpServletResponse.SC_OK);

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/InputStreamConsumer.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/InputStreamConsumer.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/InputStreamConsumer.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/util/InputStreamConsumer.java Mon Feb 11 20:17:18 2008
@@ -21,47 +21,88 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
 
 /**
- * Used to consume an entire input stream. Don't use this for network
- * streams or any other stream that doesn't have a known length. This is
- * intended for reading resources from jars and the local file system only.
+ * Used to consume entire input streams and transform them into data buffers.
+ * These are all blocking routines and should never be called from a thread
+ * that will cause deadlock.
  */
 public class InputStreamConsumer {
 
   /**
-   * Loads content and returns it as a raw byte array.
+   * Consumes the entire contents of the stream. Only safe to use if you are
+   * sure that you're consuming a fixed-size buffer.
    * @param is
    * @return The contents of the stream.
    * @throws IOException on stream reading error.
    */
   public static byte[] readToByteArray(InputStream is) throws IOException {
-    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-    byte[] buf = new byte[8192];
-    int read = 0;
+    return readToByteArray(is, Integer.MAX_VALUE);
+  }
 
-    while ((read = is.read(buf)) > 0) {
-      baos.write(buf, 0, read);
+  /**
+   * Reads at most maxBytes bytes from the stream.
+   * @param is
+   * @param maxBytes
+   * @return The bytes that were read
+   * @throws IOException
+   */
+  public static byte[] readToByteArray(InputStream is, int maxBytes)
+      throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    byte[] chunk = new byte[8192];
+    int chunkSize;
+    while (out.size() < maxBytes && (chunkSize = is.read(chunk)) != -1) {
+      out.write(chunk, 0, chunkSize);
     }
-
-    return baos.toByteArray();
+    return out.toByteArray();
   }
 
   /**
    * Loads content from the given input stream as a UTF-8-encoded string.
+   * Use only when you're sure of the finite length of the input stream.
+   * If you're not sure, use {@code readToString(InputStream, maxBytes)}.
    *
    * @param is
    * @return The contents of the stream.
    * @throws IOException on stream reading error.
    */
   public static String readToString(InputStream is) throws IOException {
-    byte[] bytes = readToByteArray(is);
+    return readToString(is, Integer.MAX_VALUE);
+  }
+
+  /**
+   * Loads content from the given input stream as a UTF-8-encoded string.
+   *
+   * @param is
+   * @return The contents of the stream.
+   * @throws IOException on stream reading error.
+   */
+  public static String readToString(InputStream is, int maxBytes)
+      throws IOException {
+    byte[] bytes = readToByteArray(is, maxBytes);
     try {
       return new String(bytes, "UTF-8");
     } catch (UnsupportedEncodingException e) {
       // UTF-8 is required by the Java spec.
       throw new RuntimeException("UTF-8 not supported!", e);
     }
+  }
+
+  /**
+   * Consumes all of is and sends it to os. This is not the same as
+   * Piped Input / Output streams because it reads the entire input first.
+   * This means that you won't get deadlock, but it also means that this is
+   * not necessarily suitable for normal piping tasks. Use a piped stream for
+   * that sort of work.
+   *
+   * @param is
+   * @param os
+   * @throws IOException
+   */
+  public static void pipe(InputStream is, OutputStream os) throws IOException {
+    os.write(readToByteArray(is));
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetServerTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetServerTest.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetServerTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetServerTest.java Mon Feb 11 20:17:18 2008
@@ -72,8 +72,10 @@
     RemoteContent results = new RemoteContent(200, DATETIME_XML.getBytes(), null);
     ProcessingOptions options = new ProcessingOptions();
 
+    RemoteContentRequest req = new RemoteContentRequest(DATETIME_URI);
+
     expect(specCache.get(eq(DATETIME_URI_STRING))).andReturn(null);
-    expect(fetcher.fetch(eq(DATETIME_URI.toURL()),
+    expect(fetcher.fetch(eq(req),
                          eq(options))).andReturn(results);
     specCache.put(eq(DATETIME_URI_STRING), isA(GadgetSpec.class));
     replay();
@@ -99,8 +101,10 @@
     RemoteContent results = new RemoteContent(200, DATETIME_XML.getBytes(), null);
     ProcessingOptions options = new ProcessingOptions();
 
+    RemoteContentRequest req = new RemoteContentRequest(DATETIME_URI);
+
     expect(specCache.get(eq(DATETIME_URI_STRING))).andReturn(null);
-    expect(fetcher.fetch(eq(DATETIME_URI.toURL()),
+    expect(fetcher.fetch(eq(req),
                          eq(options))).andReturn(results);
     specCache.put(eq(DATETIME_URI_STRING), isA(GadgetSpec.class));
     replay();

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/RemoteContentTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/RemoteContentTest.java?rev=620700&r1=620699&r2=620700&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/RemoteContentTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/RemoteContentTest.java Mon Feb 11 20:17:18 2008
@@ -20,6 +20,9 @@
 
 import junit.framework.TestCase;
 
+import org.apache.shindig.util.InputStreamConsumer;
+
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
@@ -78,12 +81,13 @@
     assertEquals("\u4F60\u597D", content.getResponseAsString());
   }
 
-  public void testPreserveBinaryData() {
+  public void testPreserveBinaryData() throws Exception {
     byte[] data = new byte[] {
         (byte)0x00, (byte)0xDE, (byte)0xEA, (byte)0xDB, (byte)0xEE, (byte)0xF0
     };
     addHeader("Content-Type", "application/octet-stream");
     RemoteContent content = new RemoteContent(200, data, headers);
-    assertEquals(data, content.getByteArray());
+    byte[] out = InputStreamConsumer.readToByteArray(content.getResponse());
+    assertTrue(Arrays.equals(data, out));
   }
 }