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/14 00:24:54 UTC
svn commit: r744279 - in /incubator/shindig/trunk/java/gadgets/src:
main/java/org/apache/shindig/gadgets/rewrite/
main/java/org/apache/shindig/gadgets/templates/
test/java/org/apache/shindig/gadgets/rewrite/
test/java/org/apache/shindig/gadgets/templates/
Author: awiner
Date: Fri Feb 13 23:24:53 2009
New Revision: 744279
URL: http://svn.apache.org/viewvc?rev=744279&view=rev
Log:
SHINDIG-729 (in part): Partial implementation of server-side OpenSocial templating. Implemented by Lev Epshteyn, with some tweaks from Adam Winer.
This code is not yet enabled by default - to enable it, bind SocialMarkupHtmlParser as the GadgetHtmlParser, and include
the Pipeline and Template rewriters (in that order) in the list of rewriters.
Implemented functionality includes:
- ${} support in content and attributes
- @if and @repeat
TODOs include:
- Built-in tags (os:Html, os:Repeat, etc.)
- Custom tags
- An end-to-end test (once the code is enabled by default)
Added:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/
incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateProcessorTest.java
Modified:
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java
incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java?rev=744279&r1=744278&r2=744279&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java Fri Feb 13 23:24:53 2009
@@ -17,13 +17,18 @@
*/
package org.apache.shindig.gadgets.rewrite;
+import com.google.common.collect.Maps;
+
import org.apache.shindig.gadgets.GadgetException;
import org.apache.shindig.gadgets.http.HttpResponse;
import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.json.JSONObject;
import org.w3c.dom.Document;
+import java.util.Map;
+
/**
* Object that maintains a String representation of arbitrary contents
* and a consistent view of those contents as an HTML parse tree.
@@ -33,6 +38,7 @@
private HttpResponse contentSource;
private Document document;
private final GadgetHtmlParser contentParser;
+ private final Map<String, JSONObject> pipelinedData;
private static final String MUTABLE_CONTENT_LISTENER = "MutableContentListener";
@@ -49,6 +55,7 @@
public MutableContent(GadgetHtmlParser contentParser, String content) {
this.contentParser = contentParser;
this.content = content;
+ pipelinedData = Maps.newHashMap();
}
/**
@@ -58,6 +65,7 @@
public MutableContent(GadgetHtmlParser contentParser, HttpResponse contentSource) {
this.contentParser = contentParser;
this.contentSource = contentSource;
+ pipelinedData = Maps.newHashMap();
}
@@ -141,4 +149,12 @@
public boolean hasDocument() {
return (document != null);
}
+
+ public void addPipelinedData(String key, JSONObject value) {
+ pipelinedData.put(key, value);
+ }
+
+ public Map<String, JSONObject> getPipelinedData() {
+ return pipelinedData;
+ }
}
Modified: 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=744279&r1=744278&r2=744279&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataContentRewriter.java Fri Feb 13 23:24:53 2009
@@ -194,7 +194,7 @@
String key = entry.getKey();
// TODO: escape key
- // TODO: push to MutableContent
+ content.addPipelinedData(key, entry.getValue());
script.append("osd.DataContext.putDataSet(\"")
.append(key)
.append("\",")
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,146 @@
+/*
+ * 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.xml.DomUtil;
+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.spec.Feature;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import org.json.JSONObject;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * This ContentRewriter uses a TemplateProcessor to replace os-template
+ * tag contents of a gadget spec with their rendered equivalents.
+ *
+ * Only templates without the @name and @tag attributes are processed
+ * automatically.
+ */
+public class TemplateRewriter implements ContentRewriter {
+
+ public final static Set<String> TAGS = ImmutableSet.of("script");
+
+ /**
+ * Provider of the processor. TemplateRewriters are stateless and multithreaded,
+ * processors are not.
+ */
+ private final Provider<TemplateProcessor> processor;
+
+ @Inject
+ public TemplateRewriter(Provider<TemplateProcessor> processor) {
+ this.processor = processor;
+ }
+
+ public RewriterResults rewrite(HttpRequest request, HttpResponse original,
+ MutableContent content) {
+ return null;
+ }
+
+ public RewriterResults rewrite(Gadget gadget, MutableContent content) {
+ Feature f = gadget.getSpec().getModulePrefs().getFeatures()
+ .get("opensocial-templates");
+ if (f != null) {
+ return rewriteImpl(gadget, content);
+ }
+ return null;
+ }
+
+ private RewriterResults rewriteImpl(Gadget gadget, MutableContent content) {
+ List<Element> tagList =
+ DomUtil.getElementsByTagNameCaseInsensitive(content.getDocument(), TAGS);
+ final Map<String, JSONObject> pipelinedData = content.getPipelinedData();
+
+ List<Element> templates = ImmutableList.copyOf(
+ Iterables.filter(tagList, new Predicate<Element>() {
+ public boolean apply(Element element) {
+ String type = element.getAttribute("type");
+ String name = element.getAttribute("name");
+ String tag = element.getAttribute("tag");
+ String require = element.getAttribute("require");
+ // Templates with "tag" or "name" can't be processed; templates
+ // that require data that isn't available on the server can't
+ // be processed either
+ return "text/os-template".equals(type)
+ && "".equals(name)
+ && "".equals(tag)
+ && checkRequiredData(require, pipelinedData.keySet());
+ }
+ }));
+
+ if (templates.isEmpty()) {
+ return null;
+ }
+
+ TemplateContext templateContext = new TemplateContext(pipelinedData);
+ GadgetELResolver globalGadgetVars = new GadgetELResolver(gadget.getContext());
+
+ for (Element template : templates) {
+ DocumentFragment result = processor.get().processTemplate(
+ template, templateContext, globalGadgetVars);
+ // Note: replaceNode errors when replacing Element with DocumentFragment
+ template.getParentNode().insertBefore(result, template);
+ // TODO: clients that need to update data that is initially available,
+ // e.g. paging through friend lists, will still need the template
+ template.getParentNode().removeChild(template);
+ }
+
+ // TODO: Deactivate the "os-templates" feature if all templates have
+ // been rendered.
+ // Note: This may not always be correct behavior since client may want
+ // some purely client-side templating (such as from libraries)
+ MutableContent.notifyEdit(content.getDocument());
+ return RewriterResults.cacheableIndefinitely();
+ }
+
+ /**
+ * Checks that all the required data is available at rewriting time.
+ * @param requiredData A string of comma-separated data set names
+ * @param availableData A map of available data sets
+ * @return true if all required data sets are present, false otherwise
+ */
+ private static boolean checkRequiredData(String requiredData, Set<String> availableData) {
+ if ("".equals(requiredData)) {
+ return true;
+ }
+ StringTokenizer st = new StringTokenizer(requiredData, ",");
+ while (st.hasMoreTokens()) {
+ if (!availableData.contains(st.nextToken().trim())) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,63 @@
+/*
+ * 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.templates;
+
+import org.json.JSONObject;
+
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Context for processing a single template.
+ */
+public class TemplateContext {
+ private final Map<String, JSONObject> top;
+ private Object cur = null;
+ // TODO: support unique Id
+ private Map<String, ? extends Object> context = ImmutableMap.of();
+
+ public TemplateContext(Map<String, JSONObject> top) {
+ this.top = top;
+ }
+
+ public Map<String, ? extends Object> getTop() {
+ return top;
+ }
+
+ public Object getCur() {
+ return cur != null ? cur : top;
+ }
+
+ public Object setCur(Object data) {
+ Object oldCur = cur;
+ cur = data;
+ return oldCur;
+ }
+
+ public Map<String, ? extends Object> getContext() {
+ return context;
+ }
+
+ public Map<String, ? extends Object> setContext(Map<String, ? extends Object> newContext) {
+ Map<String, ? extends Object> oldContext = context;
+ context = newContext;
+ return oldContext;
+ }
+}
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,144 @@
+/*
+ * 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.templates;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.PropertyNotWritableException;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * ELResolver used to process OpenSocial templates. Provides three variables:
+ * <ul>
+ * <li>"Top": Global values </li>
+ */
+public class TemplateELResolver extends ELResolver {
+ public static final String PROPERTY_TOP = "Top";
+ public static final String PROPERTY_CONTEXT = "Context";
+ public static final String PROPERTY_CUR = "Cur";
+
+ private static final Set<String> TOP_LEVEL_PROPERTIES =
+ ImmutableSet.of(PROPERTY_TOP, PROPERTY_CONTEXT, PROPERTY_CUR);
+
+ private final TemplateContext templateContext;
+
+ public TemplateELResolver(TemplateContext templateContext) {
+ this.templateContext = templateContext;
+ }
+
+ @Override
+ public Class<?> getCommonPropertyType(ELContext context, Object base) {
+ if (base == null) {
+ return String.class;
+ }
+
+ return null;
+ }
+
+ @Override
+ public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+ Object base) {
+ return null;
+ }
+
+ @Override
+ public Class<?> getType(ELContext context, Object base, Object property) {
+ // TODO: implement
+ return null;
+ }
+
+ @Override
+ public Object getValue(ELContext context, Object base, Object property) {
+ if (base == null) {
+ if (TOP_LEVEL_PROPERTIES.contains(property)) {
+ context.setPropertyResolved(true);
+ if (PROPERTY_TOP.equals(property)) {
+ return templateContext.getTop();
+ } else if (PROPERTY_CONTEXT.equals(property)) {
+ return templateContext.getContext();
+ } else {
+ return templateContext.getCur();
+ }
+ }
+
+ // Check variables.
+ if (property instanceof String) {
+ ValueExpression valueExp = context.getVariableMapper().resolveVariable((String) property);
+ if (valueExp != null) {
+ context.setPropertyResolved(true);
+ return valueExp.getValue(context);
+ }
+ }
+
+ // Check current context next.
+ Object cur = templateContext.getCur();
+ // Resolve through "cur" as if it were a value - if "isPropertyResolved()"
+ // is true, it was handled
+ Object value = context.getELResolver().getValue(context, cur, property);
+ if (context.isPropertyResolved()) {
+ if (value != null) {
+ return value;
+ } else {
+ context.setPropertyResolved(false);
+ }
+ }
+
+ // Check current scope variables next.
+ Map<String, ? extends Object> scope = templateContext.getContext();
+ if (scope.containsKey(property)) {
+ context.setPropertyResolved(true);
+ return scope.get(property);
+ }
+
+ // Look at Top context last.
+ scope = templateContext.getTop();
+ if (scope.containsKey(property)) {
+ context.setPropertyResolved(true);
+ return scope.get(property);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isReadOnly(ELContext context, Object base, Object property) {
+ if (base == null && TOP_LEVEL_PROPERTIES.contains(property)) {
+ context.setPropertyResolved(true);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void setValue(ELContext context, Object base, Object property, Object value) {
+ if (base == null && TOP_LEVEL_PROPERTIES.contains(property)) {
+ throw new PropertyNotWritableException();
+ }
+ }
+
+}
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,402 @@
+/*
+ * 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.templates;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSerializer;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.logging.Logger;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.ValueExpression;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+/**
+ * Implements a DOM-based OS templates compiler.
+ * Supports:
+ * - ${...} style expressions in content and attributes
+ * - @if attribute
+ * - @repeat attribute
+ * TODO:
+ * - Handle built-in/custom tags
+ */
+public class TemplateProcessor {
+
+ private static final Logger logger = Logger.getLogger(TemplateProcessor.class.getName());
+
+ public static final String PROPERTY_INDEX = "Index";
+ public static final String PROPERTY_COUNT = "Count";
+
+ public static final String ATTRIBUTE_IF = "if";
+ public static final String ATTRIBUTE_INDEX = "index";
+ public static final String ATTRIBUTE_REPEAT = "repeat";
+ public static final String ATTRIBUTE_VAR = "var";
+
+ private final Expressions expressions;
+ // Reused buffer for creating template output
+ private final StringBuilder outputBuffer;
+
+ private TemplateContext templateContext;
+ private ELContext elContext;
+
+ @Inject
+ public TemplateProcessor(Expressions expressions) {
+ this.expressions = expressions;
+ outputBuffer = new StringBuilder();
+ }
+
+ /**
+ * Process an entire template.
+ *
+ * @param template the DOM template, typically a script element
+ * @param templateContext a template context providing top-level
+ * variables
+ * @param globals ELResolver providing global variables other
+ * than those in the templateContext
+ * @return a document fragment with the resolved content
+ */
+ public DocumentFragment processTemplate(Element template,
+ TemplateContext templateContext, ELResolver globals) {
+
+ this.templateContext = templateContext;
+ this.elContext = expressions.newELContext(globals,
+ new TemplateELResolver(templateContext));
+
+ DocumentFragment result = template.getOwnerDocument().createDocumentFragment();
+ processChildrenOf(result, template);
+ return result;
+ }
+
+ /**
+ * Process a node.
+ *
+ * @param result the target node where results should be inserted
+ * @param source the source node of the template being processed
+ */
+ private void processNode(Node result, Node source) {
+ switch (source.getNodeType()) {
+ case Node.TEXT_NODE:
+ processText(result, source.getTextContent());
+ break;
+ case Node.ELEMENT_NODE:
+ processElement(result, (Element) source);
+ break;
+ case Node.DOCUMENT_NODE:
+ processChildrenOf(result, source);
+ break;
+ }
+ }
+
+ /**
+ * Process text content by including non-expression content verbatim and
+ * escaping expression content.
+
+ * @param result the target node where results should be inserted
+ * @param textContent the text content being processed
+ */
+ private void processText(Node result, String textContent) {
+ Document ownerDocument = result.getOwnerDocument();
+
+ int start = 0;
+ int current = 0;
+ while (current < textContent.length()) {
+ current = textContent.indexOf("${", current);
+ // No expressions, we're done
+ if (current < 0) {
+ break;
+ }
+
+ // An escaped expression "\${"
+ if (current > 0 && textContent.charAt(current - 1) == '\\') {
+ // Drop the \ by outputting everything before it, and moving past
+ // the ${
+ if (current - 1 > start) {
+ String staticText = textContent.substring(start, current - 1);
+ result.appendChild(ownerDocument.createTextNode(staticText));
+ }
+
+ start = current;
+ current = current + 2;
+ continue;
+ }
+
+ // Not a real expression, we're done
+ int expressionEnd = textContent.indexOf('}', current + 2);
+ if (expressionEnd < 0) {
+ break;
+ }
+
+ // Append the existing static text, if any
+ if (current > start) {
+ result.appendChild(ownerDocument.createTextNode(textContent.substring(start, current)));
+ }
+
+ // Isolate the expression, parse and evaluate
+ String expression = textContent.substring(current, expressionEnd + 1);
+ String value = processString(expression);
+
+ if (!"".equals(value)) {
+ // And now escape
+ outputBuffer.setLength(0);
+ try {
+ NekoSerializer.printEscapedText(value, outputBuffer);
+ } catch (IOException e) {
+ // Can't happen writing to StringBuilder
+ throw new RuntimeException(e);
+ }
+
+ result.appendChild(ownerDocument.createTextNode(outputBuffer.toString()));
+ }
+
+ // And continue with the next expression
+ current = start = expressionEnd + 1;
+ }
+
+ // Add any static text left over
+ if (start < textContent.length()) {
+ result.appendChild(ownerDocument.createTextNode(textContent.substring(start)));
+ }
+ }
+
+
+ /**
+ * Process repeater state, if needed, on an element.
+ */
+ private void processElement(Node result, Element element) {
+ Attr repeat = element.getAttributeNode(ATTRIBUTE_REPEAT);
+ if (repeat != null) {
+ // TODO: Is Iterable the right interface here? The spec calls for
+ // length to be available.
+ Iterable<?> dataList = processIterable(repeat.getValue());
+ if (dataList != null) {
+ // Compute list size
+ int size = Iterables.size(dataList);
+
+ // Save the initial EL state
+ Map<String, ? extends Object> oldContext = templateContext.getContext();
+ Object oldCur = templateContext.getCur();
+ ValueExpression oldVarExpression = null;
+
+ // Set the new Context variable. Copy the old context to preserve
+ // any existing "index" variable
+ Map<String, Object> loopData = Maps.newHashMap(oldContext);
+ loopData.put(PROPERTY_COUNT, size);
+ templateContext.setContext(loopData);
+
+ // TODO: This means that any loop with @var doesn't make the loop
+ // variable available in the default expression context.
+ // Update the specification to make this explicit.
+ Attr varAttr = element.getAttributeNode(ATTRIBUTE_VAR);
+ if (varAttr == null) {
+ oldCur = templateContext.getCur();
+ } else {
+ oldVarExpression = elContext.getVariableMapper().resolveVariable(varAttr.getValue());
+ }
+
+ Attr indexVarAttr = element.getAttributeNode(ATTRIBUTE_INDEX);
+ String indexVar = indexVarAttr == null ? PROPERTY_INDEX : indexVarAttr.getValue();
+
+ int index = 0;
+ for (Object data : dataList) {
+ loopData.put(indexVar, index++);
+
+ // Set up context for rendering inner node
+ templateContext.setCur(data);
+ if (varAttr != null) {
+ ValueExpression varExpression = expressions.constant(data, Object.class);
+ elContext.getVariableMapper().setVariable(varAttr.getValue(), varExpression);
+ }
+
+ processElementInner(result, element);
+ }
+
+ // Restore EL state
+ if (varAttr == null) {
+ templateContext.setCur(oldCur);
+ } else {
+ elContext.getVariableMapper().setVariable(varAttr.getValue(), oldVarExpression);
+ }
+
+ templateContext.setContext(oldContext);
+ }
+ } else {
+ processElementInner(result, element);
+ }
+ }
+
+ /**
+ * Process conditionals and non-repeat attributes on an element
+ */
+ private void processElementInner(Node result, Element element) {
+ Attr ifAttribute = element.getAttributeNode(ATTRIBUTE_IF);
+ if (ifAttribute != null) {
+ if (!processBoolean(ifAttribute.getValue())) {
+ return;
+ }
+ }
+
+ Element resultNode = (Element) element.cloneNode(false);
+ result.appendChild(resultNode);
+
+ // Remove special attributes
+ resultNode.removeAttribute(ATTRIBUTE_IF);
+ resultNode.removeAttribute(ATTRIBUTE_REPEAT);
+ resultNode.removeAttribute(ATTRIBUTE_INDEX);
+ resultNode.removeAttribute(ATTRIBUTE_VAR);
+
+ processAttributes(resultNode);
+ processChildrenOf(resultNode, element);
+ }
+
+ /**
+ * Process expressions on attributes
+ */
+ private void processAttributes(Element element) {
+ NamedNodeMap attributes = element.getAttributes();
+ for (int i = 0; i < attributes.getLength(); i++) {
+ Attr attribute = (Attr) attributes.item(i);
+ attribute.setNodeValue(processString(attribute.getValue()));
+ }
+ }
+
+ /** Process the children of an element or document. */
+ private void processChildrenOf(Node result, Node parent) {
+ NodeList nodes = parent.getChildNodes();
+ for (int i = 0; i < nodes.getLength(); i++) {
+ processNode(result, nodes.item(i));
+ }
+ }
+
+ private String processString(String expression) {
+ String result = "";
+ try {
+ ValueExpression expr = expressions.parse(expression, String.class);
+ result = (String) expr.getValue(elContext);
+ } catch (ELException e) {
+ logger.warning(e.getMessage());
+ }
+ return result;
+ }
+
+ private boolean processBoolean(String expression) {
+ Boolean result = false;
+ try {
+ ValueExpression expr = expressions.parse(expression, Boolean.class);
+ result = (Boolean) expr.getValue(elContext);
+ } catch (ELException e) {
+ logger.warning(e.getMessage());
+ }
+ return result == null ? false : result;
+ }
+
+ private Iterable<?> processIterable(String expression) {
+ Iterable<?> result = null;
+ try {
+ ValueExpression expr = expressions.parse(expression, Object.class);
+ Object value = expr.getValue(elContext);
+ return coerceToIterable(value);
+ } catch (ELException e) {
+ logger.warning(e.getMessage());
+ }
+ return result;
+ }
+
+ /**
+ * Coerce objects to iterables. Iterables and JSONArrays have the obvious
+ * coercion. JSONObjects are coerced to single-element lists, unless
+ * they have a "list" property that is in array, in which case that's used.
+ */
+ private Iterable<?> coerceToIterable(Object value) {
+ if (value == null) {
+ return ImmutableList.of();
+ }
+
+ if (value instanceof Iterable) {
+ return ((Iterable<?>) value);
+ }
+
+ if (value instanceof JSONArray) {
+ final JSONArray array = (JSONArray) value;
+ // TODO: Extract JSONArrayIterator class?
+ return new Iterable<Object>() {
+ public Iterator<Object> iterator() {
+ return new Iterator<Object>() {
+ private int i = 0;
+
+ public boolean hasNext() {
+ return i < array.length();
+ }
+
+ public Object next() {
+ if (i >= array.length()) {
+ throw new NoSuchElementException();
+ }
+
+ try {
+ return array.getJSONObject(i++);
+ } catch (Exception e) {
+ throw new ELException(e);
+ }
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ };
+ }
+
+ if (value instanceof JSONObject) {
+ JSONObject json = (JSONObject) value;
+
+ // Does this object have a "list" property that is an array?
+ // TODO: add to specification
+ Object childList = json.opt("list");
+ if (childList != null && childList instanceof JSONArray) {
+ return coerceToIterable(childList);
+ }
+
+ // A scalar JSON value is treated as a single element list.
+ return ImmutableList.of(json);
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,160 @@
+/*
+ * 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.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.expressions.Expressions;
+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.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Provider;
+
+/**
+ * Tests for TemplateRewriter
+ */
+public class TemplateRewriterTest {
+
+ private GadgetSpec gadgetSpec;
+ private Gadget gadget;
+ private MutableContent content;
+ private TemplateRewriter rewriter;
+
+ private static final Uri GADGET_URI = Uri.parse("http://example.org/gadget.php");
+
+ private static final String CONTENT_PLAIN =
+ "<script type='text/os-template'>Hello, ${user.name}</script>";
+
+ private static final String CONTENT_REQUIRE =
+ "<script type='text/os-template' require='user'>Hello, ${user.name}</script>";
+
+ private static final String CONTENT_REQUIRE_MISSING =
+ "<script type='text/os-template' require='foo'>Hello, ${user.name}</script>";
+
+ private static final String CONTENT_WITH_NAME =
+ "<script type='text/os-template' name='myTemplate'>Hello, ${user.name}</script>";
+
+ private static final String CONTENT_WITH_TAG =
+ "<script type='text/os-template' tag='foo:Bar'>Hello, ${user.name}</script>";
+
+
+ @Before
+ public void setUp() {
+ rewriter = new TemplateRewriter(
+ new Provider<TemplateProcessor>() {
+ public TemplateProcessor get() {
+ return new TemplateProcessor(Expressions.sharedInstance());
+ }
+ });
+ }
+
+ @Test
+ public void simpleTemplate() throws Exception {
+ // Render a simple template
+ testExpectingTransform(getGadgetXml(CONTENT_PLAIN), "simple");
+ }
+
+ @Test
+ public void noTemplateFeature() throws Exception {
+ // Without opensocial-templates feature, shouldn't render
+ testExpectingNoTransform(getGadgetXml(CONTENT_PLAIN, false), "no feature");
+ }
+
+ @Test
+ public void requiredDataPresent() throws Exception {
+ // Required data is present - render
+ testExpectingTransform(getGadgetXml(CONTENT_REQUIRE), "required data");
+ }
+
+ @Test
+ public void requiredDataMissing() throws Exception {
+ // Required data is missing - don't render
+ testExpectingNoTransform(getGadgetXml(CONTENT_REQUIRE_MISSING), "missing data");
+ }
+
+ @Test
+ public void nameAttributePresent() throws Exception {
+ // Don't render templates with a @name
+ testExpectingNoTransform(getGadgetXml(CONTENT_WITH_NAME), "with @name");
+ }
+
+ @Test
+ public void tagAttributePresent() throws Exception {
+ // Don't render templates with a @tag
+ testExpectingNoTransform(getGadgetXml(CONTENT_WITH_TAG), "with @tag");
+ }
+
+ private void testExpectingTransform(String code, String condition) throws Exception {
+ setupGadget(code);
+ rewriter.rewrite(gadget, content);
+ assertTrue("Template wasn't transformed (" + condition + ")",
+ content.getContent().indexOf("Hello, John") > 0);
+ assertTrue("Template tag wasn't removed (" + condition + ")",
+ content.getContent().indexOf("text/os-template") < 0);
+ }
+
+ private void testExpectingNoTransform(String code, String condition) throws Exception {
+ setupGadget(code);
+ rewriter.rewrite(gadget, content);
+ assertTrue("Template was transformed (" + condition + ")",
+ content.getContent().indexOf("${user.name}") > 0);
+ assertTrue("Template tag was removed (" + condition + ")",
+ content.getContent().indexOf("text/os-template") > 0);
+ }
+
+ private void setupGadget(String gadgetXml) throws SpecParserException, JSONException {
+ 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());
+ putPipelinedData("user", new JSONObject("{ name: 'John'}"));
+ }
+
+ private void putPipelinedData(String key, JSONObject data) {
+ content.addPipelinedData(key, data);
+ }
+
+ private static String getGadgetXml(String content) {
+ return getGadgetXml(content, true);
+ }
+
+ private static String getGadgetXml(String content, boolean requireFeature) {
+ String feature = requireFeature ?
+ "<Require feature='opensocial-templates'/>" : "";
+ return "<Module>" + "<ModulePrefs title='Title'>"
+ + feature + "</ModulePrefs>"
+ + "<Content>"
+ + " <![CDATA[" + content + "]]>"
+ + "</Content></Module>";
+ }
+}
Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateProcessorTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateProcessorTest.java?rev=744279&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateProcessorTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateProcessorTest.java Fri Feb 13 23:24:53 2009
@@ -0,0 +1,175 @@
+/*
+ * 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.templates;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSerializer;
+import org.apache.shindig.gadgets.parse.nekohtml.SocialMarkupHtmlParser;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+
+import java.io.IOException;
+import java.util.Map;
+
+import javax.el.ELResolver;
+
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Unit tests for TemplateProcessor.
+ * TODO: Refactor to remove boilerplate.
+ * TODO: Add tests for special vars.
+ * TODO: Add test for @var in @repeat loops.
+ */
+public class TemplateProcessorTest {
+
+ private Expressions expressions;
+
+ private TemplateContext context;
+ private TemplateProcessor processor;
+ private Map<String, JSONObject> variables;
+ private ELResolver resolver;
+
+ private SocialMarkupHtmlParser parser;
+
+ @Before
+ public void setUp() throws Exception {
+ expressions = new Expressions();
+ variables = Maps.newHashMap();
+ processor = new TemplateProcessor(expressions);
+ resolver = new RootELResolver();
+ parser = new SocialMarkupHtmlParser(new ParseModule.DOMImplementationProvider().get());
+ context = new TemplateContext(variables);
+
+ addVariable("foo", new JSONObject("{ title: 'bar' }"));
+ addVariable("user", new JSONObject("{ id: '101', name: { first: 'John', last: 'Doe' }}"));
+ addVariable("toys", new JSONObject("{ list: [{name: 'Ball'}, {name: 'Car'}]}"));
+ addVariable("xss", new JSONObject("{ script: '<script>alert();</script>'," +
+ "quote:'\"><script>alert();</script>'}"));
+ }
+
+ @Test
+ public void testTextNode() throws Exception {
+ String output = executeTemplate("${foo.title}");
+ assertEquals("bar", output);
+ }
+
+ @Test
+ public void testPlainText() throws Exception {
+ // Verify that plain text is not interfered with, or incorrectly escaped
+ String output = executeTemplate("<span>foo&&bar</span>");
+ assertEquals("<span>foo&&bar</span>", output);
+ }
+
+ @Test
+ public void testTextNodeEscaping() throws Exception {
+ String output = executeTemplate("${xss.script}");
+ assertFalse("Escaping not performed: \"" + output + "\"", output.contains("<script>alert("));
+ }
+
+ @Test
+ public void testAppending() throws Exception {
+ String output = executeTemplate("${user.id}${user.name.first}");
+ assertEquals("101John", output);
+
+ output = executeTemplate("foo${user.id}bar${user.name.first}baz");
+ assertEquals("foo101barJohnbaz", output);
+
+ output = executeTemplate("foo${user.nope}bar${user.nor}baz");
+ assertEquals("foobarbaz", output);
+ }
+
+ @Test
+ public void testEscapedExpressions() throws Exception {
+ String output = executeTemplate("\\${escaped}");
+ assertEquals("${escaped}", output);
+
+ output = executeTemplate("foo\\${escaped}bar");
+ assertEquals("foo${escaped}bar", output);
+ }
+
+ @Test
+ public void testElement() throws Exception {
+ String output = executeTemplate("<span title=\"${user.id}\">${user.name.first} baz</span>");
+ assertEquals("<span title=\"101\">John baz</span>", output);
+ }
+
+ @Test
+ public void testAttributeEscaping() throws Exception {
+ String output = executeTemplate("<span title=\"${xss.quote}\">${user.name.first} baz</span>");
+ assertFalse(output.contains("\"><script>alert("));
+ }
+
+ @Test
+ public void testRepeat() throws Exception {
+ String output = executeTemplate("<span repeat=\"${toys}\">${name}</span>");
+ assertEquals("<span>Ball</span><span>Car</span>", output);
+ }
+
+ @Test
+ public void testConditional() throws Exception {
+ String output = executeTemplate(
+ "<span repeat=\"${toys}\">" +
+ "<span if=\"${name == 'Car'}\">Car</span>" +
+ "<span if=\"${name != 'Car'}\">Not Car</span>" +
+ "</span>");
+ assertEquals("<span><span>Not Car</span></span><span><span>Car</span></span>", output);
+ }
+
+ private String executeTemplate(String markup) throws Exception {
+ Element template = prepareTemplate(markup);
+ DocumentFragment result = processor.processTemplate(template, context, resolver);
+ return serialize(result);
+ }
+
+ private Element prepareTemplate(String markup) throws GadgetException {
+ String content = "<script type=\"text/os-template\">" + markup + "</script>";
+ Document document = parser.parseDom(content);
+ return (Element) document.getElementsByTagName("script").item(0);
+ }
+
+ private String serialize(Node node) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ NodeList children = node.getChildNodes();
+ for (int i = 0; i < children.getLength(); i++) {
+ Node child = children.item(i);
+ NekoSerializer.serialize(child, sb);
+ }
+ return sb.toString();
+ }
+
+ private void addVariable(String key, JSONObject value) {
+ variables.put(key, value);
+ }
+}