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 &lt;script src&gt;
+   */
+  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.
+    }
+  }
+}