Author: awiner
Date: Wed Apr  1 19:10:52 2009
New Revision: 761018

URL: http://svn.apache.org/viewvc?rev=761018&view=rev
Log:
SHINDIG-1002: HttpRequest format="text" does not function
and
SHINDIG-739: Data Pipelining (in part)

Follow revised spec of os:HttpRequest output format, specifically:
   - Move content into a "content" property
   - Include headers (a limited set) and http status code
   - Support JSON arrays in addition to text
   - Support HTTP errors - report them in an "error" block
   - If JSON parsing fails, treat as a 406 error
The above makes resolving SHINDIG-1002 tractable, and:

      <script xmlns:os="http://ns.opensocial.org/2008/markup"; 
type="text/os-data">
        <os:HttpRequest key="google" href="http://www.google.com"; 
format="text"/>
      </script>

      <script type="text/os-template" 
xmlns:os="http://ns.opensocial.org/2008/markup";>
        <os:Html code="${google.content}"/>
     </script>

... now sorta works (except that there isn't any content fixup for relative 
URLs).

Write lots of tests for the HTTP pipeline format, both unit and end-to-end.

Modified:
    
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
    
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
    
incubator/shindig/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
    
incubator/shindig/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml

Modified: 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java?rev=761018&r1=761017&r2=761018&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
 Wed Apr  1 19:10:52 2009
@@ -42,12 +42,14 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Callable;
 
 import javax.el.ELResolver;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.inject.Inject;
 
@@ -59,6 +61,9 @@
   private final ContainerConfig config;
 
   private static final Charset UTF8 = Charset.forName("UTF-8");
+  private static Set<String> HTTP_RESPONSE_HEADERS =
+    ImmutableSet.of("content-type", "location", "set-cookie");
+  
   private final Expressions expressions;
 
   @Inject
@@ -227,32 +232,50 @@
 
       public Data(HttpResponse response) {
         String format = preload.getAttributes().get("format");
-        JSONObject data = new JSONObject();
+        JSONObject wrapper = new JSONObject();
 
         try {
-          data.put("id", key);
-
-          if (format == null || "json".equals(format)) {
-            try {
-              // TODO: support arrays and objects
-              data.put("data", new JSONObject(response.getResponseAsString()));
-            } catch (JSONException je) {
-              data.put("code", 500);
-              data.put("message", je.getMessage());
-            }
+          wrapper.put("id", key);
+          if (response.getHttpStatusCode() >= 400) {
+            wrapper.put("error", createJSONError(response.getHttpStatusCode(), 
null, response));
           } else {
-            if (response.isError()) {
-              data.put("code", response.getHttpStatusCode());
-              data.put("message", response.getResponseAsString());
+            // Create {data: {status: [CODE], content: {...}|[...]|"...", 
headers:{...}}} 
+            JSONObject data = new JSONObject();
+            wrapper.put("data", data);
+
+            // Add the status
+            data.put("status", response.getHttpStatusCode());
+            String responseText = response.getResponseAsString();
+            
+            // Add allowed headers
+            JSONObject headers = createJSONHeaders(response);
+            if (headers != null) {
+              data.put("headers", headers);
+            }
+            
+            // And add the parsed content
+            if (format == null || "json".equals(format)) {
+              try {
+                if (responseText.startsWith("[")) {
+                  data.put("content", new JSONArray(responseText));
+                } else {
+                  data.put("content", new JSONObject(responseText));
+                }
+              } catch (JSONException je) {
+                // JSON parse failed: create a 406 error, and remove the 
"data" section
+                wrapper.remove("data");
+                wrapper.put("error", createJSONError(
+                    HttpResponse.SC_NOT_ACCEPTABLE, je.getMessage(), 
response));
+              }
             } else {
-              data.put("data", response.getResponseAsString());
+              data.put("content", responseText);
             }
           }
         } catch (JSONException outerJe) {
           throw new RuntimeException(outerJe);
         }
 
-        this.data = data;
+        this.data = wrapper;
       }
 
       public Collection<Object> toJson() {
@@ -261,6 +284,54 @@
     }
   }
 
