Attached is an updated patch for the directory listings cache. I've made
the following changes:

  * the cache is now implemented in a separate class 
    (org.apache.catalina.util.ExpiringCache)

  * added the following servlet parameters:
      - listings-cache [true]
      - listings-cache-size [1000]
      - listings-cache-ttl [5000]

  * fixed the cache to key on the full resource path
    (I was under the mistaken impression that contextPath was enough)

I also modified the synchronization behavior a bit. Previously if
directory A were being rendered then a request for directory B would
wait for it to finish before proceeding. With this patch a request will
only wait if a previous request is already rendering the same directory.
I believe this is closer to the ideal behavior since threads will never
waste resources rendering the same directory twice, but independent
directory listings will proceed in parallel.

Please let me know if you would like any further changes.
-- 
Rafael H. Schloming <[EMAIL PROTECTED]>
Index: container/catalina/src/share/org/apache/catalina/servlets/DefaultServlet.java
===================================================================
--- container/catalina/src/share/org/apache/catalina/servlets/DefaultServlet.java	(revision 347964)
+++ container/catalina/src/share/org/apache/catalina/servlets/DefaultServlet.java	(working copy)
@@ -32,6 +32,7 @@
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
 import java.util.Enumeration;
 import java.util.StringTokenizer;
 import java.util.Vector;
@@ -54,6 +55,7 @@
 import javax.xml.transform.stream.StreamSource;
 
 import org.apache.catalina.Globals;
+import org.apache.catalina.util.ExpiringCache;
 import org.apache.catalina.util.ServerInfo;
 import org.apache.catalina.util.StringManager;
 import org.apache.catalina.util.URLEncoder;
@@ -144,14 +146,32 @@
      * the platform default is used.
      */
     protected String fileEncoding = null;
-    
-    
+
+
     /**
      * Minimum size for sendfile usage in bytes.
      */
     protected int sendfileSize = 48 * 1024;
-    
-    
+
+
+    /**
+     * The maximum number of directory listings to cache.
+     */
+    protected int listingsCacheSize = 1000;
+
+
+    /**
+     * The maximum lifetime of cached directory listings.
+     */
+    protected long listingsCacheTTL = 5000; // 5 seconds
+
+
+    /**
+     * The expiring cache of directory listings, or null if this
+     * feature is disabled.
+     */
+    protected ExpiringCache listingsCache = null;
+
     // ----------------------------------------------------- Static Initializer
 
 
@@ -253,7 +273,36 @@
         } catch (Throwable t) {
             ;
         }
+        try {
+            value = getServletConfig().getInitParameter("listings-cache-size");
+            listingsCacheSize = Integer.parseInt(value);
+        } catch (Throwable t) {
+            ;
+        }
+        try {
+            value = getServletConfig().getInitParameter("listings-cache-ttl");
+            listingsCacheTTL = Long.parseLong(value);
+        } catch (Throwable t) {
+            ;
+        }
+        try {
+            value = getServletConfig().getInitParameter("listings-cache");
+            if (value == null || new Boolean(value).booleanValue()) {
+                listingsCache = new ExpiringCache
+                    (100, listingsCacheSize, listingsCacheTTL, (float) 0.75) {
+                    protected Object key(Object[] args) {
+                        return args[0] + "/" + ((CacheEntry) args[1]).name;
+                    }
 
+                    protected Object fault(Object[] args) {
+                        return doRender((String) args[0], (CacheEntry) args[1]);
+                    }
+                };
+            }
+        } catch (Throwable t) {
+            ;
+        }
+
         globalXsltFile = getServletConfig().getInitParameter("globalXsltFile");
         localXsltFile = getServletConfig().getInitParameter("localXsltFile");
         readmeFile = getServletConfig().getInitParameter("readmeFile");
@@ -1111,22 +1160,36 @@
         return result;
     }
 
+    protected InputStream render
+        (String contextPath, CacheEntry cacheEntry) {
+        byte[] bytes;
+        if (listingsCache == null) {
+            bytes = doRender(contextPath, cacheEntry);
+        } else {
+            bytes = (byte[]) listingsCache.get(new Object[] {contextPath, cacheEntry});
+        }
+        return new ByteArrayInputStream(bytes);
+    }
 
-
     /**
      *  Decide which way to render. HTML or XML.
      */
