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

svn commit: r743158 - in /incubator/shindig/trunk/java: common/src/main/java/org/apache/shindig/common/xml/ gadgets/src/main/java/org/apache/shindig/gadgets/ gadgets/src/main/java/org/apache/shindig/gadgets/spec/ gadgets/src/test/java/org/apache/shindi...

Author: etnu
Date: Tue Feb 10 23:13:21 2009
New Revision: 743158

URL: http://svn.apache.org/viewvc?rev=743158&view=rev
Log:
Applying SHINDIG-757 patch.


Modified:
    incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java
    incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java
    incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java

Modified: incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java (original)
+++ incubator/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java Tue Feb 10 23:13:21 2009
@@ -18,6 +18,7 @@
 package org.apache.shindig.common.xml;
 
 import org.apache.shindig.common.uri.Uri;
+
 import org.w3c.dom.Element;
 import org.w3c.dom.NamedNodeMap;
 import org.w3c.dom.Node;
@@ -26,15 +27,16 @@
 import org.xml.sax.SAXException;
 import org.xml.sax.SAXParseException;
 
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
 /**
  * Utility class for simplifying parsing of xml documents. Documents are not validated, and
  * loading of external files (xinclude, external entities, DTDs, etc.) are disabled.
@@ -303,4 +305,15 @@
       throw new XmlException(e);
     }
   }
+
+  /**
+   * Same as {@link #parse(String)}, but throws a RuntimeException instead of XmlException.
+   */
+  public static Element parseSilent(String xml) {
+    try {
+      return parse(xml);
+    } catch (XmlException e) {
+      throw new RuntimeException(e);
+    }
+  }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java Tue Feb 10 23:13:21 2009
@@ -22,18 +22,24 @@
 import org.apache.shindig.common.cache.CacheProvider;
 import org.apache.shindig.common.cache.SoftExpiringCache;
 import org.apache.shindig.common.uri.Uri;
-import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.ApplicationManifest;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
 
-import com.google.common.base.Preconditions;
+import com.google.common.base.Objects;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
+import org.w3c.dom.Element;
+
 import java.net.URI;