+  private static JSONObject createJSONHeaders(HttpResponse response)
+      throws JSONException {
+    JSONObject headers = null;
+    
+    // Add allowed headers
+    for (String header: HTTP_RESPONSE_HEADERS) {
+      Collection<String> values = response.getHeaders(header);
+      if (values != null && !values.isEmpty()) {
+        JSONArray array = new JSONArray();
+        for (String value : values) {
+          array.put(value);
+        }
+
+        if (headers == null) {
+          headers = new JSONObject();
+        }
+
+        headers.put(header, array);
+      }
+    }
+    
+    return headers;
+  }
+  
+  /**
+   * Create {error: { code: [CODE], data: {content: "....", headers: {...}}}}
+   */
+  private static JSONObject createJSONError(int code, String message, 
HttpResponse response)
+      throws JSONException {
+    JSONObject error = new JSONObject();
+    error.put("code", code);
+    if (message != null) {
+      error.put("message", message);
+    }
+
+    JSONObject data = new JSONObject();
+    error.put("data", data);
+    data.put("content", response.getResponseAsString());
+
+    // Add allowed headers
+    JSONObject headers = createJSONHeaders(response);
+    if (headers != null) {
+      data.put("headers", headers);
+    }
+    
+    return error;
+  }
+  
   private Uri getSocialUri(GadgetContext context) {
     String jsonUri = config.getString(context.getContainer(), 
"gadgets.osDataUri");
     Preconditions.checkNotNull(jsonUri, "No JSON URI available for social 
preloads");

Modified: 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java?rev=761018&r1=761017&r2=761018&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
 (original)
+++ 
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
 Wed Apr  1 19:10:52 2009
@@ -19,6 +19,7 @@
 package org.apache.shindig.gadgets.preload;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import org.apache.shindig.config.ContainerConfig;
@@ -68,6 +69,13 @@
       + "  <os:HttpRequest key=\"p\" href=\"" + HTTP_REQUEST_URL + "\" "
       + "refreshInterval=\"60\" method=\"POST\"/>" + "</Content></Module>";
 
+  private static final String XML_WITH_HTTP_REQUEST_FOR_TEXT = "<Module 
xmlns:os=\""
+    + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+    + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\"; view=\"profile\">"
+    + "  <os:HttpRequest key=\"p\" format=\"text\" href=\"" + HTTP_REQUEST_URL 
+ "\" "
+    + "refreshInterval=\"60\" method=\"POST\"/>" + "</Content></Module>";
+
   private static final String XML_WITH_HTTP_REQUEST_AND_PARAMS = "<Module 
xmlns:os=\""
     + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
     + "<ModulePrefs title=\"Title\"/>"
@@ -141,11 +149,88 @@
   }
 
   @Test
-  public void testHttpPreload() throws Exception {
-    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML_WITH_HTTP_REQUEST);
+  public void testHttpPreloadOfJsonObject() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("{foo: 'bar'}")
+        .create();
+    String expectedResult = "{data: {status: 200, content: {foo: 'bar'}}, id: 
'p'}";
 
-    String httpResult = "{foo: 'bar'}";
-    RecordingRequestPipeline pipeline = new 
RecordingRequestPipeline(httpResult);
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadOfJsonArrayWithHeaders() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("[1, 2]")
+        .addHeader("content-type", "application/json")
+        .addHeader("set-cookie", "cookiecookie")
+        .addHeader("not-ok", "shouldn'tbehere")
+        .create();
+    
+    String expectedResult = "{data: {status: 200, headers:" +
+               "{'content-type': ['application/json; charset=UTF-8'], 
'set-cookie': ['cookiecookie']}," +
+               "content: [1, 2]}, id: 'p'}";
+
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadOfJsonWithErrorCode() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("not found")
+        .addHeader("content-type", "text/html")
+        .setHttpStatusCode(HttpResponse.SC_NOT_FOUND)
+        .create();
+    
+    String expectedResult = "{error: {code: 404, data:" +
+               "{headers: {'content-type': ['text/html; charset=UTF-8']}," +
+            "content: 'not found'}}, id: 'p'}";
+
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadWithBadJson() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("notjson")
+        .addHeader("content-type", "text/html")
+        .create();
+
+    JSONObject result = new JSONObject(executeHttpPreload(response, 
XML_WITH_HTTP_REQUEST));
+    assertFalse(result.has("data"));
+    
+    JSONObject error = result.getJSONObject("error");
+    assertEquals(HttpResponse.SC_NOT_ACCEPTABLE, error.getInt("code"));
+  }
+
+  @Test
+  public void testHttpPreloadOfText() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("{foo: 'bar'}")
+        .addHeader("content-type", "application/json")
+        .create();
+    // Even though the response was actually JSON, @format=text, so the content
+    // will be a block of text
+    String expectedResult = "{data: {status: 200, headers:" +
+            "{'content-type': ['application/json; charset=UTF-8']}," +
+            "content: '{foo: \\'bar\\'}'}, id: 'p'}";
+
+    String resultString = executeHttpPreload(response, 
XML_WITH_HTTP_REQUEST_FOR_TEXT);
+    assertEquals(new JSONObject(expectedResult).toString(), resultString);
+  }
+
+  private void verifyHttpPreload(HttpResponse response, String expectedJson) 
throws Exception {
+    String resultString = executeHttpPreload(response, XML_WITH_HTTP_REQUEST);
+    assertEquals(new JSONObject(expectedJson).toString(), resultString);
+  }
+  
+  /**
+   * Run an HTTP Preload test, returning the String result.
+   */
+  private String executeHttpPreload(HttpResponse response, String xml) throws 
Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(response);
     PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, 
