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 2008/03/11 10:53:01 UTC

svn commit: r635862 [3/5] - in /incubator/shindig/trunk: config/ features/core/ features/setprefs/ java/gadgets/ java/gadgets/src/main/java/org/apache/shindig/gadgets/ java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ java/gadgets/src/main/ja...

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsServlet.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsServlet.java Tue Mar 11 02:52:52 2008
@@ -17,16 +17,11 @@
  */
 package org.apache.shindig.gadgets.http;
 
+import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.GadgetFeature;
 import org.apache.shindig.gadgets.GadgetFeatureFactory;
 import org.apache.shindig.gadgets.GadgetFeatureRegistry;
-import org.apache.shindig.gadgets.GadgetServerConfigReader;
 import org.apache.shindig.gadgets.JsLibrary;
-import org.apache.shindig.gadgets.ProcessingOptions;
-import org.apache.shindig.gadgets.RenderingContext;
-import org.apache.shindig.gadgets.SyndicatorConfig;
-import org.json.JSONException;
-import org.json.JSONObject;
 
 import java.io.IOException;
 import java.util.Arrays;
@@ -45,7 +40,6 @@
  */
 public class JsServlet extends HttpServlet {
   private CrossServletState servletState;
-  private static final long START_TIME = System.currentTimeMillis();
 
   @Override
   public void init(ServletConfig config) throws ServletException {
@@ -58,7 +52,8 @@
     // If an If-Modified-Since header is ever provided, we always say
     // not modified. This is because when there actually is a change,
     // cache busting should occur.
-    if (req.getHeader("If-Modified-Since") != null) {
+    if (req.getHeader("If-Modified-Since") != null &&
+        req.getParameter("v") != null) {
       resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
       return;
     }
@@ -88,15 +83,10 @@
     GadgetFeatureRegistry registry
         = servletState.getGadgetServer().getConfig().getFeatureRegistry();
     if (registry.getIncludedFeatures(needed, found, missing)) {
-      String containerParam = req.getParameter("c");
-      RenderingContext context;
-      context = "1".equals(containerParam) ?
-                RenderingContext.CONTAINER :
-                RenderingContext.GADGET;
-
       StringBuilder jsData = new StringBuilder();
 
-      ProcessingOptions opts = new HttpProcessingOptions(req);
+      // Probably incorrect to be using a context here...
+      GadgetContext context = new HttpGadgetContext(req);
       Set<String> features = new HashSet<String>(found.size());
       do {
         for (GadgetFeatureRegistry.Entry entry : found) {
@@ -105,10 +95,9 @@
             features.add(entry.getName());
             GadgetFeatureFactory factory = entry.getFeature();
             GadgetFeature feature = factory.create();
-            for (JsLibrary lib : feature.getJsLibraries(context, opts)) {
-              // TODO: type url js files fail here.
+            for (JsLibrary lib : feature.getJsLibraries(context)) {
               if (lib.getType() != JsLibrary.Type.URL) {
-                if (opts.getDebug()) {
+                if (context.getDebug()) {
                   jsData.append(lib.getDebugContent());
                 } else {
                   jsData.append(lib.getContent());
@@ -124,50 +113,18 @@
         return;
       }
 
-      GadgetServerConfigReader serverConfig
-          = servletState.getGadgetServer().getConfig();
-      SyndicatorConfig syndConf = serverConfig.getSyndicatorConfig();
-      JSONObject syndFeatures = syndConf.getJsonObject(opts.getSyndicator(),
-                                                       "gadgets.features");
-
-      if (syndFeatures != null && context != RenderingContext.CONTAINER) {
-        String[] featArray = features.toArray(new String[features.size()]);
-        try {
-          JSONObject featureConfig = new JSONObject(syndFeatures, featArray);
-          jsData.append("gadgets.config.init(")
-                .append(featureConfig.toString())
-                .append(");");
-        } catch (JSONException e) {
-          throw new RuntimeException(e);
-        }
+      if (req.getParameter("v") != null) {
+        // Versioned files get cached indefinitely
+        HttpUtil.setCachingHeaders(resp, 0);
+      } else {
+        // Unversioned files get cached for 1 hour.
+        HttpUtil.setCachingHeaders(resp, 60 * 60);
       }
-
-      setCachingHeaders(resp);
       resp.setContentType("text/javascript; charset=utf-8");
       resp.setContentLength(jsData.length());
       resp.getOutputStream().write(jsData.toString().getBytes());
     } else {
       resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
     }
-  }
-
-  /**
-   * Sets HTTP headers that instruct the browser to cache indefinitely.
-   * Implementations should take care to use cache-busting techniques on the
-   * url.
-   *
-   * @param response The HTTP response
-   */
-  private void setCachingHeaders(HttpServletResponse response) {
-
-    // Most browsers accept this. 2030 is the last round year before
-    // the end of time.
-    response.setHeader("Expires", "Tue, 01 Jan 2030 00:00:01 GMT");
-
-    // IE seems to need this (10 years should be enough).
-    response.setHeader("Cache-Control", "public,max-age=315360000");
-
-    // Firefox requires this for certain cases.
-    response.setDateHeader("Last-Modified", START_TIME);
   }
 }

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetContext.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetContext.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetContext.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetContext.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,200 @@
+/*
+ * 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.http;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.UserPrefs;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Extracts context from JSON input.
+ */
+public class JsonRpcGadgetContext extends GadgetContext {
+  private final URI url;
+  @Override
+  public URI getUrl() {
+    if (url == null) {
+      return super.getUrl();
+    }
+    return url;
+  }
+
+  private final Integer moduleId;
+  @Override
+  public int getModuleId() {
+    if (moduleId == null) {
+      return super.getModuleId();
+    }
+    return moduleId;
+  }
+
+
+  private final Locale locale;
+  @Override
+  public Locale getLocale() {
+    if (locale == null) {
+      return super.getLocale();
+    }
+    return locale;
+  }
+
+  /**
+   * @param obj
+   * @return The locale, if appropriate parameters are set, or null.
+   */
+  private static Locale getLocale(JSONObject obj) {
+    String language = obj.optString("language");
+    String country = obj.optString("country");
+    if (language == null || country == null) {
+      return null;
+    }
+    return new Locale(language, country);
+  }
+
+  private final RenderingContext renderingContext;
+  @Override
+  public RenderingContext getRenderingContext() {
+    if (renderingContext == null) {
+      return super.getRenderingContext();
+    }
+    return renderingContext;
+  }
+
+  private final Boolean ignoreCache;
+  @Override
+  public boolean getIgnoreCache() {
+    if (ignoreCache == null) {
+      return super.getIgnoreCache();
+    }
+    return ignoreCache;
+  }
+
+  private final String syndicator;
+  @Override
+  public String getSyndicator() {
+    if (syndicator == null) {
+      return super.getSyndicator();
+    }
+    return syndicator;
+  }
+
+  private final Boolean debug;
+  @Override
+  public boolean getDebug() {
+    if (debug == null) {
+      return super.getDebug();
+    }
+    return debug;
+  }
+
+  private final String view;
+  @Override
+  public String getView() {
+    if (view == null) {
+      return super.getView();
+    }
+    return view;
+  }
+
+  private final UserPrefs userPrefs;
+  @Override
+  public UserPrefs getUserPrefs() {
+    if (userPrefs == null) {
+      return super.getUserPrefs();
+    }
+    return userPrefs;
+  }
+
+  /**
+   * @param json
+   * @return UserPrefs, if any are set for this request.
+   * @throws JSONException
+   */
+  @SuppressWarnings("unchecked")
+  private static UserPrefs getUserPrefs(JSONObject json) throws JSONException {
+    JSONObject prefs = json.optJSONObject("prefs");
+    if (prefs == null) {
+      return null;
+    }
+    Map<String, String> p = new HashMap<String, String>();
+    Iterator i = prefs.keys();
+    while (i.hasNext()) {
+      String key = (String)i.next();
+      p.put(key, prefs.getString(key));
+    }
+    return new UserPrefs(p);
+  }
+
+  /**
+   *
+   * @param json
+   * @return URL from the request, or null if not present
+   * @throws JSONException
+   */
+  private static URI getUrl(JSONObject json) throws JSONException {
+    try {
+      String url = json.getString("url");
+      return new URI(url);
+    } catch (URISyntaxException e) {
+      return null;
+    }
+  }
+
+  /**
+   * @param json
+   * @return module id from the request, or null if not present
+   * @throws JSONException
+   */
+  private static Integer getModuleId(JSONObject json) throws JSONException {
+    if (json.has("moduleId")) {
+      return Integer.valueOf(json.getInt("moduleId"));
+    }
+    return null;
+  }
+
+  /**
+   * @param context
+   * @param gadget
+   * @throws JSONException
+   */
+  public JsonRpcGadgetContext(JSONObject context, JSONObject gadget)
+      throws JSONException {
+    url = getUrl(gadget);
+    moduleId = getModuleId(gadget);
+    userPrefs = getUserPrefs(gadget);
+
+    locale = getLocale(context);
+    view = context.optString("view");
+    ignoreCache = context.optBoolean("ignoreCache");
+    syndicator = context.optString("syndicator");
+    debug = context.optBoolean("debug");
+    renderingContext = RenderingContext.CONTAINER;
+  }
+}

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetJob.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetJob.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetJob.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcGadgetJob.java Tue Mar 11 02:52:52 2008
@@ -20,14 +20,10 @@
 package org.apache.shindig.gadgets.http;
 
 import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.GadgetServer;
-import org.apache.shindig.gadgets.GadgetView;
-import org.apache.shindig.gadgets.RenderingContext;
-import org.apache.shindig.gadgets.UserPrefs;
 
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Locale;
 import java.util.concurrent.Callable;
 
 /**
@@ -35,8 +31,7 @@
  */
 public class JsonRpcGadgetJob implements Callable<Gadget> {
   private final GadgetServer gadgetServer;
-  private final JsonRpcContext context;
-  private final JsonRpcGadget gadget;
+  private final GadgetContext context;
 
   /**
    * {@inheritDoc}
@@ -44,28 +39,16 @@
    *  @throws RpcException
    */
   public Gadget call() throws RpcException {
-    GadgetView.ID gadgetId;
     try {
-      gadgetId = new Gadget.GadgetId(new URI(gadget.getUrl()),
-                                     gadget.getModuleId());
-      return gadgetServer.processGadget(gadgetId,
-                                        new UserPrefs(gadget.getUserPrefs()),
-                                        new Locale(context.getLanguage(),
-                                                   context.getCountry()),
-                                        RenderingContext.CONTAINER,
-                                        new JsonRpcProcessingOptions(context));
-    } catch (URISyntaxException e) {
-      throw new RpcException(gadget, "Bad url");
-    } catch (GadgetServer.GadgetProcessException e) {
-      throw new RpcException(gadget, e);
+      return gadgetServer.processGadget(context);
+    } catch (GadgetException e) {
+      throw new RpcException(context, e);
     }
   }
 
   public JsonRpcGadgetJob(GadgetServer gadgetServer,
-                          JsonRpcContext context,
-                          JsonRpcGadget gadget) {
+                          GadgetContext context) {
     this.gadgetServer = gadgetServer;
     this.context = context;
-    this.gadget = gadget;
   }
 }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcRequest.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcRequest.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcRequest.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/JsonRpcRequest.java Tue Mar 11 02:52:52 2008
@@ -20,15 +20,18 @@
 package org.apache.shindig.gadgets.http;
 
 import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.GadgetServer;
-import org.apache.shindig.gadgets.GadgetSpec;
-import org.apache.shindig.gadgets.ProcessingOptions;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.View;
+
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.net.URI;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
@@ -42,8 +45,7 @@
  *
  */
 public class JsonRpcRequest {
-  private final JsonRpcContext context;
-  private final List<JsonRpcGadget> gadgets;
+  private final List<GadgetContext> gadgets;
 
   /**
    * Processes the request and returns a JSON object
@@ -59,83 +61,59 @@
     CompletionService<Gadget> processor =
       new ExecutorCompletionService<Gadget>(server.getConfig().getExecutor());
 
-    for (JsonRpcGadget gadget : gadgets) {
-      processor.submit(new JsonRpcGadgetJob(server, context, gadget));
+    for (GadgetContext gadget : gadgets) {
+      processor.submit(new JsonRpcGadgetJob(server, gadget));
     }
 
-    ProcessingOptions options = new JsonRpcProcessingOptions(context);
-
     int numJobs = gadgets.size();
     do {
       try {
-        Gadget outGadget = processor.take().get();
+        Gadget gadget = processor.take().get();
         JSONObject gadgetJson = new JSONObject();
 
-        if (outGadget.getTitleURI() != null) {
-          gadgetJson.put("titleUrl", outGadget.getTitleURI().toString());
+        GadgetSpec spec = gadget.getSpec();
+        ModulePrefs prefs = spec.getModulePrefs();
+
+        JSONObject views = new JSONObject();
+        for (View view : spec.getViews().values()) {
+          views.put(view.getName(), new JSONObject()
+               // .put("content", view.getContent())
+               .put("type", view.getType().toString().toLowerCase())
+               .put("quirks", view.getQuirks()));
         }
-        gadgetJson.put("url", outGadget.getId().getURI().toString())
-                  .put("moduleId", outGadget.getId().getModuleId())
-                  .put("title", outGadget.getTitle())
-                  .put("contentType",
-                      outGadget.getView(options.getView()).getType()
-                      .toString().toLowerCase());
 
         // Features.
-        Set<String> feats = outGadget.getRequires().keySet();
+        Set<String> feats = prefs.getFeatures().keySet();
         String[] features = feats.toArray(new String[feats.size()]);
-        gadgetJson.put("features", features);
 
-        JSONObject prefs = new JSONObject();
+        JSONObject userPrefs = new JSONObject();
 
         // User pref specs
-        for (GadgetSpec.UserPref pref : outGadget.getUserPrefs()) {
+        for (UserPref pref : spec.getUserPrefs()) {
           JSONObject up = new JSONObject()
               .put("displayName", pref.getDisplayName())
               .put("type", pref.getDataType().toString().toLowerCase())
               .put("default", pref.getDefaultValue())
               .put("enumValues", pref.getEnumValues());
-          prefs.put(pref.getName(), up);
-        }
-        gadgetJson.put("userPrefs", prefs);
-
-        // Content
-        String iframeUrl = servletState.getIframeUrl(outGadget, options);
-        gadgetJson.put("content", iframeUrl);
-
-        // Extended spec data.
-        String directoryTitle = outGadget.getDirectoryTitle();
-        if (directoryTitle != null) {
-          gadgetJson.put("directoryTitle", directoryTitle);
-        }
-
-        URI thumbnail = outGadget.getThumbnail();
-        if (thumbnail != null) {
-          gadgetJson.put("thumbnail", thumbnail.toString());
-        }
-
-        URI screenshot = outGadget.getScreenshot();
-        if (screenshot != null) {
-          gadgetJson.put("screenshot", screenshot.toString());
-        }
-
-        String author = outGadget.getAuthor();
-        if (author != null) {
-          gadgetJson.put("author", author);
-        }
-
-        String authorEmail = outGadget.getAuthorEmail();
-        if (authorEmail != null) {
-          gadgetJson.put("authorEmail", authorEmail);
-        }
-
-        // Categories
-        List<String> cats = outGadget.getCategories();
-        if (cats != null) {
-          String[] categories = cats.toArray(new String[cats.size()]);
-          gadgetJson.put("categories", outGadget.getCategories().toArray());
+          userPrefs.put(pref.getName(), up);
         }
 
+        gadgetJson.put("iframeUrl", servletState.getIframeUrl(gadget))
+                  .put("url", gadget.getContext().getUrl().toString())
+                  .put("moduleId", gadget.getContext().getModuleId())
+                  .put("title", prefs.getTitle())
+                  .put("titleUrl", prefs.getTitleUrl().toString())
+                  .put("views", views)
+                  .put("features", features)
+                  .put("userPrefs", userPrefs)
+                  // extended meta data
+                  .put("directoryTitle", prefs.getDirectoryTitle())
+                  .put("thumbnail", prefs.getThumbnail().toString())
+                  .put("screenshot", prefs.getScreenshot().toString())
+                  .put("author", prefs.getAuthor())
+                  .put("authorEmail", prefs.getAuthorEmail())
+                  .put("categories", prefs.getCategories())
+                  .put("screenshot", prefs.getScreenshot().toString());
         out.append("gadgets", gadgetJson);
       } catch (InterruptedException e) {
         throw new RpcException("Incomplete processing", e);
@@ -146,21 +124,18 @@
         RpcException e = (RpcException)ee.getCause();
         // Just one gadget failed; mark it as such.
         try {
-          JsonRpcGadget gadget = e.getGadget();
+          GadgetContext context = e.getContext();
 
-          if (gadget == null) {
+          if (context == null) {
             throw e;
           }
 
           JSONObject errorObj = new JSONObject();
-          errorObj.put("url", gadget.getUrl())
-                  .put("moduleId", gadget.getModuleId());
-          if (e.getCause() instanceof GadgetServer.GadgetProcessException) {
-            GadgetServer.GadgetProcessException gpe
-                = (GadgetServer.GadgetProcessException)e.getCause();
-            for (GadgetException ge : gpe.getComponents()) {
-              errorObj.append("errors", ge.getMessage());
-            }
+          errorObj.put("url", context.getUrl())
+                  .put("moduleId", context.getModuleId());
+          if (e.getCause() instanceof GadgetException) {
+            GadgetException gpe = (GadgetException)e.getCause();
+            errorObj.append("errors", gpe.getMessage());
           } else {
             errorObj.append("errors", e.getMessage());
           }
@@ -186,11 +161,10 @@
       if (gadgets.length() == 0) {
         throw new RpcException("No gadgets requested.");
       }
-      this.context = new JsonRpcContext(context);
 
-      List<JsonRpcGadget> gadgetList = new LinkedList<JsonRpcGadget>();
+      List<GadgetContext> gadgetList = new LinkedList<GadgetContext>();
       for (int i = 0, j = gadgets.length(); i < j; ++i) {
-        gadgetList.add(new JsonRpcGadget(gadgets.getJSONObject(i)));
+        gadgetList.add(new JsonRpcGadgetContext(context, gadgets.getJSONObject(i)));
       }
       this.gadgets = Collections.unmodifiableList(gadgetList);
     } catch (JSONException e) {

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyHandler.java Tue Mar 11 02:52:52 2008
@@ -21,11 +21,11 @@
 import org.apache.shindig.gadgets.GadgetException;
 import org.apache.shindig.gadgets.GadgetSigner;
 import org.apache.shindig.gadgets.GadgetToken;
-import org.apache.shindig.gadgets.ProcessingOptions;
 import org.apache.shindig.gadgets.RemoteContent;
 import org.apache.shindig.gadgets.RemoteContentFetcher;
 import org.apache.shindig.gadgets.RemoteContentRequest;
 import org.apache.shindig.util.InputStreamConsumer;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 
@@ -40,8 +40,10 @@
 import java.util.Collections;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -52,25 +54,24 @@
 
   private final RemoteContentFetcher fetcher;
 
+  private final static Set<String> DISALLOWED_RESPONSE_HEADERS
+    = new HashSet<String>();
+  static {
+    DISALLOWED_RESPONSE_HEADERS.add("set-cookie");
+    DISALLOWED_RESPONSE_HEADERS.add("content-length");
+  }
+
   public ProxyHandler(RemoteContentFetcher fetcher) {
     this.fetcher = fetcher;
   }
 
   public void fetchJson(HttpServletRequest request,
                         HttpServletResponse response,
-                        GadgetSigner signer)
+                        CrossServletState state)
       throws ServletException, IOException, GadgetException {
-    GadgetToken token = extractAndValidateToken(request, signer);
-    String url = request.getParameter("url");
-    URL originalUrl = validateUrl(url);
-    URL signedUrl = signUrl(originalUrl, token, request);
 
     // Fetch the content and convert it into JSON.
-    // TODO: Fetcher needs to handle variety of HTTP methods.
-    RemoteContent results = fetchContent(signedUrl,
-                                         request,
-                                         new HttpProcessingOptions(request));
-
+    RemoteContent results = fetchContent(request, state);
     response.setStatus(results.getHttpStatusCode());
     if (results.getHttpStatusCode() == HttpServletResponse.SC_OK) {
       String output;
@@ -79,14 +80,14 @@
         JSONObject resp = new JSONObject()
             .put("body", results.getResponseAsString())
             .put("rc", results.getHttpStatusCode());
-        String json = new JSONObject().put(url, resp).toString();
-        output = UNPARSEABLE_CRUFT + json;
+        String url = request.getParameter("url");
+        JSONObject json = new JSONObject().put(url, resp);
+        output = UNPARSEABLE_CRUFT + json.toString();
       } catch (JSONException e) {
         output = "";
       }
-
-      setCachingHeaders(response);
       response.setContentType("application/json; charset=utf-8");
+      response.setHeader("Pragma", "no-cache");
       response.setHeader("Content-Disposition", "attachment;filename=p.txt");
       response.getWriter().write(output);
     }
@@ -94,31 +95,28 @@
 
   public void fetch(HttpServletRequest request,
                     HttpServletResponse response,
-                    GadgetSigner signer)
+                    CrossServletState state)
       throws ServletException, IOException, GadgetException {
-    GadgetToken token = extractAndValidateToken(request, signer);
-    URL originalUrl = validateUrl(request.getParameter("url"));
-    URL signedUrl = signUrl(originalUrl, token, request);
-
-    // TODO: Fetcher needs to handle variety of HTTP methods.
-    RemoteContent results = fetchContent(signedUrl,
-                                         request,
-                                         new HttpProcessingOptions(request));
+    RemoteContent results = fetchContent(request, state);
+
+    if (request.getHeader("If-Modified-Since") != null) {
+      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      return;
+    }
 
     int status = results.getHttpStatusCode();
     response.setStatus(status);
     if (status == HttpServletResponse.SC_OK) {
-      // Fill out the response.
-      setCachingHeaders(response);
       Map<String, List<String>> headers = results.getAllHeaders();
+      if (headers.get("Cache-Control") == null) {
+        // Cache for 1 hour by default for images.
+        HttpUtil.setCachingHeaders(response, 60 * 60);
+      }
       for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
         String name = entry.getKey();
         List<String> values = entry.getValue();
-        if (name != null
-            && values != null
-            && name.compareToIgnoreCase("Cache-Control") != 0
-            && name.compareToIgnoreCase("Expires") != 0
-            && name.compareToIgnoreCase("Content-Length") != 0) {
+        if (name != null && values != null
+            && !DISALLOWED_RESPONSE_HEADERS.contains(name.toLowerCase())) {
           for (String value : values) {
             response.addHeader(name, value);
           }
@@ -131,66 +129,75 @@
 
   /**
    * Fetch the content for a request
+   *
+   * @param request
    */
   @SuppressWarnings("unchecked")
-  private RemoteContent fetchContent(URL signedUrl,
-                                     HttpServletRequest request,
-                                     ProcessingOptions procOptions)
-      throws ServletException {
+  private RemoteContent fetchContent(HttpServletRequest request,
+      CrossServletState state) throws ServletException, GadgetException {
     String encoding = request.getCharacterEncoding();
     if (encoding == null) {
       encoding = "UTF-8";
     }
+
     try {
-      if ("POST".equals(request.getMethod())) {
-        String method = getParameter(request, "httpMethod", "GET");
-        String postData = URLDecoder.decode(
-            getParameter(request, "postData", ""), encoding);
+      URL originalUrl = validateUrl(request.getParameter("url"));
+      GadgetSigner signer = state.getGadgetSigner();
+      URL signedUrl;
+      if ("signed".equals(request.getParameter("authz"))) {
+        GadgetToken token = extractAndValidateToken(request, signer);
+        if (token == null) {
+          return new RemoteContent(HttpServletResponse.SC_UNAUTHORIZED,
+              "Invalid security token.".getBytes(), null);
+        }
+        signedUrl = signUrl(originalUrl, token, request);
+      } else {
+        signedUrl = originalUrl;
+      }
+      String method = request.getMethod();
+      Map<String, List<String>> headers = null;
+      byte[] postBody = null;
+
+      if ("POST".equals(method)) {
+        method = getParameter(request, "httpMethod", "GET");
+        postBody = URLDecoder.decode(
+            getParameter(request, "postData", ""), encoding).getBytes();
 
-        Map<String, List<String>> headers;
         String headerData = request.getParameter("headers");
-        if (headerData == null) {
+        if (headerData == null || headerData.length() == 0) {
           headers = Collections.emptyMap();
         } else {
-          if (headerData.length() == 0) {
-            headers = Collections.emptyMap();
-          } else {
-            // We actually only accept single key value mappings now.
-            headers = new HashMap<String, List<String>>();
-            String[] headerList = headerData.split("&");
-            for (String header : headerList) {
-              String[] parts = header.split("=");
-              if (parts.length != 2) {
-                // Malformed headers
-                return RemoteContent.ERROR;
-              }
-              headers.put(URLDecoder.decode(parts[0], encoding),
-                  Arrays.asList(URLDecoder.decode(parts[1], encoding)));
+          // We actually only accept single key value mappings now.
+          headers = new HashMap<String, List<String>>();
+          String[] headerList = headerData.split("&");
+          for (String header : headerList) {
+            String[] parts = header.split("=");
+            if (parts.length != 2) {
+              // Malformed headers
+              return RemoteContent.ERROR;
             }
+            headers.put(URLDecoder.decode(parts[0], encoding),
+                Arrays.asList(URLDecoder.decode(parts[1], encoding)));
           }
         }
-
-        removeUnsafeHeaders(headers);
-
-        RemoteContentRequest req = new RemoteContentRequest(
-            signedUrl.toURI(), headers, postData.getBytes());
-        if ("POST".equals(method)) {
-          return fetcher.fetchByPost(req, procOptions);
-        } else {
-          return fetcher.fetch(req, procOptions);
-        }
       } else {
-        Map<String, List<String>> headers = new HashMap<String, List<String>>();
+        postBody = null;
+        headers = new HashMap<String, List<String>>();
         Enumeration<String> headerNames = request.getHeaderNames();
         while (headerNames.hasMoreElements()) {
           String header = headerNames.nextElement();
           headers.put(header, Collections.list(request.getHeaders(header)));
         }
-        removeUnsafeHeaders(headers);
-        RemoteContentRequest req
-            = new RemoteContentRequest(signedUrl.toURI(), headers);
-        return fetcher.fetch(req, procOptions);
       }
+
+      removeUnsafeHeaders(headers);
+
+      RemoteContentRequest.Options options
+          = new RemoteContentRequest.Options();
+      options.ignoreCache = "1".equals(request.getParameter("nocache"));
+      RemoteContentRequest req = new RemoteContentRequest(
+          method, signedUrl.toURI(), headers, postBody, options);
+      return fetcher.fetch(req);
     } catch (UnsupportedEncodingException e) {
       throw new ServletException(e);
     } catch (URISyntaxException e) {
@@ -207,7 +214,7 @@
     final String[] badHeaders = new String[] {
         // No legitimate reason to over ride these.
         // TODO: We probably need to test variations as well.
-        "Host", "Accept-Encoding", "Accept"
+        "Host"
     };
     for (String bad : badHeaders) {
       headers.remove(bad);
@@ -215,33 +222,36 @@
   }
 
   /**
-   * Validates that the given url is valid for this reques.t
+   * Validates that the given url is valid for this request.
    *
-   * @param url
+   * @param urlToValidate
    * @return A URL object of the URL
    * @throws ServletException if the URL fails security checks or is malformed.
    */
-  private URL validateUrl(String url) throws ServletException {
-    if (url == null) {
+  private URL validateUrl(String urlToValidate) throws ServletException {
+    if (urlToValidate == null) {
       throw new ServletException("url parameter is missing.");
     }
     try {
-      URI origin = new URI(url);
-      if (origin.getScheme() == null) {
-        throw new ServletException("Invalid URL " + origin.toString());
+
+      URI url = new URI(urlToValidate);
+
+      if (url.getScheme() == null) {
+        throw new ServletException("Invalid URL " + url.toString());
       }
-      if (!origin.getScheme().equals("http")) {
-        throw new ServletException("Unsupported scheme: " + origin.getScheme());
+      if (!url.getScheme().equals("http")) {
+        throw new ServletException("Unsupported scheme: " + url.getScheme());
       }
-      if (origin.getPath() == null || origin.getPath().length() == 0) {
+      if (url.getPath() == null || url.getPath().length() == 0) {
         // Forcibly set the path to "/" if it is empty
-        origin = new URI(origin.getScheme(),
-            origin.getUserInfo(), origin.getHost(),
-            origin.getPort(),
-            "/", origin.getQuery(),
-            origin.getFragment());
+        url = new URI(url.getScheme(),
+                      url.getUserInfo(),
+                      url.getHost(),
+                      url.getPort(),
+                      "/", url.getQuery(),
+                      url.getFragment());
       }
-      return origin.toURL();
+      return url.toURL();
     } catch (URISyntaxException use) {
       throw new ServletException("Malformed URL " + use.getMessage());
     } catch (MalformedURLException mfe) {
@@ -250,6 +260,7 @@
   }
 
   /**
+   * @param request
    * @return A valid token for the given input.
    * @throws GadgetException
    */
@@ -263,26 +274,12 @@
   }
 
   /**
-   * Sets HTTP caching headers
-   *
-   * @param response The HTTP response
-   */
-  private void setCachingHeaders(HttpServletResponse response) {
-    // TODO: Re-implement caching behavior if appropriate.
-    response.setHeader("Cache-Control", "private; max-age=0");
-    response.setDateHeader("Expires", System.currentTimeMillis() - 30);
-  }
-
-  /**
    * Sign a URL with a GadgetToken if needed
    * @return The signed url.
    */
   @SuppressWarnings("unchecked")
   private URL signUrl(URL originalUrl, GadgetToken token,
       HttpServletRequest request) throws GadgetException {
-    if (token == null || !"signed".equals(request.getParameter("authz"))) {
-      return originalUrl;
-    }
     String method = getParameter(request, "httpMethod", "GET");
     return token.signUrl(originalUrl, method, request.getParameterMap());
   }

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyServlet.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyServlet.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyServlet.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/ProxyServlet.java Tue Mar 11 02:52:52 2008
@@ -18,8 +18,8 @@
  */
 package org.apache.shindig.gadgets.http;
 
-import org.apache.shindig.gadgets.GadgetServerConfigReader;
 import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetServerConfigReader;
 
 import java.io.IOException;
 import java.util.logging.Level;
@@ -53,11 +53,9 @@
     String output = request.getParameter("output");
     try {
       if ("js".equals(output)) {
-        proxyHandler.fetchJson(
-            request, response, servletState.getGadgetSigner(request));
+        proxyHandler.fetchJson(request, response, servletState);
       } else {
-        proxyHandler.fetch(
-            request, response, servletState.getGadgetSigner(request));
+        proxyHandler.fetch(request, response, servletState);
       }
     } catch (GadgetException ge) {
       outputError(ge, response);
@@ -65,13 +63,14 @@
   }
 
   @Override
-  protected void doPost(HttpServletRequest request, HttpServletResponse response)
-      throws ServletException, IOException {
+  protected void doPost(HttpServletRequest request,
+      HttpServletResponse response) throws ServletException, IOException {
     // Currently they are identical
     doGet(request, response);
   }
 
-  private void outputError(GadgetException excep, HttpServletResponse resp) throws IOException {
+  private void outputError(GadgetException excep, HttpServletResponse resp)
+      throws IOException {
     StringBuilder err = new StringBuilder();
     err.append(excep.getCode().toString());
     err.append(' ');

Modified: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcException.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcException.java?rev=635862&r1=635861&r2=635862&view=diff
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcException.java (original)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RpcException.java Tue Mar 11 02:52:52 2008
@@ -19,38 +19,40 @@
 
 package org.apache.shindig.gadgets.http;
 
+import org.apache.shindig.gadgets.GadgetContext;
+
 /**
  * Contains RPC-specific exceptions.
  */
 public class RpcException extends Exception {
-  private final JsonRpcGadget gadget;
+  private final GadgetContext context;
 
-  public JsonRpcGadget getGadget() {
-    return gadget;
+  public GadgetContext getContext() {
+    return context;
   }
 
   public RpcException(String message) {
     super(message);
-    gadget = null;
+    context = null;
   }
 
   public RpcException(String message, Throwable cause) {
     super(message, cause);
-    gadget = null;
+    context = null;
   }
 
-  public RpcException(JsonRpcGadget gadget, Throwable cause) {
+  public RpcException(GadgetContext context, Throwable cause) {
     super(cause);
-    this.gadget = gadget;
+    this.context = context;
   }
 
-  public RpcException(JsonRpcGadget gadget, String message) {
+  public RpcException(GadgetContext context, String message) {
     super(message);
-    this.gadget = gadget;
+    this.context = context;
   }
 
-  public RpcException(JsonRpcGadget gadget, String message, Throwable cause) {
+  public RpcException(GadgetContext context, String message, Throwable cause) {
     super(message, cause);
-    this.gadget = gadget;
+    this.context = context;
   }
 }

Added: 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=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a Require or Optional tag.
+ * No substitutions on any fields.
+ */
+public class Feature {
+  /**
+   * Require@feature
+   * Optional@feature
+   */
+  private final String name;
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Require.Param
+   * Optional.Param
+   *
+   * Flattened into a map where Param@name is the key and Param content is
+   * the value.
+   */
+  private final Map<String, String> params;
+  public Map<String, String> getParams() {
+    return params;
+  }
+
+  /**
+   * Whether this is a Require or an Optional feature.
+   */
+  private final boolean required;
+  public boolean getRequired() {
+    return required;
+  }
+
+  /**
+   * Produces an xml representation of the feature.
+   */
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append(required ? "<Require" : "<Optional")
+       .append(" feature=\"")
+       .append(name)
+       .append("\">");
+    for (Map.Entry<String, String> entry : params.entrySet()) {
+      buf.append("\n<Param name=\"")
+         .append(entry.getKey())
+         .append("\">")
+         .append(entry.getValue())
+         .append("</Param>");
+    }
+    buf.append(required ? "</Require>" : "</Optional>");
+    return buf.toString();
+  }
+
+  /**
+   * Creates a new Feature from an xml node.
+   *
+   * @param feature The feature to create
+   * @throws SpecParserException When the Require or Optional tag is not valid
+   */
+  public Feature(Element feature) throws SpecParserException {
+    this.required = feature.getNodeName().equals("Require");
+    String name = XmlUtil.getAttribute(feature, "feature");
+    if (name == null) {
+      throw new SpecParserException(
+          (required ? "Require" : "Optional") +"@feature is required.");
+    }
+    this.name = name;
+    NodeList children = feature.getElementsByTagName("Param");
+    if (children.getLength() > 0) {
+      Map<String, String> params = new HashMap<String, String>();
+      for (int i = 0, j = children.getLength(); i < j; ++i) {
+        Element param = (Element)children.item(i);
+        String paramName = XmlUtil.getAttribute(param, "name");
+        if (paramName == null) {
+          throw new SpecParserException("Param@name is required");
+        }
+        params.put(paramName, param.getTextContent());
+      }
+      this.params = Collections.unmodifiableMap(params);
+    } else {
+      this.params = Collections.emptyMap();
+    }
+  }
+}

Added: 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=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.gadgets.Substitutions;
+import org.apache.shindig.util.HashUtil;
+import org.apache.shindig.util.XmlException;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Represents a gadget specification root element (Module).
+ * @see <a href="http://code.google.com/apis/gadgets/docs/gadgets-extended-xsd.html">gadgets spec</a>
+ */
+public class GadgetSpec {
+  public static final String DEFAULT_VIEW = "default";
+  public static final Locale DEFAULT_LOCALE = new Locale("all", "ALL");
+
+  /**
+   * The url for this gadget spec.
+   */
+  private final URI url;
+  public URI getUrl() {
+    return url;
+  }
+
+  /**
+   * A checksum of the gadget's content.
+   */
+  private final String checksum;
+  public String getChecksum() {
+    return checksum;
+  }
+
+  /**
+   * ModulePrefs
+   */
+  private ModulePrefs modulePrefs;
+  public ModulePrefs getModulePrefs() {
+    return modulePrefs;
+  }
+
+  /**
+   * UserPref
+   */
+  private List<UserPref> userPrefs;
+  public List<UserPref> getUserPrefs() {
+    return userPrefs;
+  }
+
+  /**
+   * Content
+   * Mapping is view -> Content section.
+   */
+  private Map<String, View> views;
+  public Map<String, View> getViews() {
+    return views;
+  }
+
+  /**
+   * Retrieves a single view by name.
+   *
+   * @param name The name of the view you want to see
+   * @return The view object, if it exists, or null.
+   */
+  public View getView(String name) {
+    View view = views.get(name);
+    if (view == null && !name.equals(DEFAULT_VIEW)) {
+      return views.get(DEFAULT_VIEW);
+    }
+    return view;
+  }
+
+  /**
+   * Performs substitutions on the spec. See individual elements for
+   * details on what gets substituted.
+   *
+   * @param substituter
+   * @return The substituted spec.
+   */
+  public GadgetSpec substitute(Substitutions substituter) {
+    GadgetSpec spec = new GadgetSpec(this);
+    spec.modulePrefs = modulePrefs.substitute(substituter);
+    if (userPrefs.size() == 0) {
+      spec.userPrefs = Collections.emptyList();
+    } else {
+      List<UserPref> prefs = new ArrayList<UserPref>(userPrefs.size());
+      for (UserPref pref : userPrefs) {
+        prefs.add(pref.substitute(substituter));
+      }
+      spec.userPrefs = Collections.unmodifiableList(prefs);
+    }
+    Map<String, View> viewMap = new HashMap<String, View>(views.size());
+    for (View view : views.values()) {
+     viewMap.put(view.getName(), view.substitute(substituter));
+    }
+    spec.views = Collections.unmodifiableMap(viewMap);
+
+    return spec;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Module>\n")
+       .append(modulePrefs).append("\n");
+    for (UserPref pref : userPrefs) {
+      buf.append(pref).append("\n");
+    }
+    for (Map.Entry<String, View> view : views.entrySet()) {
+      buf.append(view.getValue()).append("\n");
+    }
+    buf.append("</Module>");
+    return buf.toString();
+  }
+
+  /**
+   * Creates a new Module from the given xml input.
+   *
+   * @param url
+   * @param xml
+   * @throws SpecParserException
+   */
+  public GadgetSpec(URI url, String xml) throws SpecParserException {
+    Element doc;
+    try {
+      doc = XmlUtil.parse(xml);
+    } catch (XmlException e) {
+      throw new SpecParserException(e);
+    }
+    this.url = url;
+
+    // This might not be good enough; should we take message bundle changes
+    // into account?
+    this.checksum = HashUtil.checksum(xml.getBytes());
+
+    NodeList children = doc.getChildNodes();
+
+    ModulePrefs modulePrefs = null;
+    List<UserPref> userPrefs = new LinkedList<UserPref>();
+    Map<String, List<Element>> views = new HashMap<String, List<Element>>();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      if (!(child instanceof Element)) {
+        continue;
+      }
+      Element element = (Element)child;
+      String name = element.getTagName();
+      if ("ModulePrefs".equals(name)) {
+        if (modulePrefs == null) {
+          modulePrefs = new ModulePrefs(element, url);
+        } else {
+          throw new SpecParserException(
+              "Only 1 ModulePrefs is allowed.");
+        }
+      }
+      if ("UserPref".equals(name)) {
+        UserPref pref = new UserPref(element);
+        userPrefs.add(pref);
+      }
+      if ("Content".equals(name)) {
+        String viewNames = XmlUtil.getAttribute(element, "view", "default");
+        for (String view : viewNames.split(",")) {
+          view = view.trim();
+          List<Element> viewElements = views.get(view);
+          if (viewElements == null) {
+            viewElements = new LinkedList<Element>();
+            views.put(view, viewElements);
+          }
+          viewElements.add(element);
+        }
+      }
+    }
+
+    if (modulePrefs == null) {
+      throw new SpecParserException(
+          "At least 1 ModulePrefs is required.");
+    } else {
+      this.modulePrefs = modulePrefs;
+    }
+
+    if (views.size() == 0) {
+      throw new SpecParserException("At least 1 Content is required.");
+    } else {
+      Map<String, View> tmpViews = new HashMap<String, View>();
+      for (Map.Entry<String, List<Element>> view : views.entrySet()) {
+        View v = new View(view.getKey(), view.getValue());
+        tmpViews.put(v.getName(), v);
+      }
+      this.views = Collections.unmodifiableMap(tmpViews);
+    }
+
+    if (userPrefs.size() > 0) {
+      this.userPrefs = Collections.unmodifiableList(userPrefs);
+    } else {
+      this.userPrefs = Collections.emptyList();
+    }
+  }
+
+  /**
+   * Constructs a GadgetSpec for substitute calls.
+   * @param spec
+   */
+  private GadgetSpec(GadgetSpec spec) {
+    url = spec.url;
+    checksum = spec.checksum;
+  }
+}
\ No newline at end of file

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.gadgets.Substitutions;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+
+/**
+ * Represents a ModuleSpec.Icon tag.
+ *
+ * TODO: Support substitution
+ */
+public class Icon {
+  /**
+   * Icon@mode
+   * Probably better labeled "encoding"; currently only base64 is supported.
+   * If mode is not set, content must be a url. Otherwise, content is
+   * a mode-encoded image with a mime type equal to type.
+   */
+  private final String mode;
+  public String getMode() {
+    return mode;
+  }
+
+  /**
+   * Icon@type
+   * Mime type of the icon
+   */
+  private final String type;
+  public String getType() {
+    return type;
+  }
+
+  /**
+   * Icon#CDATA
+   *
+   * Message Bundles
+   */
+  private String content;
+  public String getContent() {
+    return content;
+  }
+
+  /**
+   * Substitutes the icon fields according to the spec.
+   *
+   * @param substituter
+   * @return The substituted icon
+   */
+  public Icon substitute(Substitutions substituter) {
+    Icon icon = new Icon(this);
+    icon.content
+        = substituter.substituteString(Substitutions.Type.MESSAGE, content);
+    return icon;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Icon type=\"")
+       .append(type)
+       .append("\" mode=\"")
+       .append(mode)
+       .append("\">")
+       .append(content)
+       .append("</Icon>");
+    return buf.toString();
+  }
+
+  /**
+   * Currently does not validate icon data.
+   * @param element
+   */
+  public Icon(Element element) throws SpecParserException {
+    mode = XmlUtil.getAttribute(element, "mode");
+    if (mode != null && !mode.equals("base64")) {
+      throw new SpecParserException(
+          "The only valid value for Icon@mode is \"base64\"");
+    }
+    type = XmlUtil.getAttribute(element, "type", "");
+    content = element.getTextContent();
+  }
+
+  /**
+   * Creates an icon for substitute()
+   *
+   * @param icon
+   */
+  private Icon(Icon icon) {
+    mode = icon.mode;
+    type = icon.type;
+  }
+}
\ No newline at end of file

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+/**
+ * Represents a Locale tag.
+ * Generally compatible with java.util.Locale, but with some extra
+ * localization data from the spec.
+ * Named "LocaleSpec" so as to not conflict with java.util.Locale
+ *
+ * No localization.
+ * No user pref substitution.
+ */
+public class LocaleSpec {
+
+  /**
+   * Locale@lang
+   */
+  private final String language;
+  public String getLanguage() {
+    return language;
+  }
+
+  /**
+   * Locale@country
+   */
+  private final String country;
+  public String getCountry() {
+    return country;
+  }
+
+  /**
+   * Locale@language_direction
+   */
+  private final String languageDirection;
+  public String getLanguageDirection() {
+    return languageDirection;
+  }
+
+  /**
+   * Locale@messages
+   */
+  private final URI messages;
+  public URI getMessages() {
+    return messages;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Locale lang=\"")
+       .append(language)
+       .append("\" country=\"")
+       .append(country)
+       .append("\" language_direction=\"")
+       .append(languageDirection)
+       .append("\" messages=\"")
+       .append(messages)
+       .append("\"/>");
+    return buf.toString();
+  }
+
+  /**
+   * @param element
+   * @param specUrl The url that the spec is loaded from. messages is assumed
+   *     to be relative to this path.
+   * @throws SpecParserException If language_direction is not valid
+   */
+  public LocaleSpec(Element element, URI specUrl) throws SpecParserException {
+    language = XmlUtil.getAttribute(element, "lang", "all").toLowerCase();
+    country = XmlUtil.getAttribute(element, "country", "ALL").toUpperCase();
+    languageDirection
+        = XmlUtil.getAttribute(element, "language_direction", "ltr");
+    if (!("ltr".equals(languageDirection) ||
+          "rtl".equals(languageDirection))) {
+      throw new SpecParserException(
+          "Locale@language_direction must be ltr or rtl");
+    }
+    String messages = XmlUtil.getAttribute(element, "messages");
+    if (messages == null) {
+      this.messages = URI.create("");
+    } else {
+      try {
+        this.messages = new URL(specUrl.toURL(), messages).toURI();
+      } catch (URISyntaxException e) {
+        throw new SpecParserException("Locale@messages url is invalid.");
+      } catch (MalformedURLException e) {
+        throw new SpecParserException("Locale@messages url is invalid.");
+      }
+    }
+  }
+}

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.util.XmlException;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a messagebundle structure.
+ */
+public class MessageBundle {
+
+  public static final MessageBundle EMPTY = new MessageBundle();
+
+  private final Map<String, String> messages;
+  /**
+   * @return A read-only view of the message bundle.
+   */
+  public Map<String, String> getMessages() {
+    return messages;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<messagebundle>\n");
+    for (Map.Entry<String, String> entry : messages.entrySet()) {
+      buf.append("<msg name=\"").append(entry.getKey()).append("\">")
+         .append(entry.getValue())
+         .append("</msg>\n");
+    }
+    buf.append("</messagebundle>");
+    return buf.toString();
+  }
+
+  /**
+   * Constructs a message bundle from input xml
+   * @param xml
+   * @throws SpecParserException
+   */
+  public MessageBundle(String xml) throws SpecParserException {
+    Element doc;
+    try {
+      doc = XmlUtil.parse(xml);
+    } catch (XmlException e) {
+      throw new SpecParserException(e);
+    }
+
+    Map<String, String> messages = new HashMap<String, String>();
+
+    NodeList nodes = doc.getElementsByTagName("msg");
+    for (int i = 0, j = nodes.getLength(); i < j; ++i) {
+      Element msg = (Element)nodes.item(i);
+      String name = XmlUtil.getAttribute(msg, "name");
+      if (name == null) {
+        throw new SpecParserException(
+            "All message bundle entries must have a name attribute.");
+      }
+      messages.put(name, msg.getTextContent());
+    }
+    this.messages = Collections.unmodifiableMap(messages);
+  }
+
+  private MessageBundle() {
+    this.messages = Collections.emptyMap();
+  }
+}

Added: 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=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,437 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.gadgets.Substitutions;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Represents the ModulePrefs element of a gadget spec.
+ *
+ * This encapsulates most gadget meta data, including everything except for
+ * Content and UserPref nodes.
+ */
+public class ModulePrefs {
+  // Canonical spec items first.
+
+  /**
+   * ModulePrefs@title
+   *
+   * User Pref + Message Bundle + Bidi
+   */
+  private String title;
+  public String getTitle() {
+    return title;
+  }
+
+  /**
+   * ModulePrefs@title_url
+   *
+   * User Pref + Message Bundle + Bidi
+   */
+  private URI titleUrl;
+  public URI getTitleUrl() {
+    return titleUrl;
+  }
+
+  /**
+   * ModulePrefs@description
+   *
+   * Message Bundles
+   */
+  private String description;
+  public String getDescription() {
+    return description;
+  }
+
+  /**
+   * ModulePrefs@author
+   *
+   * Message Bundles
+   */
+  private String author;
+  public String getAuthor() {
+    return author;
+  }
+
+  /**
+   * ModulePrefs@author_email
+   *
+   * Message Bundles
+   */
+  private String authorEmail;
+  public String getAuthorEmail() {
+    return authorEmail;
+  }
+
+  /**
+   * ModulePrefs@screenshot
+   *
+   * Message Bundles
+   */
+  private URI screenshot;
+  public URI getScreenshot() {
+    return screenshot;
+  }
+
+  /**
+   * ModulePrefs@thumbnail
+   *
+   * Message Bundles
+   */
+  private URI thumbnail;
+  public URI getThumbnail() {
+    return thumbnail;
+  }
+
+  // Extended data (typically used by directories)
+
+  /**
+   * ModulePrefs@directory_title
+   *
+   * Message Bundles
+   */
+  private String directoryTitle;
+  public String getDirectoryTitle() {
+    return directoryTitle;
+  }
+
+  /*
+   * The following ModulePrefs attributes are skipped:
+   *
+   * author_affiliation
+   * author_location
+   * author_photo
+   * author_aboutme
+   * author_quote
+   * author_link
+   * show_stats
+   * show_in_directory
+   */
+
+  /**
+   * ModuleSpec@width
+   */
+  private final int width;
+  public int getWidth() {
+    return width;
+  }
+
+  /**
+   * ModuleSpec@width
+   */
+  private final int height;
+  public int getHeight() {
+    return height;
+  }
+
+  /**
+   * ModuleSpec@category
+   * ModuleSpec@category2
+   * These fields are flattened into a single list.
+   */
+  private final List<String> categories;
+  public List<String> getCategories() {
+    return categories;
+  }
+
+  /**
+   * Skipped:
+   *
+   * singleton
+   * render_inline
+   * scaling
+   * scrolling
+   */
+
+  /**
+   * ModuleSpec.Require
+   * ModuleSpec.Optional
+   */
+  private final Map<String, Feature> features;
+  public Map<String, Feature> getFeatures() {
+    return features;
+  }
+
+  /**
+   * ModuleSpec.Preload
+   */
+  private final List<URI> preloads;
+  public List<URI> getPreloads() {
+    return preloads;
+  }
+
+  /**
+   * ModuleSpec.Icon
+   */
+  private List<Icon> icons;
+  public List<Icon> getIcons() {
+    return icons;
+  }
+
+  /**
+   * ModuleSpec.Locale
+   */
+  private final Map<Locale, LocaleSpec> locales;
+  public Map<Locale, LocaleSpec> getLocales() {
+    return locales;
+  }
+
+  /**
+   * Attempts to retrieve a valid LocaleSpec for the given Locale.
+   * First tries to find an exact language / country match.
+   * Then tries to find a match for language / all.
+   * Then tries to find a match for all / all.
+   * Finally gives up.
+   * @param locale
+   * @return The locale spec, if there is a matching one, or null.
+   */
+  public LocaleSpec getLocale(Locale locale) {
+    if (locales.size() == 0) {
+      return null;
+    }
+    LocaleSpec localeSpec = locales.get(locale);
+    if (localeSpec == null) {
+      locale = new Locale(locale.getLanguage(), "ALL");
+      localeSpec = locales.get(locale);
+      if (localeSpec == null) {
+        localeSpec = locales.get(GadgetSpec.DEFAULT_LOCALE);
+      }
+    }
+
+    return localeSpec;
+  }
+
+  /**
+   * Produces a new ModulePrefs by substituting hangman variables from
+   * substituter. See comments on individual fields to see what actually
+   * has substitutions performed.
+   *
+   * @param substituter
+   */
+  public ModulePrefs substitute(Substitutions substituter) {
+    ModulePrefs prefs = new ModulePrefs(this);
+
+    // Icons, if any
+    if (icons.size() == 0) {
+      prefs.icons = Collections.emptyList();
+    } else {
+      List<Icon> iconList = new ArrayList<Icon>(icons.size());
+      for (Icon icon : icons) {
+        iconList.add(icon.substitute(substituter));
+      }
+      prefs.icons = Collections.unmodifiableList(iconList);
+    }
+
+    Substitutions.Type type = Substitutions.Type.MESSAGE;
+    // Most attributes only get strings.
+    prefs.author = substituter.substituteString(type, author);
+    prefs.authorEmail = substituter.substituteString(type, authorEmail);
+    prefs.description = substituter.substituteString(type, description);
+    prefs.directoryTitle = substituter.substituteString(type, directoryTitle);
+    prefs.screenshot = substituter.substituteUri(type, screenshot);
+    prefs.thumbnail = substituter.substituteUri(type, thumbnail);
+
+    // All types.
+    prefs.title = substituter.substituteString(null, title);
+    prefs.titleUrl = substituter.substituteUri(null, titleUrl);
+    return prefs;
+  }
+
+
+  /**
+   * Walks child nodes of the given node.
+   * @param element
+   * @param visitors Map of tag names to visitors for that tag.
+   */
+  private static void walk(Element element, Map<String, ElementVisitor> visitors)
+      throws SpecParserException {
+    NodeList children = element.getChildNodes();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      ElementVisitor visitor = visitors.get(child.getNodeName());
+      if (visitor != null) {
+        visitor.visit((Element)child);
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<ModulePrefs")
+       .append(" title=\"").append(title).append("\"")
+       .append(" author=\"").append(author).append("\"")
+       .append(" author_email=\"").append(authorEmail).append("\"")
+       .append(" description=\"").append(description).append("\"")
+       .append(" directory_title=\"").append(directoryTitle).append("\"")
+       .append(" screenshot=\"").append(screenshot).append("\"")
+       .append(" thumbnail=\"").append(thumbnail).append("\"")
+       .append(" height=\"").append(height).append("\"")
+       .append(" width=\"").append(width).append("\"")
+       .append(" category1=\"").append(categories.get(0)).append("\"")
+       .append(" category2=\"").append(categories.get(1)).append("\"")
+       .append(">\n");
+    for (URI preload : preloads) {
+      buf.append("<Preload href=\"").append(preload).append("\"/>\n");
+    }
+    for (Feature feature : features.values()) {
+      buf.append(feature).append("\n");
+    }
+    for (Icon icon : icons) {
+      buf.append(icon).append("\n");
+    }
+    for (LocaleSpec locale : locales.values()) {
+      buf.append(locale).append("\n");
+    }
+    buf.append("</ModulePrefs>");
+    return buf.toString();
+  }
+
+  /**
+   * @param element
+   * @param specUrl
+   */
+  public ModulePrefs(Element element, URI specUrl) throws SpecParserException {
+    title = XmlUtil.getAttribute(element, "title");
+    if (title == null) {
+      throw new SpecParserException("ModulePrefs@title is required.");
+    }
+    URI emptyUri = URI.create("");
+    titleUrl = XmlUtil.getUriAttribute(element, "title_url", emptyUri);
+    author = XmlUtil.getAttribute(element, "author", "");
+    authorEmail = XmlUtil.getAttribute(element, "author_email", "");
+    description = XmlUtil.getAttribute(element, "description", "");
+    directoryTitle = XmlUtil.getAttribute(element, "directory_title", "");
+    screenshot = XmlUtil.getUriAttribute(element, "screenshot", emptyUri);
+    thumbnail = XmlUtil.getUriAttribute(element, "thumbnail", emptyUri);
+
+    String height = XmlUtil.getAttribute(element, "height");
+    if (height == null) {
+      this.height = 0;
+    } else {
+      this.height = Integer.parseInt(height);
+    }
+    String width = XmlUtil.getAttribute(element, "width");
+    if (width == null) {
+      this.width = 0;
+    } else {
+      this.width = Integer.parseInt(width);
+    }
+    categories = Arrays.asList(
+        XmlUtil.getAttribute(element, "category1", ""),
+        XmlUtil.getAttribute(element, "category2", ""));
+
+    // Child elements
+    PreloadVisitor preloadVisitor = new PreloadVisitor();
+    FeatureVisitor featureVisitor = new FeatureVisitor();
+    IconVisitor iconVisitor = new IconVisitor();
+    LocaleVisitor localeVisitor = new LocaleVisitor(specUrl);
+    Map<String, ElementVisitor> visitors = new HashMap<String, ElementVisitor>();
+    visitors.put("Preload", preloadVisitor);
+    visitors.put("Optional", featureVisitor);
+    visitors.put("Require", featureVisitor);
+    visitors.put("Icon", iconVisitor);
+    visitors.put("Locale", localeVisitor);
+    walk(element, visitors);
+    preloads = Collections.unmodifiableList(preloadVisitor.preloads);
+    features = Collections.unmodifiableMap(featureVisitor.features);
+    icons = Collections.unmodifiableList(iconVisitor.icons);
+    locales = Collections.unmodifiableMap(localeVisitor.locales);
+  }
+
+  /**
+   * Creates an empty module prefs for substitute() to use.
+   */
+  private ModulePrefs(ModulePrefs prefs) {
+    categories = prefs.getCategories();
+    preloads = prefs.getPreloads();
+    features = prefs.getFeatures();
+    locales = prefs.getLocales();
+    height = prefs.getHeight();
+    width = prefs.getWidth();
+  }
+}
+
+interface ElementVisitor {
+  public void visit(Element element) throws SpecParserException;
+}
+
+/**
+ * Processes ModulePrefs.Preload into a list.
+ */
+class PreloadVisitor implements ElementVisitor {
+  final List<URI> preloads = new LinkedList<URI>();
+  public void visit(Element element) throws SpecParserException {
+    URI href = XmlUtil.getUriAttribute(element, "href");
+    if (href == null) {
+      throw new SpecParserException("Preload@href is required.");
+    }
+    preloads.add(href);
+  }
+}
+
+/**
+ * Processes ModulePrefs.Require and ModulePrefs.Optional
+ */
+class FeatureVisitor implements ElementVisitor {
+  final Map<String, Feature> features = new HashMap<String, Feature>();
+  public void visit (Element element) throws SpecParserException {
+    Feature feature = new Feature(element);
+    features.put(feature.getName(), feature);
+  }
+}
+
+/**
+ * Processes ModulePrefs.Icon
+ */
+class IconVisitor implements ElementVisitor {
+  final List<Icon> icons = new LinkedList<Icon>();
+  public void visit(Element element) throws SpecParserException {
+    icons.add(new Icon(element));
+  }
+}
+
+/**
+ * Process ModulePrefs.Locale
+ */
+class LocaleVisitor implements ElementVisitor {
+  final URI base;
+  final Map<Locale, LocaleSpec> locales
+      = new HashMap<Locale, LocaleSpec>();
+  public void visit(Element element) throws SpecParserException {
+    LocaleSpec locale = new LocaleSpec(element, base);
+    locales.put(new Locale(locale.getLanguage(), locale.getCountry()), locale);
+  }
+  public LocaleVisitor(URI base) {
+    this.base = base;
+  }
+}

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.util.XmlException;
+
+/**
+ * Exceptions for Gadget Spec parsing.
+ */
+public class SpecParserException extends GadgetException {
+  /**
+   * @param message
+   */
+  public SpecParserException(String message) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, message);
+  }
+
+  public SpecParserException(XmlException e) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, e);
+  }
+}

Added: incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java
URL: http://svn.apache.org/viewvc/incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java?rev=635862&view=auto
==============================================================================
--- incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java (added)
+++ incubator/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java Tue Mar 11 02:52:52 2008
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.gadgets.Substitutions;
+import org.apache.shindig.util.XmlUtil;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a UserPref tag.
+ */
+public class UserPref {
+  /**
+   * UserPref@name
+   * Message bundles
+   */
+  private String name;
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * UserPref@display_name
+   * Message bundles
+   */
+  private String displayName;
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  /**
+   * UserPref@default_value
+   * Message bundles
+   */
+  private String defaultValue;
+  public String getDefaultValue() {
+    return defaultValue;
+  }
+
+  /**
+   * UserPref@required
+   */
+  private final boolean required;
+  public boolean getRequired() {
+    return required;
+  }
+
+  /**
+   * UserPref@datatype
+   */
+  private final DataType dataType;
+  public DataType getDataType() {
+    return dataType;
+  }
+
+  /**
+   * UserPref.EnumValue
+   * Collapsed so that EnumValue@value is the key and EnumValue@display_value
+   * is the value. If display_value is not present, value will be used.
+   * Message bundles are substituted into display_value, but not value.
+   */
+  private Map<String, String> enumValues;
+  public Map<String, String> getEnumValues() {
+    return enumValues;
+  }
+
+  /**
+   * Performs substitutions on the pref. See field comments for details on what
+   * is substituted.
+   *
+   * @param substituter
+   * @return The substituted pref.
+   */
+  public UserPref substitute(Substitutions substituter) {
+    UserPref pref = new UserPref(this);
+    Substitutions.Type type = Substitutions.Type.MESSAGE;
+    pref.displayName = substituter.substituteString(type, displayName);
+    pref.defaultValue = substituter.substituteString(type, defaultValue);
+    if (enumValues.size() == 0) {
+      pref.enumValues = Collections.emptyMap();
+    } else {
+      Map<String, String> values
+          = new HashMap<String, String>(enumValues.size());
+      for (Map.Entry<String, String> entry  : enumValues.entrySet()) {
+        values.put(entry.getKey(),
+            substituter.substituteString(type, entry.getValue()));
+      }
+      pref.enumValues = Collections.unmodifiableMap(values);
+    }
+    return pref;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<UserPref name=\"")
+       .append(name)
+       .append("\" display_name=\"")
+       .append(displayName)
+       .append("\" default_value=\"")
+       .append(defaultValue)
+       .append("\" required=\"")
+       .append(required)
+       .append("\" datatype=\"")
+       .append(dataType.toString().toLowerCase())
+       .append("\"");
+    if (enumValues.size() == 0) {
+      buf.append("/>");
+    } else {
+      buf.append("\n");
+      for (Map.Entry<String, String> entry : enumValues.entrySet()) {
+        buf.append("<EnumValue value=\"")
+           .append(entry.getKey())
+           .append("\" value=\"")
+           .append("\" display_value=\"")
+           .append(entry.getValue())
+           .append("\"/>\n");
+      }
+      buf.append("</UserPref>");
+    }
+    return buf.toString();
+  }
+
+  /**
+   * @param element
+   * @throws SpecParserException
+   */
+  public UserPref(Element element) throws SpecParserException {
+    String name = XmlUtil.getAttribute(element, "name");
+    if (name == null) {
+      throw new SpecParserException("UserPref@name is required.");
+    }
+    this.name = name;
+
+    displayName = XmlUtil.getAttribute(element, "display_name", name);
+    defaultValue = XmlUtil.getAttribute(element, "default_value", "");
+    required = XmlUtil.getBoolAttribute(element, "required");
+
+    String dataType = XmlUtil.getAttribute(element, "datatype");
+    if (dataType == null) {
+      throw new SpecParserException("UserPref@datatype is required.");
+    }
+    this.dataType = DataType.parse(dataType);
+
+    NodeList children = element.getElementsByTagName("EnumValue");
+    if (children.getLength() > 0) {
+      Map<String, String> enumValues = new HashMap<String, String>();
+      for (int i = 0, j = children.getLength(); i < j; ++i) {
+        Element child = (Element)children.item(i);
+        String value = XmlUtil.getAttribute(child, "value");
+        if (value == null) {
+          throw new SpecParserException("EnumValue@value is required.");
+        }
+        String displayValue
+            = XmlUtil.getAttribute(child, "display_value", value);
+        enumValues.put(value, displayValue);
+      }
+      this.enumValues = Collections.unmodifiableMap(enumValues);
+    } else {
+      this.enumValues = Collections.emptyMap();
+    }
+  }
+
+  /**
+   * Produces a UserPref suitable for substitute()
+   * @param userPref
+   */
+  private UserPref(UserPref userPref) {
+    name = userPref.name;
+    dataType = userPref.dataType;
+    required = userPref.required;
+  }
+
+  /**
+   * Possible values for UserPref@datatype
+   */
+  public static enum DataType {
+    STRING, HIDDEN, BOOL, ENUM, LIST, NUMBER;
+
+    /**
+     * Parses a data type from the input string.
+     *
+     * @param value
+     * @return The data type of the given value.
+     */
+    public static DataType parse(String value) {
+      for (DataType type : DataType.values()) {
+        if (type.toString().compareToIgnoreCase(value) == 0) {
+          return type;
+        }
+      }
+      return STRING;
+    }
+  }
+}