+import java.util.concurrent.ExecutorService;
 import java.util.logging.Logger;
 
 /**
@@ -42,23 +48,28 @@
 @Singleton
 public class DefaultGadgetSpecFactory implements GadgetSpecFactory {
   public static final String CACHE_NAME = "gadgetSpecs";
+
+  static final String VERSION_PARAM = "version";
+  static final String LABEL_PARAM = "label";
+  static final String DEFAULT_LABEL = "production";
   static final String RAW_GADGETSPEC_XML_PARAM_NAME = "rawxml";
   static final Uri RAW_GADGET_URI = Uri.parse("http://localhost/raw.xml");
-  static final String ERROR_SPEC = "<Module><ModulePrefs title='Error'/><Content/></Module>";
-  static final String ERROR_KEY = "parse.exception";
-  static final Logger LOG = Logger.getLogger(DefaultGadgetSpecFactory.class.getName());
 
+  private final Logger logger = Logger.getLogger(DefaultGadgetSpecFactory.class.getName());
+  private final ExecutorService executor;
   private final RequestPipeline pipeline;
-  private final SoftExpiringCache<Uri, GadgetSpec> cache;
+  final SoftExpiringCache<Uri, Object> cache;
   private final long refresh;
 
   @Inject
-  public DefaultGadgetSpecFactory(RequestPipeline pipeline,
+  public DefaultGadgetSpecFactory(ExecutorService executor,
+                                  RequestPipeline pipeline,
                                   CacheProvider cacheProvider,
                                   @Named("shindig.cache.xml.refreshInterval") long refresh) {
+    this.executor = executor;
     this.pipeline = pipeline;
-    Cache<Uri, GadgetSpec> baseCache = cacheProvider.createCache(CACHE_NAME);
-    this.cache = new SoftExpiringCache<Uri, GadgetSpec>(baseCache);
+    Cache<Uri, Object> baseCache = cacheProvider.createCache(CACHE_NAME);
+    this.cache = new SoftExpiringCache<Uri, Object>(baseCache);
     this.refresh = refresh;
   }
 
@@ -68,66 +79,116 @@
       // Set URI to a fixed, safe value (localhost), preventing a gadget rendered
       // via raw XML (eg. via POST) to be rendered on a locked domain of any other
       // gadget whose spec is hosted non-locally.
-      return new GadgetSpec(RAW_GADGET_URI, rawxml);
+      try {
+        return new GadgetSpec(RAW_GADGET_URI, XmlUtil.parse(rawxml), rawxml);
+      } catch (XmlException e) {
+        throw new SpecParserException(e);
+      }
     }
-    return getGadgetSpec(context.getUrl(), context.getContainer(), context.getIgnoreCache());
+
+    return fetchObject(Uri.fromJavaUri(context.getUrl()), context, false);
   }
 
-  /**
-   * Retrieves a gadget specification from the cache or from the Internet.
-   * TODO: This should be removed. Too much context is missing from this request.
-   */
-  public GadgetSpec getGadgetSpec(URI gadgetUri, boolean ignoreCache) throws GadgetException {
-    return getGadgetSpec(gadgetUri, ContainerConfig.DEFAULT_CONTAINER, ignoreCache);
+  public GadgetSpec getGadgetSpec(final URI gadgetUri, final boolean ignoreCache)
+      throws GadgetException {
+    return getGadgetSpec(new GadgetContext() {
+      @Override
+      public URI getUrl() {
+        return gadgetUri;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    });
   }
 
-  private GadgetSpec getGadgetSpec(URI gadgetUri, String container, boolean ignoreCache)
+  private GadgetSpec getSpecFromManifest(ApplicationManifest manifest, GadgetContext context)
       throws GadgetException {
-    Uri uri = Uri.fromJavaUri(gadgetUri);
-    if (ignoreCache) {
-      return fetchObjectAndCache(uri, container, ignoreCache);
+    String version = context.getParameter(VERSION_PARAM);
+
+    if (version == null) {
+      // TODO: The label param should only be used for metadata calls. This should probably be
+      // exposed up a layer in the stack, perhaps at the interface level.
+      String label = Objects.firstNonNull(context.getParameter(LABEL_PARAM), DEFAULT_LABEL);
+
+      version = manifest.getVersion(label);
+
+      if (version == null) {
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+            "Unable to find a suitable version for the given manifest.");
+      }
+    }
+
+    Uri specUri = manifest.getGadget(version);
+
+    if (specUri == null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "No gadget spec available for the given version.");
     }
 
-    SoftExpiringCache.CachedObject<GadgetSpec> cached = cache.getElement(uri);
+    return fetchObject(specUri, context, true);
+  }
+
+  private GadgetSpec fetchObject(Uri uri, GadgetContext context, boolean noManifests)
+      throws GadgetException {
+
+    Object obj = null;
+    if (!context.getIgnoreCache()) {
+      SoftExpiringCache.CachedObject<Object> cached = cache.getElement(uri);
+      if (cached != null) {
+        obj = cached.obj;
+        if (cached.isExpired) {
+          // We write to the cache to avoid any race conditions with multiple writers.
+          // This causes a double write, but that's better than a write per thread or synchronizing
+          // this block.
+          cache.addElement(uri, obj, refresh);
+          executor.execute(new ObjectUpdater(uri, context, obj));
+        }
+      }
+    }
 
-    GadgetSpec spec = null;
-    if (cached == null || cached.isExpired) {
+    if (obj == null) {
       try {
-        spec = fetchObjectAndCache(uri, container, ignoreCache);
+        obj = fetchFromNetwork(uri, context);
       } catch (GadgetException e) {
-        // Enforce negative caching.
-        if (cached != null) {
-          spec = cached.obj;
-          Preconditions.checkNotNull(spec);
-        } else {
-          // We create this dummy spec to avoid the cost of re-parsing when a remote site is out.
-          spec = new GadgetSpec(uri, ERROR_SPEC);
-          spec.setAttribute(ERROR_KEY, e);
-        }
-        LOG.info("GadgetSpec fetch failed for " + uri + " - using cached.");
-        cache.addElement(uri, spec, refresh);
+        obj = e;
+      }
+
+      cache.addElement(uri, obj, refresh);
+    }
+
+    if (obj instanceof GadgetSpec) {
+      return (GadgetSpec) obj;
+    }
+
+    if (obj instanceof ApplicationManifest) {
+      if (noManifests) {
+        throw new SpecParserException("Manifests may not reference other manifests.");
       }
-    } else {
-      spec = cached.obj;
+
+      return getSpecFromManifest((ApplicationManifest) obj, context);
     }
 
-    GadgetException exception = (GadgetException) spec.getAttribute(ERROR_KEY);
-    if (exception != null) {
-      throw exception;
+    if (obj instanceof GadgetException) {
+      throw (GadgetException) obj;
     }
-    return spec;
+
+    // Some big bug.
+    throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+        "Unknown object type stored for input URI " + uri);
   }
 
   /**
    * Retrieves a gadget specification from the Internet, processes its views and
    * adds it to the cache.
    */
-  private GadgetSpec fetchObjectAndCache(Uri url, String container, boolean ignoreCache)
-      throws GadgetException {
-    HttpRequest request = new HttpRequest(url)
-        .setIgnoreCache(ignoreCache)
-        .setGadget(url)
-        .setContainer(container);
+  private Object fetchFromNetwork(Uri uri, GadgetContext context) throws GadgetException {
+    HttpRequest request = new HttpRequest(uri)
+        .setIgnoreCache(context.getIgnoreCache())
+        .setGadget(uri)
+        .setContainer(context.getContainer());
 
     // Since we don't allow any variance in cache time, we should just force the cache time
     // globally. This ensures propagation to shared caches when this is set.
@@ -140,8 +201,42 @@
                                 response.getHttpStatusCode());
     }
 
-    GadgetSpec spec = new GadgetSpec(url, response.getResponseAsString());
-    cache.addElement(url, spec, refresh);
-    return spec;
+    try {
+      String content = response.getResponseAsString();
+      Element element = XmlUtil.parse(content);
+      if (ApplicationManifest.NAMESPACE.equals(element.getNamespaceURI())) {
+        return new ApplicationManifest(uri, element);
+      }
+      return new GadgetSpec(uri, element, content);
+    } catch (XmlException e) {
+      throw new SpecParserException(e);
+    }
+  }
+
+  private class ObjectUpdater implements Runnable {
+    private final Uri uri;
+    private final GadgetContext context;
+    private final Object old;
+
+    public ObjectUpdater(Uri uri, GadgetContext context, Object old) {
+      this.uri = uri;
+      this.context = context;
+      this.old = old;
+    }
+
+    public void run() {
+      try {
+        Object newObject = fetchFromNetwork(uri, context);
+        cache.addElement(uri, newObject, refresh);
+      } catch (GadgetException e) {
+        if (old != null) {
+          logger.info("Failed to update " + uri + ". Using cached version.");
+          cache.addElement(uri, old, refresh);
+        } else {
+          logger.info("Failed to update " + uri + ". Applying negative cache.");
+          cache.addElement(uri, e, refresh);
+        }
+      }
+    }
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java Tue Feb 10 23:13:21 2009
@@ -24,17 +24,21 @@
 
 import java.net.URI;
 
-/** Factory of gadget specs */
-
+/**
+ * Factory of gadget specs.
+ */
 @ImplementedBy(DefaultGadgetSpecFactory.class)
-
 public interface GadgetSpecFactory {
 
   /** Return a gadget spec for a context */
   public GadgetSpec getGadgetSpec(GadgetContext context) throws GadgetException;
 
-  /** Return a gadget spec for a URI */
-  public GadgetSpec getGadgetSpec(URI gadgetUri, boolean ignoreCache)
-      throws GadgetException;
+  /**
+   * Return a gadget spec for a URI.
+   *
+   * @deprecated Use {@link #getGadgetSpec(GadgetContext)} instead.
+   */
+  @Deprecated
+  public GadgetSpec getGadgetSpec(URI gadgetUri, boolean ignoreCache) throws GadgetException;
 
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java Tue Feb 10 23:13:21 2009
@@ -37,8 +37,9 @@
 
   private final Map<String, String> versions;
   private final Map<String, Uri> gadgets;
+  private final Uri uri;
 
-  public ApplicationManifest(Uri base, Element xml) throws SpecParserException {
+  public ApplicationManifest(Uri uri, Element xml) throws SpecParserException {
     ImmutableMap.Builder<String, String> versions = ImmutableMap.builder();
     ImmutableMap.Builder<String, Uri> gadgets = ImmutableMap.builder();
 
@@ -46,18 +47,19 @@
     for (int i = 0, j = nodes.getLength(); i < j; ++i) {
       Element gadget = (Element) nodes.item(i);
       String version = getVersionString(gadget);
-      Uri spec = getSpecUri(base, gadget);
+      Uri spec = getSpecUri(uri, gadget);
       gadgets.put(version, spec);
       for (String label : getLabels(gadget)) {
         versions.put(label, version);
       }
     }
 
+    this.uri = uri;
     this.versions = versions.build();
     this.gadgets = gadgets.build();
   }
 
-  private static Uri getSpecUri(Uri base, Element gadget) throws SpecParserException {
+  private static Uri getSpecUri(Uri baseUri, Element gadget) throws SpecParserException {
     NodeList specs = gadget.getElementsByTagName("spec");
 
     if (specs.getLength() > 1) {
@@ -68,7 +70,11 @@
 
     try {
       String relative = specs.item(0).getTextContent();
-      return base.resolve(Uri.parse(relative));
+      Uri specUri = baseUri.resolve(Uri.parse(relative));
+      if (specUri.equals(baseUri)) {
+        throw new SpecParserException("Manifest is self-referencing.");
+      }
+      return specUri;
     } catch (IllegalArgumentException e) {
       throw new SpecParserException("Invalid spec URI.");
     }
@@ -98,6 +104,13 @@
   }
 
   /**
+   * @return The URI of this manifest.
+   */
+  public Uri getUri() {
+    return uri;
+  }
+
+  /**
    * @return The gadget specified for the version string, or null if the version doesn't exist.
    */
   public Uri getGadget(String version) {

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java Tue Feb 10 23:13:21 2009
@@ -19,7 +19,6 @@
 
 import org.apache.shindig.common.uri.Uri;
 import org.apache.shindig.common.util.HashUtil;
-import org.apache.shindig.common.xml.XmlException;
 import org.apache.shindig.common.xml.XmlUtil;
 import org.apache.shindig.gadgets.variables.Substitutions;
 
@@ -36,7 +35,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Represents a gadget specification root element (Module).
@@ -49,20 +47,17 @@
   /**
    * Creates a new Module from the given xml input.
    *
-   * @throws SpecParserException If xml can not be parsed as a valid gadget spec.
+   * @param url The original url of the gadget.
+   * @param doc The pre-parsed xml document.
+   * @param original Unparsed input XML. Used to generate checksums.
+   *
+   * @throws SpecParserException If xml can not be processed as a valid gadget spec.
    */
-  public GadgetSpec(Uri url, String xml) throws SpecParserException {
-    Element doc;
-    try {
-      doc = XmlUtil.parse(xml);
-    } catch (XmlException e) {
-      throw new SpecParserException("Malformed XML in file " + url.toString(), e);
-    }
+  public GadgetSpec(Uri url, Element doc, String original) throws SpecParserException {
     this.url = url;
 
-    // This might not be good enough; should we take message bundle changes
-    // into account?
-    this.checksum = HashUtil.checksum(xml.getBytes());
+    // This might not be good enough; should we take message bundle changes into account?
+    this.checksum = HashUtil.checksum(original.getBytes());
 
     NodeList children = doc.getChildNodes();
 
@@ -80,8 +75,7 @@
         if (modulePrefs == null) {
           modulePrefs = new ModulePrefs(element, url);
         } else {
-          throw new SpecParserException(
-              "Only 1 ModulePrefs is allowed.");
+          throw new SpecParserException("Only 1 ModulePrefs is allowed.");
         }
       }
       if ("UserPref".equals(name)) {
@@ -126,6 +120,13 @@
   }
 
   /**
+   * Use for testing.
+   */
+  public GadgetSpec(Uri url, String xml) throws SpecParserException {
+    this(url, XmlUtil.parseSilent(xml), xml);
+  }
+
+  /**
    * Constructs a GadgetSpec for substitute calls.
    * @param spec
    */

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java Tue Feb 10 23:13:21 2009
@@ -19,19 +19,25 @@
 package org.apache.shindig.gadgets;
 
 import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
 import static org.easymock.classextension.EasyMock.replay;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
 import org.apache.shindig.common.cache.CacheProvider;
 import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.cache.SoftExpiringCache;
+import org.apache.shindig.common.testing.TestExecutorService;
 import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
 import org.apache.shindig.config.ContainerConfig;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
 import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.ApplicationManifest;
 import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
 
 import org.easymock.EasyMock;
 import org.junit.Test;
@@ -42,44 +48,42 @@
  * Tests for DefaultGadgetSpecFactory
  */
 public class DefaultGadgetSpecFactoryTest {
-  protected final static Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
-  private final static Uri REMOTE_URL = Uri.parse("http://example.org/remote.html");
-  private final static String LOCAL_CONTENT = "Hello, local content!";
-  private final static String ALT_LOCAL_CONTENT = "Hello, local content!";
-  private final static String RAWXML_CONTENT = "Hello, rawxml content!";
-  private final static String LOCAL_SPEC_XML
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final Uri ALT_SPEC_URL = Uri.parse("http://example.org/gadget2.xml");
+  private static final Uri MANIFEST_URI = Uri.parse("http://example.org/manifest.xml");
+  private static final String LOCAL_CONTENT = "Hello, local content!";
+  private static final String ALT_LOCAL_CONTENT = "Hello, local content!";
+  private static final String RAWXML_CONTENT = "Hello, rawxml content!";
+  private static final String MANIFEST_XML
+      = "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>" + SPEC_URL + "</spec>" +
+        "</gadget>" +
+        "<gadget>" +
+        "  <label>development</label>" +
+        "  <version>2.0</version>" +
+        "  <spec>" + ALT_SPEC_URL + "</spec>" +
+        "</gadget>" +
+        "</app>";
+  private static final String LOCAL_SPEC_XML
       = "<Module>" +
         "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
         "  <Content type='html'>" + LOCAL_CONTENT + "</Content>" +
         "</Module>";
-  private final static String ALT_LOCAL_SPEC_XML
+  private static final String ALT_LOCAL_SPEC_XML
       = "<Module>" +
         "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
         "  <Content type='html'>" + ALT_LOCAL_CONTENT + "</Content>" +
         "</Module>";
-  private final static String RAWXML_SPEC_XML
+  private static final String RAWXML_SPEC_XML
       = "<Module>" +
         "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
         "  <Content type='html'>" + RAWXML_CONTENT + "</Content>" +
         "</Module>";
-  private final static String URL_SPEC_XML
-      = "<Module>" +
-        "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
-        "  <Content type='url' href='" + REMOTE_URL + "'/>" +
-        "</Module>";
 
-  private final static GadgetContext NO_CACHE_CONTEXT = new GadgetContext() {
-    @Override
-    public boolean getIgnoreCache() {
-      return true;
-    }
-    @Override
-    public URI getUrl() {
-      return SPEC_URL.toJavaUri();
-    }
-  };
-
-  private final static GadgetContext RAWXML_GADGET_CONTEXT = new GadgetContext() {
+  private static final GadgetContext RAWXML_GADGET_CONTEXT = new GadgetContext() {
     @Override
     public boolean getIgnoreCache() {
       // This should be ignored by calling code.
@@ -102,12 +106,14 @@
 
   private static final int MAX_AGE = 10000;
 
+  private final CountingExecutor executor = new CountingExecutor();
+
   private final RequestPipeline pipeline = EasyMock.createNiceMock(RequestPipeline.class);
 
   private final CacheProvider cacheProvider = new LruCacheProvider(5);
 
   private final DefaultGadgetSpecFactory specFactory
-      = new DefaultGadgetSpecFactory(pipeline, cacheProvider, MAX_AGE);
+      = new DefaultGadgetSpecFactory(executor, pipeline, cacheProvider, MAX_AGE);
 
   private static HttpRequest createIgnoreCacheRequest() {
     return new HttpRequest(SPEC_URL)
@@ -122,6 +128,20 @@
         .setContainer(ContainerConfig.DEFAULT_CONTAINER);
   }
 
+  private static GadgetContext createContext(final Uri uri, final boolean ignoreCache) {
+    return new GadgetContext() {
+      @Override
+      public URI getUrl() {
+        return uri.toJavaUri();
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    };
+  }
+
   @Test
   public void specFetched() throws Exception {
     HttpRequest request = createIgnoreCacheRequest();
@@ -129,22 +149,99 @@
     expect(pipeline.execute(request)).andReturn(response);
     replay(pipeline);
 
-    GadgetSpec spec = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, true));
 
     assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
   }
 
   @Test
-  public void specFetchedWithContext() throws Exception {
-    HttpRequest request = createIgnoreCacheRequest();
+  public void specFetchedFromManifest() throws Exception {
+    HttpRequest gadgetRequest = createIgnoreCacheRequest();
+    HttpResponse gadgetResponse = new HttpResponse(LOCAL_SPEC_XML);
+    expect(pipeline.execute(gadgetRequest)).andReturn(gadgetResponse);
 
-    HttpResponse response = new HttpResponse(LOCAL_SPEC_XML);
+    HttpRequest manifestRequest = createIgnoreCacheRequest();
+    manifestRequest.setUri(MANIFEST_URI);
+    HttpResponse manifestResponse = new HttpResponse(MANIFEST_XML);
+    expect(pipeline.execute(manifestRequest)).andReturn(manifestResponse);
+
+    replay(pipeline);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(MANIFEST_URI, true));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void nestedManifestThrows() throws Exception {
+    HttpResponse manifestResponse = new HttpResponse(MANIFEST_XML);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(manifestResponse).anyTimes();
+
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(MANIFEST_URI, true));
+  }
+
+  @Test
+  public void manifestFetchedWithDefaults() throws Exception {
+    ApplicationManifest manifest
+        = new ApplicationManifest(MANIFEST_URI, XmlUtil.parse(MANIFEST_XML));
+    specFactory.cache.addElement(MANIFEST_URI, manifest, 1000);
+
+    GadgetSpec cachedSpec = new GadgetSpec(SPEC_URL, XmlUtil.parse(LOCAL_SPEC_XML));
+    specFactory.cache.addElement(SPEC_URL, cachedSpec, 1000);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(MANIFEST_URI, false));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void manifestFetchedByVersion() throws Exception {
+    ApplicationManifest manifest
+        = new ApplicationManifest(MANIFEST_URI, XmlUtil.parse(MANIFEST_XML));
+    specFactory.cache.addElement(MANIFEST_URI, manifest, 1000);
+
+    GadgetSpec cachedSpec = new GadgetSpec(ALT_SPEC_URL, XmlUtil.parse(ALT_LOCAL_SPEC_XML));
+    specFactory.cache.addElement(ALT_SPEC_URL, cachedSpec, 1000);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(new GadgetContext() {
+      @Override
+      public URI getUrl() {
+        return MANIFEST_URI.toJavaUri();
+      }
+
+      @Override
+      public String getParameter(String name) {
+        if (name.equals(DefaultGadgetSpecFactory.VERSION_PARAM)) {
+          return "2.0";
+        }
+        return null;
+      }
+    });
+
+    assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void specRefetchedAsync() throws Exception {
+    HttpRequest request = createCacheableRequest();
+    HttpResponse response = new HttpResponse(ALT_LOCAL_SPEC_XML);
     expect(pipeline.execute(request)).andReturn(response);
     replay(pipeline);
 
-    GadgetSpec spec = specFactory.getGadgetSpec(NO_CACHE_CONTEXT);
+    specFactory.cache.addElement(
+        SPEC_URL, new GadgetSpec(SPEC_URL, XmlUtil.parse(LOCAL_SPEC_XML)), -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
 
     assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    assertEquals(1, executor.runnableCount);
   }
 
   @Test
@@ -176,8 +273,12 @@
     expect(pipeline.execute(retriedRequest)).andReturn(updatedResponse).once();
     replay(pipeline);
 
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
-    GadgetSpec spec = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    SoftExpiringCache.CachedObject<Object> inCache = specFactory.cache.getElement(SPEC_URL);
+    specFactory.cache.addElement(SPEC_URL, inCache.obj, -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
 
     assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
   }
@@ -195,35 +296,26 @@
     expect(pipeline.execute(retriedRequest)).andReturn(HttpResponse.notFound()).once();
     replay(pipeline);
 
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
-    GadgetSpec spec = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    SoftExpiringCache.CachedObject<Object> inCache = specFactory.cache.getElement(SPEC_URL);
+    specFactory.cache.addElement(SPEC_URL, inCache.obj, -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
 
     assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
   }
 
   @Test
   public void ttlPropagatesToPipeline() throws Exception {
-    CapturingPipeline capturingpipeline = new CapturingPipeline();
-
-    DefaultGadgetSpecFactory forcedCacheFactory
-        = new DefaultGadgetSpecFactory(capturingpipeline, cacheProvider, 10000);
-
-    forcedCacheFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
+    CapturingPipeline capturingPipeline = new CapturingPipeline();
 
-    assertEquals(10, capturingpipeline.request.getCacheTtl());
-  }
-
-  @Test
-  public void typeUrlNotFetchedRemote() throws Exception {
-    HttpRequest request = createIgnoreCacheRequest();
-    HttpResponse response = new HttpResponse(URL_SPEC_XML);
-    expect(pipeline.execute(request)).andReturn(response);
-    replay(pipeline);
+    GadgetSpecFactory forcedCacheFactory = new DefaultGadgetSpecFactory(
+        new TestExecutorService(), capturingPipeline, cacheProvider, 10000);
 
-    GadgetSpec spec = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    forcedCacheFactory.getGadgetSpec(createContext(SPEC_URL, false));
 
-    assertEquals(REMOTE_URL, spec.getView(GadgetSpec.DEFAULT_VIEW).getHref());
-    assertEquals("", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+    assertEquals(10, capturingPipeline.request.getCacheTtl());
   }
 
   @Test(expected = GadgetException.class)
@@ -232,7 +324,7 @@
     expect(pipeline.execute(request)).andReturn(HttpResponse.error());
     replay(pipeline);
 
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
   }
 
   @Test(expected = GadgetException.class)
@@ -243,8 +335,8 @@
     expect(pipeline.execute(secondRequest)).andReturn(HttpResponse.error()).once();
     replay(pipeline);
 
-    GadgetSpec original = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
-    GadgetSpec cached = specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    GadgetSpec original = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+    GadgetSpec cached = specFactory.getGadgetSpec(createContext(SPEC_URL, true));
 
     assertEquals(original.getUrl(), cached.getUrl());
     assertEquals(original.getChecksum(), cached.getChecksum());
@@ -256,7 +348,7 @@
     expect(pipeline.execute(request)).andReturn(new HttpResponse("malformed junk"));
     replay(pipeline);
 
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
   }
 
   @Test(expected = GadgetException.class)
@@ -266,25 +358,41 @@
     replay(pipeline);
 
     try {
-      specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
+      specFactory.getGadgetSpec(createContext(SPEC_URL, false));
       fail("No exception thrown on bad parse");
     } catch (GadgetException e) {
       // Expected.
     }
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), false);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
   }
 
   @Test(expected = GadgetException.class)
-  public void throwingpipelineRethrows() throws Exception {
+  public void throwingPipelineRethrows() throws Exception {
     HttpRequest request = createIgnoreCacheRequest();
     expect(pipeline.execute(request)).andThrow(
         new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT));
     replay(pipeline);
 
-    specFactory.getGadgetSpec(SPEC_URL.toJavaUri(), true);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void negativeCachingEnforced() throws Exception {
+    specFactory.cache.addElement(SPEC_URL, new SpecParserException("broken"), 1000);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+  }
+
+  private static class CountingExecutor extends TestExecutorService {
+    int runnableCount = 0;
+
+    @Override
+    public void execute(Runnable r) {
+      runnableCount++;
+      r.run();
+    }
   }
 
-  protected static class CapturingPipeline implements RequestPipeline {
+  private static class CapturingPipeline implements RequestPipeline {
     HttpRequest request;
 
     public HttpResponse execute(HttpRequest request) {

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java Tue Feb 10 23:13:21 2009
@@ -23,6 +23,7 @@
 import org.apache.shindig.gadgets.Gadget;
 import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
 import org.apache.shindig.gadgets.http.HttpRequest;
 import org.apache.shindig.gadgets.http.HttpResponse;
 import org.apache.shindig.gadgets.http.HttpResponseBuilder;
@@ -67,8 +68,7 @@
         SPEC_URL,
         defaultRewriterFeature,
         DEFAULT_PROXY_BASE);
-    injector = Guice.createInjector(new ParseModule(), new PropertiesModule(),
-        new TestRequestPipelineModule());
+    injector = Guice.createInjector(new ParseModule(), new PropertiesModule(), new TestModule());
     parser = injector.getInstance(GadgetHtmlParser.class);
     fakeResponse = new HttpResponseBuilder().setHeader("Content-Type", "unknown")
         .setResponse(new byte[]{ (byte)0xFE, (byte)0xFF}).create();
@@ -158,13 +158,27 @@
     }
   }
 
-  private static class TestRequestPipelineModule extends AbstractModule {
+  private static class TestModule extends AbstractModule {
 
     @Override
     protected void configure() {
       bind(RequestPipeline.class).toInstance(new RequestPipeline() {
         public HttpResponse execute(HttpRequest request) { return null; }
       });
+
+      bind(GadgetSpecFactory.class).toInstance(new GadgetSpecFactory() {
+
+        public GadgetSpec getGadgetSpec(GadgetContext context) {
+          // TODO Auto-generated method stub
+          return null;
+        }
+
+        public GadgetSpec getGadgetSpec(URI gadgetUri, boolean ignoreCache) {
+          // TODO Auto-generated method stub
+          return null;
+        }
+
+      });
     }
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java Tue Feb 10 23:13:21 2009
@@ -41,6 +41,7 @@
     ApplicationManifest manifest = new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
 
     assertEquals(BASE_URI.resolve(Uri.parse("app.xml")), manifest.getGadget("1.0"));
+    assertEquals(BASE_URI, manifest.getUri());
   }
 
   @Test(expected = SpecParserException.class)
@@ -150,4 +151,18 @@
     new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
   }
 
+  @Test(expected = SpecParserException.class)
+  public void selfReferencingManifest() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>whoever</spec>" +
+        "  <spec>" + BASE_URI + "</spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
 }

Modified: incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java?rev=743158&r1=743157&r2=743158&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java Tue Feb 10 23:13:21 2009
@@ -20,7 +20,8 @@
 package org.apache.shindig.gadgets.spec;
 
 import org.apache.shindig.common.uri.Uri;
-import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.common.xml.XmlUtil;
 import org.apache.shindig.gadgets.variables.Substitutions;
 import org.apache.shindig.gadgets.variables.Substitutions.Type;
 
@@ -28,6 +29,7 @@
 
 public class GadgetSpecTest extends TestCase {
   private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+
   public void testBasic() throws Exception {
     String xml = "<Module>" +
                  "<ModulePrefs title=\"title\"/>" +
@@ -41,6 +43,21 @@
     assertEquals("Hello!", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
   }
 
+  public void testAlternativeConstructor() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\"/>" +
+                 "<UserPref name=\"foo\" datatype=\"string\"/>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, XmlUtil.parse(xml), xml);
+    assertEquals("title", spec.getModulePrefs().getTitle());
+    assertEquals(UserPref.DataType.STRING,
+        spec.getUserPrefs().get(0).getDataType());
+    assertEquals("Hello!", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    assertEquals(HashUtil.checksum(xml.getBytes()), spec.getChecksum());
+  }
+
   public void testMultipleContentSections() throws Exception {
     String xml = "<Module>" +
                  "<ModulePrefs title=\"title\"/>" +
@@ -80,18 +97,6 @@
     }
   }
 
-  public void testMalformedXml() throws Exception {
-    String xml = "<Module><ModulePrefs/>";
-    try {
-      new GadgetSpec(SPEC_URL, xml);
-      fail("No exception thrown on malformed XML.");
-    } catch (SpecParserException e) {
-      // OK
-      assertEquals(GadgetException.Code.MALFORMED_XML_DOCUMENT, e.getCode());
-      assertTrue(e.getMessage().contains(SPEC_URL.toString()));
-    }
-  }
-
   public void testSubstitutions() throws Exception {
     Substitutions substituter = new Substitutions();
     String title = "Hello, World!";