You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@roller.apache.org by ag...@apache.org on 2005/11/28 20:44:39 UTC

svn commit: r349478 [1/2] - in /incubator/roller/trunk: src/org/roller/presentation/ src/org/roller/presentation/cache/ src/org/roller/presentation/filters/ src/org/roller/presentation/pagecache/ src/org/roller/presentation/pagecache/rollercache/ src/o...

Author: agilliland
Date: Mon Nov 28 11:44:28 2005
New Revision: 349478

URL: http://svn.apache.org/viewcvs?rev=349478&view=rev
Log:
new caching code.

- new org.roller.presentation.cache package with new caching implementations
- 4 new caching filters in org.roller.presentation.filters
- a new lightweight ParsedRequest object with 2 subclasses
- deleted old caching code except for a few classes which moved to org.roller.presentation.util
- updated roller.properties with new cache controlling properties



Added:
    incubator/roller/trunk/src/org/roller/presentation/ParsedRequest.java
    incubator/roller/trunk/src/org/roller/presentation/WeblogFeedRequest.java
    incubator/roller/trunk/src/org/roller/presentation/WeblogPageRequest.java
    incubator/roller/trunk/src/org/roller/presentation/cache/
    incubator/roller/trunk/src/org/roller/presentation/cache/Cache.java
    incubator/roller/trunk/src/org/roller/presentation/cache/CacheFactory.java
    incubator/roller/trunk/src/org/roller/presentation/cache/CacheHandler.java
    incubator/roller/trunk/src/org/roller/presentation/cache/CacheManager.java
    incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringCacheEntry.java
    incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheFactoryImpl.java
    incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheImpl.java
    incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheFactoryImpl.java
    incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheImpl.java
    incubator/roller/trunk/src/org/roller/presentation/filters/FeedCacheFilter.java
    incubator/roller/trunk/src/org/roller/presentation/filters/IfModifiedFeedCacheFilter.java
    incubator/roller/trunk/src/org/roller/presentation/filters/MainPageCacheFilter.java
    incubator/roller/trunk/src/org/roller/presentation/filters/WeblogPageCacheFilter.java
    incubator/roller/trunk/src/org/roller/presentation/util/CacheHttpServletResponseWrapper.java
      - copied, changed from r349452, incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/CacheHttpServletResponseWrapper.java
    incubator/roller/trunk/src/org/roller/presentation/util/ResponseContent.java
      - copied, changed from r349452, incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java
    incubator/roller/trunk/src/org/roller/presentation/util/SplitPrintWriter.java
      - copied, changed from r349452, incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java
    incubator/roller/trunk/src/org/roller/presentation/util/SplitServletOutputStream.java
      - copied, changed from r349452, incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java
Removed:
    incubator/roller/trunk/src/org/roller/presentation/pagecache/FilterHandler.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/PageCacheFilter.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/package.html
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/CacheHttpServletResponseWrapper.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler2.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java
    incubator/roller/trunk/src/org/roller/presentation/pagecache/rollercache/package.html
Modified:
    incubator/roller/trunk/web/WEB-INF/classes/roller.properties