containerConfig,
         expressions);
     view = "profile";
@@ -164,9 +249,6 @@
     Collection<Object> result = tasks.iterator().next().call().toJson();
     assertEquals(1, result.size());
 
-    String expectedResult = "{data: {foo: 'bar'}, id: 'p'}";
-    assertEquals(new JSONObject(expectedResult).toString(), 
result.iterator().next().toString());
-
     // Should have only fetched one request
     assertEquals(1, pipeline.requests.size());
     HttpRequest request = pipeline.requests.get(0);
@@ -174,6 +256,8 @@
     assertEquals(HTTP_REQUEST_URL, request.getUri().toString());
     assertEquals("POST", request.getMethod());
     assertEquals(60, request.getCacheTtl());
+    
+    return result.iterator().next().toString();
   }
 
   @Test
@@ -297,19 +381,21 @@
     assertTrue(tasks.isEmpty());
   }
 
-  // TODO: test HttpPreloads
-
   private static class RecordingRequestPipeline implements RequestPipeline {
     public final List<HttpRequest> requests = Lists.newArrayList();
-    private final String content;
+    private final HttpResponse response;
 
     public RecordingRequestPipeline(String content) {
-      this.content = content;
+      this(new HttpResponseBuilder().setResponseString(content).create());
+    }
+
+    public RecordingRequestPipeline(HttpResponse response) {
+      this.response = response;
     }
 
     public HttpResponse execute(HttpRequest request) {
       requests.add(request);
-      return new HttpResponseBuilder().setResponseString(content).create();
+      return response;
     }
 
     public void normalizeProtocol(HttpRequest request) throws GadgetException 
{}

Modified: 
incubator/shindig/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java?rev=761018&r1=761017&r2=761018&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
 (original)
+++ 
incubator/shindig/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
 Wed Apr  1 19:10:52 2009
@@ -175,7 +175,7 @@
     assertEquals("Digg", me.getJSONObject("name").getString("familyName"));
     
     JSONObject json = jsonObjects.get("json").getJSONObject("data");
-    JSONObject expected = new JSONObject("{key: 'value'}");
+    JSONObject expected = new JSONObject("{content: {key: 'value'}, status: 
200}");
     assertEquals(expected.toString(), json.toString());
   }
 
@@ -238,6 +238,12 @@
     assertEquals(3, ifList.size());
     assertEquals(1, page.getElementsByTagName("b").getLength());
     assertEquals(1, getChildrenByTagName(ifList.get(2), "b").size());
+    
+    Element jsonPipeline = page.getElementById("json");
+    assertEquals("value", jsonPipeline.getTextContent().trim());
+
+    Element textPipeline = page.getElementById("text");
+    assertEquals("{'key': 'value'}", textPipeline.getTextContent().trim());
   }
 
   // HtmlUnits implementation of Element.getElementsByTagName() is just

Modified: 
incubator/shindig/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml
URL: 
http://svn.apache.org/viewvc/incubator/shindig/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml?rev=761018&r1=761017&r2=761018&view=diff
==============================================================================
--- 
incubator/shindig/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml
 (original)
+++ 
incubator/shindig/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml
 Wed Apr  1 19:10:52 2009
@@ -25,8 +25,10 @@
   <Content type="html">
     <![CDATA[
       <script xmlns:os="http://ns.opensocial.org/2008/markup"; 
type="text/os-data">
-           <os:PeopleRequest key="friends" userId="@viewer" 
groupId="@friends"/>
-         </script>
+        <os:PeopleRequest key="friends" userId="@viewer" groupId="@friends"/>
+        <os:HttpRequest key="json" href="test.json"/>
+        <os:HttpRequest key="text" href="test.json" format="text"/>
+    </script>
 
       <script type="text/os-template" 
xmlns:os="http://ns.opensocial.org/2008/markup";>
         <ul id="attrs">
@@ -48,7 +50,10 @@
             </os:If>
           </li>
         </ul>
-         </script>
+        
+        <span id="json">${json.content.key}</span>
+        <span id="text">${text.content}</span>
+      </script>
     ]]>
   </Content>
 </Module>


Reply via email to