/*
* 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.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.variables.Substitutions;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import javax.el.PropertyNotFoundException;
import javax.el.ValueExpression;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
/**
* Parsing code for <os:*> elements.
*/
public class PipelinedData {
private boolean needsViewer;
private boolean needsOwner;
private Map<String, BatchItemData> allPreloads;
public static final String OPENSOCIAL_NAMESPACE = "http://ns.opensocial.org/2008/markup";
public static final String EXTENSION_NAMESPACE = "http://ns.opensocial.org/2009/extensions";
public PipelinedData(Element element, Uri base) throws SpecParserException {
Map<String, BatchItemData> allPreloads = Maps.newHashMap();
// TODO: extract this loop into XmlUtils.getChildrenWithNamespace
for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) {
if (!(node instanceof Element)) {
continue;
}
Element child = (Element) node;
if (EXTENSION_NAMESPACE.equals(child.getNamespaceURI())) {
if ("Variable".equals(child.getLocalName())) {
allPreloads.put(child.getAttribute("key"), createVariableRequest(child));
}
} else if (OPENSOCIAL_NAMESPACE.equals(child.getNamespaceURI())) {
String elementName = child.getLocalName();
String key = child.getAttribute("key");
if (key == null) {
throw new SpecParserException("Missing key attribute on os:" + elementName);
}
try {
if ("PeopleRequest".equals(elementName)) {
allPreloads.put(key, createPeopleRequest(child));
} else if ("ViewerRequest".equals(elementName)) {
allPreloads.put(key, createViewerRequest(child));
} else if ("OwnerRequest".equals(elementName)) {
allPreloads.put(key, createOwnerRequest(child));
} else if ("PersonAppDataRequest".equals(elementName)) {
// TODO: delete when 0.9 app data retrieval is supported
allPreloads.put(key, createPersonAppDataRequest(child));
} else if ("ActivitiesRequest".equals(elementName)) {
allPreloads.put(key, createActivityRequest(child));
} else if ("DataRequest".equals(elementName)) {
allPreloads.put(key, createDataRequest(child));
} else if ("HttpRequest".equals(elementName)) {
allPreloads.put(key, createHttpRequest(child, base));
} else {
// TODO: This is wrong - the spec should parse, but should preload
// notImplemented
throw new SpecParserException("Unknown element <os:" + elementName + '>');
}
} catch (ELException ele) {
throw new SpecParserException(new XmlException(ele));
}
}
}
this.allPreloads = Collections.unmodifiableMap(allPreloads);
}
private BatchItemData createVariableRequest(Element child) {
return new VariableData(child.getAttribute("value"));
}
private PipelinedData(PipelinedData socialData, Substitutions substituter) {
Map<String, BatchItemData> allPreloads = Maps.newHashMap();
for (Map.Entry<String, BatchItemData> preload : socialData.allPreloads.entrySet()) {
allPreloads.put(preload.getKey(), preload.getValue().substitute(substituter));
}
this.allPreloads = Collections.unmodifiableMap(allPreloads);
}
/**
* Allows the creation of a view from an existing view so that localization
* can be performed.
*/
public PipelinedData substitute(Substitutions substituter) {
return new PipelinedData(this, substituter);
}
public interface Batch {
Map<String, BatchItem> getPreloads();
Batch getNextBatch(ELResolver rootObjects);
}
/** Temporary type until BatchItem is made fully polymorphic */
public enum BatchType {
SOCIAL,
HTTP,
VARIABLE
}
/** Item within a batch */
public interface BatchItem {
BatchType getType();
Object getData();
}
/** Shared data used to generate BatchItems */
interface BatchItemData {
BatchItem evaluate(Expressions expressions, ELContext elContext);
BatchItemData substitute(Substitutions substituter);
}
/**
* 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 org.apache.shindig.gadgets.GadgetELResolver
* @return a batch, or null if no batch could be created
*/
public Batch getBatch(Expressions expressions, ELResolver rootObjects) {
return getBatch(expressions, rootObjects, allPreloads);
}
/**
* Create a Batch of preload requests
* @param expressions expressions instance for parsing expressions
* @param rootObjects an ELResolver that can evaluate currently available
* root objects.
* @param currentPreloads the remaining social/http preloads
*/
private Batch getBatch(Expressions expressions, ELResolver rootObjects,
Map<String, BatchItemData> currentPreloads) {
ELContext elContext = expressions.newELContext(rootObjects);
Map<String, BatchItem> evaluatedPreloads = Maps.newHashMap();
Map<String, BatchItemData> pendingPreloads = null;
if (currentPreloads != null) {
for (Map.Entry<String, BatchItemData> preload : currentPreloads.entrySet()) {
try {
BatchItem value = preload.getValue().evaluate(expressions, elContext);
evaluatedPreloads.put(preload.getKey(), value);
} catch (PropertyNotFoundException pe) {
// Property-not-found: presume that this is because a top-level
// variable isn't available yet, which means that this needs to be
// postponed to the next batch.
if (pendingPreloads == null) {
pendingPreloads = Maps.newHashMap();
}
pendingPreloads.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 (evaluatedPreloads.isEmpty() && pendingPreloads == null) {
return null;
}
return new BatchImpl(expressions, evaluatedPreloads, pendingPreloads);
}
/** Batch implementation */
class BatchImpl implements Batch {
private final Expressions expressions;
private final Map<String, BatchItem> evaluatedPreloads;
private final Map<String, BatchItemData> pendingPreloads;
public BatchImpl(Expressions expressions, Map<String, BatchItem> evaluatedPreloads,
Map<String, BatchItemData> pendingPreloads) {
this.expressions = expressions;
this.evaluatedPreloads = evaluatedPreloads;
this.pendingPreloads = pendingPreloads;
}
public Batch getNextBatch(ELResolver rootObjects) {
return getBatch(expressions, rootObjects, pendingPreloads);
}
public Map<String, BatchItem> getPreloads() {
return evaluatedPreloads;
}
}
public boolean needsViewer() {
return needsViewer;
}
public boolean needsOwner() {
return needsOwner;
}
/** Handle the os:PeopleRequest element */
private SocialData createPeopleRequest(Element child) throws ELException {
SocialData expression = new SocialData(child.getAttribute("key"), "people.get");
copyAttribute("groupId", child, expression, String.class);
copyAttribute("userId", child, expression, JSONArray.class);
updateUserArrayState("userId", child);
copyAttribute("personId", child, expression, JSONArray.class);
updateUserArrayState("personId", child);
copyAttribute("startIndex", child, expression, Integer.class);
copyAttribute("count", child, expression, Integer.class);
copyAttribute("sortBy", child, expression, String.class);
copyAttribute("sortOrder", child, expression, String.class);
copyAttribute("filterBy", child, expression, String.class);
copyAttribute("filterOperation", child, expression, String.class);
copyAttribute("filterValue", child, expression, String.class);
copyAttribute("fields", child, expression, JSONArray.class);
return expression;
}
/** Handle the os:ViewerRequest element */
private SocialData createViewerRequest(Element child) throws ELException {
return createPersonRequest(child, "@viewer");
}
/** Handle the os:OwnerRequest element */
private SocialData createOwnerRequest(Element child) throws ELException {
return createPersonRequest(child, "@owner");
}
private SocialData createPersonRequest(Element child, String userId) throws ELException {
SocialData expression = new SocialData(child.getAttribute("key"), "people.get");
expression.addProperty("userId", userId, JSONArray.class);
updateUserState(userId);
copyAttribute("fields", child, expression, JSONArray.class);
return expression;
}
/** Handle the os:PersonAppDataRequest element */
private SocialData createPersonAppDataRequest(Element child) throws ELException {
SocialData expression = new SocialData(child.getAttribute("key"), "appdata.get");
copyAttribute("groupId", child, expression, String.class);
copyAttribute("userId", child, expression, JSONArray.class);
updateUserArrayState("userId", child);
copyAttribute("appId", child, expression, String.class);
copyAttribute("fields", child, expression, JSONArray.class);
return expression;
}
/** Handle the os:ActivitiesRequest element */
private SocialData createActivityRequest(Element child) throws ELException {
SocialData expression = new SocialData(child.getAttribute("key"), "activities.get");
copyAttribute("groupId", child, expression, String.class);
copyAttribute("userId", child, expression, JSONArray.class);
updateUserArrayState("userId", child);
copyAttribute("appId", child, expression, String.class);
// TODO: SHINDIG-711 should be activityIds?
copyAttribute("activityId", child, expression, JSONArray.class);
copyAttribute("fields", child, expression, JSONArray.class);
copyAttribute("startIndex", child, expression, Integer.class);
copyAttribute("count", child, expression, Integer.class);
// TODO: add activity paging support
return expression;
}
/** Handle the os:DataRequest element */
private SocialData createDataRequest(Element child) throws ELException, SpecParserException {
String method = child.getAttribute("method");
if (method == null) {
throw new SpecParserException("Missing @method attribute on os:DataRequest");
}
// TODO: should we support anything that doesn't end in .get?
// i.e, should this be a whitelist not a blacklist?
if (method.endsWith(".update")
|| method.endsWith(".create")
|| method.endsWith(".delete")) {
throw new SpecParserException("Unsupported @method attribute \"" + method + "\" on os:DataRequest");
}
SocialData expression = new SocialData(child.getAttribute("key"), method);
NamedNodeMap nodeMap = child.getAttributes();
for (int i = 0; i < nodeMap.getLength(); i++) {
Node attrNode = nodeMap.item(i);
// Skip namespaced attributes
if (attrNode.getNamespaceURI() != null) {
continue;
}
// Use getNodeName() instead of getLocalName(). NekoHTML has an incorrect
// implementation of node name that returns null.
String name = attrNode.getNodeName();
// Skip the built-in names
if ("method".equals(name) || "key".equals(name)) {
continue;
}
String value = attrNode.getNodeValue();
expression.addProperty(name, value, Object.class);
}
return expression;
}
/** Handle an os:HttpRequest element */
private HttpData createHttpRequest(Element child, Uri base) throws ELException {
HttpData data = new HttpData(child, base);
// Update needsOwner and needsViewer
if (data.authz != AuthType.NONE) {
if (data.signOwner) {
needsOwner = true;
}
if (data.signViewer) {
needsViewer = true;
}
}
return data;
}
private void copyAttribute(String name, Element element, SocialData expression, Class<?> type)
throws ELException {
if (element.hasAttribute(name)) {
expression.addProperty(name, element.getAttribute(name), type);
}
}
/** Look for @viewer, @owner within a userId attribute */
private void updateUserArrayState(String name, Element element) {
if (element.hasAttribute(name)) {
// TODO: check after Expression evaluation?
StringTokenizer tokens = new StringTokenizer(element.getAttribute(name), ",");
while (tokens.hasMoreTokens()) {
updateUserState(tokens.nextToken());
}
}
}
/** Updates whether this batch of SocialData needs owner or viewer data */
private void updateUserState(String userId) {
if ("@owner".equals(userId)) {
needsOwner = true;
} else if ("@viewer".equals(userId) || "@me".equals(userId)) {
needsViewer = true;
}
}
/**
* A single pipelined HTTP makerequest.
*/
private static class HttpData implements BatchItemData {
private final AuthType authz;
private final Uri base;
private final String href;
private final boolean signOwner;
private final boolean signViewer;
private final Map<String, String> attributes;
private static final Set<String> KNOWN_ATTRIBUTES =
ImmutableSet.of("authz", "href", "sign_owner", "sign_viewer");
/**
* Create an HttpData off an <os:makeRequest> element.
*/
public HttpData(Element element, Uri base) throws ELException {
this.base = base;
this.authz = element.hasAttribute("authz") ?
AuthType.parse(element.getAttribute("authz")) : AuthType.NONE;
// TODO: Spec question; should EL values be URL escaped?
this.href = element.getAttribute("href");
// TODO: Spec question; should sign_* default to true?
this.signOwner = booleanValue(element, "sign_owner", true);
this.signViewer = booleanValue(element, "sign_viewer", true);
// TODO: many of these attributes should not be EL enabled
ImmutableMap.Builder<String, String> attributes = ImmutableMap.builder();
for (int i = 0; i < element.getAttributes().getLength(); i++) {
Node attr = element.getAttributes().item(i);
if (!KNOWN_ATTRIBUTES.contains(attr.getNodeName())) {
attributes.put(attr.getNodeName(), attr.getNodeValue());
}
}
this.attributes = attributes.build();
}
private HttpData(HttpData data, Substitutions substituter) {
this.base = data.base;
this.authz = data.authz;
this.href = substituter.substituteString(data.href);
this.signOwner = data.signOwner;
this.signViewer = data.signViewer;
this.attributes = data.attributes;
}
/** Run substitutions over an HttpData */
public HttpData substitute(Substitutions substituter) {
return new HttpData(this, substituter);
}
/**
* Evaluate expressions and return a RequestAuthenticationInfo.
* @throws ELException if expression evaluation fails.
*/
public BatchItem evaluate(Expressions expressions, ELContext context)
throws ELException {
String hrefString = String.valueOf(expressions.parse(href, String.class)
.getValue(context));
final Uri evaluatedHref;
try {
evaluatedHref = base.resolve(Uri.parse(hrefString));
} catch (IllegalArgumentException e) {
throw new ELException("bad Uri '" + hrefString + "' - " + e.getMessage(), e);
}
final Map<String, String> evaluatedAttributes = Maps.newHashMap();
for (Map.Entry<String, String> attr : attributes.entrySet()) {
ValueExpression expression = expressions.parse(attr.getValue(), String.class);
evaluatedAttributes.put(attr.getKey(),
String.valueOf(expression.getValue(context)));
}
final RequestAuthenticationInfo info = new RequestAuthenticationInfo() {
public Map<String, String> getAttributes() {
return evaluatedAttributes;
}
public AuthType getAuthType() {
return authz;
}
public Uri getHref() {
return evaluatedHref;
}
public boolean isSignOwner() {
return signOwner;
}
public boolean isSignViewer() {
return signViewer;
}
};
return new BatchItem() {
public Object getData() {
return info;
}
public BatchType getType() {
return BatchType.HTTP;
}
};
}
/** Parse a boolean expression off an XML attribute. */
private boolean booleanValue(Element element, String attrName,
boolean defaultValue) {
if (!element.hasAttribute(attrName)) {
return defaultValue;
}
return "true".equalsIgnoreCase(element.getAttribute(attrName));
}
}
/**
* A single social data request.
*/
private static class SocialData implements BatchItemData {
private final List<Property> properties = Lists.newArrayList();
private final String id;
private final String method;
public SocialData(String id, String method) {
this.id = id;
this.method = method;
}
public void addProperty(String name, String value, Class<?> type) throws ELException {
properties.add(new Property(name, value, type));
}
/** Create the JSON request form for the social data */
private JSONObject toJson(Expressions expressions, ELContext elContext) throws ELException {
JSONObject object = new JSONObject();
try {
object.put("method", method);
object.put("id", id);
JSONObject params = new JSONObject();
for (Property property : properties) {
property.set(expressions, elContext, params);
}
object.put("params", params);
} catch (JSONException je) {
throw new ELException(je);
}
return object;
}
/** Single property for an expression */
private static class Property {
private final String name;
private final String value;
private final Class<?> type;
public Property(String name, String value, Class<?> type) {
this.name = name;
this.value = value;
this.type = type;
}
public void set(Expressions expressions, ELContext elContext, JSONObject object)
throws ELException {
ValueExpression expression = expressions.parse(value, type);
Object value = expression.getValue(elContext);
try {
if (value != null) {
object.put(name, value);
}
} catch (JSONException e) {
throw new ELException("Error parsing property \"" + name + '\"', e);
}
}
}
public BatchItem evaluate(Expressions expressions, ELContext elContext) throws ELException {
final JSONObject jsonResult = toJson(expressions, elContext);
return new BatchItem() {
public Object getData() {
return jsonResult;
}
public BatchType getType() {
return BatchType.SOCIAL;
}
};
}
public BatchItemData substitute(Substitutions substituter) {
// TODO: support hangman substution on social data?
return this;
}
}
private static class VariableData implements BatchItemData {
private final String value;
public VariableData(String value) {
this.value = value;
}
public BatchItem evaluate(Expressions expressions, ELContext elContext) throws ELException {
ValueExpression expression = expressions.parse(value, Object.class);
final Object result = expression.getValue(elContext);
return new BatchItem() {
public Object getData() {
return result;
}
public BatchType getType() {
return BatchType.VARIABLE;
}
};
}
public BatchItemData substitute(Substitutions substituter) {
return this;
}
}
}