Added: incubator/roller/trunk/src/org/roller/presentation/ParsedRequest.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/ParsedRequest.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/ParsedRequest.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/ParsedRequest.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,84 @@
+/*
+ * WeblogRequest.java
+ *
+ * Created on November 7, 2005, 12:29 PM
+ */
+
+package org.roller.presentation;
+
+import java.util.Locale;
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * An abstract class representing any request made to Roller that has been
+ * parsed in order to extract relevant pieces of information from the url.
+ *
+ * NOTE: It is extremely important to mention that this class and all of its
+ * subclasses are meant to be extremely light weight.  Meaning they should
+ * avoid any time consuming operations at all costs, especially operations
+ * which require a trip to the db.  Those operations should be used very, very
+ * sparingly and should only be triggered when it's guaranteed that they are
+ * needed.
+ *
+ * @author Allen Gilliland
+ */
+public abstract class ParsedRequest {
+    
+    HttpServletRequest request = null;
+    
+    private String language = null;
+    private String authenticUser = null;
+    
+    
+    ParsedRequest() {}
+    
+    
+    /**
+     * Parse the given http request and extract any information we can.
+     *
+     * This abstract version of the constructor gathers info likely to be
+     * relevant to all requests to Roller.
+     */
+    public ParsedRequest(HttpServletRequest request) throws Exception {
+        
+        // keep a reference to the original request
+        this.request = request;
+        
+        // login status
+        java.security.Principal prince = request.getUserPrincipal();
+        if(prince != null) {
+            this.authenticUser = prince.getName();
+        }
+        
+    }
+    
+    
+    public String getLanguage() {
+        if(this.language == null) {
+            // determine language
+            // this operation can possibly require a trip to the db, so we only
+            // do it when we know our user needs to know the language
+            Locale locale = LanguageUtil.getViewLocale(request);
+            this.language = locale.getLanguage();
+        }
+
+        return this.language;
+    }
+    
+    
+    public String getAuthenticUser() {
+        return this.authenticUser;
+    }
+    
+    
+    public boolean isLoggedIn() {
+        return (this.authenticUser != null);
+    }
+
+    
+    public HttpServletRequest getRequest() {
+        return this.request;
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/WeblogFeedRequest.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/WeblogFeedRequest.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/WeblogFeedRequest.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/WeblogFeedRequest.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,162 @@
+/*
+ * WeblogFeedRequest.java
+ *
+ * Created on November 7, 2005, 1:59 PM
+ */
+
+package org.roller.presentation;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.RollerException;
+import org.roller.pojos.WeblogTemplate;
+
+
+/**
+ * Represents a request for a Roller weblog feed.
+ * 
+ * any of /rss/*, /atom/*, /flavor/*, or /planetrss
+ *
+ * We use this class as a helper to parse an incoming url and sort out the
+ * information embedded in the url for later use.
+ * 
+ * @author Allen Gilliland
+ */
+public class WeblogFeedRequest extends ParsedRequest {
+    
+    private static Log mLogger = LogFactory.getLog(WeblogFeedRequest.class);
+    
+    private static Set feedServlets = new HashSet();
+    
+    private String context = null;
+    private String flavor = null;
+    private String weblogHandle = null;
+    private String weblogCategory = null;
+    private boolean excerpts = false;
+    
+    
+    static {
+        // initialize our servlet list
+        feedServlets.add("rss");
+        feedServlets.add("flavor");
+        feedServlets.add("atom");
+        feedServlets.add("planetrss");
+    }
+    
+    
+    /**
+     * Construct the WeblogFeedRequest by parsing the incoming url
+     */
+    public WeblogFeedRequest(HttpServletRequest request) throws Exception {
+        
+        super(request);
+        
+        // parse the request object and figure out what we've got
+        mLogger.debug("parsing url "+request.getRequestURL());
+        
+        String servlet = request.getServletPath();
+        String pathInfo = request.getPathInfo();
+        
+        // what servlet is our destination?
+        if(servlet != null) {
+            // strip off the leading slash
+            servlet = servlet.substring(1);
+            
+            if(feedServlets.contains(servlet)) {
+                this.context = "weblog";
+                this.flavor = servlet;
+            } else {
+                // not a request to a feed servlet
+                throw new RollerException("not a weblog feed request, "+request.getRequestURL());
+            }
+        }
+        
+        // parse the path info
+        if(pathInfo != null && pathInfo.trim().length() > 1) {
+            // strip off the leading slash
+            pathInfo = pathInfo.substring(1);
+            String[] pathElements = pathInfo.split("/");
+            
+            if(pathElements[0].length() > 0) {
+                this.weblogHandle = pathElements[0];
+            }
+            
+        } else {
+            
+            // no path info means this was a non-weblog request
+            // we handle a few exceptions for this which include
+            //   /rss - main rss feed
+            //   /planetrss - main planet rss feed
+            //   /atom - main atom feed
+            //   /flavor - main flavor feed
+            if(servlet.equals("rss") || servlet.equals("atom") || 
+                    servlet.equals("flavor")) {
+                
+                this.context = "main";
+            } else if(servlet.equals("planetrss")) {
+                
+                this.context = "planet";
+            }
+        }
+        
+        /* 
+         * parse request parameters
+         *
+         * the only params we currently care about are:
+         *   flavor - defines the feed type
+         *   catname - specifies a weblog category
+         *   path - specifies a weblog category
+         *   excerpts - specifies the feed should only include excerpts
+         *
+         */
+        if(request.getParameter("flavor") != null) {
+            this.flavor = request.getParameter("flavor");
+        }
+        
+        if(request.getParameter("path") != null) {
+            this.weblogCategory = request.getParameter("path");
+        }
+        
+        if(request.getParameter("catname") != null) {
+            this.weblogCategory = request.getParameter("catname");
+        }
+        
+        if(request.getParameter("excerpts") != null) {
+            this.excerpts = Boolean.valueOf(request.getParameter("excerpts")).booleanValue();
+        }
+        
+        // one small final adjustment.
+        // if our flavor is "flavor" then that means someone is just getting
+        // the default flavor, which is rss, so let's set that
+        if(this.flavor.equals("flavor")) {
+            this.flavor = "rss";
+        }
+        
+    }
+    
+
+    public String getContext() {
+        return context;
+    }
+
+    public String getFlavor() {
+        return flavor;
+    }
+
+    public String getWeblogHandle() {
+        return weblogHandle;
+    }
+
+    public String getWeblogCategory() {
+        return weblogCategory;
+    }
+
+    public boolean isExcerpts() {
+        return excerpts;
+    }
+
+}

Added: incubator/roller/trunk/src/org/roller/presentation/WeblogPageRequest.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/WeblogPageRequest.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/WeblogPageRequest.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/WeblogPageRequest.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,205 @@
+/*
+ * WeblogRequest.java
+ *
+ * Created on November 5, 2005, 12:05 PM
+ */
+
+package org.roller.presentation;
+
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.RollerException;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.util.Utilities;
+
+
+/**
+ * Represents a request for a Roller weblog page.
+ * 
+ * any url from ... /page/*
+ * 
+ * We use this class as a helper to parse an incoming url and sort out the
+ * information embedded in the url for later use.
+ * 
+ * @author Allen Gilliland
+ */
+public class WeblogPageRequest extends ParsedRequest {
+    
+    private static Log mLogger = LogFactory.getLog(WeblogPageRequest.class);
+    
+    // various page types
+    public static final String MAIN = "main";
+    public static final String PERMALINK = "permalink";
+    public static final String ARCHIVE = "archive";
+    
+    private String context = null;
+    private String pageType = null;
+    private String weblogHandle = null;
+    private String weblogAnchor = null;
+    private String weblogPage = null;
+    private String weblogCategory = null;
+    private String weblogDate = null;
+    
+    
+    /**
+     * Construct the WeblogPageRequest by parsing the incoming url
+     */
+    public WeblogPageRequest(HttpServletRequest request) throws Exception {
+        
+        // let our parent take care of their business first
+        super(request);
+        
+        // parse the request object and figure out what we've got
+        mLogger.debug("parsing url "+request.getRequestURL());
+        
+        String servlet = request.getServletPath();
+        String pathInfo = request.getPathInfo();
+        
+        // make sure this request was destined for the page servlet
+        if(servlet != null) {
+            // strip off the leading slash
+            servlet = servlet.substring(1);
+            
+            if("page".equals(servlet)) {
+                this.context = "weblog";
+            } else {
+                // not a request to the page servlet
+                throw new RollerException("not a weblog page request, "+request.getRequestURL());
+            }
+        }
+        
+        /*
+         * parse path info
+         *
+         * we expect one of the following forms of urls ...
+         *
+         * [handle] - get default page for user for today's date
+         * [handle]/[date] - get default page for user for specified date
+         * [handle]/[pagelink] - get specified page for today's date
+         * [handle]/[pagelink]/[date] - get specified page for specified date
+         * [handle]/[pagelink]/[anchor] - get specified page & entry (by anchor)
+         * [handle]/[pagelink]/[date]/[anchor] - get specified page & entry (by anchor)
+         */
+        if(pathInfo != null && pathInfo.trim().length() > 1) {
+            // strip off the leading slash
+            pathInfo = pathInfo.substring(1);
+            String[] pathElements = pathInfo.split("/");
+            
+            if ( pathElements.length == 1 ) {
+                
+                // /handle
+                this.weblogHandle = pathElements[0];
+                this.weblogPage = WeblogTemplate.DEFAULT_PAGE;
+                this.pageType = MAIN;
+                
+            } else if ( pathElements.length == 2 ) {
+                
+                // /handle/date or /handle/page
+                this.weblogHandle = pathElements[0];
+                this.weblogPage = WeblogTemplate.DEFAULT_PAGE;
+                
+                if(this.isValidDateString(pathElements[1])) {
+                    this.weblogDate = pathElements[1];
+                    this.pageType = ARCHIVE;
+                } else {
+                    this.weblogPage = pathElements[1];
+                    this.pageType = MAIN;
+                }
+                
+            } else if ( pathElements.length == 3 ) {
+                
+                // /handle/page/date or /handle/page/anchor
+                this.weblogHandle = pathElements[0];
+                this.weblogPage = pathElements[1];
+                
+                if(this.isValidDateString(pathElements[2])) {
+                    this.weblogDate = pathElements[2];
+                    this.pageType = ARCHIVE;
+                } else {
+                    this.weblogAnchor = pathElements[2];
+                    this.pageType = PERMALINK;
+                }
+                
+            } else if ( pathElements.length == 4 ) {
+                
+                // /handle/page/date/anchor
+                this.weblogHandle = pathElements[0];
+                this.weblogPage = pathElements[1];
+                this.weblogDate = pathElements[2];
+                this.weblogAnchor = pathElements[3];
+                this.pageType = PERMALINK;
+            }
+            
+        } else {
+            // invalid request ... path info is empty
+            throw new RollerException("not a weblog page request, "+request.getRequestURL());
+        }
+        
+        
+        /*
+         * parse request parameters
+         *
+         * the only params we currently care about are:
+         *   anchor - specifies a weblog entry
+         *   entry - specifies a weblog entry
+         *   catname - specifies a weblog category
+         */
+        if(request.getParameter("anchor") != null) {
+            this.weblogAnchor = request.getParameter("anchor");
+            this.pageType = PERMALINK;
+        }
+        
+        if(request.getParameter("entry") != null) {
+            this.weblogAnchor = request.getParameter("entry");
+            this.pageType = PERMALINK;
+        }
+        
+        if(request.getParameter("catname") != null) {
+            String cat = request.getParameter("catname");
+            
+            this.weblogCategory = cat;
+            this.pageType = ARCHIVE;
+        }
+
+    }
+    
+    
+    private boolean isValidDateString(String dateString) {
+        return (dateString != null && dateString.length() > 3 && StringUtils.isNumeric(dateString));
+    }
+
+    public String getContext() {
+        return context;
+    }
+
+    public String getWeblogHandle() {
+        return weblogHandle;
+    }
+
+    public String getWeblogAnchor() {
+        return weblogAnchor;
+    }
+
+    public String getWeblogPage() {
+        return weblogPage;
+    }
+
+    public String getWeblogCategory() {
+        return weblogCategory;
+    }
+
+    public String getWeblogDate() {
+        return weblogDate;
+    }
+
+    public String getPageType() {
+        return pageType;
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/Cache.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/Cache.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/Cache.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/Cache.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,61 @@
+/*
+ * RollerCache.java
+ *
+ * Created on September 18, 2005, 10:59 AM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.Map;
+import java.util.Set;
+
+
+/**
+ * Base interface representing a presentation cache in Roller.
+ *
+ * @author Allen Gilliland
+ */
+public interface Cache {
+    
+    /**
+     * put an item in the cache.
+     */
+    public void put(String key, Object value);
+    
+    
+    /**
+     * get an item from the cache.
+     */
+    public Object get(String key);
+    
+    
+    /**
+     * remove an item from the cache.
+     */
+    public void remove(String key);
+    
+    
+    /**
+     * remove a set of items from the cache.
+     */
+    public void remove(Set keys);
+    
+    
+    /**
+     * clear the entire cache.
+     */
+    public void clear();
+    
+    
+    /**
+     * get a list of keys used in the cache.
+     */
+    public Set keySet();
+    
+    
+    /**
+     * get cache stats.
+     */
+    public Map stats();
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/CacheFactory.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/CacheFactory.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/CacheFactory.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/CacheFactory.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,25 @@
+/*
+ * CacheFactory.java
+ *
+ * Created on October 26, 2005, 3:25 PM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.Map;
+
+
+/**
+ * An interface representing a cache factory.  Implementors of this interface
+ * are responsible for providing a method to construct cache implementations.
+ *
+ * In Roller you switch between various caching options by choosing a different
+ * cache factory before starting up the application.
+ *
+ * @author Allen Gilliland
+ */
+public interface CacheFactory {
+    
+    public Cache constructCache(Map properties);
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/CacheHandler.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/CacheHandler.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/CacheHandler.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/CacheHandler.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,53 @@
+/*
+ * CacheHandler.java
+ *
+ * Created on November 5, 2005, 7:33 PM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.Map;
+import org.roller.pojos.BookmarkData;
+import org.roller.pojos.CommentData;
+import org.roller.pojos.FolderData;
+import org.roller.pojos.RefererData;
+import org.roller.pojos.UserData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.pojos.WebsiteData;
+
+
+/**
+ * A class which utilizes a cache.
+ *
+ * The primary purpose of this interface is to force cache handlers to implement
+ * the set of invalidate() methods which server as notifications of changed
+ * objects.  Various caches can determine for themselves how to deal with changes
+ * to each type of object.
+ *
+ * @author Allen Gilliland
+ */
+public interface CacheHandler {
+    
+    public void invalidate(WeblogEntryData entry);
+    
+    public void invalidate(WebsiteData website);
+    
+    public void invalidate(BookmarkData bookmark);
+    
+    public void invalidate(FolderData folder);
+
+    public void invalidate(CommentData comment);
+
+    public void invalidate(RefererData referer);
+
+    public void invalidate(UserData user);
+
+    public void invalidate(WeblogCategoryData category);
+
+    public void invalidate(WeblogTemplate template);
+    
+    public Map getStats();
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/CacheManager.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/CacheManager.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/CacheManager.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/CacheManager.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,311 @@
+/*
+ * CacheManager.java
+ *
+ * Created on September 30, 2005, 4:28 PM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.config.RollerConfig;
+import org.roller.pojos.BookmarkData;
+import org.roller.pojos.CommentData;
+import org.roller.pojos.FolderData;
+import org.roller.pojos.RefererData;
+import org.roller.pojos.UserData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.pojos.WebsiteData;
+
+
+/**
+ * A governing class for Roller cache objects.
+ *
+ * The purpose of the CacheManager is to provide a level of abstraction between
+ * classes that use a cache and the implementations of a cache.  This allows
+ * us to create easily pluggable cache implementations.
+ * 
+ * The other purpose is to provide a single interface for interacting with all
+ * Roller caches at the same time.  This is beneficial because as data
+ * changes in the system we often need to notify all caches that some part of
+ * their cached data needs to be invalidated, and the CacheManager makes that
+ * process easier.
+ *
+ * @author Allen Gilliland
+ */
+public class CacheManager {
+    
+    private static Log mLogger = LogFactory.getLog(CacheManager.class);
+    
+    private static final String DEFAULT_FACTORY = 
+            "org.roller.presentation.cache.ExpiringLRUCacheFactoryImpl";
+    
+    // a reference to the cache factory in use
+    private static CacheFactory mCacheFactory = null;
+    
+    // a list of all cache handlers who have obtained a cache
+    private static Set cacheHandlers = new HashSet();
+    
+    
+    static {
+        // lookup what cache factory we want to use
+        String classname = RollerConfig.getProperty("cache.factory.classname");
+        
+        // use reflection to instantiate our factory class
+        try {
+            Class factoryClass = Class.forName(classname);
+            mCacheFactory = (CacheFactory) factoryClass.newInstance();
+        } catch(ClassCastException cce) {
+            mLogger.error("It appears that your factory does not implement "+
+                    "the CacheFactory interface",cce);
+        } catch(Exception e) {
+            mLogger.error("Unable to instantiate cache factory ["+classname+"]"+
+                    " falling back on default", e);
+        }
+        
+        if(mCacheFactory == null) try {
+            // hmm ... failed to load the specified cache factory
+            // lets try our default
+            Class factoryClass = Class.forName(DEFAULT_FACTORY);
+            mCacheFactory = (CacheFactory) factoryClass.newInstance();
+        } catch(Exception e) {
+            mLogger.fatal("Failed to instantiate a cache factory", e);
+        }
+        
+        mLogger.info("Cache Manager Initialized.");
+        mLogger.info("Default cache factory = "+mCacheFactory.getClass().getName());
+        
+        // finally, add custom handlers
+        String customHandlers = RollerConfig.getProperty("cache.customHandlers");
+        if(customHandlers != null && customHandlers.trim().length() > 0) {
+            
+            String[] cHandlers = customHandlers.split(",");
+            for(int i=0; i < cHandlers.length; i++) {
+                // use reflection to instantiate the handler class
+                try {
+                    Class handlerClass = Class.forName(cHandlers[i]);
+                    CacheHandler customHandler = 
+                            (CacheHandler) handlerClass.newInstance();
+                    
+                    cacheHandlers.add(customHandler);
+                } catch(ClassCastException cce) {
+                    mLogger.error("It appears that your handler does not implement "+
+                            "the CacheHandler interface",cce);
+                } catch(Exception e) {
+                    mLogger.error("Unable to instantiate cache handler ["+cHandlers[i]+"]", e);
+                }
+            }
+        }
+        
+    }
+    
+    
+    // a non-instantiable class
+    private CacheManager() {}
+    
+    
+    /**
+     * Ask the CacheManager to construct a cache.
+     *
+     * Normally the CacheManager will use whatever CacheFactory has been
+     * chosen for the system via the cache.factory.classname property.
+     * However, it is possible to override the use of the default factory by
+     * supplying a cache.factory property to this method.  The value should
+     * be the full classname for the factory you want to use for constructing
+     * the cache.
+     *
+     * example:
+     *   cache.factory -> org.roller.presentation.cache.LRUCacheFactoryImpl
+     *
+     * This allows Roller admins the ability to choose a caching strategy to
+     * use for the whole system, but override it in certain places where they
+     * see fit.  It also allows users to write their own caching modifications
+     * and have them used only by specific caches.
+     */
+    public static Cache constructCache(CacheHandler handler, Map properties) {
+        
+        mLogger.debug("Constructing new cache with props "+properties);
+        
+        Cache cache = null;
+        
+        if(properties != null && properties.containsKey("cache.factory")) {
+            // someone wants a custom cache instance
+            String classname = (String) properties.get("cache.factory");
+            
+            try {
+                // use reflection to instantiate the factory class
+                Class factoryClass = Class.forName(classname);
+                CacheFactory factory = (CacheFactory) factoryClass.newInstance();
+                
+                // now ask for a new cache
+                cache = factory.constructCache(properties);
+            } catch(ClassCastException cce) {
+                mLogger.error("It appears that your factory ["+classname+
+                        "] does not implement the CacheFactory interface",cce);
+            } catch(Exception e) {
+                mLogger.error("Unable to instantiate cache factory ["+classname+
+                        "] falling back on default", e);
+            }
+        }
+        
+        if(cache == null) {
+            // ask our default cache factory for a new cache instance
+            cache = mCacheFactory.constructCache(properties);
+        }
+        
+        // register the handler for this new cache
+        cacheHandlers.add(handler);
+        
+        return cache;
+    }
+    
+    
+    /**
+     * Register a CacheHandler to listen for object invalidations.
+     *
+     * This is here so that it's possible to to add classes which would respond
+     * to object invalidations without necessarily having to create a cache.
+     *
+     * An example would be a handler designed to notify other machines in a 
+     * cluster when an object has been invalidated, or possibly the search
+     * index management classes are interested in knowing when objects are
+     * invalidated.
+     */
+    public static void registerHandler(CacheHandler handler) {
+        
+        mLogger.debug("Registering handler "+handler);
+        
+        cacheHandlers.add(handler);
+    }
+    
+    
+    public static void invalidate(WeblogEntryData entry) {
+        
+        mLogger.debug("invalidating entry = "+entry.getAnchor());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(entry);
+        }
+    }
+    
+    
+    public static void invalidate(WebsiteData website) {
+        
+        mLogger.debug("invalidating website = "+website.getHandle());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(website);
+        }
+    }
+    
+    
+    public static void invalidate(BookmarkData bookmark) {
+        
+        mLogger.debug("invalidating bookmark = "+bookmark.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(bookmark);
+        }
+    }
+    
+    
+    public static void invalidate(FolderData folder) {
+        
+        mLogger.debug("invalidating folder = "+folder.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(folder);
+        }
+    }
+    
+    
+    public static void invalidate(CommentData comment) {
+        
+        mLogger.debug("invalidating comment = "+comment.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(comment);
+        }
+    }
+    
+    
+    public static void invalidate(RefererData referer) {
+        
+        mLogger.debug("invalidating referer = "+referer.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(referer);
+        }
+    }
+    
+    
+    public static void invalidate(UserData user) {
+        
+        mLogger.debug("invalidating user = "+user.getUserName());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(user);
+        }
+    }
+    
+    
+    public static void invalidate(WeblogCategoryData category) {
+        
+        mLogger.debug("invalidating category = "+category.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(category);
+        }
+    }
+    
+    
+    public static void invalidate(WeblogTemplate template) {
+        
+        mLogger.debug("invalidating template = "+template.getId());
+        
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            ((CacheHandler) handlers.next()).invalidate(template);
+        }
+    }
+
+    
+    /**
+     * Compile stats from all registered handlers.
+     *
+     * This is basically a hacky version of instrumentation which is being
+     * thrown in because we don't have a fully instrumentation strategy yet.
+     * This is here with the full expectation that it will be replaced by
+     * something a bit more elaborate, like JMX.
+     */
+    public static Map getStats() {
+        
+        Map allStats = new HashMap();
+        
+        CacheHandler handler = null;
+        Iterator handlers = cacheHandlers.iterator();
+        while(handlers.hasNext()) {
+            handler = (CacheHandler) handlers.next();
+            
+            allStats.put(handler.getClass().getName(), handler.getStats());
+        }
+        
+        return allStats;
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringCacheEntry.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringCacheEntry.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringCacheEntry.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringCacheEntry.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,70 @@
+/*
+ * ExpiringCacheEntry.java
+ *
+ * Created on November 4, 2005, 12:10 PM
+ */
+
+package org.roller.presentation.cache;
+
+/**
+ * A cache entry that expires.
+ *
+ * We use this class to wrap objects being cached and associate a timestamp
+ * and timeout period with them so we can know when they expire.
+ *
+ * @author Allen Gilliland
+ */
+public class ExpiringCacheEntry {
+    
+    private Object value;
+    private long timeCached = -1;
+    private long timeout = 0;
+    
+    
+    public ExpiringCacheEntry(Object value, long timeout) {
+        this.value = value;
+        
+        // make sure that we don't support negative values
+        if(timeout > 0) {
+            this.timeout = timeout;
+        }
+        
+        this.timeCached = System.currentTimeMillis();
+    }
+    
+    
+    public long getTimeCached() {
+        return this.timeCached;
+    }
+    
+    
+    public long getTimeout() {
+        return this.timeout;
+    }
+    
+    
+    /**
+     * Retrieve the value of this cache entry.
+     *
+     * If the value has expired then we return null.
+     */
+    public Object getValue() {
+        if(this.hasExpired()) {
+            return null;
+        } else {
+            return this.value;
+        }
+    }
+    
+    
+    /**
+     * Determine if this cache entry has expired.
+     */
+    public boolean hasExpired() {
+        
+        long now = System.currentTimeMillis();
+        
+        return ((this.timeCached + this.timeout) < now);
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheFactoryImpl.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheFactoryImpl.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheFactoryImpl.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheFactoryImpl.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,56 @@
+/*
+ * LRUCacheFactoryImpl.java
+ *
+ * Created on October 26, 2005, 3:33 PM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * Roller Expiring LRU cache factory.
+ *
+ * @author Allen Gilliland
+ */
+public class ExpiringLRUCacheFactoryImpl implements CacheFactory {
+    
+    private static Log mLogger = 
+            LogFactory.getLog(ExpiringLRUCacheFactoryImpl.class);
+    
+    
+    // protected so only the CacheManager can instantiate us
+    protected ExpiringLRUCacheFactoryImpl() {}
+    
+    
+    /**
+     * Construct a new instance of a Roller Expiring LRUCache.
+     */
+    public Cache constructCache(Map properties) {
+        
+        int size = 100;
+        long timeout = 15 * 60;
+        
+        try {
+            size = Integer.parseInt((String) properties.get("size"));
+        } catch(Exception e) {
+            // ignored
+        }
+        
+        try {
+            timeout = Long.parseLong((String) properties.get("timeout"));
+        } catch(Exception e) {
+            // ignored
+        }
+        
+        Cache cache = new ExpiringLRUCacheImpl(size, timeout);
+        
+        mLogger.debug("new cache constructed. size="+size+", timeout="+timeout);
+        
+        return cache;
+    }
+    
+}
\ No newline at end of file

Added: incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheImpl.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheImpl.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheImpl.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/ExpiringLRUCacheImpl.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,85 @@
+/*
+ * ExpiringLRUCacheImpl.java
+ *
+ * Created on November 6, 2005, 10:33 AM
+ */
+
+package org.roller.presentation.cache;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * An LRU cache where entries expire after a given timeout period.
+ *
+ * @author Allen Gilliland
+ */
+public class ExpiringLRUCacheImpl extends LRUCacheImpl {
+    
+    private static Log mLogger = LogFactory.getLog(ExpiringLRUCacheImpl.class);
+    
+    private long timeout = 0;
+    
+    
+    public ExpiringLRUCacheImpl() {
+        
+        super();
+        this.timeout = 60 * 60 * 1000;
+    }
+    
+    
+    public ExpiringLRUCacheImpl(int maxsize, long timeout) {
+        
+        super(maxsize);
+        
+        // timeout is specified in seconds; only positive values allowed
+        if(timeout > 0) {
+            this.timeout = timeout * 1000;
+        }
+    }
+    
+    
+    /**
+     * Store an entry in the cache.
+     *
+     * We wrap the cached object in our ExpiringCacheEntry object so that we
+     * can track when the entry has expired.
+     */
+    public synchronized void put(String key, Object value) {
+        
+        ExpiringCacheEntry entry = new ExpiringCacheEntry(value, this.timeout);
+        super.put(key, entry);
+    }
+    
+    
+    /**
+     * Retrieve an entry from the cache.
+     *
+     * This LRU cache supports timeouts, so if the cached object has expired
+     * then we return null, just as if the entry wasn't found.
+     */
+    public Object get(String key) {
+        
+        Object value = null;
+        ExpiringCacheEntry entry = null;
+        
+        synchronized(this) {
+            entry = (ExpiringCacheEntry) super.get(key);
+        }
+        
+        if (entry != null) {
+            
+            value = entry.getValue();
+            
+            // if the value is null then that means this entry expired
+            if (value == null) {
+                mLogger.debug("entry expired ["+key+"]");
+                super.remove(key);
+            }
+        }
+        
+        return value;
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheFactoryImpl.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheFactoryImpl.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheFactoryImpl.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheFactoryImpl.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,48 @@
+/*
+ * LRUCacheFactoryImpl.java
+ *
+ * Created on November 6, 2005, 10:48 AM
+ */
+
+package org.roller.presentation.cache;
+
+import java.util.Map;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * Roller LRU Cache factory.
+ *
+ * @author Allen Gilliland
+ */
+public class LRUCacheFactoryImpl implements CacheFactory {
+    
+    private static Log mLogger = LogFactory.getLog(LRUCacheFactoryImpl.class);
+    
+    
+    // protected so that only the CacheManager can instantiate us
+    protected LRUCacheFactoryImpl() {}
+    
+    
+    /**
+     * Construct a new instance of a Roller LRUCache.
+     */
+    public Cache constructCache(Map properties) {
+        
+        int size = 100;
+        
+        try {
+            size = Integer.parseInt((String) properties.get("size"));
+        } catch(Exception e) {
+            // ignored
+        }
+        
+        Cache cache = new LRUCacheImpl(size);
+        
+        mLogger.debug("new cache constructed. size="+size);
+        
+        return cache;
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheImpl.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheImpl.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheImpl.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/cache/LRUCacheImpl.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,104 @@
+/*
+ * LRUCacheImpl.java
+ *
+ * Created on November 6, 2005, 10:33 AM
+ */
+package org.roller.presentation.cache;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * A simple LRU Cache.
+ *
+ * @author Allen Gilliland
+ */
+public class LRUCacheImpl implements Cache {
+    
+    private static Log mLogger = LogFactory.getLog(LRUCacheImpl.class);
+    
+    private Map cache = null;
+    
+    
+    protected LRUCacheImpl() {
+        
+        this.cache = new LRULinkedHashMap(100);
+    }
+    
+    
+    protected LRUCacheImpl(int maxsize) {
+        
+        this.cache = new LRULinkedHashMap(maxsize);
+    }
+    
+    
+    /**
+     * Store an entry in the cache.
+     */
+    public synchronized void put(String key, Object value) {
+        
+        this.cache.put(key, value);
+    }
+    
+    
+    /**
+     * Retrieve an entry from the cache.
+     */
+    public Object get(String key) {
+        
+        return this.cache.get(key);
+    }
+    
+    
+    public synchronized void remove(String key) {
+        
+        this.cache.remove(key);
+    }
+    
+    
+    public synchronized void remove(Set keys) {
+        
+        Iterator it = keys.iterator();
+        while(it.hasNext())
+            this.cache.remove((String) it.next());
+    }
+    
+    
+    public synchronized void clear() {
+        
+        this.cache.clear();
+    }
+    
+    
+    public Set keySet() {
+        return this.cache.keySet();
+    }
+    
+    
+    public Map stats() {
+        
+        return new HashMap();
+    }
+    
+    
+    // David Flanaghan: http://www.davidflanagan.com/blog/000014.html
+    private static class LRULinkedHashMap extends LinkedHashMap {
+        protected int maxsize;
+        
+        public LRULinkedHashMap(int maxsize) {
+            super(maxsize * 4 / 3 + 1, 0.75f, true);
+            this.maxsize = maxsize;
+        }
+        
+        protected boolean removeEldestEntry(Map.Entry eldest) {
+            return this.size() > this.maxsize;
+        }
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/filters/FeedCacheFilter.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/filters/FeedCacheFilter.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/filters/FeedCacheFilter.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/filters/FeedCacheFilter.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,347 @@
+/*
+ * RssCacheFilter.java
+ *
+ * Created on November 5, 2005, 6:32 PM
+ */
+
+package org.roller.presentation.filters;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.config.RollerConfig;
+import org.roller.pojos.BookmarkData;
+import org.roller.pojos.CommentData;
+import org.roller.pojos.FolderData;
+import org.roller.pojos.RefererData;
+import org.roller.pojos.UserData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.pojos.WebsiteData;
+import org.roller.presentation.WeblogFeedRequest;
+import org.roller.presentation.cache.Cache;
+import org.roller.presentation.cache.CacheHandler;
+import org.roller.presentation.cache.CacheManager;
+import org.roller.presentation.util.CacheHttpServletResponseWrapper;
+import org.roller.presentation.util.ResponseContent;
+
+
+/**
+ * A filter used for caching fully rendered xml feeds.
+ *
+ * This filter should only be applied to /rss/*, /atom/*, /flavor/*, /planetrss
+ *
+ * @web.filter name="FeedCacheFilter"
+ *
+ * @author  Allen Gilliland
+ */
+public class FeedCacheFilter implements Filter, CacheHandler {
+    
+    private static Log mLogger = LogFactory.getLog(FeedCacheFilter.class);
+    
+    private Cache mFeedCache = null;
+    
+    // for metrics
+    private double hits = 0;
+    private double misses = 0;
+    private double purges = 0;
+    private Date startTime = new Date();
+    
+    
+    /**
+     * Process filter.
+     */
+    public void doFilter(ServletRequest req,
+                        ServletResponse res,
+                        FilterChain chain)
+            throws IOException, ServletException {
+        
+        mLogger.debug("entering");
+        
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+        
+        WeblogFeedRequest feedRequest = null;
+        try {
+            feedRequest = new WeblogFeedRequest(request);
+        } catch(Exception e) {
+            // some kind of error parsing the request
+            mLogger.error("error creating weblog feed request", e);
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        String key = "feedCache:"+this.generateKey(feedRequest);
+        
+        ResponseContent respContent = (ResponseContent) this.mFeedCache.get(key);
+        if (respContent == null) {
+            
+            mLogger.debug("MISS "+key);
+            this.misses++;
+            
+            try {
+                CacheHttpServletResponseWrapper cacheResponse =
+                        new CacheHttpServletResponseWrapper(response);
+                
+                chain.doFilter(request, cacheResponse);
+                
+                cacheResponse.flushBuffer();
+                
+                // only cache if there wasn't an exception
+                if (request.getAttribute("DisplayException") == null) {
+                    ResponseContent rc = cacheResponse.getContent();
+                    
+                    this.mFeedCache.put(key, rc);
+                } else {
+                    mLogger.error("Display exception "+key);
+                }
+                
+            } catch (java.net.SocketException se) {
+                // ignored
+            } catch (Exception e) {
+                // something unexpected and bad happened
+                mLogger.error("Error rendering feed "+key+" - "+e.getMessage());
+            }
+            
+        } else {
+            
+            mLogger.debug("HIT "+key);
+            this.hits++;
+            
+            try {
+                respContent.writeTo(response);
+            } catch (java.net.SocketException se) {
+                // ignored
+            } catch (Exception e) {
+                mLogger.error("Error with cached response "+key+" - "+e.getMessage());
+            }
+            
+        }
+        
+        mLogger.debug("exiting");
+    }
+    
+    
+    /**
+     * Generate a cache key from a parsed weblog feed request.
+     * This generates a key of the form ...
+     *
+     * <context>[/handle]/<flavor>[/category]/<language>[/excerpts]
+     *
+     * examples ...
+     *
+     * main/rss/en
+     * planet/rss/en
+     * weblog/foo/rss/MyCategory/en
+     * weblog/foo/atom/en/excerpts
+     *
+     */
+    private String generateKey(WeblogFeedRequest feedRequest) {
+        
+        StringBuffer key = new StringBuffer();
+        key.append(feedRequest.getContext());
+        
+        if(feedRequest.getContext().equals("weblog")) {
+            key.append("/").append(feedRequest.getWeblogHandle());
+            key.append("/").append(feedRequest.getFlavor());
+            
+            if(feedRequest.getWeblogCategory() != null) {
+                String cat = feedRequest.getWeblogCategory();
+                if(cat.startsWith("/"))
+                    cat = cat.substring(1).replaceAll("/","_");
+                
+                key.append("/").append(cat);
+            }
+        } else {
+            key.append("/").append(feedRequest.getFlavor());
+        }
+        
+        // add language
+        key.append("/").append(feedRequest.getLanguage());
+        
+        if(feedRequest.isExcerpts()) {
+            key.append("/excerpts");
+        }
+        
+        return key.toString();
+    }
+    
+    
+    /**
+     * A weblog entry has changed.
+     */
+    public void invalidate(WeblogEntryData entry) {
+        this.invalidate(entry.getWebsite());
+    }
+    
+    
+    /**
+     * A weblog has changed.
+     */
+    public void invalidate(WebsiteData website) {
+        
+        mLogger.debug("invalidating website = "+website.getHandle());
+        
+        // we need to remove the following cached items if they exist
+        //   - the main feed
+        //   - the planet feed
+        //   - all weblog feeds
+        
+        Set removeSet = new HashSet();
+        
+        // TODO: it would be nice to be able to do this without iterating 
+        //       over the entire cache key set
+        String key = null;
+        Iterator allKeys = this.mFeedCache.keySet().iterator();
+        while(allKeys.hasNext()) {
+            key = (String) allKeys.next();
+            
+            if(key.startsWith("feedCache:main")) {
+                removeSet.add(key);
+            } else if(key.startsWith("feedCache:planet")) {
+                removeSet.add(key);
+            } else if(key.startsWith("feedCache:weblog/"+website.getHandle())) {
+                removeSet.add(key);
+            }
+        }
+        
+        this.mFeedCache.remove(removeSet);
+    }
+    
+    
+    /**
+     * A bookmark has changed.
+     */
+    public void invalidate(BookmarkData bookmark) {
+        // ignored
+    }
+    
+    
+    /**
+     * A folder has changed.
+     */
+    public void invalidate(FolderData folder) {
+        // ignored
+    }
+    
+    
+    /**
+     * A comment has changed.
+     */
+    public void invalidate(CommentData comment) {
+        // ignored
+    }
+    
+    
+    /**
+     * A referer has changed.
+     */
+    public void invalidate(RefererData referer) {
+        // ignored
+    }
+    
+    
+    /**
+     * A user profile has changed.
+     */
+    public void invalidate(UserData user) {
+        // ignored
+    }
+    
+    
+    /**
+     * A category has changed.
+     */
+    public void invalidate(WeblogCategoryData category) {
+        this.invalidate(category.getWebsite());
+    }
+    
+    
+    /**
+     * A weblog template has changed.
+     */
+    public void invalidate(WeblogTemplate template) {
+        // ignored
+    }
+    
+    
+    public Map getStats() {
+        
+        Map stats = new HashMap();
+        stats.put("startTime", this.startTime);
+        stats.put("hits", new Double(this.hits));
+        stats.put("misses", new Double(this.misses));
+        stats.put("purges", new Double(this.purges));
+        
+        // calculate efficiency
+        if((misses - purges) > 0) {
+            double efficiency = hits / (misses + hits);
+            stats.put("efficiency", new Double(efficiency * 100));
+        }
+        
+        return stats;
+    }
+    
+    
+    /**
+     * Destroy method for this filter
+     */
+    public void destroy() {
+    }
+    
+    
+    /**
+     * Init method for this filter
+     */
+    public void init(FilterConfig filterConfig) {
+        
+        mLogger.info("Initializing feed cache");
+        
+        String factory = RollerConfig.getProperty("cache.feed.factory");
+        String size = RollerConfig.getProperty("cache.feed.size");
+        String timeout = RollerConfig.getProperty("cache.feed.timeout");
+        
+        int cacheSize = 100;
+        try {
+            cacheSize = Integer.parseInt(size);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache size ["+size+"], using default");
+        }
+        
+        long cacheTimeout = 30 * 60;
+        try {
+            cacheTimeout = Long.parseLong(timeout);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache timeout ["+timeout+
+                    "], using default");
+        }
+        
+        
+        Map props = new HashMap();
+        props.put("timeout", ""+cacheTimeout);
+        props.put("size", ""+cacheSize);
+        
+        if(factory != null && factory.trim().length() > 0)
+            props.put("cache.factory", factory);
+        
+        mLogger.info(props);
+        
+        mFeedCache = CacheManager.constructCache(this, props);
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/filters/IfModifiedFeedCacheFilter.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/filters/IfModifiedFeedCacheFilter.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/filters/IfModifiedFeedCacheFilter.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/filters/IfModifiedFeedCacheFilter.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,347 @@
+/*
+ * IfModifiedFeedCacheFilter.java
+ *
+ * Created on November 9, 2005, 2:47 PM
+ */
+
+package org.roller.presentation.filters;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.RollerException;
+import org.roller.config.RollerConfig;
+import org.roller.model.Roller;
+import org.roller.model.RollerFactory;
+import org.roller.model.UserManager;
+import org.roller.model.WeblogManager;
+import org.roller.pojos.BookmarkData;
+import org.roller.pojos.CommentData;
+import org.roller.pojos.FolderData;
+import org.roller.pojos.RefererData;
+import org.roller.pojos.UserData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.pojos.WebsiteData;
+import org.roller.presentation.WeblogFeedRequest;
+import org.roller.presentation.cache.Cache;
+import org.roller.presentation.cache.CacheHandler;
+import org.roller.presentation.cache.CacheManager;
+
+
+/**
+ * A filter used for caching last modified dates.
+ *
+ * This may be applied to /rss/*, /atom/*, /flavor/*, and /planetrss
+ * 
+ * @web.filter name="IfModifiedFeedCacheFilter"
+ *
+ * @author Allen Gilliland
+ */
+public class IfModifiedFeedCacheFilter implements Filter, CacheHandler {
+    
+    private static Log mLogger = 
+            LogFactory.getLog(IfModifiedFeedCacheFilter.class);
+    
+    private Cache mCache = null;
+    
+    SimpleDateFormat dateFormatter =
+            new SimpleDateFormat("EEE MMM d HH:mm:ss z yyyy");
+    
+    
+    /**
+     * Process filter.
+     */
+    public void doFilter(ServletRequest req, 
+                        ServletResponse res, 
+                        FilterChain chain)
+            throws IOException, ServletException {
+        
+        mLogger.debug("entering");
+        
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+        
+        WeblogFeedRequest feedRequest = null;
+        try {
+            feedRequest = new WeblogFeedRequest(request);
+        } catch(Exception e) {
+            mLogger.error("error creating feed request", e);
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        String key = "ifmod:"+this.generateKey(feedRequest);
+        
+        Date updateTime = null;
+        try {
+            updateTime = (Date) this.mCache.get(key);
+            
+            if (updateTime == null) {
+                mLogger.debug("MISS "+key);
+                
+                if(feedRequest.getWeblogHandle() != null) {
+                    Roller roller = RollerFactory.getRoller();
+                    UserManager umgr = roller.getUserManager();
+                    WeblogManager wmgr = roller.getWeblogManager();
+                    
+                    updateTime = wmgr.getWeblogLastPublishTime(
+                            umgr.getWebsiteByHandle(feedRequest.getWeblogHandle()),
+                            feedRequest.getWeblogCategory());
+                    
+                    this.mCache.put(key, updateTime);
+                }
+                
+            } else {
+                mLogger.debug("HIT "+key);
+            }
+            
+            // Check the incoming if-modified-since header
+            Date sinceDate =
+                    new Date(request.getDateHeader("If-Modified-Since"));
+            
+            if (updateTime != null) {
+                // convert date (JDK 1.5 workaround)
+                String date = dateFormatter.format(updateTime);
+                updateTime = new Date(date);
+                if (updateTime.compareTo(sinceDate) <= 0) {
+                    mLogger.debug("NOT_MODIFIED "+key);
+                    response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+                    return;
+                }
+            }
+            
+        } catch (RollerException e) {
+            // Thrown by getLastPublishedDate if there is a db-type error
+            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+            return;
+        } catch (IllegalArgumentException e) {
+            // Thrown by getDateHeader if not in valid format. This can be
+            // safely ignored, the only consequence is that the NOT MODIFIED
+            // response is not set.
+        }
+        
+        // Set outgoing last modified header
+        if (updateTime != null) {
+            response.setDateHeader("Last-Modified", updateTime.getTime());
+        }
+        
+        chain.doFilter(request, response);
+        
+        mLogger.debug("exiting");
+    }
+    
+    
+    /**
+     * Generate a cache key from a parsed weblog feed request.
+     * This generates a key of the form ...
+     *
+     * <context>/[handle]/<flavor>/[category]/<language>/[excerpts]
+     *
+     * examples ...
+     *
+     * main/rss/en
+     * planet/rss/en
+     * weblog/foo/rss/MyCategory/en
+     * weblog/foo/atom/en/excerpts
+     *
+     */
+    private String generateKey(WeblogFeedRequest feedRequest) {
+        
+        StringBuffer key = new StringBuffer();
+        key.append(feedRequest.getContext());
+        
+        if(feedRequest.getContext().equals("weblog")) {
+            key.append("/").append(feedRequest.getWeblogHandle());
+            key.append("/").append(feedRequest.getFlavor());
+            
+            if(feedRequest.getWeblogCategory() != null) {
+                String cat = feedRequest.getWeblogCategory();
+                if(cat.startsWith("/"))
+                    cat = cat.substring(1).replaceAll("/","_");
+                
+                key.append("/").append(cat);
+            }
+        } else {
+            key.append("/").append(feedRequest.getFlavor());
+        }
+        
+        // add language
+        key.append("/").append(feedRequest.getLanguage());
+        
+        if(feedRequest.isExcerpts()) {
+            key.append("/excerpts");
+        }
+        
+        return key.toString();
+    }
+    
+    
+    /**
+     * A weblog entry has changed.
+     */
+    public void invalidate(WeblogEntryData entry) {
+        this.invalidate(entry.getWebsite());
+    }
+    
+    
+    /**
+     * A weblog has changed.
+     */
+    public void invalidate(WebsiteData website) {
+        
+        mLogger.debug("invalidating website = "+website.getHandle());
+        
+        // we need to remove the following cached items if they exist
+        //   - the main feed
+        //   - the planet feed
+        //   - all weblog feeds
+        
+        Set removeSet = new HashSet();
+        
+        // TODO: it would be nice to be able to do this without iterating 
+        //       over the entire cache key set
+        String key = null;
+        Iterator allKeys = this.mCache.keySet().iterator();
+        while(allKeys.hasNext()) {
+            key = (String) allKeys.next();
+            
+            if(key.startsWith("ifmod:main")) {
+                removeSet.add(key);
+            } else if(key.startsWith("ifmod:planet")) {
+                removeSet.add(key);
+            } else if(key.startsWith("ifmod:weblog/"+website.getHandle())) {
+                removeSet.add(key);
+            }
+        }
+        
+        this.mCache.remove(removeSet);
+    }
+    
+    
+    /**
+     * A bookmark has changed.
+     */
+    public void invalidate(BookmarkData bookmark) {
+        // ignored
+    }
+    
+    
+    /**
+     * A folder has changed.
+     */
+    public void invalidate(FolderData folder) {
+        // ignored
+    }
+    
+    
+    /**
+     * A comment has changed.
+     */
+    public void invalidate(CommentData comment) {
+        // ignored
+    }
+    
+    
+    /**
+     * A referer has changed.
+     */
+    public void invalidate(RefererData referer) {
+        // ignored
+    }
+    
+    
+    /**
+     * A user profile has changed.
+     */
+    public void invalidate(UserData user) {
+        // ignored
+    }
+    
+    
+    /**
+     * A category has changed.
+     */
+    public void invalidate(WeblogCategoryData category) {
+        this.invalidate(category.getWebsite());
+    }
+    
+    
+    /**
+     * A weblog template has changed.
+     */
+    public void invalidate(WeblogTemplate template) {
+        // ignored
+    }
+    
+    
+    public Map getStats() {
+        
+        Map stats = new HashMap();
+        
+        return stats;
+    }
+    
+    
+    /**
+     * Destroy method for this filter
+     */
+    public void destroy() {
+    }
+    
+    
+    /**
+     * Init method for this filter
+     */
+    public void init(FilterConfig filterConfig) {
+        
+        mLogger.info("Initializing if-modified cache");
+        
+        String factory = RollerConfig.getProperty("cache.feed.ifmodified.factory");
+        String size = RollerConfig.getProperty("cache.feed.ifmodified.size");
+        String timeout = RollerConfig.getProperty("cache.feed.ifmodified.timeout");
+        
+        int cacheSize = 100;
+        try {
+            cacheSize = Integer.parseInt(size);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache size ["+size+"], using default");
+        }
+        
+        long cacheTimeout = 30 * 60;
+        try {
+            cacheTimeout = Long.parseLong(timeout);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache timeout ["+timeout+"], using default");
+        }
+        
+        
+        Map props = new HashMap();
+        props.put("timeout", ""+cacheTimeout);
+        props.put("size", ""+cacheSize);
+        
+        if(factory != null && factory.trim().length() > 0)
+            props.put("cache.factory", factory);
+        
+        mLogger.info(props);
+        
+        mCache = CacheManager.constructCache(this, props);
+    }
+    
+}

Added: incubator/roller/trunk/src/org/roller/presentation/filters/MainPageCacheFilter.java
URL: http://svn.apache.org/viewcvs/incubator/roller/trunk/src/org/roller/presentation/filters/MainPageCacheFilter.java?rev=349478&view=auto
==============================================================================
--- incubator/roller/trunk/src/org/roller/presentation/filters/MainPageCacheFilter.java (added)
+++ incubator/roller/trunk/src/org/roller/presentation/filters/MainPageCacheFilter.java Mon Nov 28 11:44:28 2005
@@ -0,0 +1,304 @@
+/*
+ * MainPageCacheFilter.java
+ *
+ * Created on November 7, 2005, 2:32 PM
+ */
+package org.roller.presentation.filters;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.config.RollerConfig;
+import org.roller.pojos.BookmarkData;
+import org.roller.pojos.CommentData;
+import org.roller.pojos.FolderData;
+import org.roller.pojos.RefererData;
+import org.roller.pojos.UserData;
+import org.roller.pojos.WeblogCategoryData;
+import org.roller.pojos.WeblogEntryData;
+import org.roller.pojos.WeblogTemplate;
+import org.roller.pojos.WebsiteData;
+import org.roller.presentation.LanguageUtil;
+import org.roller.presentation.cache.Cache;
+import org.roller.presentation.cache.CacheHandler;
+import org.roller.presentation.cache.CacheManager;
+import org.roller.presentation.util.CacheHttpServletResponseWrapper;
+import org.roller.presentation.util.ResponseContent;
+
+
+/**
+ * A filter used for caching the fully rendered pages ... /main.do, /planet.do
+ *
+ * @web.filter name="MainPageCacheFilter"
+ *
+ * @author  Allen Gilliland
+ */
+public class MainPageCacheFilter implements Filter, CacheHandler {
+    
+    private static Log mLogger = LogFactory.getLog(MainPageCacheFilter.class);
+    
+    private boolean excludeOwnerPages = false;
+    private Cache mPageCache = null;
+    
+    // for metrics
+    private double hits = 0;
+    private double misses = 0;
+    private double purges = 0;
+    private Date startTime = new Date();
+    
+    
+    /**
+     * Process filter.
+     */
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
+            throws IOException, ServletException {
+        
+        mLogger.debug("entering");
+        
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+        
+        String key = null;
+        Principal prince = null;
+        
+        String servlet = request.getServletPath();
+        if(servlet.equals("/main.do")) {
+            key = "main/page";
+        } else if(servlet.equals("/planet.do")) {
+            key = "planet/page";
+        } else {
+            // not a main page request
+            mLogger.warn("not a main page "+servlet);
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        try {
+            // determine language
+            Locale locale = LanguageUtil.getViewLocale(request);
+            key += "/" + locale.getLanguage();
+            
+            // login status
+            prince = request.getUserPrincipal();
+            if(prince != null) {
+                key += "/user=" + prince.getName();
+            }
+            
+        } catch(Exception e) {
+            // problem getting language?
+            mLogger.error("problem parsing request", e);
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        
+        ResponseContent respContent = null;
+        if(!this.excludeOwnerPages || prince == null) {
+            respContent = (ResponseContent) this.mPageCache.get(key);
+        }
+        
+        if (respContent == null) {
+            
+            mLogger.debug("MISS "+key);
+            this.misses++;
+            
+            try {
+                CacheHttpServletResponseWrapper cacheResponse =
+                        new CacheHttpServletResponseWrapper(response);
+                
+                chain.doFilter(request, cacheResponse);
+                
+                cacheResponse.flushBuffer();
+                
+                // only cache if we didn't get an exception
+                if (request.getAttribute("DisplayException") == null) {
+                    ResponseContent rc = cacheResponse.getContent();
+                    
+                    // only cache if this is not a logged in user?
+                    if(!this.excludeOwnerPages || prince == null) {
+                        this.mPageCache.put(key, rc);
+                    } else {
+                        mLogger.debug("SKIPPED "+key);
+                    }
+                } else {
+                    mLogger.error("Display exception "+key);
+                }
+                
+            } catch (java.net.SocketException se) {
+                // ignored
+            } catch (Exception e) {
+                // something unexpected and bad happened
+                mLogger.error("Error rendering page "+key, e);
+            }
+            
+        } else {
+            
+            mLogger.debug("HIT "+key);
+            this.hits++;
+            
+            try {
+                respContent.writeTo(response);
+            } catch (java.net.SocketException se) {
+                // ignored
+            } catch (Exception e) {
+                mLogger.error("Error with cached response "+key, e);
+            }
+            
+        }
+        
+        mLogger.debug("exiting");
+    }
+    
+    
+    /**
+     * A weblog entry has changed.
+     */
+    public void invalidate(WeblogEntryData entry) {
+        this.mPageCache.clear();
+    }
+    
+    
+    /**
+     * A weblog has changed.
+     */
+    public void invalidate(WebsiteData website) {
+        this.mPageCache.clear();
+    }
+    
+    
+    /**
+     * A bookmark has changed.
+     */
+    public void invalidate(BookmarkData bookmark) {
+        // ignored
+    }
+    
+    
+    /**
+     * A folder has changed.
+     */
+    public void invalidate(FolderData folder) {
+        // ignored
+    }
+    
+    
+    /**
+     * A comment has changed.
+     */
+    public void invalidate(CommentData comment) {
+        // ignored
+    }
+    
+    
+    /**
+     * A referer has changed.
+     */
+    public void invalidate(RefererData referer) {
+        // ignored
+    }
+    
+    
+    /**
+     * A user profile has changed.
+     */
+    public void invalidate(UserData user) {
+        // ignored
+    }
+    
+    
+    /**
+     * A category has changed.
+     */
+    public void invalidate(WeblogCategoryData category) {
+        // ignored
+    }
+    
+    
+    /**
+     * A weblog template has changed.
+     */
+    public void invalidate(WeblogTemplate template) {
+        // ignored
+    }
+    
+    
+    public Map getStats() {
+        
+        Map stats = new HashMap();
+        stats.put("startTime", this.startTime);
+        stats.put("hits", new Double(this.hits));
+        stats.put("misses", new Double(this.misses));
+        stats.put("purges", new Double(this.purges));
+        
+        // calculate efficiency
+        if((misses - purges) > 0) {
+            double efficiency = hits / (misses + hits);
+            stats.put("efficiency", new Double(efficiency * 100));
+        }
+        
+        return stats;
+    }
+    
+    
+    /**
+     * Destroy method for this filter
+     */
+    public void destroy() {
+    }
+    
+    
+    /**
+     * Init method for this filter
+     */
+    public void init(FilterConfig filterConfig) {
+        
+        mLogger.info("Initializing main page cache");
+        
+        String factory = RollerConfig.getProperty("cache.mainpage.factory");
+        String size = RollerConfig.getProperty("cache.mainpage.size");
+        String timeout = RollerConfig.getProperty("cache.mainpage.timeout");
+        this.excludeOwnerPages = 
+                RollerConfig.getBooleanProperty("cache.mainpage.excludeOwnerEditPages");
+        
+        int cacheSize = 20;
+        try {
+            cacheSize = Integer.parseInt(size);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache size ["+size+"], using default");
+        }
+        
+        long cacheTimeout = 30 * 60;
+        try {
+            cacheTimeout = Long.parseLong(timeout);
+        } catch (Exception e) {
+            mLogger.warn("Invalid cache timeout ["+timeout+
+                    "], using default");
+        }
+        
+        
+        Map props = new HashMap();
+        props.put("timeout", ""+cacheTimeout);
+        props.put("size", ""+cacheSize);
+        
+        if(factory != null && factory.trim().length() > 0)
+            props.put("cache.factory", factory);
+        
+        mLogger.info(props);
+        
+        mPageCache = CacheManager.constructCache(this, props);
+    }
+    
+}