You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by jo...@apache.org on 2009/11/02 21:34:20 UTC
svn commit: r832093 [2/3] - in /incubator/shindig/trunk: features/
features/src/main/javascript/features/
features/src/main/javascript/features/analytics/
features/src/main/javascript/features/auth-refresh/
features/src/main/javascript/features/core.au...
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java?rev=832093&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java Mon Nov 2 20:34:16 2009
@@ -0,0 +1,543 @@
+/*
+ * 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.features;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+/**
+ * Mechanism for loading feature.xml files from a location keyed by a String.
+ * That String might be the location of a text file which in turn contains
+ * other feature file locations; a directory; or a feature.xml file itself.
+ */
+@Singleton
+public class FeatureRegistry {
+ public static final char FILE_SEPARATOR = ',';
+ public static final String RESOURCE_SCHEME = "res";
+ public static final String FILE_SCHEME = "file";
+
+ private static final Logger logger
+ = Logger.getLogger("org.apache.shindig.gadgets");
+
+ // Map keyed by FeatureNode object created as a lookup for transitive feature deps.
+ private final Map<Collection<String>, List<FeatureResource>> cache = new MapMaker().makeMap();
+ private final Map<Collection<String>, List<FeatureResource>> cacheIgnoreUnsupported =
+ new MapMaker().makeMap();
+
+ private final FeatureParser parser;
+ private final FeatureResourceLoader resourceLoader;
+ private final Map<String, FeatureNode> featureMap;
+
+ @Inject
+ public FeatureRegistry(FeatureResourceLoader resourceLoader) {
+ this.parser = new FeatureParser();
+ this.resourceLoader = resourceLoader;
+ this.featureMap = Maps.newHashMap();
+ }
+
+ /**
+ * For compatibility with GadgetFeatureRegistry, and to provide a programmatic hook
+ * for adding feature files by config, @Inject the @Named featureFiles variable.
+ * @param featureFiles
+ * @throws GadgetException
+ */
+ @Inject(optional = true)
+ public void addDefaultFeatures(
+ @Named("shindig.features.default") String featureFiles) throws GadgetException {
+ register(featureFiles);
+ }
+
+ /**
+ * Reads and registers all of the features in the directory, or the file, specified by
+ * the given resourceKey. Invalid features or invalid paths will yield a
+ * GadgetException.
+ *
+ * All features registered by this method must be valid (well-formed XML, resource
+ * references all return successfully), and each "batch" of registered features
+ * must be able to be assimilated into the current features tree in a valid fashion.
+ * That is, their dependencies must all be valid features as well, and the
+ * dependency tree must not contain circular dependencies.
+ *
+ * @param resourceKey The file or directory to load the feature from. If feature.xml
+ * is passed in directly, it will be loaded as a single feature. If a
+ * directory is passed, any features in that directory (recursively) will
+ * be loaded. If res://*.txt or res:*.txt is passed, we will look for named resources
+ * in the text file. If path is prefixed with res:// or res:, the file
+ * is treated as a resource, and all references are assumed to be
+ * resources as well. Multiple locations may be specified by separating
+ * them with a comma.
+ * @throws GadgetException If any of the files can't be read, are malformed, or invalid.
+ */
+ public void register(String resourceKey) throws GadgetException {
+ try {
+ for (String location : StringUtils.split(resourceKey, FILE_SEPARATOR)) {
+ Uri uriLoc = getComponentUri(location);
+
+ if (uriLoc.getScheme() != null && uriLoc.getScheme().equals(RESOURCE_SCHEME)) {
+ List<String> resources = Lists.newArrayList();
+
+ // Load as resource using ResourceLoader.
+ location = uriLoc.getPath();
+ if (location.startsWith("/")) {
+ // Accommodate res:// URIs.
+ location = location.substring(1);
+ }
+ logger.info("Loading resources from: " + uriLoc.toString());
+
+ if (location.endsWith(".txt")) {
+ // Text file contains a list of other resource files to load
+ for (String resource : getResourceContent(location).split("[\r\n]+")) {
+ resource = resource.trim();
+ if (resource.length () > 0 && resource.charAt(0) != '#') {
+ // Skip blank/commented lines.
+ resource = getComponentUri(resource.trim()).getPath();
+ resources.add(resource);
+ }
+ }
+ } else {
+ resources.add(location);
+ }
+
+ loadResources(resources);
+ } else {
+ // Load files in directory structure.
+ logger.info("Loading files from: " + location);
+
+ loadFile(uriLoc.getPath());
+ }
+ }
+
+ // Connect the dependency graph made up of all features and validate there
+ // are no circular deps.
+ connectDependencyGraph();
+ } catch (IOException e) {
+ throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
+ }
+ }
+
+ /**
+ * For the given list of needed features, retrieves all the FeatureResource objects that
+ * contain their content and that of their transitive dependencies.
+ *
+ * Resources are returned in order of their place in the dependency tree, with "bottom"/
+ * depended-on resources returned before those that depend on them. Resource objects
+ * within a given feature are returned in the order specified in their corresponding
+ * feature.xml file. In the case of a dependency tree "tie" eg. A depends on [B, C], B and C
+ * depend on D - resources are returned in the dependency order specified in feature.xml.
+ *
+ * Fills the "unsupported" list, if provided, with unknown features in the needed list.
+ *
+ * @param ctx Context for the request.
+ * @param needed List of all needed features.
+ * @param unsupported If non-null, a List populated with unknown features from the needed list.
+ * @return List of FeatureResources that may be used to render the needed features.
+ * @throws GadgetException
+ */
+ public List<FeatureResource> getFeatureResources(
+ GadgetContext ctx, Collection<String> needed, List<String> unsupported) {
+ Map<Collection<String>, List<FeatureResource>> useCache =
+ (unsupported != null) ? cache : cacheIgnoreUnsupported;
+
+ List<FeatureResource> resources = Lists.newLinkedList();
+
+ if (useCache.containsKey(needed)) {
+ return useCache.get(needed);
+ }
+
+ List<FeatureNode> fullTree = getTransitiveDeps(needed, unsupported);
+
+ String targetBundleType =
+ ctx.getRenderingContext() == RenderingContext.CONTAINER ? "container" : "gadget";
+
+ for (FeatureNode entry : fullTree) {
+ for (FeatureBundle bundle : entry.getBundles()) {
+ if (bundle.getType().equals(targetBundleType)) {
+ if (containerMatch(bundle.getAttribs().get("container"), ctx.getContainer())) {
+ resources.addAll(bundle.getResources());
+ }
+ }
+ }
+ }
+
+ if (unsupported == null || unsupported.isEmpty()) {
+ useCache.put(needed, resources);
+ }
+
+ return resources;
+ }
+
+ /**
+ * Returns all known FeatureResources in dependency order, as described in getFeatureResources.
+ * Returns only GADGET-context resources. This is a convenience method largely for calculating
+ * JS checksum.
+ * @return List of all known (RenderingContext.GADGET) FeatureResources.
+ */
+ public List<FeatureResource> getAllFeatures() {
+ return getFeatureResources(
+ new GadgetContext(), Lists.newArrayList(featureMap.keySet()), null);
+ }
+
+ /**
+ * Calculates and returns a dependency-ordered (as in getFeatureResources) list of features
+ * included directly or transitively from the specified list of needed features.
+ * This API ignores any unknown features among the needed list.
+ * @param needed List of features for which to obtain an ordered dep list.
+ * @return Ordered list of feature names, as described.
+ */
+ public List<String> getFeatures(List<String> needed) {
+ List<FeatureNode> fullTree = getTransitiveDeps(needed, Lists.<String>newLinkedList());
+ List<String> allFeatures = Lists.newLinkedList();
+ for (FeatureNode node : fullTree) {
+ allFeatures.add(node.name);
+ }
+ return allFeatures;
+ }
+
+ // Visible for testing.
+ String getResourceContent(String resource) throws IOException {
+ return ResourceLoader.getContent(resource);
+ }
+
+ // Provided for backward compatibility with existing feature loader configurations.
+ // res://-prefixed URIs are actually scheme = res, host = "", path = "/stuff". We want res:path.
+ // Package-private for use by FeatureParser as well.
+ static Uri getComponentUri(String str) {
+ Uri uri = null;
+ if (str.startsWith("res://")) {
+ uri = new UriBuilder().setScheme(RESOURCE_SCHEME).setPath(str.substring(6)).toUri();
+ } else {
+ uri = Uri.parse(str);
+ }
+ return uri;
+ }
+
+ private List<FeatureNode> getTransitiveDeps(Collection<String> needed, List<String> unsupported) {
+ final List<FeatureNode> requested = Lists.newArrayList();
+ for (String featureName : needed) {
+ if (featureMap.containsKey(featureName)) {
+ requested.add(featureMap.get(featureName));
+ } else {
+ if (unsupported != null) unsupported.add(featureName);
+ }
+ }
+
+ Comparator<FeatureNode> nodeDepthComparator = new Comparator<FeatureNode>() {
+ public int compare(FeatureNode one, FeatureNode two) {
+ if (one.nodeDepth > two.nodeDepth ||
+ (one.nodeDepth == two.nodeDepth &&
+ requested.indexOf(one) < requested.indexOf(two))) {
+ return -1;
+ }
+ return 1;
+ }
+ };
+ // Before getTransitiveDeps() is called, all nodes and their graphs have been validated
+ // to have no circular dependencies, with their tree depth calculated. The requested
+ // features here may overlap in the tree, so we need to be sure not to double-include
+ // deps. Consider case where feature A depends on B and C, which both depend on D.
+ // If the requested features list is [A, C], we want to include A's tree in the appropriate
+ // order, and avoid double-including C (and its dependency D). Thus we sort by node depth
+ // first - A's tree is deeper than that of C, so *if* A's tree contains C, traversing
+ // it first guarantees that C is eventually included.
+ Collections.sort(requested, nodeDepthComparator);
+
+ Set<String> alreadySeen = Sets.newHashSet();
+ List<FeatureNode> fullDeps = Lists.newLinkedList();
+ for (FeatureNode requestedFeature : requested) {
+ for (FeatureNode toAdd : requestedFeature.getTransitiveDeps()) {
+ if (!alreadySeen.contains(toAdd.name)) {
+ alreadySeen.add(toAdd.name);
+ fullDeps.add(toAdd);
+ }
+ }
+ }
+
+ return fullDeps;
+ }
+
+ private boolean containerMatch(String containerAttrib, String container) {
+ if (containerAttrib == null || containerAttrib.length() == 0) {
+ // Nothing specified = all match.
+ return true;
+ }
+ Set<String> containers = Sets.newHashSet();
+ for (String attr : containerAttrib.split(",")) {
+ containers.add(attr.trim());
+ }
+ return containers.contains(container);
+ }
+
+ private void connectDependencyGraph() throws GadgetException {
+ // Iterate through each raw dependency, adding the corresponding feature to the graph.
+ // Collect as many feature dep tree errors as possible before erroring out.
+ List<String> problems = Lists.newLinkedList();
+ List<FeatureNode> theFeatures = Lists.newLinkedList();
+
+ // First hook up all first-order dependencies.
+ for (Map.Entry<String, FeatureNode> featureEntry : featureMap.entrySet()) {
+ String name = featureEntry.getKey();
+ FeatureNode feature = featureEntry.getValue();
+
+ for (String rawDep : feature.getRawDeps()) {
+ if (!featureMap.containsKey(rawDep)) {
+ problems.add("Feature [" + name + "] has dependency on unknown feature: " + rawDep);
+ } else {
+ feature.addDep(featureMap.get(rawDep));
+ theFeatures.add(feature);
+ }
+ }
+ }
+
+ // Then hook up the transitive dependency graph to validate there are
+ // no loops present.
+ for (FeatureNode feature : theFeatures) {
+ try {
+ // Validates the dependency tree ensuring no circular dependencies,
+ // and calculates the depth of the dependency tree rooted at the node.
+ feature.completeNodeGraph();
+ } catch (GadgetException e) {
+ problems.add(e.getMessage());
+ }
+ }
+
+ if (problems.size() > 0) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Problems found processing features:\n");
+ for (String problem : problems) {
+ sb.append(problem).append("\n");
+ }
+ throw new GadgetException(GadgetException.Code.INVALID_CONFIG, sb.toString());
+ }
+ }
+
+ private void loadResources(List<String> resources) throws GadgetException {
+ try {
+ for (String resource : resources) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine("Processing resource: " + resource);
+ }
+
+ String content = getResourceContent(resource);
+ Uri parent = new UriBuilder().setScheme(RESOURCE_SCHEME).setPath(resource).toUri();
+ loadFeature(parent, content);
+ }
+ } catch (IOException e) {
+ throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
+ }
+ }
+
+ private void loadFile(String filePath) throws GadgetException, IOException {
+ File file = new File(filePath);
+ if (!file.exists() || !file.canRead()) {
+ throw new GadgetException(GadgetException.Code.INVALID_CONFIG,
+ "Feature file '" + filePath + "' doesn't exist or can't be read");
+ }
+
+ File[] toLoad = null;
+ if (file.isDirectory()) {
+ toLoad = file.listFiles();
+ } else {
+ toLoad = new File[] { file };
+ }
+
+ for (File featureFile : toLoad) {
+ String featureFilePath = featureFile.getAbsolutePath();
+ if (featureFilePath.toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
+ String content = ResourceLoader.getContent(featureFile);
+ Uri parent = new UriBuilder().setScheme("file").setPath(featureFilePath).toUri();
+ loadFeature(parent, content);
+ } else {
+ if (logger.isLoggable(Level.FINEST)) {
+ logger.finest(featureFile.getAbsolutePath() + " doesn't seem to be an XML file.");
+ }
+ }
+ }
+ }
+
+ protected void loadFeature(Uri parent, String xml) throws GadgetException {
+ FeatureParser.ParsedFeature parsed = parser.parse(parent, xml);
+
+ // Duplicate feature = OK, just indicate it's being overridden.
+ if (featureMap.containsKey(parsed.getName())) {
+ if (logger.isLoggable(Level.WARNING)) {
+ logger.warning("Overriding feature: " + parsed.getName() + " with def at: " + parent);
+ }
+ }
+
+ // Walk through all parsed bundles, pulling resources and creating FeatureBundles/Nodes.
+ List<FeatureBundle> bundles = Lists.newArrayList();
+ for (FeatureParser.ParsedFeature.Bundle parsedBundle : parsed.getBundles()) {
+ List<FeatureResource> resources = Lists.newArrayList();
+ for (FeatureParser.ParsedFeature.Resource parsedResource : parsedBundle.getResources()) {
+ if (parsedResource.getSource() == null) {
+ resources.add(new InlineFeatureResource(parsedResource.getContent()));
+ } else {
+ // Load using resourceLoader
+ resources.add(
+ resourceLoader.load(parsedResource.getSource(), parsedResource.getAttribs()));
+ }
+ }
+ bundles.add(new FeatureBundle(parsedBundle.getType(), parsedBundle.getAttribs(), resources));
+ }
+
+ // Add feature to the master Map. The dependency tree isn't connected/validated/linked yet.
+ featureMap.put(parsed.getName(), new FeatureNode(parsed.getName(), bundles, parsed.getDeps()));
+ }
+
+ private static class InlineFeatureResource extends FeatureResource.Default {
+ private final String content;
+
+ private InlineFeatureResource(String content) {
+ this.content = content;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getDebugContent() {
+ return content;
+ }
+ }
+
+ private static class FeatureBundle {
+ private final String type;
+ private final Map<String, String> attribs;
+ private final List<FeatureResource> resources;
+
+ private FeatureBundle(String type, Map<String, String> attribs,
+ List<FeatureResource> resources) {
+ this.type = type;
+ this.attribs = Collections.unmodifiableMap(attribs);
+ this.resources = Collections.unmodifiableList(resources);
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public Map<String, String> getAttribs() {
+ return attribs;
+ }
+
+ public List<FeatureResource> getResources() {
+ return resources;
+ }
+ }
+
+ private static class FeatureNode {
+ private final String name;
+ private final List<FeatureBundle> bundles;
+ private final List<String> requestedDeps;
+ private final List<FeatureNode> depList;
+ private List<FeatureNode> transitiveDeps;
+ private boolean calculatedDepsStale;
+ private int nodeDepth = 0;
+
+ private FeatureNode(String name, List<FeatureBundle> bundles, List<String> rawDeps) {
+ this.name = name;
+ this.bundles = Collections.unmodifiableList(bundles);
+ this.requestedDeps = Collections.unmodifiableList(rawDeps);
+ this.depList = Lists.newLinkedList();
+ this.transitiveDeps = Lists.newArrayList(this);
+ this.calculatedDepsStale = false;
+ }
+
+ public List<FeatureBundle> getBundles() {
+ return bundles;
+ }
+
+ public List<String> getRawDeps() {
+ return requestedDeps;
+ }
+
+ public void addDep(FeatureNode dep) {
+ depList.add(dep);
+ calculatedDepsStale = true;
+ }
+
+ private List<FeatureNode> getDepList() {
+ List<FeatureNode> revOrderDeps = Lists.newArrayList(depList);
+ Collections.reverse(revOrderDeps);
+ return Collections.unmodifiableList(revOrderDeps);
+ }
+
+ public void completeNodeGraph() throws GadgetException {
+ if (!calculatedDepsStale) {
+ return;
+ }
+
+ this.nodeDepth = 0;
+ this.transitiveDeps = Lists.newLinkedList();
+ this.transitiveDeps.add(this);
+
+ Queue<Pair<FeatureNode, Pair<Integer, String>>> toTraverse = Lists.newLinkedList();
+ toTraverse.add(Pair.of(this, Pair.of(0, "")));
+
+ while (!toTraverse.isEmpty()) {
+ Pair<FeatureNode, Pair<Integer, String>> next = toTraverse.poll();
+ String debug = next.two.two + (next.two.one > 0 ? " -> " : "") + next.one.name;
+ if (next.one == this && next.two.one != 0) {
+ throw new GadgetException(GadgetException.Code.INVALID_CONFIG,
+ "Feature dep loop detected: " + debug);
+ }
+ // Breadth-first list of dependencies.
+ this.transitiveDeps.add(next.one);
+ this.nodeDepth = Math.max(this.nodeDepth, next.two.one);
+ for (FeatureNode nextDep : next.one.getDepList()) {
+ toTraverse.add(Pair.of(nextDep, Pair.of(next.two.one + 1, debug)));
+ }
+ }
+
+ Collections.reverse(this.transitiveDeps);
+ calculatedDepsStale = false;
+ }
+
+ public List<FeatureNode> getTransitiveDeps() {
+ return this.transitiveDeps;
+ }
+ }
+}
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java?rev=832093&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java Mon Nov 2 20:34:16 2009
@@ -0,0 +1,75 @@
+/*
+ * 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.features;
+
+/**
+ * Interface yielding content/code for JS features.
+ */
+public interface FeatureResource {
+ /**
+ * @return "Normal"-mode content for the feature, eg. obfuscated JS.
+ */
+ String getContent();
+
+ /**
+ * @return Debug-mode content for the feature.
+ */
+ String getDebugContent();
+
+ /**
+ * @return True if the content is actually a URL to be included via <script src>
+ */
+ boolean isExternal();
+
+ /**
+ * @return True if the JS can be cached by intermediary proxies or not.
+ */
+ boolean isProxyCacheable();
+
+ /**
+ * Helper base class to avoid having to implement rarely-overridden isExternal/isProxyCacheable
+ * functionality in FeatureResource.
+ */
+ public abstract class Default implements FeatureResource {
+ public boolean isExternal() {
+ return false;
+ }
+
+ public boolean isProxyCacheable() {
+ return true;
+ }
+ }
+
+ public class Simple extends Default {
+ private final String content;
+ private final String debugContent;
+
+ public Simple(String content, String debugContent) {
+ this.content = content;
+ this.debugContent = debugContent;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getDebugContent() {
+ return debugContent;
+ }
+ }
+}
Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java?rev=832093&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java Mon Nov 2 20:34:16 2009
@@ -0,0 +1,198 @@
+/*
+ * 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.features;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * Class that loads FeatureResource objects used to populate JS feature code.
+ */
+public class FeatureResourceLoader {
+ private static final Logger logger
+ = Logger.getLogger("org.apache.shindig.gadgets");
+
+ private HttpFetcher fetcher;
+
+ @Inject
+ public void setHttpFetcher(HttpFetcher fetcher) {
+ this.fetcher = fetcher;
+ }
+
+ /**
+ * Primary, and only public, method of FeatureResourceLoader. Loads the resource
+ * keyed at the given {@code uri}, which was decorated with the provided list of attributes.
+ *
+ * The default implementation loads both file and res-schema resources using
+ * ResourceLoader, attempting to load optimized content for files named [file].js as [file].opt.js.
+ *
+ * Override this method to provide custom functionality. Basic loadFile, loadResource, and loadUri
+ * methods are kept protected for easy reuse.
+ *
+ * @param uri Uri of resource to be loaded.
+ * @param attribs Attributes decorating the resource in the corresponding feature.xml
+ * @return FeatureResource object providing content and debugContent loading capability.
+ * @throws GadgetException If any failure occurs during this process.
+ */
+ public FeatureResource load(Uri uri, Map<String, String> attribs) throws GadgetException {
+ try {
+ if (uri.getScheme().equals("file")) {
+ return loadFile(uri.getPath(), attribs);
+ } else if (uri.getScheme().equals("res")) {
+ return loadResource(uri.getPath(), attribs);
+ }
+ return loadUri(uri, attribs);
+ } catch (IOException e) {
+ throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, e);
+ }
+ }
+
+ protected FeatureResource loadFile(String path, Map<String, String> attribs) throws IOException {
+ return new DualModeStaticResource(path, getFileContent(new File(getOptPath(path))),
+ getFileContent(new File(path)));
+ }
+
+ protected String getFileContent(File file) {
+ try {
+ return ResourceLoader.getContent(file);
+ } catch (IOException e) {
+ // This is fine; errors happen downstream.
+ return null;
+ }
+ }
+
+ protected FeatureResource loadResource(
+ String path, Map<String, String> attribs) throws IOException {
+ return new DualModeStaticResource(path, getResourceContent(getOptPath(path)),
+ getResourceContent(path));
+ }
+
+ protected String getResourceContent(String resource) {
+ try {
+ return ResourceLoader.getContent(resource);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ protected FeatureResource loadUri(Uri uri, Map<String, String> attribs) {
+ String inline = attribs.get("inline");
+ inline = inline != null ? inline : "";
+ return new UriResource(fetcher, uri, "1".equals(inline) || "true".equalsIgnoreCase(inline));
+ }
+
+ protected String getOptPath(String orig) {
+ if (orig.endsWith(".js") && !orig.endsWith(".opt.js")) {
+ return orig.substring(0, orig.length() - 3) + ".opt.js";
+ }
+ return orig;
+ }
+
+ private static class DualModeStaticResource extends FeatureResource.Default {
+ private final String content;
+ private final String debugContent;
+
+ private DualModeStaticResource(String path, String content, String debugContent) {
+ this.content = content != null ? content : debugContent;
+ this.debugContent = debugContent != null ? debugContent : content;
+ if (this.content == null) {
+ throw new IllegalArgumentException("Problems reading resource: " + path);
+ }
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public String getDebugContent() {
+ return debugContent;
+ }
+ }
+
+ private static class UriResource implements FeatureResource {
+ private final HttpFetcher fetcher;
+ private final Uri uri;
+ private final boolean isInline;
+ private String content;
+ private long lastLoadTryMs;
+
+ private UriResource(HttpFetcher fetcher, Uri uri, boolean isInline) {
+ this.fetcher = fetcher;
+ this.uri = uri;
+ this.isInline = isInline;
+ this.lastLoadTryMs = 0;
+ this.content = getContent();
+ }
+
+ public String getContent() {
+ if (isExternal()) {
+ return uri.toString();
+ } else if (content != null) {
+ // Variable content is a one-time content cache for inline JS features.
+ return content;
+ }
+
+ // Try to load the content. Ideally, and most of the time, this
+ // will happen immediately at startup. However, if the target server is
+ // down it shouldn't hose the entire server, so in that case we defer
+ // and try at most once per minute thereafter, the delay in place to
+ // avoid overwhelming a server down on its heels.
+ long now = System.currentTimeMillis();
+ if (fetcher != null && now > (lastLoadTryMs + (60 * 1000))) {
+ lastLoadTryMs = now;
+ try {
+ HttpRequest request = new HttpRequest(uri);
+ HttpResponse response = fetcher.fetch(request);
+ if (response.getHttpStatusCode() == HttpResponse.SC_OK) {
+ content = response.getResponseAsString();
+ } else {
+ logger.warning("Unable to retrieve remote library from " + uri);
+ }
+ } catch (GadgetException e) {
+ logger.warning("Unable to retrieve remote library from " + uri);
+ }
+ }
+
+ return content;
+ }
+
+ public String getDebugContent() {
+ return getContent();
+ }
+
+ public boolean isExternal() {
+ return !isInline;
+ }
+
+ public boolean isProxyCacheable() {
+ return content != null;
+ }
+
+ }
+}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java Mon Nov 2 20:34:16 2009
@@ -23,8 +23,8 @@
import org.apache.shindig.gadgets.GadgetBlacklist;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetException;
-import org.apache.shindig.gadgets.GadgetFeatureRegistry;
import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.spec.View;
import org.apache.shindig.gadgets.variables.VariableSubstituter;
@@ -44,19 +44,19 @@
private final VariableSubstituter substituter;
private final ContainerConfig containerConfig;
private final GadgetBlacklist blacklist;
- private final GadgetFeatureRegistry gadgetFeatureRegistry;
+ private final FeatureRegistry featureRegistry;
@Inject
public Processor(GadgetSpecFactory gadgetSpecFactory,
VariableSubstituter substituter,
ContainerConfig containerConfig,
GadgetBlacklist blacklist,
- GadgetFeatureRegistry gadgetFeatureRegistry) {
+ FeatureRegistry featureRegistry) {
this.gadgetSpecFactory = gadgetSpecFactory;
this.substituter = substituter;
this.blacklist = blacklist;
this.containerConfig = containerConfig;
- this.gadgetFeatureRegistry = gadgetFeatureRegistry;
+ this.featureRegistry = featureRegistry;
}
/**
@@ -87,7 +87,7 @@
return new Gadget()
.setContext(context)
- .setGadgetFeatureRegistry(gadgetFeatureRegistry)
+ .setGadgetFeatureRegistry(featureRegistry)
.setSpec(spec)
.setCurrentView(getView(context, spec));
} catch (GadgetException e) {
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java Mon Nov 2 20:34:16 2009
@@ -22,8 +22,6 @@
import org.apache.shindig.common.xml.DomUtil;
import org.apache.shindig.gadgets.Gadget;
import org.apache.shindig.gadgets.GadgetException;
-import org.apache.shindig.gadgets.JsFeatureLoader;
-import org.apache.shindig.gadgets.JsLibrary;
import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
import org.apache.shindig.gadgets.rewrite.MutableContent;
import org.w3c.dom.Document;
@@ -45,13 +43,7 @@
private static final String I18N_FEATURE_NAME = "opensocial-i18n";
private static final String DATA_PATH = "features/i18n/data/";
private Map<Locale, String> i18nConstantsCache = new ConcurrentHashMap<Locale, String>();
- private JsFeatureLoader jsFeatureLoader;
- @Inject
- public OpenSocialI18NGadgetRewriter(JsFeatureLoader jsFeatureLoader) {
- this.jsFeatureLoader = jsFeatureLoader;
- }
-
public void rewrite(Gadget gadget, MutableContent mutableContent) {
// Don't touch sanitized gadgets.
if (gadget.sanitizeOutput()) {
@@ -82,15 +74,16 @@
} else {
// load gadgets.i18n.DateTimeConstants and gadgets.i18n.NumberFormatConstants
String localeName = getLocaleNameForLoadingI18NConstants(locale);
- JsLibrary dateTimeConstants = jsFeatureLoader.createJsLibrary(JsLibrary.Type.RESOURCE,
- DATA_PATH + "DateTimeConstants__" + localeName + ".js",
- "opensocial-i18n", null);
- JsLibrary numberConstants = jsFeatureLoader.createJsLibrary(JsLibrary.Type.RESOURCE,
- DATA_PATH + "NumberFormatConstants__" + localeName + ".js",
- "opensocial-i18n", null);
- inlineJs.append(dateTimeConstants.getContent())
- .append('\n').append(numberConstants.getContent());
- i18nConstantsCache.put(locale, inlineJs.toString());
+ String dateTimeConstantsResource = "DateTimeConstants__" + localeName + ".js";
+ String numberConstantsResource = "NumberFormatConstants__" + localeName + ".js";
+ try {
+ inlineJs.append(ResourceLoader.getContent(dateTimeConstantsResource))
+ .append('\n').append(ResourceLoader.getContent(numberConstantsResource));
+ i18nConstantsCache.put(locale, inlineJs.toString());
+ } catch (IOException e) {
+ throw new GadgetException(GadgetException.Code.INVALID_CONFIG,
+ "Unexpected inability to load i18n data for locale: " + localeName);
+ }
}
Element inlineTag = headTag.getOwnerDocument().createElement("script");
headTag.appendChild(inlineTag);
@@ -105,16 +98,14 @@
try {
attemptToLoadResource(language);
localeName = language;
- } catch (IOException e) {
- }
+ } catch (IOException e) { }
}
if (!country.equalsIgnoreCase("ALL")) {
try {
attemptToLoadResource(localeName + '_' + country);
localeName += '_' + country;
- } catch (IOException e) {
- }
+ } catch (IOException e) { }
}
return localeName;
}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java Mon Nov 2 20:34:16 2009
@@ -26,17 +26,16 @@
import org.apache.shindig.gadgets.Gadget;
import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.GadgetException;
-import org.apache.shindig.gadgets.GadgetFeature;
-import org.apache.shindig.gadgets.GadgetFeatureRegistry;
-import org.apache.shindig.gadgets.JsLibrary;
import org.apache.shindig.gadgets.MessageBundleFactory;
-import org.apache.shindig.gadgets.RenderingContext;
import org.apache.shindig.gadgets.UnsupportedFeatureException;
import org.apache.shindig.gadgets.UrlGenerator;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
import org.apache.shindig.gadgets.preload.PreloadException;
import org.apache.shindig.gadgets.preload.PreloadedData;
import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
import org.apache.shindig.gadgets.spec.Feature;
import org.apache.shindig.gadgets.spec.MessageBundle;
import org.apache.shindig.gadgets.spec.ModulePrefs;
@@ -50,7 +49,6 @@
import java.util.Arrays;
import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -83,6 +81,8 @@
*/
public class RenderingGadgetRewriter implements GadgetRewriter {
private static final Logger LOG = Logger.getLogger(RenderingGadgetRewriter.class.getName());
+
+ private static final int INLINE_JS_BUFFER = 50;
static final String DEFAULT_CSS =
"body,td,div,span,p{font-family:arial,sans-serif;}" +
@@ -92,22 +92,22 @@
static final String INSERT_BASE_ELEMENT_KEY = "gadgets.insertBaseElement";
static final String FEATURES_KEY = "gadgets.features";
- private final MessageBundleFactory messageBundleFactory;
- private final ContainerConfig containerConfig;
- private final GadgetFeatureRegistry featureRegistry;
- private final UrlGenerator urlGenerator;
- private final RpcServiceLookup rpcServiceLookup;
- private Set<String> defaultForcedLibs = ImmutableSet.of();
+ protected final MessageBundleFactory messageBundleFactory;
+ protected final ContainerConfig containerConfig;
+ protected final FeatureRegistry featureRegistry;
+ protected final UrlGenerator urlGenerator;
+ protected final RpcServiceLookup rpcServiceLookup;
+ protected Set<String> defaultExternLibs = ImmutableSet.of();
/**
* @param messageBundleFactory Used for injecting message bundles into gadget output.
*/
@Inject
public RenderingGadgetRewriter(MessageBundleFactory messageBundleFactory,
- ContainerConfig containerConfig,
- GadgetFeatureRegistry featureRegistry,
- UrlGenerator urlGenerator,
- RpcServiceLookup rpcServiceLookup) {
+ ContainerConfig containerConfig,
+ FeatureRegistry featureRegistry,
+ UrlGenerator urlGenerator,
+ RpcServiceLookup rpcServiceLookup) {
this.messageBundleFactory = messageBundleFactory;
this.containerConfig = containerConfig;
this.featureRegistry = featureRegistry;
@@ -118,11 +118,11 @@
@Inject
public void setDefaultForcedLibs(@Named("shindig.gadget-rewrite.default-forced-libs")String forcedLibs) {
if (forcedLibs != null && forcedLibs.length() > 0) {
- defaultForcedLibs = ImmutableSortedSet.copyOf(Arrays.asList(forcedLibs.split(":")));
+ defaultExternLibs = ImmutableSortedSet.copyOf(Arrays.asList(forcedLibs.split(":")));
}
}
- public void rewrite(Gadget gadget, MutableContent mutableContent) {
+ public void rewrite(Gadget gadget, MutableContent mutableContent) throws RewritingException {
// Don't touch sanitized gadgets.
if (gadget.sanitizeOutput()) {
return;
@@ -183,11 +183,11 @@
} catch (GadgetException e) {
// TODO: Rewriter interface needs to be modified to handle GadgetException or
// RewriterException or something along those lines.
- throw new RuntimeException(e);
+ throw new RewritingException(e.getLocalizedMessage(), e);
}
}
- private void injectBaseTag(Gadget gadget, Node headTag) {
+ protected void injectBaseTag(Gadget gadget, Node headTag) {
GadgetContext context = gadget.getContext();
if (containerConfig.getBool(context.getContainer(), INSERT_BASE_ELEMENT_KEY)) {
Uri base = gadget.getSpec().getUrl();
@@ -201,7 +201,7 @@
}
}
- private void injectOnLoadHandlers(Node bodyTag) {
+ protected void injectOnLoadHandlers(Node bodyTag) {
Element onloadScript = bodyTag.getOwnerDocument().createElement("script");
bodyTag.appendChild(onloadScript);
onloadScript.appendChild(bodyTag.getOwnerDocument().createTextNode(
@@ -211,84 +211,102 @@
/**
* Injects javascript libraries needed to satisfy feature dependencies.
*/
- private void injectFeatureLibraries(Gadget gadget, Node headTag) throws GadgetException {
+ protected void injectFeatureLibraries(Gadget gadget, Node headTag) throws GadgetException {
// TODO: If there isn't any js in the document, we can skip this. Unfortunately, that means
// both script tags (easy to detect) and event handlers (much more complex).
GadgetContext context = gadget.getContext();
- String forcedLibs = context.getParameter("libs");
+ String externParam = context.getParameter("libs");
- // List of forced libraries we need
- Set<String> forced;
+ // List of extern libraries we need
+ Set<String> extern;
- // gather the libraries we'll need to generate the forced libs
- if (forcedLibs == null || forcedLibs.length() == 0) {
+ // gather the libraries we'll need to generate the extern libs
+ if (externParam == null || externParam.length() == 0) {
// Don't bother making a mutable copy if the list is empty
- forced = (defaultForcedLibs.isEmpty()) ? defaultForcedLibs : Sets.newTreeSet(defaultForcedLibs);
+ extern = (defaultExternLibs.isEmpty()) ? defaultExternLibs :
+ Sets.newTreeSet(defaultExternLibs);
} else {
- forced = Sets.newTreeSet(Arrays.asList(forcedLibs.split(":")));
+ extern = Sets.newTreeSet(Arrays.asList(externParam.split(":")));
}
- if (!forced.isEmpty()) {
- String jsUrl = urlGenerator.getBundledJsUrl(forced, context);
+
+ if (!extern.isEmpty()) {
+ String jsUrl = urlGenerator.getBundledJsUrl(extern, context);
Element libsTag = headTag.getOwnerDocument().createElement("script");
libsTag.setAttribute("src", jsUrl);
headTag.appendChild(libsTag);
-
- // Forced transitive deps need to be added as well so that they don't get pulled in twice.
- // Without this, a shared dependency between forced and non-forced libs would get pulled into
- // both the external forced script and the inlined script.
- // TODO: Figure out a clean way to avoid having to call getFeatures twice.
- for (GadgetFeature dep : featureRegistry.getFeatures(forced)) {
- forced.add(dep.getName());
+ }
+
+ List<String> unsupported = Lists.newLinkedList();
+ List<FeatureResource> externResources =
+ featureRegistry.getFeatureResources(gadget.getContext(), extern, unsupported);
+ if (!unsupported.isEmpty()) {
+ throw new UnsupportedFeatureException("In extern &libs=: " + unsupported.toString());
+ }
+
+ // Get all resources requested by the gadget's requires/optional features.
+ Map<String, Feature> featureMap = gadget.getSpec().getModulePrefs().getFeatures();
+ List<String> gadgetFeatureKeys = Lists.newArrayList(gadget.getDirectFeatureDeps());
+ List<FeatureResource> gadgetResources =
+ featureRegistry.getFeatureResources(gadget.getContext(), gadgetFeatureKeys, unsupported);
+ if (!unsupported.isEmpty()) {
+ List<String> requiredUnsupported = Lists.newLinkedList();
+ for (String notThere : unsupported) {
+ if (!featureMap.containsKey(notThere) || featureMap.get(notThere).getRequired()) {
+ // if !containsKey, the lib was forced with Gadget.addFeature(...) so implicitly req'd.
+ requiredUnsupported.add(notThere);
+ }
+ }
+ if (!requiredUnsupported.isEmpty()) {
+ throw new UnsupportedFeatureException(requiredUnsupported.toString());
}
}
- // Make this read-only
- forced = ImmutableSet.copyOf(forced);
-
- // Inline any libs that weren't forced. The ugly context switch between inline and external
- // Js is needed to allow both inline and external scripts declared in feature.xml.
- String container = context.getContainer();
- Collection<GadgetFeature> features = getFeatures(gadget, forced);
+
+ // Calculate inlineResources as all resources that are needed by the gadget to
+ // render, minus all those included through externResources.
+ // TODO: profile and if needed, optimize this a bit.
+ List<FeatureResource> inlineResources = Lists.newArrayList(gadgetResources);
+ inlineResources.removeAll(externResources);
// Precalculate the maximum length in order to avoid excessive garbage generation.
int size = 0;
- for (GadgetFeature feature : features) {
- for (JsLibrary library : feature.getJsLibraries(RenderingContext.GADGET, container)) {
- if (library.getType().equals(JsLibrary.Type.URL)) {
- size += library.getContent().length();
+ for (FeatureResource resource : inlineResources) {
+ if (!resource.isExternal()) {
+ if (context.getDebug()) {
+ size += resource.getDebugContent().length();
+ } else {
+ size += resource.getContent().length();
}
}
}
- // Really inexact.
- StringBuilder inlineJs = new StringBuilder(size);
+ List<String> allRequested = Lists.newArrayList(gadgetFeatureKeys);
+ allRequested.addAll(extern);
+ String libraryConfig =
+ getLibraryConfig(gadget, featureRegistry.getFeatures(allRequested));
+
+ // Size has a small fudge factor added to it for delimiters and such.
+ StringBuilder inlineJs = new StringBuilder(size + libraryConfig.length() + INLINE_JS_BUFFER);
- for (GadgetFeature feature : features) {
- for (JsLibrary library : feature.getJsLibraries(RenderingContext.GADGET, container)) {
- if (library.getType().equals(JsLibrary.Type.URL)) {
- if (inlineJs.length() > 0) {
- Element inlineTag = headTag.getOwnerDocument().createElement("script");
- headTag.appendChild(inlineTag);
- inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(inlineJs.toString()));
- inlineJs.setLength(0);
- }
- Element referenceTag = headTag.getOwnerDocument().createElement("script");
- referenceTag.setAttribute("src", library.getContent());
- headTag.appendChild(referenceTag);
- } else {
- if (!forced.contains(feature.getName())) {
- // already pulled this file in from the shared contents.
- if (context.getDebug()) {
- inlineJs.append(library.getDebugContent());
- } else {
- inlineJs.append(library.getContent());
- }
- inlineJs.append(";\n");
- }
+ // Inline any libs that weren't extern. The ugly context switch between inline and external
+ // Js is needed to allow both inline and external scripts declared in feature.xml.
+ for (FeatureResource resource : inlineResources) {
+ String theContent = context.getDebug() ? resource.getDebugContent() : resource.getContent();
+ if (resource.isExternal()) {
+ if (inlineJs.length() > 0) {
+ Element inlineTag = headTag.getOwnerDocument().createElement("script");
+ headTag.appendChild(inlineTag);
+ inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(inlineJs.toString()));
+ inlineJs.setLength(0);
}
+ Element referenceTag = headTag.getOwnerDocument().createElement("script");
+ referenceTag.setAttribute("src", theContent);
+ headTag.appendChild(referenceTag);
+ } else {
+ inlineJs.append(theContent).append(";\n");
}
}
- inlineJs.append(getLibraryConfig(gadget, features));
+ inlineJs.append(libraryConfig);
if (inlineJs.length() > 0) {
Element inlineTag = headTag.getOwnerDocument().createElement("script");
@@ -298,47 +316,6 @@
}
/**
- * Get all features needed to satisfy this rendering request.
- *
- * @param forced Forced libraries; added in addition to those found in the spec. Defaults to
- * "core".
- */
- private Collection<GadgetFeature> getFeatures(Gadget gadget, Collection<String> forced)
- throws GadgetException {
- Map<String, Feature> features = gadget.getSpec().getModulePrefs().getFeatures();
- Set<String> libs = Sets.newHashSet(features.keySet());
- if (!forced.isEmpty()) {
- libs.addAll(forced);
- }
-
- libs.removeAll(gadget.getRemovedFeatures());
- libs.addAll(gadget.getAddedFeatures());
-
- Set<String> unsupported = Sets.newHashSet();
- Collection<GadgetFeature> feats = featureRegistry.getFeatures(libs, unsupported);
-
- unsupported.removeAll(forced);
-
- if (!unsupported.isEmpty()) {
- // Remove non-required libs
- Iterator<String> missingIter = unsupported.iterator();
- while (missingIter.hasNext()) {
- String missing = missingIter.next();
- Feature feature = features.get(missing);
- if (feature == null || !feature.getRequired()) {
- missingIter.remove();
- }
- }
-
- // Throw error with full list of unsupported libraries
- if (!unsupported.isEmpty()) {
- throw new UnsupportedFeatureException(unsupported.toString());
- }
- }
- return feats;
- }
-
- /**
* Creates a set of all configuration needed to satisfy the requested feature set.
*
* Appends special configuration for gadgets.util.hasFeature and gadgets.util.getFeatureParams to
@@ -350,7 +327,7 @@
* @param reqs The features needed to satisfy the request.
* @throws GadgetException If there is a problem with the gadget auth token
*/
- private String getLibraryConfig(Gadget gadget, Collection<GadgetFeature> reqs)
+ private String getLibraryConfig(Gadget gadget, List<String> reqs)
throws GadgetException {
GadgetContext context = gadget.getContext();
@@ -361,8 +338,7 @@
if (features != null) {
// Discard what we don't care about.
- for (GadgetFeature feature : reqs) {
- String name = feature.getName();
+ for (String name : reqs) {
Object conf = features.get(name);
if (conf != null) {
config.put(name, conf);
@@ -429,7 +405,7 @@
* Injects message bundles into the gadget output.
* @throws GadgetException If we are unable to retrieve the message bundle.
*/
- private void injectMessageBundles(MessageBundle bundle, Node scriptTag) throws GadgetException {
+ protected void injectMessageBundles(MessageBundle bundle, Node scriptTag) throws GadgetException {
String msgs = bundle.toJSONString();
Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.Prefs.setMessages_(");
@@ -441,7 +417,7 @@
/**
* Injects default values for user prefs into the gadget output.
*/
- private void injectDefaultPrefs(Gadget gadget, Node scriptTag) {
+ protected void injectDefaultPrefs(Gadget gadget, Node scriptTag) {
List<UserPref> prefs = gadget.getSpec().getUserPrefs();
Map<String, String> defaultPrefs = Maps.newHashMapWithExpectedSize(prefs.size());
for (UserPref up : prefs) {
@@ -458,7 +434,7 @@
*
* If preloading fails for any reason, we just output an empty object.
*/
- private void injectPreloads(Gadget gadget, Node scriptTag) {
+ protected void injectPreloads(Gadget gadget, Node scriptTag) {
List<Object> preload = Lists.newArrayList();
for (PreloadedData preloaded : gadget.getPreloads()) {
try {
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java Mon Nov 2 20:34:16 2009
@@ -22,12 +22,12 @@
import org.apache.shindig.common.servlet.HttpUtil;
import org.apache.shindig.common.servlet.InjectedServlet;
import org.apache.shindig.config.ContainerConfig;
-import org.apache.shindig.gadgets.GadgetFeature;
-import org.apache.shindig.gadgets.GadgetFeatureRegistry;
-import org.apache.shindig.gadgets.JsLibrary;
+import org.apache.shindig.gadgets.GadgetContext;
import org.apache.shindig.gadgets.RenderingContext;
import org.apache.shindig.gadgets.UrlGenerator;
import org.apache.shindig.gadgets.UrlValidationStatus;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
import com.google.inject.Inject;
@@ -44,9 +44,9 @@
*/
public class JsServlet extends InjectedServlet {
- private GadgetFeatureRegistry registry;
+ private FeatureRegistry registry;
@Inject
- public void setRegistry(GadgetFeatureRegistry registry) {
+ public void setRegistry(FeatureRegistry registry) {
this.registry = registry;
}
@@ -84,31 +84,40 @@
Set<String> needed = ImmutableSet.of(resourceName.split(":"));
String debugStr = req.getParameter("debug");
- String container = req.getParameter("container");
+ String containerParam = req.getParameter("container");
String containerStr = req.getParameter("c");
boolean debug = "1".equals(debugStr);
- if (container == null) {
- container = ContainerConfig.DEFAULT_CONTAINER;
- }
- RenderingContext context = "1".equals(containerStr) ?
+ final RenderingContext context = "1".equals(containerStr) ?
RenderingContext.CONTAINER : RenderingContext.GADGET;
+ final String container =
+ containerParam != null ? containerParam : ContainerConfig.DEFAULT_CONTAINER;
- Collection<GadgetFeature> features = registry.getFeatures(needed);
+ GadgetContext ctx = new GadgetContext() {
+ @Override
+ public RenderingContext getRenderingContext() {
+ return context;
+ }
+
+ @Override
+ public String getContainer() {
+ return container;
+ }
+ };
+ Collection<? extends FeatureResource> resources =
+ registry.getFeatureResources(ctx, needed, null);
StringBuilder jsData = new StringBuilder();
boolean isProxyCacheable = true;
- for (GadgetFeature feature : features) {
- for (JsLibrary lib : feature.getJsLibraries(context, container)) {
- if (lib.getType() != JsLibrary.Type.URL) {
- if (debug) {
- jsData.append(lib.getDebugContent());
- } else {
- jsData.append(lib.getContent());
- }
- isProxyCacheable = isProxyCacheable && lib.isProxyCacheable();
- jsData.append(";\n");
- }
+ for (FeatureResource featureResource : resources) {
+ String content = debug ? featureResource.getDebugContent() : featureResource.getContent();
+ if (!featureResource.isExternal()) {
+ jsData.append(content);
+ } else {
+ // Support external/type=url feature serving through document.write()
+ jsData.append("document.write('<script src=\"").append(content).append("\"></script>");
}
+ isProxyCacheable = isProxyCacheable && featureResource.isProxyCacheable();
+ jsData.append(";\n");
}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java Mon Nov 2 20:34:16 2009
@@ -32,6 +32,15 @@
* No substitutions on any fields.
*/
public class Feature {
+ public static final Feature CORE_FEATURE = new Feature();
+
+ // Instantiable only by CORE_FEATURE.
+ private Feature() {
+ this.params = ImmutableMultimap.of();
+ this.required = true;
+ this.name = "core";
+ }
+
/**
* Require@feature
* Optional@feature
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java Mon Nov 2 20:34:16 2009
@@ -643,18 +643,23 @@
*/
private static class FeatureVisitor implements ElementVisitor {
private final Map<String, Feature> features = Maps.newHashMap();
+ private boolean coreIncluded = false;
private static final Set<String> tags = ImmutableSet.of("Require", "Optional");
- public Set<String> tags() { return tags; }
public boolean visit (String tag, Element element) throws SpecParserException {
- if (!"Require".equals(tag) && !"Optional".equals(tag)) return false;
+ if (!tags.contains(tag)) return false;
Feature feature = new Feature(element);
+ coreIncluded = coreIncluded || feature.getName().startsWith("core");
features.put(feature.getName(), feature);
return true;
}
public void apply(ModulePrefs moduleprefs) {
+ if (!coreIncluded) {
+ // No library was explicitly included from core - add it as an implicit dependency.
+ features.put(Feature.CORE_FEATURE.getName(), Feature.CORE_FEATURE);
+ }
moduleprefs.features = ImmutableMap.copyOf(features);
}
}
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java Mon Nov 2 20:34:16 2009
@@ -24,7 +24,6 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Element;
Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java Mon Nov 2 20:34:16 2009
@@ -29,12 +29,10 @@
import org.apache.shindig.common.xml.DomUtil;
import org.apache.shindig.common.util.Utf8UrlCoder;
import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
import org.apache.shindig.gadgets.templates.TemplateProcessor;
-import org.apache.shindig.gadgets.GadgetFeatureRegistry;
-import org.apache.shindig.gadgets.RenderingContext;
-import org.apache.shindig.gadgets.GadgetFeature;
-import org.apache.shindig.gadgets.JsLibrary;
import org.apache.shindig.protocol.conversion.BeanJsonConverter;
import org.json.JSONObject;
import org.w3c.dom.Element;
@@ -44,7 +42,6 @@
import java.util.List;
import java.util.Map;
-import java.util.Collection;
import java.util.concurrent.atomic.AtomicLong;
import java.io.IOException;
@@ -57,7 +54,7 @@
static final String TAG_NAME = "Flash";
private final BeanJsonConverter beanConverter;
- private final GadgetFeatureRegistry featureRegistry;
+ private final FeatureRegistry featureRegistry;
private final String flashMinVersion;
/**
@@ -67,7 +64,7 @@
private static final String ALT_CONTENT_PREFIX = "os_xFlash_alt_";
@Inject
- public FlashTagHandler(BeanJsonConverter beanConverter, GadgetFeatureRegistry featureRegistry,
+ public FlashTagHandler(BeanJsonConverter beanConverter, FeatureRegistry featureRegistry,
@Named("shindig.template-rewrite.extension-tag-namespace") String namespace,
@Named("shindig.flash.min-version") String flashMinVersion) {
super(namespace, TAG_NAME);
@@ -204,14 +201,12 @@
}
Element swfobject = doc.createElement("script");
swfobject.setAttribute("type", "text/javascript");
- Collection<GadgetFeature> features = featureRegistry.getFeatures(ImmutableSet.of(SWFOBJECT));
- for (GadgetFeature feature : features) {
- if (feature.getName().equals(SWFOBJECT)) {
- List<JsLibrary> libraries = feature.getJsLibraries(RenderingContext.GADGET,
- processor.getTemplateContext().getGadget().getContext().getContainer());
- swfobject.setTextContent(libraries.get(0).getContent());
- break;
- }
+ List<FeatureResource> resources =
+ featureRegistry.getFeatureResources(processor.getTemplateContext().getGadget().getContext(),
+ ImmutableSet.of(SWFOBJECT), null);
+ for (FeatureResource resource : resources) {
+ // Emits all content for feature SWFOBJECT, which has no downstream dependencies.
+ swfobject.setTextContent(resource.getContent());
}
swfobject.setUserData(SWFOBJECT, SWFOBJECT, null);
head.appendChild(swfobject);
Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultUrlGeneratorTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultUrlGeneratorTest.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultUrlGeneratorTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultUrlGeneratorTest.java Mon Nov 2 20:34:16 2009
@@ -25,6 +25,8 @@
import org.apache.shindig.common.EasyMockTestCase;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.config.AbstractContainerConfig;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import com.google.caja.util.Join;
@@ -65,7 +67,7 @@
private final GadgetContext context = mock(GadgetContext.class);
private final LockedDomainService lockedDomainService = mock(LockedDomainService.class);
- private final GadgetFeatureRegistry registry = mock(GadgetFeatureRegistry.class);
+ private final FeatureRegistry registry = mock(FeatureRegistry.class);
private final FakeContainerConfig config = new FakeContainerConfig();
private UrlGenerator urlGenerator;
@@ -82,7 +84,7 @@
expect(context.getModuleId()).andReturn(MODULE_ID).anyTimes();
expect(context.getView()).andReturn(VIEW).anyTimes();
- Collection<GadgetFeature> features = Lists.newArrayList();
+ List<FeatureResource> features = Lists.newArrayList();
expect(registry.getAllFeatures()).andReturn(features);
Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java Mon Nov 2 20:34:16 2009
@@ -18,20 +18,19 @@
*/
package org.apache.shindig.gadgets;
+import static org.easymock.EasyMock.eq;
import static org.easymock.EasyMock.expect;
import org.apache.shindig.common.EasyMockTestCase;
import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import org.apache.shindig.gadgets.spec.LocaleSpec;
import org.junit.Test;
-import java.util.Arrays;
import java.util.List;
-import java.util.Set;
import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
/**
* Tests for Gadget
@@ -63,48 +62,29 @@
assertEquals("VALUE", localeSpec.getMessageBundle().getMessages().get("name"));
}
- private GadgetFeature makeFeature(String name, List<String> deps)
- throws GadgetException {
- JsLibrary lib = JsLibrary.create(JsLibrary.Type.INLINE, name, name, null);
- if (deps == null) {
- deps = Lists.newArrayList();
- }
- return new GadgetFeature(name, Arrays.asList(lib), deps);
- }
-
@Test
public void testGetFeatures() throws Exception {
String xml = "<Module>" +
"<ModulePrefs title=\"hello\">" +
"<Require feature=\"required1\"/>" +
- "<Require feature=\"required2\"/>" +
"</ModulePrefs>" +
"<Content type=\"html\"/>" +
"</Module>";
- List<GadgetFeature> features = Lists.newArrayList(
- makeFeature("required1", Lists.newArrayList("required2", "required3")),
- makeFeature("required2", Lists.newArrayList("required3", "required4", "required5")),
- makeFeature("required3", Lists.newArrayList("required4", "required5")),
- makeFeature("required4", null),
- makeFeature("required4", null));
- GadgetFeatureRegistry registry = mock(GadgetFeatureRegistry.class);
+ FeatureRegistry registry = mock(FeatureRegistry.class, true);
Gadget gadget = new Gadget()
.setContext(context)
.setGadgetFeatureRegistry(registry)
.setSpec(new GadgetSpec(Uri.parse(SPEC_URL), xml));
- Set<String> needed = Sets.newHashSet("required1", "required2");
- expect(registry.getFeatures(needed)).andReturn(features).anyTimes();
- replay(registry);
- List<String> requiredFeatures = gadget.getAllFeatures();
- assertEquals(5, requiredFeatures.size());
- // make sure the dependencies are in order.
- assertTrue(requiredFeatures.get(0).equals("required4") || requiredFeatures.get(0).equals("required5"));
- assertTrue(requiredFeatures.get(1).equals("required4") || requiredFeatures.get(0).equals("required5"));
- assertEquals("required3", requiredFeatures.get(2));
- assertEquals("required2", requiredFeatures.get(3));
- assertEquals("required1", requiredFeatures.get(4));
- // make sure we do the registry.getFeatures only once
- assertTrue(requiredFeatures == gadget.getAllFeatures());
+ List<String> needed = Lists.newArrayList("core", "required1");
+ List<String> returned = Lists.newArrayList();
+ // Call should only happen once, and be cached from there on out.
+ expect(registry.getFeatures(eq(needed))).andReturn(returned).once();
+ replay();
+ List<String> requiredFeatures1 = gadget.getAllFeatures();
+ assertEquals(returned, requiredFeatures1);
+ List<String> requiredFeatures2 = gadget.getAllFeatures();
+ assertSame(returned, requiredFeatures2);
+ verify();
}
Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java?rev=832093&r1=832092&r2=832093&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java Mon Nov 2 20:34:16 2009
@@ -24,14 +24,15 @@
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.isA;
+import com.google.common.collect.Lists;
+
import org.apache.shindig.common.EasyMockTestCase;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
import org.apache.shindig.gadgets.spec.GadgetSpec;
import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
public class HashLockedDomainServiceTest extends EasyMockTestCase {
@@ -41,16 +42,16 @@
private final ContainerConfig requiredConfig = mock(ContainerConfig.class);
private final ContainerConfig enabledConfig = mock(ContainerConfig.class);
+ @SuppressWarnings("unchecked")
private Gadget makeGadget(boolean wantsLocked, String url) {
String gadgetXml;
- List<GadgetFeature> gadgetFeatures = new ArrayList<GadgetFeature>();
+ List<String> gadgetFeatures = Lists.newArrayList();
if (wantsLocked) {
gadgetXml =
"<Module><ModulePrefs title=''>" +
" <Require feature='locked-domain'/>" +
"</ModulePrefs><Content/></Module>";
- gadgetFeatures = Arrays.asList(new GadgetFeature("locked-domain",
- new ArrayList<JsLibrary>(), null));
+ gadgetFeatures.add("locked-domain");
} else {
gadgetXml = "<Module><ModulePrefs title=''/><Content/></Module>";
}
@@ -62,8 +63,8 @@
return null;
}
- GadgetFeatureRegistry registry = mock(GadgetFeatureRegistry.class);
- expect(registry.getFeatures(isA(Collection.class))).andReturn(gadgetFeatures).anyTimes();
+ FeatureRegistry registry = mock(FeatureRegistry.class);
+ expect(registry.getFeatures(isA(List.class))).andReturn(gadgetFeatures).anyTimes();
return new Gadget().setSpec(spec).setGadgetFeatureRegistry(registry);
}
Added: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java?rev=832093&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java Mon Nov 2 20:34:16 2009
@@ -0,0 +1,121 @@
+/*
+ * 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.features;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.junit.Test;
+
+public class FeatureParserTest {
+ @Test
+ public void parseCompleteFeatureFile() throws Exception {
+ Uri parent = Uri.parse("scheme://host.com/root/path");
+ String featureXml =
+ "<feature>" +
+ " <name>the_feature</name>" +
+ " <dependency>myDep1</dependency>" +
+ " <dependency>mySecondDep</dependency>" +
+ " <gadget>" +
+ " <ignored>This tag is ignored</ignored>" +
+ " <script src=\"http://www.apache.org/file.js\"/>" +
+ " <script src=\"relative/resource.js\" gadget_attrib=\"gadget_value\"/>" +
+ " </gadget>" +
+ " <gadget container=\"container1\">" +
+ " <!-- No child values, testing outlier case -->" +
+ " </gadget>" +
+ " <container randomAttrib=\"randomValue\" secondAttrib=\"secondValue\">" +
+ " <script src=\"/authority/relative.js\" r2_attr=\"r2_val\" r3_attr=\"r3_val\"></script>" +
+ " <script>Inlined content</script>" +
+ " </container>" +
+ " <other_type>" +
+ " <script src=\"http://www.apache.org/two.js\"/>" +
+ " </other_type>" +
+ "</feature>";
+ FeatureParser.ParsedFeature parsed = new FeatureParser().parse(parent, featureXml);
+
+ // Top-level validation.
+ assertEquals("the_feature", parsed.getName());
+ assertEquals(2, parsed.getDeps().size());
+ assertEquals("myDep1", parsed.getDeps().get(0));
+ assertEquals("mySecondDep", parsed.getDeps().get(1));
+ assertEquals(4, parsed.getBundles().size());
+
+ // First gadget bundle.
+ FeatureParser.ParsedFeature.Bundle bundle1 = parsed.getBundles().get(0);
+ assertEquals("gadget", bundle1.getType());
+ assertEquals(0, bundle1.getAttribs().size());
+ assertEquals(2, bundle1.getResources().size());
+ assertNull(bundle1.getResources().get(0).getContent());
+ assertEquals(Uri.parse("http://www.apache.org/file.js"),
+ bundle1.getResources().get(0).getSource());
+ assertEquals(0, bundle1.getResources().get(0).getAttribs().size());
+ assertNull(bundle1.getResources().get(1).getContent());
+ assertEquals(Uri.parse("scheme://host.com/root/relative/resource.js"),
+ bundle1.getResources().get(1).getSource());
+ assertEquals(1, bundle1.getResources().get(1).getAttribs().size());
+ assertEquals("gadget_value", bundle1.getResources().get(1).getAttribs().get("gadget_attrib"));
+
+ // Second gadget bundle.
+ FeatureParser.ParsedFeature.Bundle bundle2 = parsed.getBundles().get(1);
+ assertEquals("gadget", bundle2.getType());
+ assertEquals(1, bundle2.getAttribs().size());
+ assertEquals("container1", bundle2.getAttribs().get("container"));
+ assertEquals(0, bundle2.getResources().size());
+
+ // Container bundle.
+ FeatureParser.ParsedFeature.Bundle bundle3 = parsed.getBundles().get(2);
+ assertEquals("container", bundle3.getType());
+ assertEquals(2, bundle3.getAttribs().size());
+ assertEquals("randomValue", bundle3.getAttribs().get("randomAttrib"));
+ assertEquals("secondValue", bundle3.getAttribs().get("secondAttrib"));
+ assertEquals(2, bundle3.getResources().size());
+ assertNull(bundle3.getResources().get(0).getContent());
+ assertEquals(Uri.parse("scheme://host.com/authority/relative.js"),
+ bundle3.getResources().get(0).getSource());
+ assertEquals(2, bundle3.getResources().get(0).getAttribs().size());
+ assertEquals("r2_val", bundle3.getResources().get(0).getAttribs().get("r2_attr"));
+ assertEquals("r3_val", bundle3.getResources().get(0).getAttribs().get("r3_attr"));
+ assertNull(bundle3.getResources().get(1).getSource());
+ assertEquals("Inlined content", bundle3.getResources().get(1).getContent());
+ assertEquals(0, bundle3.getResources().get(1).getAttribs().size());
+
+ // Other_type bundle.
+ FeatureParser.ParsedFeature.Bundle bundle4 = parsed.getBundles().get(3);
+ assertEquals("other_type", bundle4.getType());
+ assertEquals(0, bundle4.getAttribs().size());
+ assertEquals(1, bundle4.getResources().size());
+ assertNull(bundle4.getResources().get(0).getContent());
+ assertEquals(Uri.parse("http://www.apache.org/two.js"),
+ bundle4.getResources().get(0).getSource());
+ assertEquals(0, bundle4.getResources().get(0).getAttribs().size());
+ }
+
+ @Test
+ public void parseInvalidXml() {
+ try {
+ new FeatureParser().parse(Uri.parse(""), "This is not valid XML.");
+ fail("Should have failed to parse invalid XML");
+ } catch (Exception e) {
+ // Expected.
+ }
+ }
+}