You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by li...@apache.org on 2010/05/04 21:45:53 UTC

svn commit: r941012 - in /shindig/trunk/java: common/src/main/java/org/apache/shindig/common/servlet/ common/src/main/java/org/apache/shindig/common/util/ common/src/main/java/org/apache/shindig/protocol/ common/src/test/java/org/apache/shindig/common/...

Author: lindner
Date: Tue May  4 19:45:53 2010
New Revision: 941012

URL: http://svn.apache.org/viewvc?rev=941012&view=rev
Log:
Add jsonp support for json-rpc requests by using the 'request' and 'callback' parameters

Modified:
    shindig/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java
    shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java
    shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java
    shindig/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java

Modified: shindig/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java?rev=941012&r1=941011&r2=941012&view=diff
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java (original)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java Tue May  4 19:45:53 2010
@@ -20,7 +20,9 @@ package org.apache.shindig.common.servle
 
 import org.apache.shindig.common.util.TimeSource;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.regex.Pattern;
 
 /**
  * Collection of HTTP utilities
@@ -107,4 +109,24 @@ public class HttpUtil {
   public static void setDefaultTtl(int defaultTtl) {
     HttpUtil.defaultTtl = defaultTtl;
   }
-}
+
+
+  static final Pattern GET_REQUEST_CALLBACK_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_\\.]+");
+
+  public static boolean isJSONP(HttpServletRequest request) throws IllegalArgumentException {
+    String callback = request.getParameter("callback");
+
+    // Must be a GET
+    if (!"GET".equals(request.getMethod()))
+      return false;
+    
+    // No callback specified
+    if (callback == null) return false;
+
+    if (!GET_REQUEST_CALLBACK_PATTERN.matcher(callback).matches()) {
+       throw new IllegalArgumentException("Wrong format for parameter 'callback' specified. Must match: " +
+            GET_REQUEST_CALLBACK_PATTERN.toString());
+    }
+    return true;
+  }
+}
\ No newline at end of file

Modified: shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java?rev=941012&r1=941011&r2=941012&view=diff
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java (original)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java Tue May  4 19:45:53 2010
@@ -25,14 +25,13 @@ import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import javax.servlet.http.HttpServletRequest;
-
 /**
  * Utilities for converting a JSON object to and from a URL encoding
  */