-    protected InputStream render
+    protected byte[] doRender
         (String contextPath, CacheEntry cacheEntry) {
         InputStream xsltInputStream =
             findXsltInputStream(cacheEntry.context);
 
+        String str;
         if (xsltInputStream==null) {
-            return renderHtml(contextPath, cacheEntry);
+            str = renderHtml(contextPath, cacheEntry);
         } else {
-            return renderXml(contextPath, cacheEntry, xsltInputStream);
+            str = renderXml(contextPath, cacheEntry, xsltInputStream);
         }
-
+        try {
+            return str.getBytes("UTF8");
+        } catch (UnsupportedEncodingException e) {
+            return str.getBytes();
+        }
     }
 
     /**
@@ -1136,9 +1199,9 @@
      * @param contextPath Context path to which our internal paths are
      *  relative
      */
-    protected InputStream renderXml(String contextPath,
-                                    CacheEntry cacheEntry,
-                                    InputStream xsltInputStream) {
+    protected String renderXml(String contextPath,
+                               CacheEntry cacheEntry,
+                               InputStream xsltInputStream) {
 
         StringBuffer sb = new StringBuffer();
 
@@ -1230,12 +1293,11 @@
             Source xslSource = new StreamSource(xsltInputStream);
             Transformer transformer = tFactory.newTransformer(xslSource);
 
-            ByteArrayOutputStream stream = new ByteArrayOutputStream();
-            OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8");
-            StreamResult out = new StreamResult(osWriter);
+            StringWriter writer = new StringWriter();
+            StreamResult out = new StreamResult(writer);
             transformer.transform(xmlSource, out);
-            osWriter.flush();
-            return (new ByteArrayInputStream(stream.toByteArray()));
+            writer.flush();
+            return writer.toString();
         } catch (Exception e) {
             log("directory transform failure: " + e.getMessage());
             return renderHtml(contextPath, cacheEntry);
@@ -1249,7 +1311,7 @@
      * @param contextPath Context path to which our internal paths are
      *  relative
      */
-    protected InputStream renderHtml
+    protected String renderHtml
         (String contextPath, CacheEntry cacheEntry) {
 
         String name = cacheEntry.name;
@@ -1261,17 +1323,6 @@
         if (name.equals("/"))
             trim = 1;
 
-        // Prepare a writer to a buffered area
-        ByteArrayOutputStream stream = new ByteArrayOutputStream();
-        OutputStreamWriter osWriter = null;
-        try {
-            osWriter = new OutputStreamWriter(stream, "UTF8");
-        } catch (Exception e) {
-            // Should never happen
-            osWriter = new OutputStreamWriter(stream);
-        }
-        PrintWriter writer = new PrintWriter(osWriter);
-
         StringBuffer sb = new StringBuffer();
         
         // rewriteUrl(contextPath) is expensive. cache result for later reuse
@@ -1406,11 +1457,7 @@
         sb.append("</body>\r\n");
         sb.append("</html>\r\n");
 
-        // Return an input stream to the underlying bytes
-        writer.write(sb.toString());
-        writer.flush();
-        return (new ByteArrayInputStream(stream.toByteArray()));
-
+        return sb.toString();
     }
 
 
Index: container/catalina/src/share/org/apache/catalina/util/ExpiringCache.java
===================================================================
--- container/catalina/src/share/org/apache/catalina/util/ExpiringCache.java	(revision 0)
+++ container/catalina/src/share/org/apache/catalina/util/ExpiringCache.java	(revision 0)
@@ -0,0 +1,147 @@
+package org.apache.catalina.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * The ExpiringCache class implements a size limited cache that
+ * automatically refreshes each stored value after a fixed timeout has
+ * elapsed. This class is abstract and must be extended with [EMAIL PROTECTED]
+ * key(Object[])} and [EMAIL PROTECTED] fault(Object[])} methods in order to be
+ * used. This implementation is thread safe.
+ *
+ * Example:
+ *
+ * <pre>
+ *     ...
+ *     private ExpiringCache xmlCache = new ExpiringCache(100, 1000, 5000, 0.75) {
+ *         protected Object key(Object[] args) {
+ *             File file = (File) args[0];
+ *             return file.getCanonicalName();
+ *         }
+ *         protected Object fault(Object[] args) {
+ *             File file = (File) args[0];
+ *             // expensive XML parsing/processing
+ *             XMLObject xml = readXML(file);
+ *             return xml;
+ *         }
+ *     }
+ *
+ *     public XMLObject getXML(File file) {
+ *         return (XMLObject) xmlCache.get(new Object[] {file});
+ *     }
+ *     ...
+ * </pre>
+ *
+ * @author Rafael H. Schloming &lt;[EMAIL PROTECTED]&gt;
+ **/
+
+public abstract class ExpiringCache {
+
+    /**
+     * The maximum number of cache entries.
+     */
+    private int size;
+
+    /**
+     * The time until an entry is considered stale.
+     */
+    private long timeout;
+
+    /**
+     * The cache entries.
+     */
+    private Map entries;
+
+    /**
+     * Constructs a new ExpiringCache with the specified parameters.
+     *
+     * @param initial The initial size of the cache.
+     * @param size The maximum size of the cache.
+     * @param timeout The time until an entry is considered stale.
+     * @param loadFactor The loadFactor passed to the map used to
+     *                   store entries.
+     */
+    public ExpiringCache(int initial, int size, long timeout, float loadFactor) {
+        this.size = size;
+        this.timeout = timeout;
+        this.entries = new LinkedHashMap(initial, loadFactor, true) {
+            protected boolean removeEldestEntry(Map.Entry entry) {
+                return size() > ExpiringCache.this.size;
+            }
+        };
+    }
+
+    /**
+     * Retrieves and if necessary faults in a cache value. Access is
+     * synchronized so that no more than one thread at a time will
+     * fault in the value associated with a given key. This
+     * implementation uses the abstract [EMAIL PROTECTED] key(Object[])} [EMAIL PROTECTED]
+     * fault(Object[])} methods to compute the key for cache lookup
+     * and if necessary fault in a value.
+     *
+     * @param args The args used to identify and/or fault in the
+     *             desired value.
+     * @return The cached value.
+     */
+    public Object get(Object[] args) {
+        Object key = key(args);
+        Entry entry;
+        synchronized (entries) {
+            entry = (Entry) entries.get(key);
+            if (entry == null) {
+                entry = new Entry();
+                entries.put(key, entry);
+            }
+        }
+        if (isValid(entry)) {
+            return entry.value;
+        }
+        synchronized (entry) {
+            if (!isValid(entry)) {
+                entry.value = fault(args);
+                entry.timestamp = System.currentTimeMillis();
+            }
+            return entry.value;
+        }
+    }
+
+    /**
+     * Compute the key for a given cache row.
+     */
+    protected abstract Object key(Object[] args);
+
+    /**
+     * Fault in the value for a given cache row.
+     */
+    protected abstract Object fault(Object[] args);
+
+    /**
+     * Validates the cache entry.
+     *
+     * @param entry The cache entry.
+     * @return true iff <var>entry</var> contains a valid value
+     */
+    private boolean isValid(Entry entry) {
+        return System.currentTimeMillis() - entry.timestamp < timeout;
+    }
+
+    /**
+     * Helper class used to store a cache value and its associated
+     * timestmap.
+     */
+    private static class Entry {
+
+        /**
+         * The cached value.
+         */
+        Object value;
+
+        /**
+         * The timestamp when the value was computed.
+         */
+        long timestamp;
+
+    }
+
+}
Index: container/catalina/src/conf/web.xml
===================================================================
--- container/catalina/src/conf/web.xml	(revision 347964)
+++ container/catalina/src/conf/web.xml	(working copy)
@@ -39,6 +39,14 @@
   <!--                       entries can be slow and may consume            -->
   <!--                       significant proportions of server resources.   -->
   <!--                                                                      -->
+  <!--   listings-cache      Should directory listings be cached. [true]    -->
+  <!--                                                                      -->
+  <!--   listings-cache-size The maximum number of directory listings to    -->
+  <!--                       cache. [1000]                                  -->
+  <!--                                                                      -->
+  <!--   listings-cache-ttl  The maximum lifetime in milliseconds to cache  -->
+  <!--                       directory listings before refreshing. [5000]   -->
+  <!--                                                                      -->
   <!--   output              Output buffer size (in bytes) when writing     -->
   <!--                       resources to be served.  [2048]                -->
   <!--                                                                      -->

---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to