You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@shindig.apache.org by aw...@apache.org on 2009/02/11 20:31:29 UTC

svn commit: r743460 - in /incubator/shindig/trunk/java: common/src/main/java/org/apache/shindig/expressions/ common/src/test/java/org/apache/shindig/expressions/ gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/ gadgets/src/main/java/org...

Author: awiner
Date: Wed Feb 11 19:31:29 2009
New Revision: 743460

URL: http://svn.apache.org/viewvc?rev=743460&view=rev
Log:
SHINDIG-905: Implement content rewriter for data pipelining
- Added rewriter and html parser for handling <script type="text/os-data"> sections on the server.  This is turned off by default - to enable, bindg GadgetHtmlParser to SocialMarkupHtmlParser and register the PipelineDataContentRewriter.
- Rewrote pipeline data evaluation code to support multiple interdependent batches

TODOs:
- Turn this on by default
- Support errors (needs spec decisions)
- If all os-data sections are fully evaluated, disable the full opensocial-data feature, and only supply DataContext


Added:
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParser.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriterTest.java
Modified:
    incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java
    incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java

Modified: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java (original)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java Wed Feb 11 19:31:29 2009
@@ -26,7 +26,6 @@
 import javax.el.ELResolver;
 
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Maps;
 
 /**
  * ELResolver implementation that adds a map of top-level variables.
@@ -38,15 +37,14 @@
  * @see Expressions#newELContext(ELResolver...)
  */
 public class RootELResolver extends ELResolver {
-  private final Map<String, Object> map;
+  private final Map<String, ? extends Object> map;
 
   public RootELResolver() {
     this(ImmutableMap.<String, Object>of());
   }
   
   public RootELResolver(Map<String, ? extends Object> base) {
-    // TODO: if read-only is OK, then a copy is unnecessary
-    map = Maps.newHashMap(base);
+    this.map = base;
   }
   
   @Override
@@ -66,7 +64,7 @@
 
   @Override
   public Class<?> getType(ELContext context, Object base, Object property) {
-    if (base == null) {
+    if (base == null && map.containsKey(property)) {
       context.setPropertyResolved(true);
       Object value = map.get(property);
       return value == null ? null : value.getClass();
@@ -77,7 +75,7 @@
 
   @Override
   public Object getValue(ELContext context, Object base, Object property) {
-    if (base == null) {
+    if (base == null && map.containsKey(property)) {
       context.setPropertyResolved(true);
       return map.get(property);
     }
@@ -87,8 +85,9 @@
 
   @Override
   public boolean isReadOnly(ELContext context, Object base, Object property) {
-    if (base == null) {
+    if (base == null && map.containsKey(property)) {
       context.setPropertyResolved(true);
+      return true;
     }
     
     return false;
@@ -96,9 +95,5 @@
 
   @Override
   public void setValue(ELContext context, Object base, Object property, Object value) {
-    if (base == null) {
-      context.setPropertyResolved(true);
-      map.put(property.toString(), value);
-    }
   }
 }

Modified: incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java (original)
+++ incubator/shindig/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java Wed Feb 11 19:31:29 2009
@@ -19,6 +19,9 @@
 package org.apache.shindig.expressions;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Map;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
@@ -26,19 +29,23 @@
 import org.junit.Test;
 
 import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
 import javax.el.ValueExpression;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 
 public class ExpressionsTest {
   private Expressions expressions;
   private ELContext context;
+  private Map<String, Object> variables;
   
   @Before
   public void setUp() {
     expressions = new Expressions();
-    context = expressions.newELContext(new RootELResolver());
+    variables = Maps.newHashMap();
+    context = expressions.newELContext(new RootELResolver(variables));
   }
     
   @Test
@@ -91,12 +98,32 @@
     assertEquals(expected.toString(), result.toString());
   }
   
+  @Test
+  public void missingJsonSubproperty() throws Exception {
+    addVariable("object", new JSONObject("{foo: 125}"));
+    assertNull(evaluate("${object.bar.baz}", Object.class));
+  }
+
+  @Test
+  public void missingMapSubproperty() throws Exception {
+    addVariable("map", ImmutableMap.of("key", "value"));
+    assertNull(evaluate("${map.bar.baz}", Object.class));
+  }
+
+  @Test(expected = PropertyNotFoundException.class)
+  public void missingTopLevelVariable() throws Exception {
+    // Top-level properties must throw a PropertyNotFoundException when
+    // failing;  other properties must not.  Pipeline data batching
+    // relies on this
+    assertNull(evaluate("${map.bar.baz}", Object.class));
+  }
+
   private <T> T evaluate(String expression, Class<T> type) {
     ValueExpression expr = expressions.parse(expression, type);
     return type.cast(expr.getValue(context));
   }
 
   private void addVariable(String key, Object value) {
-    context.getELResolver().setValue(context, null, key, value);
+    variables.put(key, value);
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java Wed Feb 11 19:31:29 2009
@@ -39,6 +39,7 @@
 import org.cyberneko.html.HTMLEntities;
 import org.cyberneko.html.HTMLScanner;
 import org.cyberneko.html.HTMLTagBalancer;
+import org.cyberneko.html.filters.NamespaceBinder;
 import org.w3c.dom.DOMImplementation;
 import org.w3c.dom.Document;
 import org.w3c.dom.DocumentFragment;
@@ -67,26 +68,32 @@
   public NekoSimplifiedHtmlParser(DOMImplementation documentFactory) {
     this.documentFactory = documentFactory;
   }
-
+  
   @Override
   protected Document parseDomImpl(String source) {
+
+    HTMLConfiguration config = newConfiguration();
+
     HTMLScanner htmlScanner = new HTMLScanner();
     HTMLTagBalancer tagBalancer = new HTMLTagBalancer();
-    DocumentHandler handler = new DocumentHandler(source);
-    tagBalancer.setDocumentHandler(handler);
+    DocumentHandler handler = newDocumentHandler(source, htmlScanner);
+
+    if (config.getFeature("http://xml.org/sax/features/namespaces")) {
+      NamespaceBinder namespaceBinder = new NamespaceBinder();
+      namespaceBinder.setDocumentHandler(handler);
+      namespaceBinder.setDocumentSource(tagBalancer);
+      namespaceBinder.reset(config);
+      tagBalancer.setDocumentHandler(namespaceBinder);
+    } else {
+      tagBalancer.setDocumentHandler(handler);
+    }
+    
+    tagBalancer.setDocumentSource(htmlScanner);    
     htmlScanner.setDocumentHandler(tagBalancer);
 
-    HTMLConfiguration config = new HTMLConfiguration();
-    // Maintain original case for elements and attributes
-    config.setProperty("http://cyberneko.org/html/properties/names/elems", "match");
-    config.setProperty("http://cyberneko.org/html/properties/names/attrs", "no-change");
-    // Parse as fragment.
-    config.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true);
-    // Get notified of entity and character references
-    config.setFeature("http://apache.org/xml/features/scanner/notify-char-refs", true);
-    config.setFeature("http://cyberneko.org/html/features/scanner/notify-builtin-refs", true);
     tagBalancer.reset(config);
     htmlScanner.reset(config);
+    
     XMLInputSource inputSource = new XMLInputSource(null, null, null);
     inputSource.setEncoding("UTF-8");
     inputSource.setCharacterStream(new StringReader(source));
@@ -103,10 +110,34 @@
     }
   }
 
+  protected HTMLConfiguration newConfiguration() {
+    HTMLConfiguration config = new HTMLConfiguration();
+    // Maintain original case for elements and attributes
+    config.setProperty("http://cyberneko.org/html/properties/names/elems", "match");
+    config.setProperty("http://cyberneko.org/html/properties/names/attrs", "no-change");
+    // Parse as fragment.
+    config.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true);
+    // Get notified of entity and character references
+    config.setFeature("http://apache.org/xml/features/scanner/notify-char-refs", true);
+    config.setFeature("http://cyberneko.org/html/features/scanner/notify-builtin-refs", true);
+    return config;
+  }
+
+  protected DocumentHandler newDocumentHandler(String source, HTMLScanner scanner) {
+    return new DocumentHandler(source);
+  }
+
+  /**
+   * Is the given element important enough to preserve in the DOM?
+   */
+  protected boolean isElementImportant(QName qName) {
+    return elements.contains(qName.rawname.toLowerCase());
+  }
+  
   /**
    * Handler for XNI events from Neko
    */
-  private class DocumentHandler implements XMLDocumentHandler {
+  protected class DocumentHandler implements XMLDocumentHandler {
     private final Stack<Node> elementStack = new Stack<Node>();
     private final StringBuilder builder;
     private boolean inEntity = false;
@@ -172,49 +203,60 @@
 
     public void startElement(QName qName, XMLAttributes xmlAttributes, Augmentations augs)
         throws XNIException {
-      if (elements.contains(qName.rawname.toLowerCase())) {
-        if (builder.length() > 0) {
-          elementStack.peek().appendChild(document.createTextNode(builder.toString()));
-          builder.setLength(0);
-        }
-        Element element = document.createElement(qName.rawname);
-        for (int i = 0; i < xmlAttributes.getLength(); i++) {
-          element.setAttribute(xmlAttributes.getLocalName(i) , xmlAttributes.getValue(i));
-        }
-        elementStack.peek().appendChild(element);
+      if (isElementImportant(qName)) {
+        Element element = startImportantElement(qName, xmlAttributes);
+        // Not an empty element, so push on the stack
         elementStack.push(element);
       } else {
-        builder.append('<').append(qName.rawname);
-        for (int i = 0; i < xmlAttributes.getLength(); i++) {
-          builder.append(' ').append(xmlAttributes.getLocalName(i)).append("=\"");
-          appendAttributeValue(xmlAttributes.getValue(i));
-          builder.append('\"');
-        }
-        builder.append('>');
+        startUnimportantElement(qName, xmlAttributes);
       }
     }
 
     public void emptyElement(QName qName, XMLAttributes xmlAttributes, Augmentations augs)
         throws XNIException {
-      if (elements.contains(qName.rawname.toLowerCase())) {
-        if (builder.length() > 0) {
-          elementStack.peek().appendChild(document.createTextNode(builder.toString()));
-          builder.setLength(0);
-        }
-        Element element = document.createElement(qName.rawname);
-        for (int i = 0; i < xmlAttributes.getLength(); i++) {
-          element.setAttribute(xmlAttributes.getLocalName(i) , xmlAttributes.getValue(i));
-        }
-        elementStack.peek().appendChild(element);
+      if (isElementImportant(qName)) {
+        startImportantElement(qName, xmlAttributes);
       } else {
-        builder.append('<').append(qName.rawname);
-        for (int i = 0; i < xmlAttributes.getLength(); i++) {
-          builder.append(' ').append(xmlAttributes.getLocalName(i)).append("=\"");
-          appendAttributeValue(xmlAttributes.getValue(i));
-          builder.append('\"');
+        startUnimportantElement(qName, xmlAttributes);
+      }
+    }
+
+    /** Write an unimportant element into content as raw text */
+    private void startUnimportantElement(QName qName, XMLAttributes xmlAttributes) {
+      builder.append('<').append(qName.rawname);
+      for (int i = 0; i < xmlAttributes.getLength(); i++) {
+        builder.append(' ').append(xmlAttributes.getLocalName(i)).append("=\"");
+        appendAttributeValue(xmlAttributes.getValue(i));
+        builder.append('\"');
+      }
+      builder.append('>');
+    }
+
+    /** Create an Element in the DOM for an important element */
+    private Element startImportantElement(QName qName, XMLAttributes xmlAttributes) {
+      if (builder.length() > 0) {
+        elementStack.peek().appendChild(document.createTextNode(builder.toString()));
+        builder.setLength(0);
+      }
+      
+      Element element;
+      // Preserve XML namespace if present
+      if (qName.uri != null) {
+        element = document.createElementNS(qName.uri, qName.rawname);
+      } else {
+        element = document.createElement(qName.rawname);
+      }
+      
+      for (int i = 0; i < xmlAttributes.getLength(); i++) {
+        if (xmlAttributes.getURI(i) != null) {
+          element.setAttributeNS(xmlAttributes.getURI(i), xmlAttributes.getQName(i),
+              xmlAttributes.getValue(i));
+        } else {
+          element.setAttribute(xmlAttributes.getLocalName(i) , xmlAttributes.getValue(i));
         }
-        builder.append('>');
       }
+      elementStack.peek().appendChild(element);
+      return element;
     }
 
     private void appendAttributeValue(String text) {
@@ -275,7 +317,7 @@
     }
 
     public void endElement(QName qName, Augmentations augs) throws XNIException {
-      if (elements.contains(qName.rawname.toLowerCase())) {
+      if (isElementImportant(qName)) {
         if (builder.length() > 0) {
           elementStack.peek().appendChild(document.createTextNode(builder.toString()));
           builder.setLength(0);

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParser.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParser.java?rev=743460&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParser.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParser.java Wed Feb 11 19:31:29 2009
@@ -0,0 +1,115 @@
+/*
+ * 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.parse.nekohtml;
+
+import java.io.StringReader;
+
+import org.apache.xerces.xni.Augmentations;
+import org.apache.xerces.xni.QName;
+import org.apache.xerces.xni.XMLAttributes;
+import org.apache.xerces.xni.XMLString;
+import org.apache.xerces.xni.XNIException;
+import org.apache.xerces.xni.parser.XMLInputSource;
+import org.cyberneko.html.HTMLConfiguration;
+import org.cyberneko.html.HTMLScanner;
+import org.w3c.dom.DOMImplementation;
+
+import com.google.inject.Inject;
+
+public class SocialMarkupHtmlParser extends NekoSimplifiedHtmlParser {
+  @Inject
+  public SocialMarkupHtmlParser(DOMImplementation documentProvider) {
+    super(documentProvider);
+  }
+
+  @Override
+  protected boolean isElementImportant(QName name) {
+    // For now, just include everything
+    return true;
+  }
+
+  @Override
+  protected HTMLConfiguration newConfiguration() {
+    HTMLConfiguration config = super.newConfiguration();
+    config.setFeature("http://xml.org/sax/features/namespaces", true);
+    return config;
+  }
+
+  @Override
+  protected DocumentHandler newDocumentHandler(String source, HTMLScanner scanner) {
+    return new SocialMarkupDocumentHandler(source, scanner);
+  }
+
+  private class SocialMarkupDocumentHandler extends DocumentHandler {
+
+    private StringBuilder scriptContent;
+    private boolean inScript = false;
+    private final HTMLScanner scanner;
+
+    public SocialMarkupDocumentHandler(String content, HTMLScanner scanner) {
+      super(content);
+      this.scanner = scanner;
+    }
+
+    @Override
+    public void characters(XMLString text, Augmentations augs) throws XNIException {
+      if (scriptContent != null) {
+        scriptContent.append(text.ch, text.offset, text.length);
+      } else {
+        super.characters(text, augs);
+      }
+    }
+
+    @Override
+    public void endElement(QName name, Augmentations augs) throws XNIException {
+      if (scriptContent != null) {
+        XMLInputSource scriptSource = new XMLInputSource(null, null, null);
+        scriptSource.setCharacterStream(new StringReader(scriptContent.toString()));
+        scriptContent.setLength(0);
+        inScript = true;
+        
+        // Evaluate the content of the script block immediately
+        scanner.evaluateInputSource(scriptSource);
+      }
+      
+      super.endElement(name, augs);
+    }
+
+    @Override
+    public void startElement(QName name, XMLAttributes xmlAttributes, Augmentations augs)
+        throws XNIException {
+      // Start gathering the content of any os-data or os-template elements
+      if (name.rawname.toLowerCase().equals("script")) {
+        String type = xmlAttributes.getValue("type");
+        if ("text/os-data".equals(type) ||
+            "text/os-template".equals(type)) {
+          if (inScript) {
+            throw new XNIException("Nested OpenSocial script elements");
+          }
+          inScript = true;
+          if (scriptContent == null) {
+            scriptContent = new StringBuilder();
+          }
+        }
+      }
+      
+      super.startElement(name, xmlAttributes, augs);
+    }
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java Wed Feb 11 19:31:29 2009
@@ -57,6 +57,10 @@
       tasks.addAll(taskCollection);
     }
 
+    return preload(tasks);
+  }
+
+  public Preloads preload(Collection<Callable<PreloadedData>> tasks) {
     ConcurrentPreloads preloads = new ConcurrentPreloads();
     int processed = tasks.size();
     for (Callable<PreloadedData> task : tasks) {

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=743460&r1=743459&r2=743460&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 Feb 11 19:31:29 2009
@@ -23,10 +23,12 @@
 import org.apache.shindig.config.ContainerConfig;
 import org.apache.shindig.gadgets.AuthType;
 import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetELResolver;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.RequestPipeline;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.PipelinedData;
 import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
 import org.apache.shindig.gadgets.spec.View;
 
@@ -47,6 +49,8 @@
 import java.util.Map;
 import java.util.concurrent.Callable;
 
+import javax.el.ELResolver;
+
 /**
  * Preloader for loading Data Pipelining Preload data.
  */
@@ -62,6 +66,7 @@
     this.config = config;
   }
 
+  /** Create preloads from a gadget view */
   public Collection<Callable<PreloadedData>> createPreloadTasks(GadgetContext context,
       GadgetSpec gadget, PreloaderService.PreloadPhase phase) {
     View view = gadget.getView(context.getView());
@@ -69,36 +74,43 @@
         && view.getPipelinedData() != null
         && phase == PreloaderService.PreloadPhase.PROXY_FETCH) {
 
-      List<Callable<PreloadedData>> preloadList = Lists.newArrayList();
-      Map<String, Object> socialPreloads = view.getPipelinedData().getSocialPreloads(context);
+      ELResolver resolver = new GadgetELResolver(context);
+      PipelinedData.Batch batch = view.getPipelinedData().getBatch(resolver);
+      if (batch != null) {
+        return createPreloadTasks(context, batch);
+      }
+    }
 
-      // Load any social preloads into a JSONArray for delivery to
-      // JsonRpcServlet
-      if (!socialPreloads.isEmpty()) {
-        JSONArray array = new JSONArray();
-        for (Object socialRequest : socialPreloads.values()) {
-          array.put(socialRequest);
-        }
+    return Collections.emptyList();
+  }
 
-        Callable<PreloadedData> preloader = new SocialPreloadTask(context, array);
-        preloadList.add(preloader);
+  /** Create preload tasks from an explicit list of social and http preloads */
+  public Collection<Callable<PreloadedData>> createPreloadTasks(GadgetContext context,
+      PipelinedData.Batch batch) {
+    List<Callable<PreloadedData>> preloadList = Lists.newArrayList();
+
+    // Load any social preloads into a JSONArray for delivery to
+    // JsonRpcServlet
+    if (!batch.getSocialPreloads().isEmpty()) {
+      JSONArray array = new JSONArray();
+      for (Object socialRequest : batch.getSocialPreloads().values()) {
+        array.put(socialRequest);
       }
 
-      Map<String, RequestAuthenticationInfo> httpPreloads =
-          view.getPipelinedData().getHttpPreloads(context);
-      if (!httpPreloads.isEmpty()) {
-        for (Map.Entry<String, RequestAuthenticationInfo> httpPreloadEntry
-            : httpPreloads.entrySet()) {
-          preloadList.add(new HttpPreloadTask(context,  httpPreloadEntry.getValue(),
-              httpPreloadEntry.getKey()));
-        }
+      Callable<PreloadedData> preloader = new SocialPreloadTask(context, array);
+      preloadList.add(preloader);
+    }
 
+    if (!batch.getHttpPreloads().isEmpty()) {
+      for (Map.Entry<String, RequestAuthenticationInfo> httpPreloadEntry
+          : batch.getHttpPreloads().entrySet()) {
+        preloadList.add(new HttpPreloadTask(context,  httpPreloadEntry.getValue(),
+            httpPreloadEntry.getKey()));
       }
 
-      return preloadList;
     }
 
-    return Collections.emptyList();
+    return preloadList;
   }
 
   /**
@@ -130,6 +142,8 @@
 
       // Unpack the response into a map of PreloadedData responses
       final Map<String, Object> data = Maps.newHashMap();
+      // TODO: if the entire request fails, the result is an object,
+      // not an array
       JSONArray array = new JSONArray(response.getResponseAsString());
       for (int i = 0; i < array.length(); i++) {
         JSONObject arrayElement = array.getJSONObject(i);

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java Wed Feb 11 19:31:29 2009
@@ -21,6 +21,9 @@
 import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
 
+import java.util.Collection;
+import java.util.concurrent.Callable;
+
 /**
  * Handles preloading operations, such as HTTP fetches, social data retrieval, or anything else that
  * would benefit from preloading on the server instead of incurring a network request for users.
@@ -51,4 +54,9 @@
    * @return The preloads for the gadget.
    */
   Preloads preload(GadgetContext context, GadgetSpec gadget, PreloadPhase phase);
+
+  /**
+   * Execute preloads with a specific set of preload tasks.
+   */
+  Preloads preload(Collection<Callable<PreloadedData>> tasks);
 }

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java?rev=743460&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java Wed Feb 11 19:31:29 2009
@@ -0,0 +1,217 @@
+/*
+ * 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.rewrite;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.preload.PipelinedDataPreloader;
+import org.apache.shindig.gadgets.preload.PreloadException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.preload.PreloaderService;
+import org.apache.shindig.gadgets.preload.Preloads;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.DocumentTraversal;
+import org.w3c.dom.traversal.NodeFilter;
+import org.w3c.dom.traversal.NodeIterator;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.el.CompositeELResolver;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+/**
+ * ContentRewriter that resolves opensocial-data elements on the server.
+ * 
+ * This rewriter cannot be used currently without the SocialMarkupHtmlParser.
+ */
+public class PipelineDataContentRewriter implements ContentRewriter {
+
+  private static final Logger logger = Logger.getLogger(
+      PipelineDataContentRewriter.class.getName());
+  // TODO: support configuration
+  private static final int MAX_BATCH_COUNT = 3;
+  
+  private final PipelinedDataPreloader preloader;
+  private final PreloaderService preloaderService;
+
+  @Inject
+  public PipelineDataContentRewriter(PipelinedDataPreloader preloader,
+      PreloaderService preloaderService) {
+    this.preloader = preloader;
+    this.preloaderService = preloaderService;
+  }
+  
+  public RewriterResults rewrite(HttpRequest request, HttpResponse original, MutableContent content) {
+    return null;
+  }
+
+  public RewriterResults rewrite(Gadget gadget, MutableContent content) {
+    // Only bother for gadgets using the opensocial-data feature
+    if (!gadget.getSpec().getModulePrefs().getFeatures().containsKey("opensocial-data")) {
+      return null;
+    }
+    
+    Document doc = content.getDocument();
+    NodeIterator nodeIterator = ((DocumentTraversal) doc)
+        .createNodeIterator(doc, NodeFilter.SHOW_ELEMENT,
+            new NodeFilter() {
+              public short acceptNode(Node n) {
+                if ("script".equalsIgnoreCase(n.getNodeName()) &&
+                    "text/os-data".equals(((Element) n).getAttribute("type"))) {
+                  return NodeFilter.FILTER_ACCEPT;
+                }
+                return NodeFilter.FILTER_REJECT;
+              }
+            }, false);
+    
+    
+    Map<String, JSONObject> results = Maps.newHashMap();
+    
+    // Use the default objects in the GadgetContext, and any objects that
+    // have been resolved
+    List<PipelineState> pipelines = Lists.newArrayList();
+    CompositeELResolver rootObjects = new CompositeELResolver();
+    rootObjects.add(new GadgetELResolver(gadget.getContext()));
+    rootObjects.add(new RootELResolver(results));
+    
+    for (Node n = nodeIterator.nextNode(); n != null ; n = nodeIterator.nextNode()) {
+      try {
+        PipelinedData pipelineData = new PipelinedData((Element) n, gadget.getSpec().getUrl());
+        PipelinedData.Batch batch = pipelineData.getBatch(rootObjects);
+        if (batch == null) {
+          // An empty pipeline element - just remove it
+          n.getParentNode().removeChild(n);
+        } else {
+          // Not empty, ready it 
+          PipelineState state = new PipelineState();
+          state.batch = batch;
+          state.node = n;
+          pipelines.add(state);
+        }
+      } catch (SpecParserException e) {
+        // Leave the element to the client
+        logger.log(Level.INFO, "Failed to parse preload in " + gadget.getSpec().getUrl(), e);
+      }
+    }
+    
+    // No pipline elements found, return early
+    if (pipelines.isEmpty()) {
+      return null;
+    }
+
+    // Run batches until we run out
+    int batchCount = 0;
+    while (true) {
+      // Gather all tasks from the first batch
+      List<Callable<PreloadedData>> tasks = Lists.newArrayList();
+      for (PipelineState pipeline : pipelines) {
+        if (pipeline.batch != null) {
+          tasks.addAll(preloader.createPreloadTasks(gadget.getContext(), pipeline.batch));
+        }
+      }
+     
+      // No further progress - quit
+      if (tasks.isEmpty()) {
+        break;
+      }
+      
+    // And run the pipeline
+      Preloads preloads = preloaderService.preload(tasks);
+      for (PreloadedData preloaded : preloads.getData()) {
+        try {
+          for (Map.Entry<String, Object> entry : preloaded.toJson().entrySet()) {
+            JSONObject obj = (JSONObject) entry.getValue();
+            if (obj.has("data")) {
+              results.put(entry.getKey(), obj.getJSONObject("data"));
+            }
+            // TODO: handle errors?
+          }
+        } catch (PreloadException pe) {
+          // This will be thrown in the event of some unexpected exception. We can move on.
+          logger.log(Level.WARNING, "Unexpected error when preloading", pe);
+        } catch (JSONException je) {
+          throw new RuntimeException(je);
+        }
+      }
+      
+      // Advance to the next batch
+      for (PipelineState pipeline : pipelines) {
+        if (pipeline.batch != null) {
+          pipeline.batch = pipeline.batch.getNextBatch(rootObjects);
+          // Once there are no more batches, delete the associated script node.
+          if (pipeline.batch == null) {
+            pipeline.node.getParentNode().removeChild(pipeline.node);
+          }
+        }
+      }
+      
+      // TODO: necessary?
+      if (batchCount++ >= MAX_BATCH_COUNT) {
+        break;
+      }
+    }
+    
+    Element head = (Element) DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+    Element pipelineScript = doc.createElement("script");
+    pipelineScript.setAttribute("type", "text/javascript");
+
+    StringBuilder script = new StringBuilder();
+    for (Map.Entry<String, JSONObject> entry : results.entrySet()) {
+      String key = entry.getKey();
+
+      // TODO: escape key
+      // TODO: push to MutableContent
+      script.append("osd.DataContext.putDataSet(\"")
+          .append(key)
+          .append("\",")
+          .append(JsonSerializer.serialize(entry.getValue()))
+          .append(");");
+    }
+
+    pipelineScript.appendChild(doc.createTextNode(script.toString()));
+    head.appendChild(pipelineScript);
+    MutableContent.notifyEdit(doc);
+    
+    return RewriterResults.notCacheable();
+  }
+  
+  static class PipelineState {
+    public Node node;
+    public PipelinedData.Batch batch;
+    
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java Wed Feb 11 19:31:29 2009
@@ -17,6 +17,13 @@
  */
 package org.apache.shindig.gadgets.spec;
 
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -25,15 +32,10 @@
 
 import javax.el.ELContext;
 import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.PropertyNotFoundException;
 import javax.el.ValueExpression;
 
-import org.apache.shindig.common.uri.Uri;
-import org.apache.shindig.common.xml.XmlException;
-import org.apache.shindig.expressions.Expressions;
-import org.apache.shindig.gadgets.AuthType;
-import org.apache.shindig.gadgets.GadgetContext;
-import org.apache.shindig.gadgets.GadgetELResolver;
-import org.apache.shindig.gadgets.variables.Substitutions;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -131,39 +133,122 @@
   public PipelinedData substitute(Substitutions substituter) {
     return new PipelinedData(this, substituter);
   }
-
-  public Map<String, Object> getSocialPreloads(GadgetContext context) {
-    Map<String, Object> evaluatedPreloads = Maps.newHashMapWithExpectedSize(
-        socialPreloads.size());
-    Expressions expressions = Expressions.sharedInstance();
-    ELContext elContext = expressions.newELContext(new GadgetELResolver(context));
-    for (Map.Entry<String, SocialData> preload : socialPreloads.entrySet()) {
-      try {
-        evaluatedPreloads.put(preload.getKey(), preload.getValue().toJson(elContext));
-      } catch (ELException e) {
-        // TODO: Handle!?!
-        throw new RuntimeException(e);
-      }
-    }
-
-    return evaluatedPreloads;
+  
+  public interface Batch {
+    Map<String, Object> getSocialPreloads();
+    Map<String, RequestAuthenticationInfo> getHttpPreloads();
+    Batch getNextBatch(ELResolver rootObjects);
   }
 
-  public Map<String, RequestAuthenticationInfo> getHttpPreloads(GadgetContext context) {
-    Map<String, RequestAuthenticationInfo> evaluatedPreloads = Maps.newHashMapWithExpectedSize(
-        httpPreloads.size());
+  /**
+   * Gets the first batch of preload requests.  Preloads that require root
+   * objects not yet available will not be executed in this batch, but may
+   * become available in subsequent batches.
+   * 
+   * @param rootObjects an ELResolver that can evaluate currently available
+   *     root objects.
+   * @see GadgetELResolver
+   * @return a batch, or null if no batch could be created
+   */
+  public Batch getBatch(ELResolver rootObjects) {
+    return getBatch(rootObjects, socialPreloads, httpPreloads);
+  }
+  
+  /**
+   * Create a Batch of preload requests
+   * @param rootObjects an ELResolver that can evaluate currently available
+   *     root objects.
+   * @param currentSocialPreloads the remaining social preloads
+   * @param currentHttpPreloads the remaining http preloads
+   */
+  private Batch getBatch(ELResolver rootObjects, Map<String, SocialData> currentSocialPreloads,
+      Map<String, HttpData> currentHttpPreloads) {
     Expressions expressions = Expressions.sharedInstance();
-    ELContext elContext = expressions.newELContext(new GadgetELResolver(context));
-    for (Map.Entry<String, HttpData> preload : httpPreloads.entrySet()) {
-      try {
-        evaluatedPreloads.put(preload.getKey(), preload.getValue().evaluate(elContext));
-      } catch (ELException e) {
-        // TODO: Handle!?!
-        throw new RuntimeException(e);
+    ELContext elContext = expressions.newELContext(rootObjects);
+ 
+    // Evaluate all existing social preloads
+    Map<String, Object> evaluatedSocialPreloads = Maps.newHashMap();
+    Map<String, SocialData> pendingSocialPreloads = null;
+    
+    if (currentSocialPreloads != null) {
+      for (Map.Entry<String, SocialData> preload : currentSocialPreloads.entrySet()) {
+        try {
+          Object value = preload.getValue().toJson(elContext);
+          evaluatedSocialPreloads.put(preload.getKey(), value);
+        } catch (PropertyNotFoundException pnfe) {
+          // Missing top-level property: put it in the pending set
+          if (pendingSocialPreloads == null) {
+            pendingSocialPreloads = Maps.newHashMap();
+          }
+          pendingSocialPreloads.put(preload.getKey(), preload.getValue());
+        } catch (ELException e) {
+          // TODO: Handle!?!
+          throw new RuntimeException(e);
+        }
+      }
+    }    
+    // And evaluate all existing HTTP preloads
+    Map<String, RequestAuthenticationInfo> evaluatedHttpPreloads = Maps.newHashMap();
+    Map<String, HttpData> pendingHttpPreloads = null;
+    
+    if (currentHttpPreloads != null) {
+      for (Map.Entry<String, HttpData> preload : currentHttpPreloads.entrySet()) {
+        try {
+          RequestAuthenticationInfo value = preload.getValue().evaluate(elContext);
+          evaluatedHttpPreloads.put(preload.getKey(), value);
+        } catch (PropertyNotFoundException pnfe) {
+          if (pendingHttpPreloads == null) {
+            pendingHttpPreloads = Maps.newHashMap();
+          }
+          pendingHttpPreloads.put(preload.getKey(), preload.getValue());
+        } catch (ELException e) {
+          // TODO: Handle!?!
+          throw new RuntimeException(e);
+        }
       }
     }
+    
+    // Nothing evaluated or pending;  return null for the batch.  Note that
+    // there may be multiple PipelinedData objects (e.g., from multiple 
+    // <script type="text/os-data"> elements), so even if all evaluations
+    // fail here, evaluations might succeed elsewhere and free up pending preloads
+    if (evaluatedSocialPreloads.isEmpty() && evaluatedHttpPreloads.isEmpty() &&
+        pendingHttpPreloads == null && pendingSocialPreloads == null) {
+      return null;
+    }
+    
+    return new BatchImpl(evaluatedSocialPreloads, evaluatedHttpPreloads,
+        pendingSocialPreloads, pendingHttpPreloads);
+  }
+  
+  /** Batch implementation */
+  class BatchImpl implements Batch {
+
+    private final Map<String, Object> evaluatedSocialPreloads;
+    private final Map<String, RequestAuthenticationInfo> evaluatedHttpPreloads;
+    private final Map<String, SocialData> pendingSocialPreloads;
+    private final Map<String, HttpData> pendingHttpPreloads;
+
+    public BatchImpl(Map<String, Object> evaluatedSocialPreloads,
+        Map<String, RequestAuthenticationInfo> evaluatedHttpPreloads,
+        Map<String, SocialData> pendingSocialPreloads, Map<String, HttpData> pendingHttpPreloads) {
+      this.evaluatedSocialPreloads = evaluatedSocialPreloads;
+      this.evaluatedHttpPreloads = evaluatedHttpPreloads;
+      this.pendingSocialPreloads = pendingSocialPreloads;
+      this.pendingHttpPreloads = pendingHttpPreloads;
+    }
+
+    public Map<String, Object> getSocialPreloads() {
+      return evaluatedSocialPreloads;
+    }
+    
+    public Map<String, RequestAuthenticationInfo> getHttpPreloads() {
+      return evaluatedHttpPreloads;
+    }
 
-    return evaluatedPreloads;
+    public Batch getNextBatch(ELResolver rootObjects) {
+      return getBatch(rootObjects, pendingSocialPreloads, pendingHttpPreloads);
+    }
   }
 
   public boolean needsViewer() {

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java Wed Feb 11 19:31:29 2009
@@ -54,6 +54,7 @@
 import java.util.Collections;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.Callable;
 
 /**
  * Tests for HtmlRenderer
@@ -360,6 +361,11 @@
       wasPreloaded = true;
       return preloads;
     }
+
+    public Preloads preload(Collection<Callable<PreloadedData>> tasks) {
+      wasPreloaded = true;
+      return preloads;
+    }
   }
 
   private static class FakePreloads implements Preloads {

Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriterTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriterTest.java?rev=743460&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriterTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriterTest.java Wed Feb 11 19:31:29 2009
@@ -0,0 +1,315 @@
+/*
+ * 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.rewrite;
+
+import static org.easymock.EasyMock.and;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.same;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.SocialMarkupHtmlParser;
+import org.apache.shindig.gadgets.preload.ConcurrentPreloaderService;
+import org.apache.shindig.gadgets.preload.PipelinedDataPreloader;
+import org.apache.shindig.gadgets.preload.PreloadException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.preload.PreloaderService;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.easymock.Capture;
+import org.easymock.IArgumentMatcher;
+import org.easymock.classextension.EasyMock;
+import org.easymock.classextension.IMocksControl;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test of PipelineDataContentRewriter.
+ */
+public class PipelineDataContentRewriterTest {
+
+  private IMocksControl control;
+  private PipelinedDataPreloader preloader;
+  private PreloaderService preloaderService;
+  private PipelineDataContentRewriter rewriter;
+  private GadgetSpec gadgetSpec;
+  private Gadget gadget;
+  private MutableContent content;
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/gadget.php");
+
+  private static final String CONTENT =
+    "<script xmlns:os='http://ns.opensocial.org/2008/markup' type='text/os-data'>"
+      + "  <os:PeopleRequest key='me' userId='canonical'/>"
+      + "  <os:MakeRequest key='json' href='test.json'/>"
+      + "</script>";
+
+  // Two requests, one depends on the other
+  private static final String TWO_BATCH_CONTENT =
+    "<script xmlns:os='http://ns.opensocial.org/2008/markup' type='text/os-data'>"
+    + "  <os:PeopleRequest key='me' userId='${json.user}'/>"
+    + "  <os:MakeRequest key='json' href='${ViewParams.file}'/>"
+    + "</script>";
+
+  // One request, but it requires data that isn't present
+  private static final String BLOCKED_FIRST_BATCH_CONTENT =
+    "<script xmlns:os='http://ns.opensocial.org/2008/markup' type='text/os-data'>"
+    + "  <os:PeopleRequest key='me' userId='${json.user}'/>"
+    + "</script>";
+
+  private static final String XML_WITHOUT_FEATURE = "<Module>" + "<ModulePrefs title='Title'>"
+      + "</ModulePrefs>" + "<Content>" + "    <![CDATA[" + CONTENT + "]]></Content></Module>";
+
+  private static final String XML_WITHOUT_PIPELINE = "<Module>" + "<ModulePrefs title='Title'>"
+      + "<Require feature='opensocial-data'/>" + "</ModulePrefs>" + "<Content/></Module>";
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createStrictControl();
+    preloader = control.createMock(PipelinedDataPreloader.class);
+//    preloaderService = control.createMock(PreloaderService.class);
+    preloaderService = new ConcurrentPreloaderService(Executors.newSingleThreadExecutor(), null);
+    rewriter = new PipelineDataContentRewriter(preloader, preloaderService);
+  }
+
+  private void setupGadget(String gadgetXml) throws SpecParserException {
+    gadgetSpec = new GadgetSpec(GADGET_URI, gadgetXml);
+    gadget = new Gadget();
+    gadget.setSpec(gadgetSpec);
+    gadget.setContext(new GadgetContext() {});
+    gadget.setCurrentView(gadgetSpec.getView("default"));
+
+    content = new MutableContent(new SocialMarkupHtmlParser(
+        new ParseModule.DOMImplementationProvider().get()), gadget.getCurrentView().getContent());
+  }
+
+  @Test
+  public void rewrite() throws Exception {
+    setupGadget(getGadgetXml(CONTENT));
+
+    Capture<PipelinedData.Batch> batchCapture =
+      new Capture<PipelinedData.Batch>();
+    
+    // Dummy return results (the "real" return would have two values)
+    Callable<PreloadedData> callable = createPreloadTask(
+        "key", "{data: {foo: 'bar'}}");
+
+    // One batch with 1 each HTTP and Social preload
+    expect(preloader.createPreloadTasks(same(gadget.getContext()),
+            and(eqBatch(1, 1), capture(batchCapture))))
+            .andReturn(ImmutableList.of(callable));
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+
+    // Verify the data set is injected, and the os-data was deleted
+    assertTrue("Script not inserted", content.getContent().indexOf(
+        "DataContext.putDataSet(\"key\",{\"foo\":\"bar\"})") >= 0);
+    assertFalse("os-data wasn't deleted",
+        content.getContent().indexOf("type=\"text/os-data\"") >= 0);
+
+    assertTrue(batchCapture.getValue().getSocialPreloads().containsKey("me"));
+    assertTrue(batchCapture.getValue().getHttpPreloads().containsKey("json"));
+
+    control.verify();
+  }
+
+  @Test
+  public void rewriteWithTwoBatches() throws Exception {
+    setupGadget(getGadgetXml(TWO_BATCH_CONTENT));
+
+    gadget.setContext(new GadgetContext() {
+      @Override
+      public String getParameter(String property) {
+        // Provide the filename to be requested in the first batch
+        if ("view-params".equals(property)) {
+          return "{'file': 'test.json'}";
+        }
+        return null;
+      }
+    });
+
+    // First batch, the HTTP fetch
+    Capture<PipelinedData.Batch> firstBatch =
+      new Capture<PipelinedData.Batch>();
+    Callable<PreloadedData> firstTask = createPreloadTask("json",
+        "{data: {user: 'canonical'}}");
+    
+    // Second batch, the user fetch
+    Capture<PipelinedData.Batch> secondBatch =
+      new Capture<PipelinedData.Batch>();
+    Callable<PreloadedData> secondTask = createPreloadTask("me",
+        "{data: {'id':'canonical'}}");
+    
+    // First, a batch with an HTTP request
+    expect(
+        preloader.createPreloadTasks(same(gadget.getContext()),
+            and(eqBatch(0, 1), capture(firstBatch))))
+            .andReturn(ImmutableList.of(firstTask));
+    // Second, a batch with a social request
+    expect(
+        preloader.createPreloadTasks(same(gadget.getContext()),
+            and(eqBatch(1, 0), capture(secondBatch))))
+            .andReturn(ImmutableList.of(secondTask));
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+    
+    control.verify();
+
+    // Verify the data set is injected, and the os-data was deleted
+    assertTrue("First batch not inserted", content.getContent().indexOf(
+        "DataContext.putDataSet(\"json\",{\"user\":\"canonical\"})") >= 0);
+    assertTrue("Second batch not inserted", content.getContent().indexOf(
+        "DataContext.putDataSet(\"me\",{\"id\":\"canonical\"})") >= 0);
+    assertFalse("os-data wasn't deleted",
+        content.getContent().indexOf("type=\"text/os-data\"") >= 0);
+
+    // Check the evaluated HTTP request
+    RequestAuthenticationInfo request = firstBatch.getValue().getHttpPreloads().get("json");
+    assertEquals("http://example.org/test.json", request.getHref().toString());
+    
+    // Check the evaluated person request
+    JSONObject personRequest = (JSONObject) secondBatch.getValue().getSocialPreloads().get("me");
+    assertEquals("canonical", personRequest.getJSONObject("params").getJSONArray("userId").get(0));
+  }
+
+  @Test
+  public void rewriteWithBlockedBatch() throws Exception {
+    setupGadget(getGadgetXml(BLOCKED_FIRST_BATCH_CONTENT));
+
+    // Expect a batch with no content
+    expect(
+        preloader.createPreloadTasks(same(gadget.getContext()), eqBatch(0, 0)))
+            .andReturn(ImmutableList.<Callable<PreloadedData>>of());
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+    
+    control.verify();
+
+    // Check there is no DataContext inserted
+    assertFalse("DataContext write shouldn't be present", content.getContent().indexOf(
+        "DataContext.putDataSet(") > 0);
+    // And the os-data elements should be present
+    assertTrue("os-data was deleted",
+        content.getContent().indexOf("type=\"text/os-data\"") > 0);
+  }
+  
+  /** Match a batch with the specified count of social and HTTP data items */
+  private PipelinedData.Batch eqBatch(int socialCount, int httpCount) {
+    reportMatcher(new BatchMatcher(socialCount, httpCount));
+    return null;
+  }
+  
+  private static class BatchMatcher implements IArgumentMatcher {
+    private final int socialCount;
+    private final int httpCount;
+
+    public BatchMatcher(int socialCount, int httpCount) {
+      this.socialCount = socialCount;
+      this.httpCount = httpCount;
+    }
+    
+    public void appendTo(StringBuffer buffer) {
+      buffer.append("eqBuffer[social=" + socialCount + ",http=" + httpCount + "]");
+    }
+
+    public boolean matches(Object obj) {
+      if (!(obj instanceof PipelinedData.Batch)) {
+        return false;
+      }
+      
+      PipelinedData.Batch batch = (PipelinedData.Batch) obj;
+      return (socialCount == batch.getSocialPreloads().size() 
+          && httpCount == batch.getHttpPreloads().size());
+    }
+    
+  }
+  
+  @Test
+  public void rewriteWithoutPipeline() throws Exception {
+    setupGadget(XML_WITHOUT_PIPELINE);
+    control.replay();
+
+    // If there are no pipeline elements, the rewrite is a no-op
+    assertNull(rewriter.rewrite(gadget, content));
+
+    control.verify();
+  }
+
+  @Test
+  public void rewriteWithoutFeature() throws Exception {
+    // If the opensocial-data feature is present, the rewrite is a no-op
+    setupGadget(XML_WITHOUT_FEATURE);
+
+    control.replay();
+
+    assertNull(rewriter.rewrite(gadget, content));
+
+    control.verify();
+  }
+
+  /** Create a mock Callable for a single preload task */
+  private Callable<PreloadedData> createPreloadTask(final String key, String jsonResult)
+      throws JSONException {
+    final Object value = new JSONObject(jsonResult);
+    final PreloadedData preloadResult = new PreloadedData() {
+      public Map<String, Object> toJson() throws PreloadException {
+        return ImmutableMap.of(key, value);
+      }
+    };
+
+    Callable<PreloadedData> callable = new Callable<PreloadedData>() {
+      public PreloadedData call() throws Exception {
+        return preloadResult;
+      }      
+    };
+    return callable;
+  }
+
+  private static String getGadgetXml(String content) {
+    return "<Module>" + "<ModulePrefs title='Title'>"
+        + "<Require feature='opensocial-data'/>" + "</ModulePrefs>"
+        + "<Content>"
+        + "    <![CDATA[" + content + "]]>"
+        + "</Content></Module>";
+  }  
+}

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java Wed Feb 11 19:31:29 2009
@@ -21,27 +21,35 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.RootELResolver;
 import org.apache.shindig.gadgets.AuthType;
-import org.apache.shindig.gadgets.GadgetContext;
 
 import java.util.Map;
 
+import javax.el.ELResolver;
+
 import org.json.JSONObject;
 import org.junit.Before;
 import org.junit.Test;
 
+import com.google.common.collect.Maps;
+
 public class PipelinedDataTest {
-  //TODO: test os:MakeRequest
 
-  private GadgetContext context;
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/");
+  private ELResolver elResolver;
+  private Map<String, Object> elValues;
 
   @Before
   public void setUp() {
-    context = new GadgetContext();
+    elValues = Maps.newHashMap();
+    elResolver = new RootELResolver(elValues);
+    
   }
   
   @Test
@@ -67,8 +75,11 @@
         + "fields: ['name','id']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
+    assertNull(batch.getNextBatch(elResolver));
   }
   
   @Test
@@ -78,10 +89,13 @@
         + " groupId=\"group\""
         + " userId=\"first,second\""
         + " startIndex=\"20\""
-        + " count=\"10\""
-        + " fields=\"name,id\""
+        + " count=\"${count}\""
+        + " fields=\"${fields}\""
         + "/></Content>";
 
+    elValues.put("count", 10);
+    // TODO: try List, JSONArray
+    elValues.put("fields", "name,id");
     PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
     assertFalse(socialData.needsOwner());
     assertFalse(socialData.needsViewer());
@@ -94,8 +108,10 @@
         + "fields: ['name','id']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
   }
 
   @Test
@@ -114,8 +130,10 @@
         + "fields: ['name','id']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
   }
 
   @Test
@@ -134,8 +152,10 @@
         + "fields: ['name','id']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
   }
 
   @Test
@@ -155,8 +175,10 @@
         + "fields: ['foo','bar']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
   }
 
   @Test
@@ -176,8 +198,10 @@
         + "fields: ['foo','bar']"
         + "}}");
 
-    assertEquals(1, socialData.getSocialPreloads(context).size());
-    assertEquals(expected.toString(), socialData.getSocialPreloads(context).get("key").toString());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getHttpPreloads().isEmpty());
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertEquals(expected.toString(), batch.getSocialPreloads().get("key").toString());
   }
 
   @Test
@@ -191,7 +215,8 @@
     PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
     assertFalse(socialData.needsOwner());
 
-    assertTrue(socialData.getSocialPreloads(context).isEmpty());
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertNull(batch);
   }
 
   @Test(expected = SpecParserException.class)
@@ -203,21 +228,47 @@
   }
   
   @Test
+  public void testBatching() throws Exception {
+    String xml = "<Content xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+    		+ "<PeopleRequest key=\"key\" userId=\"${userId}\"/>"
+            + "<MakeRequest key=\"key2\" href=\"${key}\"/>"
+        + "</Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+    
+    PipelinedData.Batch batch = socialData.getBatch(elResolver);
+    assertTrue(batch.getSocialPreloads().isEmpty());
+    assertTrue(batch.getHttpPreloads().isEmpty());
+
+    // Now have "userId", the next batch should resolve the people request
+    elValues.put("userId", "foo");
+    batch = batch.getNextBatch(elResolver);
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertTrue(batch.getHttpPreloads().isEmpty());
+
+    elValues.put("key", "somedata");
+    batch = batch.getNextBatch(elResolver);
+    assertTrue(batch.getSocialPreloads().isEmpty());
+    assertEquals(1, batch.getHttpPreloads().size());
+    assertNull(batch.getNextBatch(elResolver));
+  }
+
+
+ @Test
   public void makeRequestDefaults() throws Exception {
     String xml = "<Content><MakeRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
         + " key=\"key\""
         + " href=\"/example.html\""
         + "/></Content>";
 
-    PipelinedData pipelinedData = new PipelinedData(
-        XmlUtil.parse(xml), Uri.parse("http://example.org/"));
-    Map<String, RequestAuthenticationInfo> httpPreloads = 
-        pipelinedData.getHttpPreloads(context);
+    PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+    PipelinedData.Batch batch = pipelinedData.getBatch(elResolver);
     
-    assertEquals(1, httpPreloads.size());
-    RequestAuthenticationInfo preload = httpPreloads.get("key");
+    assertEquals(1, batch.getHttpPreloads().size());
+    RequestAuthenticationInfo preload = batch.getHttpPreloads().get("key");
     assertEquals(AuthType.NONE, preload.getAuthType());
     assertEquals(Uri.parse("http://example.org/example.html"), preload.getHref());    
+    assertTrue(batch.getSocialPreloads().isEmpty());
   }
 
   @Test
@@ -229,15 +280,14 @@
         + " sign_owner=\"false\""
         + "/></Content>";
 
-    PipelinedData pipelinedData = new PipelinedData(
-        XmlUtil.parse(xml), Uri.parse("http://example.org/"));
-    Map<String, RequestAuthenticationInfo> httpPreloads = 
-        pipelinedData.getHttpPreloads(context);
+    PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+    PipelinedData.Batch batch = pipelinedData.getBatch(elResolver);
     
-    assertEquals(1, httpPreloads.size());
-    RequestAuthenticationInfo preload = httpPreloads.get("key");
+    assertEquals(1, batch.getHttpPreloads().size());
+    RequestAuthenticationInfo preload = batch.getHttpPreloads().get("key");
     assertEquals(AuthType.SIGNED, preload.getAuthType());
     assertTrue(preload.isSignViewer());
     assertFalse(preload.isSignOwner());
+    assertTrue(batch.getSocialPreloads().isEmpty());
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java?rev=743460&r1=743459&r2=743460&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java Wed Feb 11 19:31:29 2009
@@ -23,15 +23,16 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import java.util.Arrays;
-
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.xml.XmlUtil;
-import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.expressions.RootELResolver;
 import org.apache.shindig.gadgets.variables.Substitutions;
 import org.apache.shindig.gadgets.variables.Substitutions.Type;
-import org.junit.Test;
+
+import java.util.Arrays;
+
 import org.junit.Assert;
+import org.junit.Test;
 
 public class ViewTest {
   private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
@@ -209,10 +210,11 @@
         + " key=\"key\""
         + " fields=\"name,id\""
         + "/></Content>";
-    GadgetContext context = new GadgetContext();
     View view = new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
-    assertEquals(1, view.getPipelinedData().getSocialPreloads(context).size());
-    assertTrue(view.getPipelinedData().getSocialPreloads(context).containsKey("key"));
+    PipelinedData.Batch batch = view.getPipelinedData().getBatch(new RootELResolver());
+    
+    assertEquals(1, batch.getSocialPreloads().size());
+    assertTrue(batch.getSocialPreloads().containsKey("key"));
   }
 
   @Test(expected = SpecParserException.class)