@@ -44,10 +43,13 @@ public class JsonConversionUtil {
 
   @SuppressWarnings("unchecked")
   public static JSONObject fromRequest(HttpServletRequest request) throws JSONException {
-    //String methodName = request.getPathInfo().replaceAll("/", "");
+    Map<String, String[]> params = request.getParameterMap();
+
+    if (!params.containsKey("method")) {
+      return null;
+    }
 
     JSONObject root = new JSONObject();
-    Map<String, String[]> params = request.getParameterMap();
     root.put("method", params.get("method")[0]);
     if (params.containsKey("id")) {
       root.put("id", params.get("id")[0]);

Modified: shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java?rev=941012&r1=941011&r2=941012&view=diff
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java (original)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java Tue May  4 19:45:53 2010
@@ -20,6 +20,7 @@ package org.apache.shindig.protocol;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.servlet.HttpUtil;
 import org.apache.shindig.common.util.JsonConversionUtil;
 import org.apache.shindig.protocol.multipart.FormDataItem;
 import org.apache.shindig.protocol.multipart.MultipartFormParser;
@@ -36,6 +37,7 @@ import org.json.JSONObject;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.io.Writer;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -43,6 +45,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Future;
+import java.util.regex.Pattern;
 
 /**
  * JSON-RPC handler servlet.
@@ -72,85 +75,100 @@ public class JsonRpcServlet extends ApiS
   }
   
   @Override
-  protected void doGet(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
+  protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
       throws IOException {
     setCharacterEncodings(servletRequest, servletResponse);
     servletResponse.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
 
+    // only GET/POST
+    String method = servletRequest.getMethod();
+
+    if (!("GET".equals(method) || "POST".equals(method))) {
+      sendError(servletResponse, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "Only POST/GET"));
+      return;
+    }
+
     SecurityToken token = getSecurityToken(servletRequest);
     if (token == null) {
       sendSecurityError(servletResponse);
       return;
     }
 
-    try {
-      JSONObject request = JsonConversionUtil.fromRequest(servletRequest);
-      dispatch(request, null, servletRequest, servletResponse, token);
-    } catch (JSONException je) {
-      sendJsonParseError(je, servletResponse);
-    }
-  }
-
-  @Override
-  protected void doPost(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
-      throws IOException {
-    setCharacterEncodings(servletRequest, servletResponse);
-    servletResponse.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
 
     try {
-      checkContentTypes(ALLOWED_CONTENT_TYPES, servletRequest.getContentType());
-      SecurityToken token = getSecurityToken(servletRequest);
-      if (token == null) {
-        sendSecurityError(servletResponse);
-        return;
-      }
-
       String content = null;
-      Map<String, FormDataItem> formItems = Collections.emptyMap();  // default for most requests
+      String callback = null; // for JSONP
+      Map<String,FormDataItem> formData = Maps.newHashMap();
 
-      if (formParser.isMultipartContent(servletRequest)) {
-        formItems = new HashMap<String, FormDataItem>();
-        for (FormDataItem item : formParser.parse(servletRequest)) {
-          if (item.isFormField() && REQUEST_PARAM.equals(item.getFieldName()) && content == null) {
-            // As per spec, in case of a multipart/form-data content, there will be one form field
-            // with field name as "request". It will contain the json request. Any further form
-            // field or file item will not be parsed out, but will be exposed via getFormItem
-            // method of RequestItem.
-            if (!StringUtils.isEmpty(item.getContentType())) {
-              checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES, item.getContentType());
-            }
-            content = item.getAsString();
-          } else {
-            formItems.put(item.getFieldName(), item);
-          }
-        }
+      // Get content or deal with JSON-RPC GET
+      if ("POST".equals(method)) {
+        content = getPostContent(servletRequest, formData);
+      } else if (HttpUtil.isJSONP(servletRequest)) {
+        content = servletRequest.getParameter("request");
+        callback = servletRequest.getParameter("callback");
+      } else {
+        // GET request, fromRequest() creates the json objects directly.
+        JSONObject request = JsonConversionUtil.fromRequest(servletRequest);
 
-        if (content == null) {
-          content = "";
+        if (request != null) {
+          dispatch(request, formData, servletRequest, servletResponse, token, null);
+          return;
         }
-      } else {
-        content = IOUtils.toString(servletRequest.getInputStream(),
-            servletRequest.getCharacterEncoding());
+      }
+      
+      if (content == null) {
+        sendError(servletResponse, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "No content specified"));
+        return;
       }
 
-      if ((content.indexOf('[') != -1) && content.indexOf('[') < content.indexOf('{')) {
-        // Is a batch
+      if (isContentJsonBatch(content)) {
         JSONArray batch = new JSONArray(content);
-        dispatchBatch(batch, formItems, servletRequest, servletResponse, token);
+        dispatchBatch(batch, formData, servletRequest, servletResponse, token, callback);
       } else {
         JSONObject request = new JSONObject(content);
-        dispatch(request, formItems, servletRequest, servletResponse, token);
+        dispatch(request, formData, servletRequest, servletResponse, token, callback);
       }
+      return;
     } catch (JSONException je) {
       sendJsonParseError(je, servletResponse);
-    } catch (ContentTypes.InvalidContentTypeException icte) {
+    } catch (IllegalArgumentException e) {
+      // a bad jsonp request..
+      sendBadRequest(e, servletResponse);
+    }  catch (ContentTypes.InvalidContentTypeException icte) {
       sendBadRequest(icte, servletResponse);
     }
   }
 
+  protected String getPostContent(HttpServletRequest request, Map<String,FormDataItem> formItems)
+      throws ContentTypes.InvalidContentTypeException, IOException {
+    String content = null;
+
+    checkContentTypes(ALLOWED_CONTENT_TYPES, request.getContentType());
+
+    if (formParser.isMultipartContent(request)) {
+      for (FormDataItem item : formParser.parse(request)) {
+        if (item.isFormField() && REQUEST_PARAM.equals(item.getFieldName()) && content == null) {
+          // As per spec, in case of a multipart/form-data content, there will be one form field
+          // with field name as "request". It will contain the json request. Any further form
+          // field or file item will not be parsed out, but will be exposed via getFormItem
+          // method of RequestItem.
+          if (!StringUtils.isEmpty(item.getContentType())) {
+            checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES, item.getContentType());
+          }
+          content = item.getAsString();
+        } else {
+          formItems.put(item.getFieldName(), item);
+        }
+      }
+    } else {
+      content = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
+    }
+    return content;
+  }
+
   protected void dispatchBatch(JSONArray batch, Map<String, FormDataItem> formItems ,
       HttpServletRequest servletRequest, HttpServletResponse servletResponse,
-      SecurityToken token) throws JSONException, IOException {
+      SecurityToken token, String callback) throws JSONException, IOException {
     // Use linked hash map to preserve order
     List<Future<?>> responses = Lists.newArrayListWithCapacity(batch.length());
 
@@ -174,14 +192,19 @@ public class JsonRpcServlet extends ApiS
       }
       result.add(getJSONResponse(key, getResponseItem(responses.get(i))));
     }
-    
-    jsonConverter.append(servletResponse.getWriter(), result);
+
+    // Generate the output
+    Writer writer = servletResponse.getWriter();
+    if (callback != null) writer.append(callback+'(');
+    jsonConverter.append(writer, result);
+    if (callback != null) writer.append(");\n");
   }
 
   protected void dispatch(JSONObject request, Map<String, FormDataItem> formItems,
       HttpServletRequest servletRequest, HttpServletResponse servletResponse,
-      SecurityToken token) throws JSONException, IOException {
+      SecurityToken token, String callback) throws JSONException, IOException {
     String key = null;
+
     if (request.has("id")) {
       key = request.getString("id");
     }
@@ -194,10 +217,24 @@ public class JsonRpcServlet extends ApiS
     ResponseItem response = getResponseItem(future);
     Object result = getJSONResponse(key, response);
 
-    jsonConverter.append(servletResponse.getWriter(), result);
+    // Generate the output
+    Writer writer = servletResponse.getWriter();
+    if (callback != null) writer.append(callback+'(');
+    jsonConverter.append(writer, result);
+    if (callback != null) writer.append(");\n");
   }
 
   /**
+   * Determine if the content contains a batch request
+   *
+   * @param content json content or null
+   * @return true if content contains is a json array, not a json object or null
+   */
+  private boolean isContentJsonBatch(String content) {
+    if (content == null) return false;
+    return ((content.indexOf('[') != -1) && content.indexOf('[') < content.indexOf('{'));
+  }
+  /**
    * Wrap call to dispatcher to allow for implementation specific overrides
    * and servlet-request contextual handling
    */
@@ -295,7 +332,7 @@ public class JsonRpcServlet extends ApiS
 
   private void sendBadRequest(Throwable t, HttpServletResponse response) throws IOException {
     sendError(response, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST,
-        "Invalid batch - " + t.getMessage()));
+        "Invalid input - " + t.getMessage()));
   }
 
   private void sendJsonParseError(JSONException e, HttpServletResponse response) throws IOException {

Modified: shindig/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java?rev=941012&r1=941011&r2=941012&view=diff
==============================================================================
--- shindig/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java (original)
+++ shindig/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java Tue May  4 19:45:53 2010
@@ -17,9 +17,12 @@
  */
 package org.apache.shindig.common.util;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -27,6 +30,7 @@ import org.json.JSONObject;
 import org.junit.Assert;
 import org.junit.Test;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.Map;
 
 /**
@@ -118,6 +122,15 @@ public class JsonConversionUtilTest exte
     assertEquals("hello", resultMap.get(".a.b(1).c"));
   }
 
+  @Test
+  public void testJsonFromRequest() throws Exception {
+    HttpServletRequest fakeRequest;
+    for (String badParms : ImmutableList.of("x=1", "x=1&callback=")) {
+      fakeRequest = new FakeHttpServletRequest("http://foo.com/gadgets/rpc?" + badParms);
+      assertNull(JsonConversionUtil.fromRequest(fakeRequest));
+    }
+   }
+
   public static void assertJsonEquals(Object expected, Object actual)
       throws JSONException {
     if (expected == null) {

Modified: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java?rev=941012&r1=941011&r2=941012&view=diff
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java (original)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java Tue May  4 19:45:53 2010
@@ -19,6 +19,7 @@
 package org.apache.shindig.gadgets.servlet;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
 import org.apache.shindig.common.servlet.InjectedServlet;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -42,8 +43,6 @@ import java.util.regex.Pattern;
 public class RpcServlet extends InjectedServlet {
   static final String GET_REQUEST_REQ_PARAM = "req";
   static final String GET_REQUEST_CALLBACK_PARAM = "callback";
-  // Starts with alpha or underscore, followed by alphanum, underscore or period
-  static final Pattern GET_REQUEST_CALLBACK_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_\\.]+");
 
   private static final int POST_REQUEST_MAX_SIZE = 1024 * 128;
   private static final Logger logger = Logger.getLogger("org.apache.shindig.gadgets");
@@ -62,14 +61,9 @@ public class RpcServlet extends Injected
     String callbackValue;
 
     try {
+      HttpUtil.isJSONP(request);
       reqValue = validateParameterValue(request, GET_REQUEST_REQ_PARAM);
       callbackValue = validateParameterValue(request, GET_REQUEST_CALLBACK_PARAM);
-      if (!GET_REQUEST_CALLBACK_PATTERN.matcher(callbackValue).matches()) {
-        throw new IllegalArgumentException("Wrong format for parameter '" +
-            GET_REQUEST_CALLBACK_PARAM + "' specified. Expected: " +
-            GET_REQUEST_CALLBACK_PATTERN.toString());
-      }
-
     } catch (IllegalArgumentException e) {
       response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
       logger.log(Level.INFO, e.getMessage(), e);
@@ -150,5 +144,4 @@ public class RpcServlet extends Injected
       return success;
     }
   }
-
 }