You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@roller.apache.org by sn...@apache.org on 2005/10/21 23:46:28 UTC

svn commit: r327589 [32/72] - in /incubator/roller/branches/roller_1.x: ./ contrib/ contrib/lib/ contrib/plugins/ contrib/plugins/src/ contrib/plugins/src/org/ contrib/plugins/src/org/roller/ contrib/plugins/src/org/roller/presentation/ contrib/plugins...

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,470 @@
+/*
+ * Created on Jun 15, 2004
+ */
+package org.roller.presentation.pagecache.rollercache;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.pojos.UserData;
+import org.roller.presentation.LanguageUtil;
+import org.roller.presentation.pagecache.FilterHandler;
+import org.roller.util.LRUCache;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.TreeMap;
+
+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.roller.config.RollerConfig;
+
+/**
+ * Page cache implementation that uses a simple LRUCache. Can be configured 
+ * using filter configuration parameters:
+ * <ul>
+ * <li><b>size</b>: number of pages to keep in cache. Once cache reaches
+ * this size, each new cache entry will push out the LRU cache entry.</li>
+ * 
+ * <li><b>timeoutInterval</b>: interval to timeout pages in seconds
+ * (default is 1800 seconds). Sites with a large number of users, and thus a 
+ * lot of cache churn which makes this check unnecessary, may wish to  set this
+ *  to -1 to disable timeout checking.</li>
+ * 
+ * <li><b>timeoutRatio</b>: portion of old pages to expire on timeout 
+ * interval where 1.0 is 100% (default is 1.0). This only applies if the 
+ * timeoutInterval is set to something other than -1.</li>
+ * </ul> 
+ * @author David M Johnson
+ */
+public class LRUCacheHandler implements FilterHandler
+{
+    private static Log mLogger = 
+        LogFactory.getFactory().getInstance(LRUCacheHandler.class);
+
+    private Map mPageCache = null;
+    
+    private String mName = null;
+    
+    /** Timeout interval: how often to run timeout task (default 30 mintes) */
+    private long mTimeoutInterval = 30 * 60 * 1000; // milliseconds
+    
+    /** Timeout ratio: % of cache to expire on each timeout (default 1.0) */
+    private float mTimeoutRatio = 1.0F;
+    
+    // Statistics
+    private int misses = 0;
+    private int hits = 0;
+    
+    private final static String FILE_SEPARATOR = "/";
+    private final static char FILE_SEPARATOR_CHAR = FILE_SEPARATOR.charAt(0);
+    private final static short AVERAGE_KEY_LENGTH = 30;
+    private static final String m_strBase64Chars = 
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    
+
+    public LRUCacheHandler(FilterConfig config)
+    {      
+        mName = config.getFilterName();
+        mLogger.info("Initializing for: " + mName);
+        
+        String cacheSize = RollerConfig.getProperty("cache.filter.page.size");
+        String cacheTimeout = RollerConfig.getProperty("cache.filter.page.timeout");
+        
+        int size = 200;
+        try
+        {
+            size = Integer.parseInt(cacheSize);
+        }
+        catch (Exception e)
+        {
+            mLogger.warn(config.getFilterName() 
+                + "Can't read cache size parameter, using default...");
+        }
+        mLogger.info(mName + " size=" + size);
+        mPageCache = Collections.synchronizedMap(new LRUCache(size));
+
+        long intervalSeconds = mTimeoutInterval / 1000L;
+        try
+        {
+            mTimeoutInterval = 1000L * Long.parseLong(cacheTimeout);
+            
+            if (mTimeoutInterval == -1)
+            {
+                mLogger.info(config.getFilterName() 
+                   + "timeoutInterval of -1: timeouts are disabled");
+            }
+            else if (mTimeoutInterval < (30 * 1000))
+            {
+                mTimeoutInterval = 30 * 1000;
+                mLogger.warn(config.getFilterName() 
+                   + "timeoutInterval cannot be less than 30 seconds");
+            }
+        }
+        catch (Exception e)
+        {
+            mLogger.warn(config.getFilterName() 
+                + "Can't read timeoutInterval parameter, disabling timeout.");
+            mTimeoutInterval = -1;
+        }
+        mLogger.info(mName + " timeoutInterval=" + intervalSeconds);
+        
+        try
+        {
+            mTimeoutRatio = Float.parseFloat(
+                config.getInitParameter("timeoutRatio"));
+        }
+        catch (Exception e)
+        {
+            mLogger.warn(config.getFilterName() 
+                + "Can't read timeoutRatio parameter, using default...");
+        }
+        mLogger.info(mName + " timeoutRatio=" + mTimeoutRatio);
+        
+        if (mTimeoutInterval != -1 && mTimeoutRatio != 0.0)
+        {
+            Timer timer = new Timer();            
+            timer.scheduleAtFixedRate(new TimerTask() {
+                public void run() {
+                    timeoutCache();
+                }
+            }, mTimeoutInterval, mTimeoutInterval);        
+        }
+    }
+    
+    /**
+     * @see org.roller.presentation.pagecache.FilterHandler#destroy()
+     */
+    public void destroy()
+    {
+    }
+
+    /**
+     * @see org.roller.presentation.pagecache.FilterHandler#doFilter(
+     *      javax.servlet.ServletRequest, javax.servlet.ServletResponse,
+     *      javax.servlet.FilterChain)
+     */
+    public void doFilter(ServletRequest req, ServletResponse res,
+                    FilterChain chain) throws ServletException, IOException
+    {
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+
+        // get locale
+        Locale locale = LanguageUtil.getViewLocale(request);
+        
+        // generate language-sensitive cache-key
+        String generatedKey = null;
+        if (locale != null)
+        {
+            generatedKey = generateEntryKey(null, 
+                      request, 1, locale.getLanguage());
+        }
+        else
+        {
+            generatedKey = generateEntryKey(null, 
+                      request, 1, null);
+        }
+        
+        // Add authenticated user name, if there is one, to cache key
+        java.security.Principal prince = request.getUserPrincipal();        
+        StringBuffer keyb = new StringBuffer();
+        keyb.append(generatedKey);
+        if (prince != null)
+        {
+            keyb.append("_");
+            keyb.append(prince);
+        }
+        String key = keyb.toString();
+                
+        ResponseContent respContent = (ResponseContent)getFromCache(key);
+        if (respContent == null) 
+        {
+            try
+            {
+                CacheHttpServletResponseWrapper cacheResponse = 
+                    new CacheHttpServletResponseWrapper(response);
+                chain.doFilter(request, cacheResponse);
+                
+                // Store as the cache content the result of the response
+                // if no exception was noted by content generator.
+                if (request.getAttribute("DisplayException") == null)
+                {
+                    ResponseContent rc = cacheResponse.getContent();
+                    putToCache(key, rc);
+                }
+                else
+                {
+                    StringBuffer sb = new StringBuffer();
+                    sb.append("Display exception, cache, key=");
+                    sb.append(key);
+                    mLogger.error(sb.toString());
+                }
+            }
+            catch (java.net.SocketException se)
+            {
+                // ignore socket exceptions
+            }
+            catch (Exception e)
+            {
+                // something unexpected and bad happened
+                StringBuffer sb = new StringBuffer();
+                sb.append("Error rendering page, key=");
+                sb.append(key);
+                mLogger.error(sb.toString());
+            }           
+        }
+        else
+        {
+            try
+            {
+                respContent.writeTo(response);
+            }
+            catch (java.net.SocketException se)
+            {
+                // ignore socket exceptions
+            }
+            catch (Exception e)
+            {
+                if (mLogger.isDebugEnabled())
+                {
+                    StringBuffer sb = new StringBuffer();
+                    sb.append("Probably a client abort exception, key=");
+                    sb.append(key);
+                    mLogger.error(sb.toString());
+                }
+            }
+            
+        }
+    }
+
+    /**
+     * Purge entire cache.
+     */
+    public synchronized void flushCache(HttpServletRequest req)
+    {
+        mPageCache.clear();
+    }
+
+    /**
+     * Purge user's entries from cache.
+     */
+    public synchronized void removeFromCache(HttpServletRequest req, UserData user)
+    {
+        // TODO: can we make this a little more precise, perhaps via regex?
+        String rssString = "/rss/" + user.getUserName(); // user's pages
+        String pageString = "/page/" + user.getUserName(); // user's RSS feeds
+        String mainRssString = "/rss_"; // main RSS feed
+        List purgeList = new ArrayList();
+        
+        Iterator keys = mPageCache.keySet().iterator();
+        while (keys.hasNext())
+        {
+            String key = (String) keys.next();
+            
+            if (key.indexOf(rssString)!=-1 || key.indexOf(pageString)!=-1 || key.indexOf(mainRssString)!=-1) 
+            {
+                purgeList.add(key);
+            }
+        }
+        
+        Iterator purgeIter = purgeList.iterator();
+        while (purgeIter.hasNext())
+        {
+            String key = (String) purgeIter.next();
+            mPageCache.remove(key);
+        }
+        
+        if (mLogger.isDebugEnabled())
+        {
+            StringBuffer sb = new StringBuffer();
+            sb.append("Purged, count=");
+            sb.append(purgeList.size());
+            sb.append(", user=");
+            sb.append(user.getUserName());
+            mLogger.debug(sb.toString());
+        }        
+    }
+    
+    public synchronized void timeoutCache() 
+    {
+        if (mTimeoutRatio == 1.0)
+        {
+            mLogger.debug("Timing out whole cache: " + mName);
+            mPageCache.clear();   
+        }
+        else 
+        {
+            int numToTimeout = (int)(mTimeoutRatio * mPageCache.size());
+            mLogger.debug(
+                "Timing out " + numToTimeout + " of " + mPageCache.size()
+                + " entries from cache: " + mName);
+            ArrayList allKeys = new ArrayList(mPageCache.keySet());
+            for (int i=numToTimeout; i>0; i--) 
+            {
+                mPageCache.remove(allKeys.get(i));
+            }
+        }
+    }
+    
+    /** 
+     * Get from cache. Synchronized because "In access-ordered linked hash 
+     * maps, merely querying the map with get is a structural modification" 
+     */
+    public synchronized Object getFromCache(String key) 
+    {
+        Object entry = mPageCache.get(key);
+        
+        if (entry != null && mLogger.isDebugEnabled())
+        {
+            hits++;
+        }
+        return entry;
+    }
+
+    public synchronized void putToCache(String key, Object entry) 
+    {
+        mPageCache.put(key, entry);
+        if (mLogger.isDebugEnabled())
+        {
+            misses++;
+            
+            StringBuffer sb = new StringBuffer();
+            sb.append("Missed, cache size=");
+            sb.append(mPageCache.size());
+            sb.append(", hits=");
+            sb.append(hits);
+            sb.append(", misses=");
+            sb.append(misses);
+            sb.append(", key=");
+            sb.append(key);
+            mLogger.debug(sb.toString());
+        }
+    }
+
+    public String generateEntryKey(String key, 
+                       HttpServletRequest request, int scope, String language)
+    {
+        StringBuffer cBuffer = new StringBuffer(AVERAGE_KEY_LENGTH);
+        // Append the language if available
+        if (language != null)
+        {
+            cBuffer.append(FILE_SEPARATOR).append(language);
+        }
+        
+        //cBuffer.append(FILE_SEPARATOR).append(request.getServerName());
+        
+        if (key != null)
+        {
+            cBuffer.append(FILE_SEPARATOR).append(key);
+        }
+        else
+        {
+            String generatedKey = request.getRequestURI();
+            if (generatedKey.charAt(0) != FILE_SEPARATOR_CHAR)
+            {
+                cBuffer.append(FILE_SEPARATOR_CHAR);
+            }
+            cBuffer.append(generatedKey);
+            cBuffer.append("_").append(request.getMethod()).append("_");
+            generatedKey = getSortedQueryString(request);
+            if (generatedKey != null)
+            {
+                try
+                {
+                    java.security.MessageDigest digest = 
+                        java.security.MessageDigest.getInstance("MD5");
+                    byte[] b = digest.digest(generatedKey.getBytes());
+                    cBuffer.append("_");
+                    // Base64 encoding allows for unwanted slash characters.
+                    cBuffer.append(toBase64(b).replace('/', '_'));
+                }
+                catch (Exception e)
+                {
+                    // Ignore query string
+                }
+            }
+        }
+        return cBuffer.toString();
+    }
+
+    protected String getSortedQueryString(HttpServletRequest request)
+    {
+        Map paramMap = request.getParameterMap();
+        if (paramMap.isEmpty())
+        {
+            return null;
+        }
+        Set paramSet = new TreeMap(paramMap).entrySet();
+        StringBuffer buf = new StringBuffer();
+        boolean first = true;
+        for (Iterator it = paramSet.iterator(); it.hasNext();)
+        {
+            Map.Entry entry = (Map.Entry) it.next();
+            String[] values = (String[]) entry.getValue();
+            for (int i = 0; i < values.length; i++)
+            {
+                String key = (String) entry.getKey();
+                if ((key.length() != 10) || !"jsessionid".equals(key))
+                {
+                    if (first)
+                    {
+                        first = false;
+                    }
+                    else
+                    {
+                        buf.append('&');
+                    }
+                    buf.append(key).append('=').append(values[i]);
+                }
+            }
+        }
+        // We get a 0 length buffer if the only parameter was a jsessionid
+        if (buf.length() == 0)
+        {
+            return null;
+        }
+        else
+        {
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Convert a byte array into a Base64 string (as used in mime formats)
+     */
+    private static String toBase64(byte[] aValue)
+    {
+        int byte1;
+        int byte2;
+        int byte3;
+        int iByteLen = aValue.length;
+        StringBuffer tt = new StringBuffer();
+        for (int i = 0; i < iByteLen; i += 3)
+        {
+            boolean bByte2 = (i + 1) < iByteLen;
+            boolean bByte3 = (i + 2) < iByteLen;
+            byte1 = aValue[i] & 0xFF;
+            byte2 = (bByte2) ? (aValue[i + 1] & 0xFF) : 0;
+            byte3 = (bByte3) ? (aValue[i + 2] & 0xFF) : 0;
+            tt.append(m_strBase64Chars.charAt(byte1 / 4));
+            tt.append(m_strBase64Chars.charAt((byte2 / 16)
+                            + ((byte1 & 0x3) * 16)));
+            tt.append(((bByte2) ? m_strBase64Chars.charAt((byte3 / 64)
+                            + ((byte2 & 0xF) * 4)) : '='));
+            tt.append(((bByte3) ? m_strBase64Chars.charAt(byte3 & 0x3F) : '='));
+        }
+        return tt.toString();
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler2.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler2.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler2.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/LRUCacheHandler2.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,403 @@
+/*
+ * Created on Jun 15, 2004
+ */
+package org.roller.presentation.pagecache.rollercache;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+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.UserData;
+import org.roller.presentation.LanguageUtil;
+import org.roller.presentation.pagecache.FilterHandler;
+import org.roller.util.LRUCache2;
+
+/**
+ * Page cache implementation that uses a simple LRUCache. Can be configured 
+ * using filter configuration parameters:
+ * <ul>
+ * <li><b>size</b>: number of pages to keep in cache. Once cache reaches
+ * this size, each new cache entry will push out the LRU cache entry.</li>
+ * <li><b>timeout</b>: interval to timeout pages in milliseconds.</li>
+ * </ul> 
+ * @author David M Johnson
+ */
+public class LRUCacheHandler2 implements FilterHandler
+{
+    private static Log mLogger = 
+        LogFactory.getFactory().getInstance(LRUCacheHandler2.class);
+
+    private LRUCache2 mPageCache = null;   
+    private String mName = null;
+    
+    // Statistics
+    private int misses = 0;
+    private int hits = 0;
+    
+    private final static String FILE_SEPARATOR = "/";
+    private final static char FILE_SEPARATOR_CHAR = FILE_SEPARATOR.charAt(0);
+    private final static short AVERAGE_KEY_LENGTH = 30;
+    private static final String m_strBase64Chars = 
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";    
+
+    public LRUCacheHandler2(FilterConfig config)
+    {      
+        mName = config.getFilterName();
+        mLogger.info("Initializing for: " + mName);
+        
+        String cacheSize = RollerConfig.getProperty("cache.filter.page.size");
+        String cacheTimeout = RollerConfig.getProperty("cache.filter.page.timeout");
+        
+        int size = 200;
+        try
+        {
+            size = Integer.parseInt(cacheSize);
+        }
+        catch (Exception e)
+        {
+            mLogger.warn(config.getFilterName() 
+                + "Can't read cache size parameter, using default...");
+        }
+        mLogger.info(mName + " size=" + size);
+
+        long timeout = 30000;
+        try
+        {
+            timeout = Long.parseLong(cacheTimeout);
+        }
+        catch (Exception e)
+        {
+            mLogger.warn(config.getFilterName() 
+                + "Can't read timeout parameter, using default.");
+        }
+        mLogger.info(mName + " timeout=" + timeout + "seconds");
+        mPageCache = new LRUCache2(size, timeout*1000);
+    }
+    
+    /**
+     * @see org.roller.presentation.pagecache.FilterHandler#destroy()
+     */
+    public void destroy()
+    {
+    }
+
+    /**
+     * @see org.roller.presentation.pagecache.FilterHandler#doFilter(
+     *      javax.servlet.ServletRequest, javax.servlet.ServletResponse,
+     *      javax.servlet.FilterChain)
+     */
+    public void doFilter(ServletRequest req, ServletResponse res,
+                    FilterChain chain) throws ServletException, IOException
+    {
+        HttpServletRequest request = (HttpServletRequest) req;
+        HttpServletResponse response = (HttpServletResponse) res;
+
+        // Get locale, needed for creating cache key. 
+        // Unfortunately, getViewLocal works by parsing the request URL
+        // which we would like to avoid where possible.
+        Locale locale = null;
+        try 
+        {
+            locale = LanguageUtil.getViewLocale(request);
+        }
+        catch (Exception e)
+        {
+            // An error parsjng the request is considered to be a 404
+            mLogger.debug("Unable determine view local from request");
+            response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            return;
+        }
+        
+        // generate language-sensitive cache-key
+        String generatedKey = null;
+        if (locale != null)
+        {
+            generatedKey = generateEntryKey(null, 
+                      request, 1, locale.getLanguage());
+        }
+        else
+        {
+            generatedKey = generateEntryKey(null, 
+                      request, 1, null);
+        }
+        
+        // Add authenticated user name, if there is one, to cache key
+        java.security.Principal prince = request.getUserPrincipal();        
+        StringBuffer keyb = new StringBuffer();
+        keyb.append(generatedKey);
+        if (prince != null)
+        {
+            keyb.append("_");
+            keyb.append(prince);
+        }
+        String key = keyb.toString();
+                
+        ResponseContent respContent = (ResponseContent)getFromCache(key);
+        if (respContent == null) 
+        {
+            try
+            {
+                CacheHttpServletResponseWrapper cacheResponse = 
+                    new CacheHttpServletResponseWrapper(response);
+                
+                chain.doFilter(request, cacheResponse);                            
+                cacheResponse.flushBuffer();
+
+                // Store as the cache content the result of the response
+                // if no exception was noted by content generator.
+                if (request.getAttribute("DisplayException") == null)
+                {
+                    ResponseContent rc = cacheResponse.getContent();
+                    putToCache(key, rc);
+                }
+                else
+                {
+                    StringBuffer sb = new StringBuffer();
+                    sb.append("Display exception, cache, key=");
+                    sb.append(key);
+                    mLogger.error(sb.toString());
+                }
+            }
+            catch (java.net.SocketException se)
+            {
+                // ignore socket exceptions
+            }
+            catch (Exception e)
+            {
+                // something unexpected and bad happened
+                StringBuffer sb = new StringBuffer();
+                sb.append("Error rendering page, key=");
+                sb.append(key);
+                mLogger.error(sb.toString());
+                mLogger.debug(e);
+            }           
+        }
+        else
+        {
+            try
+            {
+                respContent.writeTo(response);
+            }
+            catch (java.net.SocketException se)
+            {
+                // ignore socket exceptions
+            }
+            catch (Exception e)
+            {
+                if (mLogger.isDebugEnabled())
+                {
+                    StringBuffer sb = new StringBuffer();
+                    sb.append("Probably a client abort exception, key=");
+                    sb.append(key);
+                    mLogger.error(sb.toString());
+                }
+            }
+            
+        }
+    }
+
+    /**
+     * Purge entire cache.
+     */
+    public synchronized void flushCache(HttpServletRequest req)
+    {
+        mPageCache.purge();
+    }
+
+    /**
+     * Purge user's entries from cache.
+     */
+    public synchronized void removeFromCache(HttpServletRequest req, UserData user)
+    {
+        // TODO: can we make this a little more precise, perhaps via regex?
+        String rssString = "/rss/" + user.getUserName(); // user's pages
+        String pageString = "/page/" + user.getUserName(); // user's RSS feeds
+        String mainRssString = "/rss_"; // main RSS feed
+        String mainPageString = "/main.do"; // main page
+        String planetPageString = "/planet.do"; // planet page
+        
+        int beforeSize = mPageCache.size();
+        mPageCache.purge(new String[] 
+        {
+            rssString, 
+            pageString, 
+            mainRssString, 
+            mainPageString, 
+            planetPageString
+        });
+        int afterSize = mPageCache.size();
+        
+        if (mLogger.isDebugEnabled())
+        {
+            StringBuffer sb = new StringBuffer();
+            sb.append("Purged, count=");
+            sb.append(beforeSize - afterSize);
+            sb.append(", user=");
+            sb.append(user.getUserName());
+            mLogger.debug(sb.toString());
+        }        
+    }
+    
+    /** 
+     * Get from cache. Synchronized because "In access-ordered linked hash 
+     * maps, merely querying the map with get is a structural modification" 
+     */
+    public synchronized Object getFromCache(String key) 
+    {
+        Object entry = mPageCache.get(key);
+        
+        if (entry != null && mLogger.isDebugEnabled())
+        {
+            hits++;
+        }
+        return entry;
+    }
+
+    public synchronized void putToCache(String key, Object entry) 
+    {
+        mPageCache.put(key, entry);
+        if (mLogger.isDebugEnabled())
+        {
+            misses++;
+            
+            StringBuffer sb = new StringBuffer();
+            sb.append("Missed, cache size=");
+            sb.append(mPageCache.size());
+            sb.append(", hits=");
+            sb.append(hits);
+            sb.append(", misses=");
+            sb.append(misses);
+            sb.append(", key=");
+            sb.append(key);
+            mLogger.debug(sb.toString());
+        }
+    }
+
+    public String generateEntryKey(String key, 
+                       HttpServletRequest request, int scope, String language)
+    {
+        StringBuffer cBuffer = new StringBuffer(AVERAGE_KEY_LENGTH);
+        // Append the language if available
+        if (language != null)
+        {
+            cBuffer.append(FILE_SEPARATOR).append(language);
+        }
+        
+        //cBuffer.append(FILE_SEPARATOR).append(request.getServerName());
+        
+        if (key != null)
+        {
+            cBuffer.append(FILE_SEPARATOR).append(key);
+        }
+        else
+        {
+            String generatedKey = request.getRequestURI();
+            if (generatedKey.charAt(0) != FILE_SEPARATOR_CHAR)
+            {
+                cBuffer.append(FILE_SEPARATOR_CHAR);
+            }
+            cBuffer.append(generatedKey);
+            cBuffer.append("_").append(request.getMethod()).append("_");
+            generatedKey = getSortedQueryString(request);
+            if (generatedKey != null)
+            {
+                try
+                {
+                    java.security.MessageDigest digest = 
+                        java.security.MessageDigest.getInstance("MD5");
+                    byte[] b = digest.digest(generatedKey.getBytes());
+                    cBuffer.append("_");
+                    // Base64 encoding allows for unwanted slash characters.
+                    cBuffer.append(toBase64(b).replace('/', '_'));
+                }
+                catch (Exception e)
+                {
+                    // Ignore query string
+                }
+            }
+        }
+        return cBuffer.toString();
+    }
+
+    protected String getSortedQueryString(HttpServletRequest request)
+    {
+        Map paramMap = request.getParameterMap();
+        if (paramMap.isEmpty())
+        {
+            return null;
+        }
+        Set paramSet = new TreeMap(paramMap).entrySet();
+        StringBuffer buf = new StringBuffer();
+        boolean first = true;
+        for (Iterator it = paramSet.iterator(); it.hasNext();)
+        {
+            Map.Entry entry = (Map.Entry) it.next();
+            String[] values = (String[]) entry.getValue();
+            for (int i = 0; i < values.length; i++)
+            {
+                String key = (String) entry.getKey();
+                if ((key.length() != 10) || !"jsessionid".equals(key))
+                {
+                    if (first)
+                    {
+                        first = false;
+                    }
+                    else
+                    {
+                        buf.append('&');
+                    }
+                    buf.append(key).append('=').append(values[i]);
+                }
+            }
+        }
+        // We get a 0 length buffer if the only parameter was a jsessionid
+        if (buf.length() == 0)
+        {
+            return null;
+        }
+        else
+        {
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Convert a byte array into a Base64 string (as used in mime formats)
+     */
+    private static String toBase64(byte[] aValue)
+    {
+        int byte1;
+        int byte2;
+        int byte3;
+        int iByteLen = aValue.length;
+        StringBuffer tt = new StringBuffer();
+        for (int i = 0; i < iByteLen; i += 3)
+        {
+            boolean bByte2 = (i + 1) < iByteLen;
+            boolean bByte3 = (i + 2) < iByteLen;
+            byte1 = aValue[i] & 0xFF;
+            byte2 = (bByte2) ? (aValue[i + 1] & 0xFF) : 0;
+            byte3 = (bByte3) ? (aValue[i + 2] & 0xFF) : 0;
+            tt.append(m_strBase64Chars.charAt(byte1 / 4));
+            tt.append(m_strBase64Chars.charAt((byte2 / 16)
+                            + ((byte1 & 0x3) * 16)));
+            tt.append(((bByte2) ? m_strBase64Chars.charAt((byte3 / 64)
+                            + ((byte2 & 0xF) * 4)) : '='));
+            tt.append(((bByte3) ? m_strBase64Chars.charAt(byte3 & 0x3F) : '='));
+        }
+        return tt.toString();
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/ResponseContent.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package org.roller.presentation.pagecache.rollercache;
+
+import java.io.*;
+
+import java.util.Locale;
+
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Holds the servlet response in a byte array so that it can be held
+ * in the cache (and, since this class is serializable, optionally
+ * persisted to disk).
+ *
+ * @version $Revision: 1.3 $
+ * @author  <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
+ */
+public class ResponseContent implements Serializable {
+    private transient ByteArrayOutputStream bout = new ByteArrayOutputStream(1000);
+    private Locale locale = null;
+    private String contentType = null;
+    private byte[] content = null;
+    private long lastModified = -1;
+
+    /**
+     * Set the content type. We capture this so that when we serve this
+     * data from cache, we can set the correct content type on the response.
+     */
+    public void setContentType(String value) {
+        contentType = value;
+    }
+
+    public long getLastModified() {
+        return lastModified;
+    }
+
+    public void setLastModified(long value) {
+        lastModified = value;
+    }
+
+    /**
+     * Set the Locale. We capture this so that when we serve this data from
+     * cache, we can set the correct locale on the response.
+     */
+    public void setLocale(Locale value) {
+        locale = value;
+    }
+
+    /**
+     * Get an output stream. This is used by the {@link SplitServletOutputStream}
+     * to capture the original (uncached) response into a byte array.
+     */
+    public OutputStream getOutputStream() {
+        return bout;
+    }
+
+    /**
+     * Gets the size of this cached content.
+     *
+     * @return The size of the content, in bytes. If no content
+     * exists, this method returns <code>-1</code>.
+     */
+    public int getSize() {
+        return (content != null) ? content.length : (-1);
+    }
+
+    /**
+     * Called once the response has been written in its entirety. This
+     * method commits the response output stream by converting the output
+     * stream into a byte array.
+     */
+    public void commit() {
+        content = bout.toByteArray();
+    }
+
+    /**
+     * Writes this cached data out to the supplied <code>ServletResponse</code>.
+     *
+     * @param response The servlet response to output the cached content to.
+     * @throws IOException
+     */
+    public void writeTo(ServletResponse response) throws IOException {
+        //Send the content type and data to this response
+        if (contentType != null) {
+            response.setContentType(contentType);
+        }
+
+        //if (response instanceof HttpServletResponse) {
+            //((HttpServletResponse) response).setDateHeader("Last-Modified", lastModified);
+        //}
+
+        response.setContentLength(content.length);
+
+        if (locale != null) {
+            response.setLocale(locale);
+        }
+
+        OutputStream out = new BufferedOutputStream(response.getOutputStream());
+        out.write(content);
+        out.flush();
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitPrintWriter.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,56 @@
+package org.roller.presentation.pagecache.rollercache;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+
+/**
+ * @author Dave Johnson
+ */
+public class SplitPrintWriter extends PrintWriter 
+{
+    private PrintWriter captureWriter = null;
+    private PrintWriter passThroughWriter = null;
+    
+    public SplitPrintWriter(PrintWriter captureWriter, PrintWriter passThroughWriter) 
+    {
+        super(passThroughWriter);
+        this.captureWriter = captureWriter;
+        this.passThroughWriter = passThroughWriter;
+    }  
+      
+    public void write(char[] cbuf)
+    {
+        captureWriter.write(cbuf);
+        passThroughWriter.write(cbuf);
+    }
+    public void write(char[] cbuf, int off, int len)
+    {       
+        captureWriter.write(cbuf,off,len);
+        passThroughWriter.write(cbuf,off,len);
+    }
+    public void write(int c)
+    {       
+        captureWriter.write(c);
+        passThroughWriter.write(c);
+    }
+    public void write(String str)
+    {       
+        captureWriter.write(str);
+        passThroughWriter.write(str);
+    }
+    public void write(String str, int off, int len)
+    {       
+        captureWriter.write(str,off,len);
+        passThroughWriter.write(str,off,len);
+    }
+    public void flush() 
+    {
+        captureWriter.flush();
+        passThroughWriter.flush();
+    }    
+    public void close() 
+    {
+        captureWriter.close();
+        passThroughWriter.close();
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/SplitServletOutputStream.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package org.roller.presentation.pagecache.rollercache;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.servlet.ServletOutputStream;
+
+/**
+ * Extends the base <code>ServletOutputStream</code> class so that
+ * the stream can be captured as it gets written. This is achieved
+ * by overriding the <code>write()</code> methods and outputting
+ * the data to two streams - the original stream and a secondary stream
+ * that is designed to capture the written data.
+ *
+ * @version $Revision: 1.1 $
+ * @author  <a href="mailto:sergek@lokitech.com">Serge Knystautas</a>
+ */
+public class SplitServletOutputStream extends ServletOutputStream {
+    OutputStream captureStream = null;
+    OutputStream passThroughStream = null;
+
+    /**
+     * Constructs a split output stream that both captures and passes through
+     * the servlet response.
+     *
+     * @param captureStream The stream that will be used to capture the data.
+     * @param passThroughStream The pass-through <code>ServletOutputStream</code>
+     * that will write the response to the client as originally intended.
+     */
+    public SplitServletOutputStream(OutputStream captureStream, OutputStream passThroughStream) {
+        this.captureStream = captureStream;
+        this.passThroughStream = passThroughStream;
+    }
+
+    /**
+     * Writes the incoming data to both the output streams.
+     *
+     * @param value The int data to write.
+     * @throws IOException
+     */
+    public void write(int value) throws IOException {
+        captureStream.write(value);
+        passThroughStream.write(value);
+    }
+
+    /**
+     * Writes the incoming data to both the output streams.
+     *
+     * @param value The bytes to write to the streams.
+     * @throws IOException
+     */
+    public void write(byte[] value) throws IOException {
+        captureStream.write(value);
+        passThroughStream.write(value);
+    }
+
+    /**
+     * Writes the incoming data to both the output streams.
+     *
+     * @param b The bytes to write out to the streams.
+     * @param off The offset into the byte data where writing should begin.
+     * @param len The number of bytes to write.
+     * @throws IOException
+     */
+    public void write(byte[] b, int off, int len) throws IOException {
+        captureStream.write(b, off, len);
+        passThroughStream.write(b, off, len);
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/package.html
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/package.html?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/package.html (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pagecache/rollercache/package.html Fri Oct 21 14:27:36 2005
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+  <title></title>
+</head>
+<body>
+In-memory LRU implemtation for PageCacheFilter.
+
+</body>
+</html>

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueProcessor.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueProcessor.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueProcessor.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueProcessor.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2005
+ * Anil R. Gangolli. All rights reserved.
+ *
+ * Distributed with the Roller Weblogger Project under the terms of the Roller Software
+ * License
+ */
+
+package org.roller.presentation.pings;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.RollerException;
+import org.roller.config.PingConfig;
+import org.roller.model.PingQueueManager;
+import org.roller.model.RollerFactory;
+import org.roller.pojos.PingQueueEntryData;
+import org.roller.pojos.PingTargetData;
+import org.roller.pojos.WebsiteData;
+import org.roller.presentation.RollerContext;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Ping Queue Processor.  Singleton encapsulating logic for processing the weblog update ping queue.
+ */
+public class PingQueueProcessor
+{
+    private static final Log logger = LogFactory.getLog(PingQueueProcessor.class);
+
+    private static PingQueueProcessor theInstance;
+
+
+    private RollerContext rollerContext;
+    private PingQueueManager pingQueueMgr;
+
+    public static PingQueueProcessor getInstance()
+    {
+        return theInstance;
+    }
+
+    private PingQueueProcessor(RollerContext rc) throws RollerException
+    {
+        rollerContext = rc;
+        pingQueueMgr = RollerFactory.getRoller().getPingQueueManager();
+    }
+
+    /**
+     * Initialize the singleton.  This is called during <code>RollerContext</code> initialization.
+     *
+     * @param rc the Roller context
+     * @throws RollerException
+     */
+    public static synchronized void init(RollerContext rc) throws RollerException
+    {
+        if (theInstance != null)
+        {
+            logger.warn("Ignoring duplicate initialization of PingQueueProcessor!");
+            return;
+        }
+        theInstance = new PingQueueProcessor(rc);
+        if (logger.isDebugEnabled()) logger.debug("Ping queue processor initialized.");
+    }
+
+    /**
+     * Process the ping queue.  Performs one pass through the ping queue, processing every entry once.  On ping failure
+     * an entry is requeued for processing on subsequent passes until the configured maximum number of attempts is
+     * reached.
+     */
+    public synchronized void processQueue()
+    {
+        if (PingConfig.getSuspendPingProcessing()) {
+            logger.info("Ping processing has been suspended.  Skipping current round of ping queue processing.");
+            return;
+        }
+
+        String absoluteContextUrl = rollerContext.getAbsoluteContextUrl();
+        if (absoluteContextUrl == null)
+        {
+            logger.warn("WARNING: Skipping current ping queue processing round because we cannot yet determine the site's absolute context url.");
+            return;
+        }
+
+        // TODO: Group by ping target and ping all sites for that target?
+        // We're currently not taking advantage of grouping by ping target site and then sending
+        // all of the pings for that target at once.  If it becomes an efficiency issue, we should do
+        // that.
+
+        try
+        {
+            if (logger.isDebugEnabled()) logger.debug("Started processing ping queue.");
+            // Get all of the entries
+            List entries = pingQueueMgr.getAllQueueEntries();
+
+            // Process each entry
+            for (Iterator i = entries.iterator(); i.hasNext();)
+            {
+                PingQueueEntryData pingQueueEntry = (PingQueueEntryData) i.next();
+                processQueueEntry(absoluteContextUrl, pingQueueEntry);
+            }
+            if (logger.isDebugEnabled()) logger.debug("Finished processing ping queue.");
+        }
+        catch (Exception ex)
+        {
+            logger.error("Unexpected exception processing ping queue!  Aborting this pass of ping queue processing.", ex);
+        }
+    }
+
+    /**
+     * Process an individual ping queue entry.
+     *
+     * @param absoluteContextUrl absolute context URL of the Roller site
+     * @param pingQueueEntry     the ping queue entry
+     * @throws RollerException only if there are problems processing the queue.  Exceptions from sending pings are
+     *                         handled, not thrown.
+     */
+    private void processQueueEntry(String absoluteContextUrl, PingQueueEntryData pingQueueEntry)
+        throws RollerException
+    {
+        if (logger.isDebugEnabled()) logger.debug("Processing ping queue entry: " + pingQueueEntry);
+
+        PingTargetData pingTarget = pingQueueEntry.getPingTarget();
+        WebsiteData website = pingQueueEntry.getWebsite();
+        boolean pingSucceeded = false;
+        if (PingConfig.getLogPingsOnly())
+        {
+            // Just log the ping and pretend it succeeded.
+            logger.info("Logging simulated ping for ping queue entry " + pingQueueEntry);
+            pingSucceeded = true;
+        }
+        else
+        {
+            // Actually process the ping
+            try
+            {
+                // Send the ping
+                WeblogUpdatePinger.sendPing(absoluteContextUrl, pingTarget, website);
+                // Consider successful ping transmission if we didn't get an exception.  We don't care here
+                // about the result of the ping if it was transmitted.
+                pingSucceeded = true;
+            }
+            catch (Exception ex)
+            {
+                // Handle the ping error, either removing or requeuing the ping queue entry.
+                handlePingError(pingQueueEntry, ex);
+            }
+        }
+        // We do this outside of the previous try-catch because we don't want an exception here to be considered a ping error.
+        if (pingSucceeded)
+        {
+            if (logger.isDebugEnabled()) logger.debug("Processed ping: " + pingQueueEntry);
+            pingQueueMgr.removeQueueEntry(pingQueueEntry);
+        }
+    }
+
+    /**
+     * Handle any ping error.
+     *
+     * @param pingQueueEntry the ping queue entry
+     * @param ex             the exception that occurred on the ping attempt
+     * @throws RollerException
+     */
+    private void handlePingError(PingQueueEntryData pingQueueEntry, Exception ex)
+        throws RollerException
+    {
+        if ((pingQueueEntry.incrementAttempts() < PingConfig.getMaxPingAttempts()) &&
+            WeblogUpdatePinger.shouldRetry(ex))
+        {
+            // We have attempts remaining, and it looks like we should retry,
+            // so requeue the entry for processing on subsequent rounds
+            logger.warn("Error on ping attempt (" + pingQueueEntry.getAttempts() + ") for " + pingQueueEntry +
+                ": [" + ex.getMessage() + "]. Will re-queue for later attempts.");
+            if (logger.isDebugEnabled()) logger.debug("Error on last ping attempt was: ", ex);
+            pingQueueMgr.storeQueueEntry(pingQueueEntry);
+        }
+        else
+        {
+            // Remove the entry
+            logger.warn("Error on ping attempt (" + pingQueueEntry.getAttempts() + ") for " + pingQueueEntry +
+                ": [" + ex.getMessage() + "].  Entry will be REMOVED from ping queue.");
+            if (logger.isDebugEnabled()) logger.debug("Error on last ping attempt was: ", ex);
+            pingQueueMgr.removeQueueEntry(pingQueueEntry);
+            // TODO: mark ping target invalid?
+        }
+    }
+
+
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueTask.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueTask.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueTask.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/PingQueueTask.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2005
+ * Anil R. Gangolli. All rights reserved.
+ *
+ * Distributed with the Roller Weblogger Project under the terms of the Roller Software
+ * License
+ */
+
+package org.roller.presentation.pings;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.roller.RollerException;
+import org.roller.model.Roller;
+import org.roller.model.RollerFactory;
+import org.roller.presentation.RollerContext;
+
+import java.util.TimerTask;
+
+/**
+ * Task for processing the ping queue at fixed intervals.   This is set up during context initialization by {@link
+ * RollerContext}.  The queue processing interval is currently set from the configuration {@link
+ * org.roller.config.PingConfig} at startup time only.
+ */
+public class PingQueueTask extends TimerTask
+{
+    private static final Log logger = LogFactory.getLog(PingQueueTask.class);
+
+    //  The periodic interval (in minutes) at which we are configured to run
+    long intervalMins;
+
+    /**
+     * Initialize the task.
+     *
+     * @param rc the Roller context.
+     * @throws RollerException
+     */
+    public void init(RollerContext rc, long intervalMins) throws RollerException
+    {
+        PingQueueProcessor.init(rc);
+        this.intervalMins = intervalMins;
+    }
+
+    /**
+     * Get the task's configured interval (in minutes).
+     *
+     * @return the tasks configured interval (in minutes).
+     */
+    public long getIntervalMins()
+    {
+        return intervalMins;
+    }
+
+    /**
+     * Run the task once.
+     */
+    public void run()
+    {
+        // Call the ping queue processor to process the queue
+        Roller roller = null;
+        try
+        {
+            roller = RollerFactory.getRoller();
+            roller.begin();
+            PingQueueProcessor.getInstance().processQueue();
+            roller.commit();
+        }
+        catch (RollerException e)
+        {
+            // This is probably duplicate logging. May want to eliminate it, but should be rare.
+            logger.error("Error while processing ping queuer", e);
+        }
+        finally
+        {
+            if (roller != null) roller.release();
+        }
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/WeblogUpdatePinger.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/WeblogUpdatePinger.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/WeblogUpdatePinger.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/WeblogUpdatePinger.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2004-2005
+ * Lance Lavandowska, Anil R. Gangolli.
+ * All rights reserved.
+ *
+ * Distributed with the Roller Weblogger Project under the terms of the Roller Software
+ * License.
+ */
+
+package org.roller.presentation.pings;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.xmlrpc.XmlRpcClient;
+import org.apache.xmlrpc.XmlRpcException;
+import org.roller.RollerException;
+import org.roller.model.RollerFactory;
+import org.roller.pojos.PingTargetData;
+import org.roller.pojos.WebsiteData;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.UnknownHostException;
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * Utility for sending a weblog update ping.
+ *
+ * @author <a href="mailto:anil@busybuddha.org">Anil Gangolli</author>
+ * @author llavandowska (for code refactored from the now-defunct <code>RollerXmlRpcClient</code>)
+ */
+public class WeblogUpdatePinger
+{
+    public static final Log logger = LogFactory.getLog(WeblogUpdatePinger.class);
+
+    /**
+     * Conveys a ping result.
+     */
+    public static class PingResult
+    {
+        boolean error;
+        String message;
+
+        public PingResult(Boolean error, String message)
+        {
+            this.error = error != null ? error.booleanValue() : false;
+            this.message = message;
+        }
+
+        public boolean isError()
+        {
+            return error;
+        }
+
+        public void setError(boolean error)
+        {
+            this.error = error;
+        }
+
+        public String getMessage()
+        {
+            return message;
+        }
+
+        public void setMessage(String message)
+        {
+            this.message = message;
+        }
+
+        public String toString()
+        {
+            return "PingResult{" +
+                "error=" + error +
+                ", message='" + message + "'" +
+                "}";
+        }
+    }
+
+    // Inhibit construction
+    private WeblogUpdatePinger()
+    {
+    }
+
+    /**
+     * Send a weblog update ping.
+     *
+     * @param absoluteContextUrl the absolute context url of the Roller site.
+     * @param pingTarget         the target site to ping
+     * @param website            the website that changed (from which the ping originates)
+     * @return the result message string sent by the server.
+     * @throws IOException
+     * @throws XmlRpcException
+     * @throws RollerException
+     */
+    public static PingResult sendPing(String absoluteContextUrl, PingTargetData pingTarget, WebsiteData website)
+        throws RollerException, IOException, XmlRpcException
+    {
+        // Figure out the url of the user's website.
+        String websiteUrl =
+            RollerFactory.getRoller().getWeblogManager().getUrl(website.getUser(), absoluteContextUrl);
+
+        // Set up the ping parameters.
+        Vector params = new Vector();
+        params.addElement(website.getName());
+        params.addElement(websiteUrl);
+        if (logger.isDebugEnabled())
+        {
+            logger.debug("Executing ping to '" + pingTarget.getPingUrl() + "' for website '" +
+                websiteUrl + "' (" + website.getName() + ")");
+        }
+
+        // Send the ping
+        XmlRpcClient client = new XmlRpcClient(pingTarget.getPingUrl());
+        Hashtable result = (Hashtable) client.execute("weblogUpdates.ping", params);
+        PingResult pingResult = new PingResult((Boolean) result.get("flerror"), (String) result.get("message"));
+        if (logger.isDebugEnabled()) logger.debug("Ping result is: " + pingResult);
+        return pingResult;
+    }
+
+    /**
+     * Decide if the given exception appears to warrant later retrial attempts.
+     *
+     * @param ex an exception thrown by the <coce>sendPing</code> operation
+     * @return true if the error warrants retrial
+     */
+    public static boolean shouldRetry(Exception ex)
+    {
+        // Determine if error appears transient (warranting retrial)
+        // We give most errors the "benefit of the doubt" by considering them transient
+        // This picks out a few that we consider non-transient
+        if (ex instanceof UnknownHostException)
+        {
+            // User probably mistyped the url in the custom target.
+            return false;
+        }
+        else if (ex instanceof MalformedURLException)
+        {
+            // This should never happen due to validations but if we get here, retrial won't fix it.
+            return false;
+        }
+        return true;
+    }
+
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/package.html
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/package.html?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/package.html (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/pings/package.html Fri Oct 21 14:27:36 2005
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+  <title></title>
+</head>
+<body>
+Classes for sending Weblogs.com-style pings.
+
+</body>
+</html>

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetAction.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetAction.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetAction.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetAction.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,190 @@
+package org.roller.presentation.planet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ResourceBundle;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts.action.Action;
+import org.apache.struts.action.ActionForm;
+import org.apache.struts.action.ActionForward;
+import org.apache.struts.action.ActionMapping;
+import org.roller.RollerException;
+import org.roller.config.RollerRuntimeConfig;
+import org.roller.model.Roller;
+import org.roller.pojos.PlanetConfigData;
+import org.roller.pojos.PlanetGroupData;
+import org.roller.presentation.RollerContext;
+import org.roller.presentation.RollerRequest;
+
+/**
+ * Main page action for Roller Planet.
+ * @struts.action name="main" path="/planet" scope="request"
+ * @struts.action-forward name="planet.page" path="/planet.jsp"
+ */
+public class PlanetAction extends Action
+{
+    private static Log mLogger = 
+        LogFactory.getFactory().getInstance(PlanetAction.class);
+    private static ResourceBundle bundle = 
+        ResourceBundle.getBundle("ApplicationResources");  
+    
+	/**
+	 * Loads model and forwards to planet.page.
+         */
+	public ActionForward execute(
+		ActionMapping mapping, ActionForm form,
+		HttpServletRequest req, HttpServletResponse res)
+		throws Exception
+	{        
+        RollerContext rctx = RollerContext.getRollerContext(req);		
+        req.setAttribute("version",rctx.getRollerVersion());
+        req.setAttribute("buildTime",rctx.getRollerBuildTime());
+        req.setAttribute("baseURL", rctx.getContextUrl(req));
+        req.setAttribute("data", new PlanetPageData(req));
+        
+        boolean allowNewUsers = 
+           RollerRuntimeConfig.getBooleanProperty("users.registration.enabled");
+
+        java.security.Principal prince = req.getUserPrincipal();
+        if (prince != null) 
+        {
+            req.setAttribute("loggedIn",Boolean.TRUE);
+            req.setAttribute("userName",prince.getName());
+        } 
+        else if (allowNewUsers)
+        {   
+            req.setAttribute("allowNewUsers",Boolean.TRUE);
+        }
+        req.setAttribute("leftPage","/theme/status.jsp");
+        
+        return mapping.findForward("planet.page");
+	}
+        
+    /**
+     * Page model. 
+     */
+    public static class PlanetPageData 
+    {
+        private HttpServletRequest mRequest = null;
+        private String mTitle = 
+                bundle.getString("planet.title.unconfigured");
+        private String mDescription = 
+                bundle.getString("planet.description.unconfigured");
+        
+        public String getTitle() {return mTitle;}
+        public String getDescription() {return mDescription;}
+        
+        public PlanetPageData(HttpServletRequest req) throws RollerException
+        {
+           mRequest = req;
+           Roller roller = 
+           RollerRequest.getRollerRequest(mRequest).getRoller();  
+           PlanetConfigData cfg = roller.getPlanetManager().getConfiguration();
+           if (cfg != null)
+           {
+               mTitle = cfg.getTitle();
+               mDescription = cfg.getDescription();
+           }
+        }
+        
+        /** 
+         * Get aggregation of entries in 'all' and 'external' groups
+         */
+        public List getAggregation(int num) throws RollerException
+        {
+            Roller roller = 
+                RollerRequest.getRollerRequest(mRequest).getRoller();            
+            return roller.getPlanetManager().getAggregation(num);
+        }
+        /** 
+         * Get named group
+         */
+        public PlanetGroupData getGroup(String name) throws RollerException
+        {
+            PlanetGroupData group = null;
+            try 
+            {
+                Roller roller = 
+                    RollerRequest.getRollerRequest(mRequest).getRoller();  
+                group = roller.getPlanetManager().getGroup(name);
+            }
+            catch (RollerException e) 
+            {
+                mLogger.error(e); 
+            }
+            return group;
+        }
+        /** 
+         * Get aggregation of entries in named group
+         */
+        public List getAggregation(String name, int num) throws RollerException
+        {
+            List ret = new ArrayList();
+            try 
+            {
+                Roller roller = 
+                    RollerRequest.getRollerRequest(mRequest).getRoller();  
+                PlanetGroupData group= roller.getPlanetManager().getGroup(name);
+                ret = roller.getPlanetManager().getAggregation(group, num);
+            }
+            catch (RollerException e) 
+            {
+                mLogger.error(e); 
+            }
+            return ret;
+        }
+        /**
+         * Get top blogs according to Technorati
+         */
+        public List getTopSubscriptions(int num) throws RollerException
+        {
+            List ret = new ArrayList();
+            try 
+            {
+                Roller roller = 
+                    RollerRequest.getRollerRequest(mRequest).getRoller();  
+                ret = roller.getPlanetManager().getTopSubscriptions(num);
+            }
+            catch (RollerException e) 
+            {
+                mLogger.error(e); 
+            }
+            return ret;
+        }
+        /**
+         * Get top blogs in a group according to Technorati
+         */
+        public List getTopSubscriptions(String name, int num) 
+        throws RollerException
+        {
+            List ret = new ArrayList();
+            try 
+            {
+                Roller roller = 
+                    RollerRequest.getRollerRequest(mRequest).getRoller();  
+                PlanetGroupData group= roller.getPlanetManager().getGroup(name);
+                ret = roller.getPlanetManager().getTopSubscriptions(group,num);
+            }
+            catch (RollerException e) 
+            {
+                mLogger.error(e); 
+            }
+            return ret;
+        }
+        /** 
+         * Get list of most popular websites in terms of day hits.
+         */
+        public List getPopularWebsites(int num) throws RollerException
+        {
+            Roller roller = 
+                RollerRequest.getRollerRequest(mRequest).getRoller();            
+            return roller.getRefererManager().getDaysPopularWebsites(num);
+        }
+    }
+}
+

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetConfigAction.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetConfigAction.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetConfigAction.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetConfigAction.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2005 Sun Microsystems, Inc.
+ *
+ * Licensed 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.roller.presentation.planet;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts.action.ActionError;
+import org.apache.struts.action.ActionErrors;
+import org.apache.struts.action.ActionForm;
+import org.apache.struts.action.ActionForward;
+import org.apache.struts.action.ActionMapping;
+import org.apache.struts.action.ActionMessage;
+import org.apache.struts.action.ActionMessages;
+import org.apache.struts.actions.DispatchAction;
+import org.roller.presentation.forms.PlanetConfigForm;
+import org.roller.config.RollerRuntimeConfig;
+import org.roller.model.PlanetManager;
+import org.roller.model.Roller;
+import org.roller.pojos.PlanetConfigData;
+import org.roller.pojos.PlanetGroupData;
+import org.roller.presentation.RollerRequest;
+import org.roller.presentation.planet.RefreshEntriesTask;
+import org.roller.presentation.planet.SyncWebsitesTask;
+
+/////////////////////////////////////////////////////////////////////////////
+/**
+ * Allows configuration of Planet Roller.
+ * 
+ * @struts.action name="planetConfigForm" path="/admin/planetConfig"
+ *                scope="request" parameter="method"
+ * 
+ * @struts.action-forward name="planetConfig.page" 
+ *                        path="/planet/PlanetConfig.jsp"
+ */
+public final class PlanetConfigAction extends DispatchAction
+{
+    private static Log logger = 
+        LogFactory.getFactory().getInstance(PlanetConfigAction.class);
+
+    /** Populate config form and forward to config page */
+    public ActionForward getConfig(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetConfig.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                PlanetConfigData config = planet.getConfiguration();
+                PlanetConfigForm form = (PlanetConfigForm)actionForm;
+                if (config != null)
+                {
+                    form.copyFrom(config, request.getLocale());
+                }
+                else 
+                {
+                    form.setTitle("Planet Roller");
+                    form.setAdminEmail(RollerRuntimeConfig.getProperty("site.adminemail"));
+                    form.setSiteUrl(RollerRuntimeConfig.getProperty("site.absoluteurl"));
+                    form.setCacheDir("/tmp");
+                }
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+
+    /** Save posted config form data */
+    public ActionForward saveConfig(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetConfig.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                PlanetConfigData config = planet.getConfiguration();
+                if (config == null)
+                {
+                    config = new PlanetConfigData();
+                }
+                PlanetConfigForm form = (PlanetConfigForm) actionForm;
+                ActionErrors errors = validate(form);
+                if (errors.isEmpty())
+                {
+                    form.copyTo(config, request.getLocale());
+                    planet.saveConfiguration(config);
+                    if (planet.getGroup("external") == null) 
+                    {
+                        PlanetGroupData group = new PlanetGroupData();
+                        group.setHandle("external");
+                        group.setTitle("external");
+                        planet.saveGroup(group);
+                    }
+                    roller.commit();
+                    ActionMessages messages = new ActionMessages();
+                    messages.add(null, new ActionMessage("planetConfig.success.saved"));
+                    saveMessages(request, messages);
+                }                
+                else
+                {
+                    saveErrors(request, errors);
+                }
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+
+    /** Refresh entries in backgrounded thread (for testing) */
+    public ActionForward refreshEntries(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetConfig.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                RefreshEntriesTask task = new RefreshEntriesTask();
+                task.init(roller, "dummy");
+                roller.getThreadManager().executeInBackground(task);
+                
+                ActionMessages messages = new ActionMessages();
+                messages.add(null, 
+                        new ActionMessage("planetConfig.success.refreshed"));
+                saveMessages(request, messages);
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+
+    /** Sync websites in backgrounded thread (for testing) */
+    public ActionForward syncWebsites(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetConfig.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = (Roller)rreq.getRoller();
+                SyncWebsitesTask task = new SyncWebsitesTask();
+                task.init(roller, "dummy");
+                roller.getThreadManager().executeInBackground(task);
+                ActionMessages messages = new ActionMessages();
+                messages.add(null, 
+                        new ActionMessage("planetConfig.success.synced"));
+                saveMessages(request, messages);
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+    
+    /** Validate config form, returns empty collection if all OK */
+    public ActionErrors validate(PlanetConfigForm form)
+    {
+        ActionErrors errors = new ActionErrors();
+        if (form.getCacheDir()==null || form.getCacheDir().trim().length()==0)
+        {
+            errors.add(null, new ActionError("planetConfig.error.feedUrl"));
+        }
+        else
+        {
+            File file = new File(form.getCacheDir());
+            if (!file.isDirectory())
+            {
+                errors.add(null, new ActionError(
+                        "planetConfig.error.cacheDirNotFound"));
+            }
+            if (!file.canWrite())
+            {
+                errors.add(null, new ActionError(
+                        "planetConfig.error.cacheDirNotWritable"));
+            }
+        }
+        if (form.getProxyHost()!=null && form.getProxyHost().trim().length()>0)
+        {
+            if (form.getProxyPort()<1)
+            {
+                errors.add(null, new ActionError(
+                        "planetConfig.error.badProxyPort"));
+            }
+        }
+        return errors;
+    }
+}
+

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetGroupsAction.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetGroupsAction.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetGroupsAction.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetGroupsAction.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,303 @@
+/*
+ * Copyright 2005 Sun Microsystems, Inc.
+ *
+ * Licensed 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.roller.presentation.planet;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.struts.action.ActionError;
+import org.apache.struts.action.ActionErrors;
+import org.apache.struts.action.ActionForm;
+import org.apache.struts.action.ActionForward;
+import org.apache.struts.action.ActionMapping;
+import org.apache.struts.action.ActionMessage;
+import org.apache.struts.action.ActionMessages;
+import org.apache.struts.actions.DispatchAction;
+import org.roller.RollerException;
+import org.roller.model.PlanetManager;
+import org.roller.model.Roller;
+import org.roller.pojos.PlanetGroupData;
+import org.roller.presentation.BasePageModel;
+import org.roller.presentation.RollerRequest;
+import org.roller.presentation.forms.PlanetGroupForm;
+
+
+/////////////////////////////////////////////////////////////////////////////
+/**
+ * Add, remove, and view user defined groups.
+ * 
+ * @struts.action name="planetGroupForm" path="/admin/planetGroups"
+ *                scope="request" parameter="method"
+ * 
+ * @struts.action-forward name="planetGroups.page" 
+ *                        path="/planet/PlanetGroups.jsp"
+ */
+public final class PlanetGroupsAction extends DispatchAction
+{
+    private static Log logger = LogFactory.getFactory().getInstance(
+            PlanetGroupsAction.class);
+
+    /** Populate page model and forward to subscription page */
+    public ActionForward getGroups(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetGroups.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                PlanetGroupForm form = (PlanetGroupForm)actionForm;
+                if (request.getParameter("groupHandle") != null)
+                {
+                    String feedUrl = request.getParameter("groupHandle");
+                    PlanetGroupData group = planet.getGroup(feedUrl);
+                    form.copyFrom(group, request.getLocale());
+                }
+                else 
+                {
+                    form.doReset(mapping, request);
+                }
+                request.setAttribute("model", 
+                    new GroupsPageModel(request, response, mapping));
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+
+    /** Cancel editing, reset form */
+    public ActionForward cancelEditing(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetGroups.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                PlanetGroupForm form = (PlanetGroupForm)actionForm;
+                
+                form.doReset(mapping, request);
+                
+                request.setAttribute("model", 
+                    new GroupsPageModel(request, response, mapping));
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            request.getSession().getServletContext().log("ERROR", e);
+            throw new ServletException(e);
+        }
+        return forward;
+    }
+    
+    /** Delete subscription, reset form  */
+    public ActionForward deleteGroup(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException 
+    {
+        ActionForward forward = mapping.findForward("planetGroups.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                PlanetGroupForm form = (PlanetGroupForm)actionForm;
+                if (form.getHandle() != null)
+                {
+                    PlanetGroupData group = planet.getGroup(form.getHandle());
+                    planet.deleteGroup(group);
+                    roller.commit();
+                    roller.release();
+                    
+                    roller.begin();
+                    form.doReset(mapping, request);
+                    
+                    request.setAttribute("model", 
+                        new GroupsPageModel(request, response, mapping));
+                    
+                    ActionMessages messages = new ActionMessages();
+                    messages.add(null, 
+                        new ActionMessage("planetSubscription.success.deleted"));
+                    saveMessages(request, messages);
+                }
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (Exception e)
+        {
+            ActionErrors errors = new ActionErrors();
+            errors.add(null, new ActionError("planetGroup.error.deleting"));
+            saveErrors(request, errors);       
+        }
+        return forward;
+    }
+
+    /** Save subscription, add to "external" group */
+    public ActionForward saveGroup(ActionMapping mapping,
+            ActionForm actionForm, HttpServletRequest request,
+            HttpServletResponse response) throws IOException, ServletException
+    {
+        ActionForward forward = mapping.findForward("planetGroups.page");
+        try
+        {
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            if (rreq.isUserAuthorizedToEdit())
+            {
+                PlanetGroupForm form = (PlanetGroupForm)actionForm;
+                Roller roller = rreq.getRoller();
+                PlanetManager planet = roller.getPlanetManager();
+                ActionErrors errors = validate(planet, form);
+                if (errors.isEmpty())
+                {
+                    PlanetGroupData group = null;
+                    if (form.getId() == null || form.getId().trim().length() == 0)
+                    {
+                        group = new PlanetGroupData();
+                    }
+                    else 
+                    {
+                        group = planet.getGroupById(form.getId());
+                    }                
+                    form.copyTo(group, request.getLocale());
+                    planet.saveGroup(group);           
+                    roller.commit();
+
+                    ActionMessages messages = new ActionMessages();
+                    messages.add(null, 
+                            new ActionMessage("planetGroups.success.saved"));
+                    saveMessages(request, messages);
+                    form.doReset(mapping, request);
+
+                    request.setAttribute("model", 
+                            new GroupsPageModel(request, response, mapping));
+                }
+                else
+                {
+                    saveErrors(request, errors);
+                }
+            }
+            else
+            {
+                forward = mapping.findForward("access-denied");
+            }
+        }
+        catch (RollerException e)
+        {
+            ActionErrors errors = new ActionErrors();
+            errors.add(null, new ActionError(
+              "planetSubscriptions.error.duringSave",e.getRootCauseMessage()));
+            saveErrors(request, errors);
+        }
+        return forward;
+    }
+    
+    /** Validate posted group */
+    private ActionErrors validate(
+            PlanetManager planet, PlanetGroupForm form)
+    {
+        ActionErrors errors = new ActionErrors();
+        if (form.getTitle()==null || form.getTitle().trim().length()==0)
+        {            
+            errors.add(null, new ActionError("planetGroups.error.title"));
+        }
+        if (form.getHandle()==null || form.getHandle().trim().length()==0)
+        {            
+            errors.add(null, new ActionError("planetGroups.error.handle"));
+        }
+        if (form.getHandle() != null && 
+        (form.getHandle().equals("all") || form.getHandle().equals("external")))
+        {
+           errors.add(null, new ActionError("planetGroups.error.nameReserved"));
+        }
+        return errors;
+    }
+
+    /** Page model */
+    public class GroupsPageModel extends BasePageModel
+    {
+        private List groups = new ArrayList();
+        private boolean unconfigured = false;
+        public GroupsPageModel(
+            HttpServletRequest request,
+            HttpServletResponse response,
+            ActionMapping mapping) throws RollerException
+        {
+            super(request, response, mapping);
+            RollerRequest rreq = RollerRequest.getRollerRequest(request);
+            Roller roller = rreq.getRoller();
+            PlanetManager planet = roller.getPlanetManager();            
+            PlanetGroupData externalGroup = planet.getGroup("external");
+            if (externalGroup != null) 
+            {
+                Iterator allgroups = planet.getGroups().iterator();
+                while (allgroups.hasNext()) 
+                {
+                    PlanetGroupData agroup = (PlanetGroupData)allgroups.next();
+                    if (    !agroup.getHandle().equals("external")
+                         && !agroup.getHandle().equals("all")) 
+                      {
+                          groups.add(agroup);
+                      }
+                }
+            }
+            else 
+            {
+                unconfigured = true;
+            }
+        }
+        public List getGroups()
+        {
+            return groups;
+        }
+        public boolean isUnconfigured()
+        {
+            return unconfigured;
+        }
+    }
+}

Added: incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetSubscriptionFormEx.java
URL: http://svn.apache.org/viewcvs/incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetSubscriptionFormEx.java?rev=327589&view=auto
==============================================================================
--- incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetSubscriptionFormEx.java (added)
+++ incubator/roller/branches/roller_1.x/src/org/roller/presentation/planet/PlanetSubscriptionFormEx.java Fri Oct 21 14:27:36 2005
@@ -0,0 +1,28 @@
+package org.roller.presentation.planet;
+
+import java.util.Locale;
+import org.roller.pojos.PlanetSubscriptionData;
+import org.roller.presentation.forms.PlanetSubscriptionForm;
+
+/**
+ * @struts.form name="planetSubscriptionFormEx"
+ */
+public class PlanetSubscriptionFormEx 
+    extends    PlanetSubscriptionForm
+    implements java.io.Serializable
+{
+    private String groupHandle = null;
+    public PlanetSubscriptionFormEx() 
+    {
+        super();
+    }
+    public String getGroupHandle() 
+    {
+        return groupHandle;
+    }
+    public void setGroupHandle(String groupHandle) 
+    {
+        this.groupHandle = groupHandle;
+    }
+}
+