Java Mailing List Archive

http://www.junlu.com/

Home » dev-digest.tomcat »

dev Digest 14 Jul 2010 22:12:34 -0000 Issue 5279

dev-digest-help

2010-07-14


Author LoginPost Reply

dev Digest 14 Jul 2010 22:12:34 -0000 Issue 5279

Topics (messages 107826 through 107835):

svn commit: r964208 - in /tomcat/trunk: bin/setclasspath.bat bin/setclasspath.sh webapps/docs/changelog.xml
 107826 by: markt.apache.org

svn commit: r964211 - in /tomcat/trunk: res/tomcat.nsi webapps/docs/changelog.xml
 107827 by: markt.apache.org

DO NOT REPLY [Bug 49182] Documentation patch for setclasspath.sh
 107828 by: bugzilla.apache.org

svn commit: r964215 - in /tomcat/trunk: java/org/apache/catalina/loader/WebappClassLoader.java webapps/docs/changelog.xml
 107829 by: markt.apache.org

DO NOT REPLY [Bug 49128] loader.WebappClassLoader.start() ignores Exception
 107830 by: bugzilla.apache.org

DO NOT REPLY [Bug 49130] NSIS - clarify that service is always installed
 107831 by: bugzilla.apache.org

DO NOT REPLY [Bug 49127] SimpleTcpReplicationManager.startInternal() ignores Exception
 107832 by: bugzilla.apache.org

svn commit: r964216 - in /tomcat/trunk: java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java webapps/docs/changelog.xml
 107833 by: markt.apache.org

svn commit: r964219 [2/2] - in /tomcat/trunk: java/org/apache/catalina/filters/ test/org/apache/catalina/filters/ webapps/docs/ webapps/docs/config/
 107834 by: markt.apache.org

svn commit: r964219 [1/2] - in /tomcat/trunk: java/org/apache/catalina/filters/ test/org/apache/catalina/filters/ webapps/docs/ webapps/docs/config/
 107835 by: markt.apache.org

Administrivia:

---------------------------------------------------------------------
To post to the list, e-mail: dev@(protected)
To unsubscribe, e-mail: dev-digest-unsubscribe@(protected)
For additional commands, e-mail: dev-digest-help@(protected)

----------------------------------------------------------------------


Attachment: dev_107826.ezm (zipped)
Author: markt
Date: Wed Jul 14 21:24:45 2010
New Revision: 964208

URL: http://svn.apache.org/viewvc?rev=964208&view=rev
Log:
Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=49182
Align setclasspath comments with reality.
Based on a patch provided by sebb.

Modified:
  tomcat/trunk/bin/setclasspath.bat
  tomcat/trunk/bin/setclasspath.sh
  tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/bin/setclasspath.bat
URL: http://svn.apache.org/viewvc/tomcat/trunk/bin/setclasspath.bat?rev=964208&r1=964207&r2=964208&view=diff
==============================================================================
--- tomcat/trunk/bin/setclasspath.bat (original)
+++ tomcat/trunk/bin/setclasspath.bat Wed Jul 14 21:24:45 2010
@@(protected)
rem limitations under the License.

rem ---------------------------------------------------------------------------
-rem Set CLASSPATH and Java options
+rem Set JAVA_HOME or JRE_HOME if not already set, ensure any provided settings
+rem are valid and consistent with the selected start-up options and set up the
+rem endorsed directory.
rem
rem $Id$
rem ---------------------------------------------------------------------------

Modified: tomcat/trunk/bin/setclasspath.sh
URL: http://svn.apache.org/viewvc/tomcat/trunk/bin/setclasspath.sh?rev=964208&r1=964207&r2=964208&view=diff
==============================================================================
--- tomcat/trunk/bin/setclasspath.sh (original)
+++ tomcat/trunk/bin/setclasspath.sh Wed Jul 14 21:24:45 2010
@@(protected) @@
# limitations under the License.

# -----------------------------------------------------------------------------
-# Set CLASSPATH and Java options
+# Set JAVA_HOME or JRE_HOME if not already set, ensure any provided settings
+# are valid and consistent with the selected start-up options and set up the
+# endorsed directory.
#
# $Id$
# -----------------------------------------------------------------------------

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=964208&r1=964207&r2=964208&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 14 21:24:45 2010
@@(protected) @@
     one of them fails, do not ignore the others. (markt/kkolinko)
    </fix>
    <fix>
+     <bug>49182</bug>: Align comments in setclasspath.[sh|bat] with
+     behaviour. Based on a patch provided by sebb. (markt)
+    </fix>
+    <fix>
     <bug>49230</bug>: Enhance JRE leak prevention listener with protection
     for the keep-alive thread started by
     <code>sun.net.www.http.HttpClient</code>. Based on a patch provided by




Attachment: dev_107827.ezm (zipped)
Author: markt
Date: Wed Jul 14 21:29:44 2010
New Revision: 964211

URL: http://svn.apache.org/viewvc?rev=964211&view=rev
Log:
Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=49130
Better describe the core package
Patch provided by sebb.

Modified:
  tomcat/trunk/res/tomcat.nsi
  tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/res/tomcat.nsi
URL: http://svn.apache.org/viewvc/tomcat/trunk/res/tomcat.nsi?rev=964211&r1=964210&r2=964211&view=diff
==============================================================================
--- tomcat/trunk/res/tomcat.nsi (original)
+++ tomcat/trunk/res/tomcat.nsi Wed Jul 14 21:29:44 2010
@@(protected)}
 ;Component-selection page
  ;Descriptions
  LangString DESC_SecTomcat ${LANG_ENGLISH} "Install the Tomcat Servlet container."
-   LangString DESC_SecTomcatCore ${LANG_ENGLISH} "Install the Tomcat Servlet container core."
+   LangString DESC_SecTomcatCore ${LANG_ENGLISH} "Install the Tomcat Servlet container core and create the Windows service."
  LangString DESC_SecTomcatService ${LANG_ENGLISH} "Automatically start Tomcat when the computer is started."
  LangString DESC_SecTomcatNative ${LANG_ENGLISH} "Install APR based Tomcat native .dll for better performance and scalability in production environments."
  LangString DESC_SecMenu ${LANG_ENGLISH} "Create a Start Menu program group for Tomcat."

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=964211&r1=964210&r2=964211&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 14 21:29:44 2010
@@(protected) @@
    </fix>
  </changelog>
 </subsection>
+ <subsection name="Other">
+   <changelog>
+    <fix>
+     <bug>49130</bug>: Better describe the core package in the Windows
+     installer, making it clear that the service will be installed. Patch
+     provided by sebb. (markt)
+    </fix>
+   </changelog>
+ </subsection>
</section>
<section name="Tomcat 7.0.0 (markt)" rtext="beta, 2010-06-29">
 <subsection name="Catalina">




Attachment: dev_107828.ezm (zipped)
https://issues.apache.org/bugzilla/show_bug.cgi?id=49182

Mark Thomas <markt@(protected):

      What   |Removed              |Added
----------------------------------------------------------------------------
        Status|NEW                 |RESOLVED
     Resolution|                   |FIXED

--- Comment #1 from Mark Thomas <markt@(protected) ---
Alternative patch based on the supplied patch applied to both .sh and .bat
files in 7.0.x. Will be included in 7.0.1 onwards.

--
Configure bugmail: https://issues.apache.org/bugzilla/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
You are the assignee for the bug.


Attachment: dev_107829.ezm (zipped)
Author: markt
Date: Wed Jul 14 21:35:37 2010
New Revision: 964215

URL: http://svn.apache.org/viewvc?rev=964215&view=rev
Log:
Fxi https://issues.apache.org/bugzilla/show_bug.cgi?id=49128
Don't swallow exceptions unnecessarily

Modified:
  tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java
  tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java?rev=964215&r1=964214&r2=964215&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java (original)
+++ tomcat/trunk/java/org/apache/catalina/loader/WebappClassLoader.java Wed Jul 14 21:35:37 2010
@@(protected)
     String encoding = null;
     try {
        encoding = System.getProperty("file.encoding");
-     } catch (Exception e) {
+     } catch (SecurityException e) {
        return;
     }
     if (encoding.indexOf("EBCDIC")!=-1) {

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=964215&r1=964214&r2=964215&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 14 21:35:37 2010
@@(protected) @@
     one of them fails, do not ignore the others. (markt/kkolinko)
    </fix>
    <fix>
+     <bug>49128</bug>: Don't swallow exceptions unnecessarily in
+     <code>WebappClassLoader.start()</code>. (markt)
+    </fix>
+    <fix>
     <bug>49182</bug>: Align comments in setclasspath.[sh|bat] with
     behaviour. Based on a patch provided by sebb. (markt)
    </fix>




Attachment: dev_107830.ezm (zipped)
https://issues.apache.org/bugzilla/show_bug.cgi?id=49128

Mark Thomas <markt@(protected):

      What   |Removed              |Added
----------------------------------------------------------------------------
        Status|NEW                 |RESOLVED
     Resolution|                   |FIXED

--- Comment #1 from Mark Thomas <markt@(protected) ---
Fixed in 7.0.x and will be in 7.0.1 onwards.

--
Configure bugmail: https://issues.apache.org/bugzilla/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
You are the assignee for the bug.


Attachment: dev_107831.ezm (zipped)
https://issues.apache.org/bugzilla/show_bug.cgi?id=49130

Mark Thomas <markt@(protected):

      What   |Removed              |Added
----------------------------------------------------------------------------
        Status|NEW                 |RESOLVED
     Resolution|                   |FIXED

--- Comment #1 from Mark Thomas <markt@(protected) ---
Thanks. Patch applied to trunk (with one additional word). Will be included in
7.0.1 onwards.

--
Configure bugmail: https://issues.apache.org/bugzilla/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
You are the assignee for the bug.


Attachment: dev_107832.ezm (zipped)
https://issues.apache.org/bugzilla/show_bug.cgi?id=49127

Mark Thomas <markt@(protected):

      What   |Removed              |Added
----------------------------------------------------------------------------
        Status|NEW                 |RESOLVED
     Resolution|                   |FIXED

--- Comment #1 from Mark Thomas <markt@(protected) ---
Fixed in 7.0.x will be in 7.0.1 onwards.

--
Configure bugmail: https://issues.apache.org/bugzilla/userprefs.cgi?tab=email
------- You are receiving this mail because: -------
You are the assignee for the bug.


Attachment: dev_107833.ezm (zipped)
Author: markt
Date: Wed Jul 14 21:39:40 2010
New Revision: 964216

URL: http://svn.apache.org/viewvc?rev=964216&view=rev
Log:
Fxi https://issues.apache.org/bugzilla/show_bug.cgi?id=49127
Don't swallow exceptions unnecessarily

Modified:
  tomcat/trunk/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java
  tomcat/trunk/webapps/docs/changelog.xml

Modified: tomcat/trunk/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java?rev=964216&r1=964215&r2=964216&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java (original)
+++ tomcat/trunk/java/org/apache/catalina/ha/session/SimpleTcpReplicationManager.java Wed Jul 14 21:39:40 2010
@@(protected)
          do {
             try {
                Thread.sleep(100);
-             }catch ( Exception sleep) {}
+             } catch (InterruptedException sleep) { /* Ignore */ }
             reqNow = System.currentTimeMillis();
             isTimeout=((reqNow-reqStart)>(1000*60));
          } while ( (!isStateTransferred()) && (!isTimeout));

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=964216&r1=964215&r2=964216&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 14 21:39:40 2010
@@(protected) @@
 <subsection name="Cluster">
  <changelog>
    <fix>
+     <bug>49127</bug>: Don't swallow exceptions unnecessarily in
+     <code>SimpleTcpReplicationManager.startInternal()</code>. (markt)
+    </fix>
+    <fix>
     <bug>49445</bug>: When session ID is changed after authentication,
     ensure the DeltaManager replicates the change in ID to the other nodes
     in the cluster. (kfujino)




Attachment: dev_107834.ezm (zipped)
Modified: tomcat/trunk/webapps/docs/config/filter.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/filter.xml?rev=964219&r1=964218&r2=964219&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/config/filter.xml (original)
+++ tomcat/trunk/webapps/docs/config/filter.xml Wed Jul 14 22:11:30 2010
@@(protected) @@

</section>

+<section name="Expires Filter">
+
+ <subsection name="Introduction">
+
+   <p>
+   ExpiresFilter is a Java Servlet API port of <a
+   href="http://httpd.apache.org/docs/2.2/mod/mod_expires.html">Apache
+   mod_expires</a>.
+   This filter controls the setting of the <tt>Expires</tt> HTTP header and the
+   <tt>max-age</tt> directive of the <tt>Cache-Control</tt> HTTP header in
+   server responses. The expiration date can set to be relative to either the
+   time the source file was last modified, or to the time of the client access.
+   </p>
+  
+   <p>
+   These HTTP headers are an instruction to the client about the document&#x27;s
+   validity and persistence. If cached, the document may be fetched from the
+   cache rather than from the source until this time has passed. After that, the
+   cache copy is considered &quot;expired&quot; and invalid, and a new copy must
+   be obtained from the source.
+   </p>
+   <p>
+   To modify <tt>Cache-Control</tt> directives other than <tt>max-age</tt> (see
+   <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9" >RFC
+   2616 section 14.9</a>), you can use other servlet filters or <a
+   href="http://httpd.apache.org/docs/2.2/mod/mod_headers.html" >Apache Httpd
+   mod_headers</a> module.
+   </p>
+    
+ </subsection>
+
+ <subsection name="Basic configuration sample">
+   <p>
+   Basic configuration to add '<tt>Expires</tt>' and '<tt>Cache-Control: max-age=</tt>'
+   headers to images, css and javascript.
+   </p>
+
+   <source>
+&lt;filter&gt;
+ &lt;filter-name&gt;ExpiresFilter&lt;/filter-name&gt;
+ &lt;filter-class&gt;org.apache.catalina.filters.ExpiresFilter&lt;/filter-class&gt;
+ &lt;init-param&gt;
+   &lt;param-name&gt;ExpiresByType image&lt;/param-name&gt;
+   &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ &lt;/init-param&gt;
+ &lt;init-param&gt;
+   &lt;param-name&gt;ExpiresByType text/css&lt;/param-name&gt;
+   &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ &lt;/init-param&gt;
+ &lt;init-param&gt;
+   &lt;param-name&gt;ExpiresByType text/javascript&lt;/param-name&gt;
+   &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ &lt;/init-param&gt;
+&lt;/filter&gt;
+...
+&lt;filter-mapping&gt;
+ &lt;filter-name&gt;ExpiresFilter&lt;/filter-name&gt;
+ &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+&lt;/filter-mapping&gt;
+
+   </source>
+  
+ </subsection>
+
+ <subsection name="Alternate Syntax">
+   <p>
+   The <tt>ExpiresDefault</tt> and <tt>ExpiresByType</tt> directives can also be
+   defined in a more readable syntax of the form:
+   </p>
+  
+   <source>
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;
+ &lt;param-value&gt;&lt;base&gt; [plus] {&lt;num&gt;  &lt;type&gt;}*&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresByType type&lt;/param-name&gt;
+ &lt;param-value&gt;&lt;base&gt; [plus]  {&lt;num&gt; &lt;type&gt;}*&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresByType type;encoding&lt;/param-name&gt;
+ &lt;param-value&gt;&lt;base&gt; [plus]  {&lt;num&gt; &lt;type&gt;}*&lt;/param-value&gt;
+&lt;/init-param&gt;
+   </source>
+   <p>
+   where <tt>&lt;base&gt;</tt> is one of:
+   <ul>
+   <li><tt>access</tt></li>
+   <li><tt>now</tt> (equivalent to &#x27;<tt>access</tt>&#x27;)</li>
+   <li><tt>modification</tt></li>
+   </ul>
+   </p>
+   <p>
+   The <tt>plus</tt> keyword is optional. <tt>&lt;num&gt;</tt> should be an
+   integer value (acceptable to <tt>Integer.parseInt()</tt>), and
+   <tt>&lt;type&gt;</tt> is one of:
+   <ul>
+   <li><tt>years</tt></li>
+   <li><tt>months</tt></li>
+   <li><tt>weeks</tt></li>
+   <li><tt>days</tt></li>
+   <li><tt>hours</tt></li>
+   <li><tt>minutes</tt></li>
+   <li><tt>seconds</tt></li>
+   </ul>
+   For example, any of the following directives can be used to make documents
+   expire 1 month after being accessed, by default:
+   </p>
+  
+   <source>
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;
+ &lt;param-value&gt;access plus 1 month&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;
+ &lt;param-value&gt;access plus 4 weeks&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;
+ &lt;param-value&gt;access plus 30 days&lt;/param-value&gt;
+&lt;/init-param&gt;
+</source>
+<p>
+The expiry time can be fine-tuned by adding several &#x27;
+<tt>&lt;num&gt; &lt;type&gt;</tt>&#x27; clauses:
+</p>
+
+<source>
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresByType text/html&lt;/param-name&gt;
+ &lt;param-value&gt;access plus 1 month 15  days 2 hours&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresByType image/gif&lt;/param-name&gt;
+ &lt;param-value&gt;modification plus 5 hours 3  minutes&lt;/param-value&gt;
+&lt;/init-param&gt;
+   </source>
+   <p>
+   Note that if you use a modification date based setting, the <tt>Expires</tt>
+   header will <strong>not</strong> be added to content that does not come from
+   a file on disk. This is due to the fact that there is no modification time
+   for such content.
+   </p>
+ </subsection>
+
+ <subsection name="Expiration headers generation eligibility">
+   <p>
+   A response is eligible to be enriched by <tt>ExpiresFilter</tt> if :
+   <ol>
+   <li>no expiration header is defined (<tt>Expires</tt> header or the
+   <tt>max-age</tt> directive of the <tt>Cache-Control</tt> header),</li>
+   <li>the response status code is not excluded by the directive
+   <tt>ExpiresExcludedResponseStatusCodes</tt>,</li>
+   <li>The <tt>Content-Type</tt> of the response matches one of the types
+   defined the in <tt>ExpiresByType</tt> directives or the
+   <tt>ExpiresDefault</tt> directive is defined.</li>
+   </ol>
+   </p>
+   <p>
+   Note : If <tt>Cache-Control</tt> header contains other directives than
+   <tt>max-age</tt>, they are concatenated with the <tt>max-age</tt> directive
+   that is added by the <tt>ExpiresFilter</tt>.
+   </p>
+
+ </subsection>
+
+ <subsection name="Expiration configuration selection">
+   <p>
+   The expiration configuration if elected according to the following algorithm:
+   <ol>
+   <li><tt>ExpiresByType</tt> matching the exact content-type returned by
+   <tt>HttpServletResponse.getContentType()</tt> possibly including the charset
+   (e.g. &#x27;<tt>text/xml;charset=UTF-8</tt>&#x27;),</li>
+   <li><tt>ExpiresByType</tt> matching the content-type without the charset if
+   <tt>HttpServletResponse.getContentType()</tt> contains a charset (e.g. &#x27;
+   <tt>text/xml;charset=UTF-8</tt>&#x27; -&gt; &#x27;<tt>text/xml</tt>&#x27;),</li>
+   <li><tt>ExpiresByType</tt> matching the major type (e.g. substring before
+   &#x27;<tt>/</tt>&#x27;) of <tt>HttpServletResponse.getContentType()</tt>
+   (e.g. &#x27;<tt>text/xml;charset=UTF-8</tt>&#x27; -&gt; &#x27;<tt>text</tt>
+   &#x27;),</li>
+   <li><tt>ExpiresDefault</tt></li>
+   </ol>
+   </p>
+ </subsection>
+
+ <subsection name="Filter Class Name">
+
+   <p>The filter class name for the Expires Filter is
+   <strong><code>org.apache.catalina.filters.ExpiresFilter</code>
+   </strong>.</p>
+
+ </subsection>
+
+ <subsection name="Initialisation parameters">
+
+   <p>The <strong>Expires Filter</strong> supports the following
+   initialisation parameters:</p>
+
+   <attributes>
+
+    <attribute name="ExpiresActive" required="false">
+     <p>
+     This directive enables or disables the generation of the <tt>Expires</tt> and
+     <tt>Cache-Control</tt> headers by this <tt>ExpiresFilter</tt>. If set to
+     <tt>Off</tt>, the headers will not be generated for any HTTP response. If set
+     to <tt>On</tt> or <tt>true</tt>, the headers will be added to served HTTP
+     responses according to the criteria defined by the
+     <tt>ExpiresByType &lt;content-type&gt;</tt> and <tt>ExpiresDefault</tt>
+     directives. Note that this directive does not guarantee that an
+     <tt>Expires</tt> or <tt>Cache-Control</tt> header will be generated. If the
+     criteria aren&#x27;t met, no header will be sent, and the effect will be as
+     though this directive wasn&#x27;t even specified.
+     </p>
+     <p>
+     Default value is <tt>true</tt>.
+     </p>
+    
+     <p>
+     <i>Sample: enable filter</i>
+     </p>
+  
+     <source>
+&lt;init-param&gt;
+ &lt;!-- supports case insensitive &#x27;On&#x27; or &#x27;true&#x27; --&gt;
+ &lt;param-name&gt;ExpiresActive&lt;/param-name&gt;
+ &lt;param-value&gt;On&lt;/param-value&gt;
+&lt;/init-param&gt;
+      </source>
+      <p>
+      <i>Sample: disable filter</i>
+      </p>
+  
+      <source>
+&lt;init-param&gt;
+ &lt;!-- supports anything different from case insensitive &#x27;On&#x27; and &#x27;true&#x27; --&gt;
+ &lt;param-name&gt;ExpiresActive&lt;/param-name&gt;
+ &lt;param-value&gt;Off&lt;/param-value&gt;
+&lt;/init-param&gt;
+      </source>
+    </attribute>
+
+    <attribute name="ExpiresExcludedResponseStatusCodes" required="false">
+      <p>
+      This directive defines the http response status codes for which the
+      <tt>ExpiresFilter</tt> will not generate expiration headers. By default, the
+      <tt>304</tt> status code (&quot;<tt>Not modified</tt>&quot;) is skipped. The
+      value is a comma separated list of http status codes.
+      </p>
+      <p>
+      This directive is useful to ease usage of <tt>ExpiresDefault</tt> directive.
+      Indeed, the behavior of <tt>304 Not modified</tt> (which does specify a
+      <tt>Content-Type</tt> header) combined with <tt>Expires</tt> and
+      <tt>Cache-Control:max-age=</tt> headers can be unnecessarily tricky to
+      understand.
+      </p>
+      <p>
+      <i>Sample : exclude response status codes 302, 500 and 503</i>
+      </p>
+      
+      <source>
+&lt;init-param&gt;
+ &lt;param-name&gt;ExpiresExcludedResponseStatusCodes&lt;/param-name&gt;
+ &lt;param-value&gt;302, 500, 503&lt;/param-value&gt;
+&lt;/init-param&gt;
+      </source>
+    </attribute>
+
+    <attribute name="ExpiresByType &lt;content-type&gt;" required="false">
+      <p>
+      This directive defines the value of the <tt>Expires</tt> header and the
+      <tt>max-age</tt> directive of the <tt>Cache-Control</tt> header generated for
+      documents of the specified type (<i>e.g.</i>, <tt>text/html</tt>). The second
+      argument sets the number of seconds that will be added to a base time to
+      construct the expiration date. The <tt>Cache-Control: max-age</tt> is
+      calculated by subtracting the request time from the expiration date and
+      expressing the result in seconds.
+      </p>
+      <p>
+      The base time is either the last modification time of the file, or the time
+      of the client&#x27;s access to the document. Which should be used is
+      specified by the <tt>&lt;code&gt;</tt> field; <tt>M</tt> means that the
+      file&#x27;s last modification time should be used as the base time, and
+      <tt>A</tt> means the client&#x27;s access time should be used. The duration
+      is expressed in seconds. <tt>A2592000</tt> stands for
+      <tt>access plus 30 days</tt> in alternate syntax.
+      </p>
+      <p>
+      The difference in effect is subtle. If <tt>M</tt> (<tt>modification</tt> in
+      alternate syntax) is used, all current copies of the document in all caches
+      will expire at the same time, which can be good for something like a weekly
+      notice that&#x27;s always found at the same URL. If <tt>A</tt> (
+      <tt>access</tt> or <tt>now</tt> in alternate syntax) is used, the date of
+      expiration is different for each client; this can be good for image files
+      that don&#x27;t change very often, particularly for a set of related
+      documents that all refer to the same images (<i>i.e.</i>, the images will be
+      accessed repeatedly within a relatively short timespan).
+      </p>
+      <p>
+      <strong>Note:</strong> When the content type includes a charset (e.g.
+      <tt>'ExpiresByType text/xml;charset=utf-8'</tt>), Tomcat removes blank chars
+      between the '<tt>;</tt>' and the '<tt>charset</tt>' keyword. Due to this,
+      configuration of an expiration with a charset must <strong>not</strong> include
+      such a space character.
+      </p>
+      <p>
+      <i>Sample:</i>
+      </p>
+      
+      <source>
+&lt;init-param&gt;
+  &lt;param-name&gt;ExpiresByType text/html&lt;/param-name&gt;
+  &lt;param-value&gt;access plus 1 month 15  days 2 hours&lt;/param-value&gt;
+&lt;/init-param&gt;
+
+&lt;init-param&gt;
+  &lt;!-- 2592000 seconds = 30 days --&gt;
+  &lt;param-name&gt;ExpiresByType image/gif&lt;/param-name&gt;
+  &lt;param-value&gt;A2592000&lt;/param-value&gt;
+&lt;/init-param&gt;
+      </source>
+      <p>
+      Note that this directive only has effect if <tt>ExpiresActive On</tt> has
+      been specified. It overrides, for the specified MIME type <i>only</i>, any
+      expiration date set by the <tt>ExpiresDefault</tt> directive.
+      </p>
+      <p>
+      You can also specify the expiration time calculation using an alternate
+      syntax, described earlier in this document.
+      </p>
+    </attribute>
+
+    <attribute name="ExpiresDefault" required="false">
+      <p>
+      This directive sets the default algorithm for calculating the
+      expiration time for all documents in the affected realm. It can be
+      overridden on a type-by-type basis by the <tt>ExpiresByType</tt> directive. See the
+      description of that directive for details about the syntax of the
+      argument, and the "alternate syntax"
+      description as well.
+      </p>
+    </attribute>
+   </attributes>
+
+ </subsection>
+
+ <subsection name="Troubleshooting">
+   <p>
+   To troubleshoot, enable logging on the
+   <tt>org.apache.catalina.filters.ExpiresFilter</tt>.
+   </p>
+   <p>
+   Extract of logging.properties
+   </p>
+  
+   <source>
+org.apache.catalina.filters.ExpiresFilter.level = FINE
+   </source>
+   <p>
+   Sample of initialization log message :
+   </p>
+  
+   <source>
+Mar 26, 2010 2:01:41 PM org.apache.catalina.filters.ExpiresFilter init
+FINE: Filter initialized with configuration ExpiresFilter[
+ active=true,
+ excludedResponseStatusCode=[304],
+ default=null,
+ byType={
+   image=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]],
+   text/css=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]],
+   text/javascript=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]]}]
+   </source>
+   <p>
+   Sample of per-request log message where <tt>ExpiresFilter</tt> adds an
+   expiration date
+   </p>
+  
+   <source>
+Mar 26, 2010 2:09:47 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody
+FINE: Request "/tomcat.gif" with response status "200" content-type "image/gif", set expiration date 3/26/10 2:19 PM
+   </source>
+   <p>
+   Sample of per-request log message where <tt>ExpiresFilter</tt> does not add
+   an expiration date
+   </p>
+  
+   <source>
+Mar 26, 2010 2:10:27 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody
+FINE: Request "/docs/config/manager.html" with response status "200" content-type "text/html", no expiration configured
+   </source>
+ </subsection>
+
+</section>

<section name="Remote Address Filter">





Attachment: dev_107835.ezm (zipped)
Author: markt
Date: Wed Jul 14 22:11:30 2010
New Revision: 964219

URL: http://svn.apache.org/viewvc?rev=964219&view=rev
Log:
Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=48998
Add the ExpiresFilter, a port of mod_expires
Patch provided by Cyrille Le Clerc

Added:
  tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java  (with props)
  tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java  (with props)
Modified:
  tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties
  tomcat/trunk/webapps/docs/changelog.xml
  tomcat/trunk/webapps/docs/config/filter.xml

Added: tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java?rev=964219&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java (added)
+++ tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java Wed Jul 14 22:11:30 2010
@@(protected) @@
+/*
+ * Copyright 2008-2009 the original author or authors.
+ *
+ * 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.apache.catalina.filters;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+import java.util.regex.Pattern;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+
+/**
+ * <p>
+ * ExpiresFilter is a Java Servlet API port of <a
+ * href="http://httpd.apache.org/docs/2.2/mod/mod_expires.html">Apache
+ * mod_expires</a> to add ' <tt>Expires</tt>' and '
+ * <tt>Cache-Control: max-age=</tt>' headers to HTTP response according to its '
+ * <tt>Content-Type</tt>'.
+ * </p>
+ *
+ * <p>
+ * Following documentation is inspired by <tt>mod_expires</tt> .
+ * </p>
+ * <h1>Summary</h1>
+ * <p>
+ * This filter controls the setting of the <tt>Expires</tt> HTTP header and the
+ * <tt>max-age</tt> directive of the <tt>Cache-Control</tt> HTTP header in
+ * server responses. The expiration date can set to be relative to either the
+ * time the source file was last modified, or to the time of the client access.
+ * </p>
+ * <p>
+ * These HTTP headers are an instruction to the client about the document&#x27;s
+ * validity and persistence. If cached, the document may be fetched from the
+ * cache rather than from the source until this time has passed. After that, the
+ * cache copy is considered &quot;expired&quot; and invalid, and a new copy must
+ * be obtained from the source.
+ * </p>
+ * <p>
+ * To modify <tt>Cache-Control</tt> directives other than <tt>max-age</tt> (see
+ * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9" >RFC
+ * 2616 section 14.9</a>), you can use other servlet filters or <a
+ * href="http://httpd.apache.org/docs/2.2/mod/mod_headers.html" >Apache Httpd
+ * mod_headers</a> module.
+ * </p>
+ * <h1>Filter Configuration</h1><h2>Basic configuration to add &#x27;
+ * <tt>Expires</tt>&#x27; and &#x27; <tt>Cache-Control: max-age=</tt>&#x27;
+ * headers to images, css and javascript</h2>
+ *
+ * <code><pre>
+ * &lt;web-app ...&gt;
+ *   ...
+ *   &lt;filter&gt;
+ *     &lt;filter-name&gt;ExpiresFilter&lt;/filter-name&gt;
+ *     &lt;filter-class&gt;org.apache.catalina.filters.ExpiresFilter&lt;/filter-class&gt;
+ *     &lt;init-param&gt;
+ *       &lt;param-name&gt;ExpiresByType image&lt;/param-name&gt;
+ *       &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ *     &lt;/init-param&gt;
+ *     &lt;init-param&gt;
+ *       &lt;param-name&gt;ExpiresByType text/css&lt;/param-name&gt;
+ *       &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ *     &lt;/init-param&gt;
+ *     &lt;init-param&gt;
+ *       &lt;param-name&gt;ExpiresByType text/javascript&lt;/param-name&gt;
+ *       &lt;param-value&gt;access plus 10 minutes&lt;/param-value&gt;
+ *     &lt;/init-param&gt;
+ *   &lt;/filter&gt;
+ *   ...
+ *   &lt;filter-mapping&gt;
+ *     &lt;filter-name&gt;ExpiresFilter&lt;/filter-name&gt;
+ *     &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
+ *     &lt;dispatcher&gt;REQUEST&lt;/dispatcher&gt;
+ *   &lt;/filter-mapping&gt;
+ *   ...
+ * &lt;/web-app&gt;
+ * </pre></code>
+ *
+ * <h2>Configuration Parameters</h2><h3>
+ * <tt>ExpiresActive</tt></h3>
+ * <p>
+ * This directive enables or disables the generation of the <tt>Expires</tt> and
+ * <tt>Cache-Control</tt> headers by this <tt>ExpiresFilter</tt>. If set to
+ * <tt>Off</tt>, the headers will not be generated for any HTTP response. If set
+ * to <tt>On</tt> or <tt>true</tt>, the headers will be added to served HTTP
+ * responses according to the criteria defined by the
+ * <tt>ExpiresByType &lt;content-type&gt;</tt> and <tt>ExpiresDefault</tt>
+ * directives. Note that this directive does not guarantee that an
+ * <tt>Expires</tt> or <tt>Cache-Control</tt> header will be generated. If the
+ * criteria aren&#x27;t met, no header will be sent, and the effect will be as
+ * though this directive wasn&#x27;t even specified.
+ * </p>
+ * <p>
+ * This parameter is optional, default value is <tt>true</tt>.
+ * </p>
+ * <p>
+ * <i>Enable filter</i>
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;!-- supports case insensitive &#x27;On&#x27; or &#x27;true&#x27; --&gt;
+ *   &lt;param-name&gt;ExpiresActive&lt;/param-name&gt;&lt;param-value&gt;On&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ * <p>
+ * <i>Disable filter</i>
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;!-- supports anything different from case insensitive &#x27;On&#x27; and &#x27;true&#x27; --&gt;
+ *   &lt;param-name&gt;ExpiresActive&lt;/param-name&gt;&lt;param-value&gt;Off&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ *
+ * <h3>
+ * <tt>ExpiresByType &lt;content-type&gt;</tt></h3>
+ * <p>
+ * This directive defines the value of the <tt>Expires</tt> header and the
+ * <tt>max-age</tt> directive of the <tt>Cache-Control</tt> header generated for
+ * documents of the specified type (<i>e.g.</i>, <tt>text/html</tt>). The second
+ * argument sets the number of seconds that will be added to a base time to
+ * construct the expiration date. The <tt>Cache-Control: max-age</tt> is
+ * calculated by subtracting the request time from the expiration date and
+ * expressing the result in seconds.
+ * </p>
+ * <p>
+ * The base time is either the last modification time of the file, or the time
+ * of the client&#x27;s access to the document. Which should be used is
+ * specified by the <tt>&lt;code&gt;</tt> field; <tt>M</tt> means that the
+ * file&#x27;s last modification time should be used as the base time, and
+ * <tt>A</tt> means the client&#x27;s access time should be used. The duration
+ * is expressed in seconds. <tt>A2592000</tt> stands for
+ * <tt>access plus 30 days</tt> in alternate syntax.
+ * </p>
+ * <p>
+ * The difference in effect is subtle. If <tt>M</tt> (<tt>modification</tt> in
+ * alternate syntax) is used, all current copies of the document in all caches
+ * will expire at the same time, which can be good for something like a weekly
+ * notice that&#x27;s always found at the same URL. If <tt>A</tt> (
+ * <tt>access</tt> or <tt>now</tt> in alternate syntax) is used, the date of
+ * expiration is different for each client; this can be good for image files
+ * that don&#x27;t change very often, particularly for a set of related
+ * documents that all refer to the same images (<i>i.e.</i>, the images will be
+ * accessed repeatedly within a relatively short timespan).
+ * </p>
+ * <p>
+ * <strong>Example:</strong>
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresByType text/html&lt;/param-name&gt;&lt;param-value&gt;access plus 1 month 15  days 2 hours&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ *
+ * &lt;init-param&gt;
+ *   &lt;!-- 2592000 seconds = 30 days --&gt;
+ *   &lt;param-name&gt;ExpiresByType image/gif&lt;/param-name&gt;&lt;param-value&gt;A2592000&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ * <p>
+ * Note that this directive only has effect if <tt>ExpiresActive On</tt> has
+ * been specified. It overrides, for the specified MIME type <i>only</i>, any
+ * expiration date set by the <tt>ExpiresDefault</tt> directive.
+ * </p>
+ * <p>
+ * You can also specify the expiration time calculation using an alternate
+ * syntax, described earlier in this document.
+ * </p>
+ * <h3>
+ * <tt>ExpiresExcludedResponseStatusCodes</tt></h3>
+ * <p>
+ * This directive defines the http response status codes for which the
+ * <tt>ExpiresFilter</tt> will not generate expiration headers. By default, the
+ * <tt>304</tt> status code (&quot;<tt>Not modified</tt>&quot;) is skipped. The
+ * value is a comma separated list of http status codes.
+ * </p>
+ * <p>
+ * This directive is useful to ease usage of <tt>ExpiresDefault</tt> directive.
+ * Indeed, the behavior of <tt>304 Not modified</tt> (which does specify a
+ * <tt>Content-Type</tt> header) combined with <tt>Expires</tt> and
+ * <tt>Cache-Control:max-age=</tt> headers can be unnecessarily tricky to
+ * understand.
+ * </p>
+ * <p>
+ * Configuration sample :
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresExcludedResponseStatusCodes&lt;/param-name&gt;&lt;param-value&gt;302, 500, 503&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ *
+ * <h3>ExpiresDefault</h3>
+ * <p>
+ * This directive sets the default algorithm for calculating the expiration time
+ * for all documents in the affected realm. It can be overridden on a
+ * type-by-type basis by the <tt>ExpiresByType</tt> directive. See the
+ * description of that directive for details about the syntax of the argument,
+ * and the "alternate syntax" description as well.
+ * </p>
+ * <h1>Alternate Syntax</h1>
+ * <p>
+ * The <tt>ExpiresDefault</tt> and <tt>ExpiresByType</tt> directives can also be
+ * defined in a more readable syntax of the form:
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;&lt;param-value&gt;&lt;base&gt; [plus] {&lt;num&gt;  &lt;type&gt;}*&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ *
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresByType type/encoding&lt;/param-name&gt;&lt;param-value&gt;&lt;base&gt; [plus]  {&lt;num&gt; &lt;type&gt;}*&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ * <p>
+ * where <tt>&lt;base&gt;</tt> is one of:
+ * <ul>
+ * <li><tt>access</tt></li>
+ * <li><tt>now</tt> (equivalent to &#x27;<tt>access</tt>&#x27;)</li>
+ * <li><tt>modification</tt></li>
+ * </ul>
+ * </p>
+ * <p>
+ * The <tt>plus</tt> keyword is optional. <tt>&lt;num&gt;</tt> should be an
+ * integer value (acceptable to <tt>Integer.parseInt()</tt>), and
+ * <tt>&lt;type&gt;</tt> is one of:
+ * <ul>
+ * <li><tt>years</tt></li>
+ * <li><tt>months</tt></li>
+ * <li><tt>weeks</tt></li>
+ * <li><tt>days</tt></li>
+ * <li><tt>hours</tt></li>
+ * <li><tt>minutes</tt></li>
+ * <li><tt>seconds</tt></li>
+ * </ul>
+ * For example, any of the following directives can be used to make documents
+ * expire 1 month after being accessed, by default:
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;&lt;param-value&gt;access plus 1 month&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ *
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;&lt;param-value&gt;access plus 4 weeks&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ *
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresDefault&lt;/param-name&gt;&lt;param-value&gt;access plus 30 days&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ * <p>
+ * The expiry time can be fine-tuned by adding several &#x27;
+ * <tt>&lt;num&gt; &lt;type&gt;</tt>&#x27; clauses:
+ * </p>
+ *
+ * <code><pre>
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresByType text/html&lt;/param-name&gt;&lt;param-value&gt;access plus 1 month 15  days 2 hours&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ *
+ * &lt;init-param&gt;
+ *   &lt;param-name&gt;ExpiresByType image/gif&lt;/param-name&gt;&lt;param-value&gt;modification plus 5 hours 3  minutes&lt;/param-value&gt;
+ * &lt;/init-param&gt;
+ * </pre></code>
+ * <p>
+ * Note that if you use a modification date based setting, the <tt>Expires</tt>
+ * header will <strong>not</strong> be added to content that does not come from
+ * a file on disk. This is due to the fact that there is no modification time
+ * for such content.
+ * </p>
+ * <h1>Expiration headers generation eligibility</h1>
+ * <p>
+ * A response is eligible to be enriched by <tt>ExpiresFilter</tt> if :
+ * <ol>
+ * <li>no expiration header is defined (<tt>Expires</tt> header or the
+ * <tt>max-age</tt> directive of the <tt>Cache-Control</tt> header),</li>
+ * <li>the response status code is not excluded by the directive
+ * <tt>ExpiresExcludedResponseStatusCodes</tt>,</li>
+ * <li>The <tt>Content-Type</tt> of the response matches one of the types
+ * defined the in <tt>ExpiresByType</tt> directives or the
+ * <tt>ExpiresDefault</tt> directive is defined.</li>
+ * </ol>
+ * </p>
+ * <p>
+ * Note :
+ * <ul>
+ * <li>If <tt>Cache-Control</tt> header contains other directives than
+ * <tt>max-age</tt>, they are concatenated with the <tt>max-age</tt> directive
+ * that is added by the <tt>ExpiresFilter</tt>.</li>
+ * </ul>
+ * </p>
+ * <h1>Expiration configuration selection</h1>
+ * <p>
+ * The expiration configuration if elected according to the following algorithm:
+ * <ol>
+ * <li><tt>ExpiresByType</tt> matching the exact content-type returned by
+ * <tt>HttpServletResponse.getContentType()</tt> possibly including the charset
+ * (e.g. &#x27;<tt>text/xml;charset=UTF-8</tt>&#x27;),</li>
+ * <li><tt>ExpiresByType</tt> matching the content-type without the charset if
+ * <tt>HttpServletResponse.getContentType()</tt> contains a charset (e.g. &#x27;
+ * <tt>text/xml;charset=UTF-8</tt>&#x27; -&gt; &#x27;<tt>text/xml</tt>&#x27;),</li>
+ * <li><tt>ExpiresByType</tt> matching the major type (e.g. substring before
+ * &#x27;<tt>/</tt>&#x27;) of <tt>HttpServletResponse.getContentType()</tt>
+ * (e.g. &#x27;<tt>text/xml;charset=UTF-8</tt>&#x27; -&gt; &#x27;<tt>text</tt>
+ * &#x27;),</li>
+ * <li><tt>ExpiresDefault</tt></li>
+ * </ol>
+ * </p>
+ * <h1>Implementation Details</h1><h2>When to write the expiration headers ?</h2>
+ * <p>
+ * The <tt>ExpiresFilter</tt> traps the &#x27;on before write response
+ * body&#x27; event to decide whether it should generate expiration headers or
+ * not.
+ * </p>
+ * <p>
+ * To trap the &#x27;before write response body&#x27; event, the
+ * <tt>ExpiresFilter</tt> wraps the http servlet response&#x27;s writer and
+ * outputStream to intercept calls to the methods <tt>write()</tt>,
+ * <tt>print()</tt>, <tt>close()</tt> and <tt>flush()</tt>. For empty response
+ * body (e.g. empty files), the <tt>write()</tt>, <tt>print()</tt>,
+ * <tt>close()</tt> and <tt>flush()</tt> methods are not called; to handle this
+ * case, the <tt>ExpiresFilter</tt>, at the end of its <tt>doFilter()</tt>
+ * method, manually triggers the <tt>onBeforeWriteResponseBody()</tt> method.
+ * </p>
+ * <h2>Configuration syntax</h2>
+ * <p>
+ * The <tt>ExpiresFilter</tt> supports the same configuration syntax as Apache
+ * Httpd mod_expires.
+ * </p>
+ * <p>
+ * A challenge has been to choose the name of the <tt>&lt;param-name&gt;</tt>
+ * associated with <tt>ExpiresByType</tt> in the <tt>&lt;filter&gt;</tt>
+ * declaration. Indeed, Several <tt>ExpiresByType</tt> directives can be
+ * declared when <tt>web.xml</tt> syntax does not allow to declare several
+ * <tt>&lt;init-param&gt;</tt> with the same name.
+ * </p>
+ * <p>
+ * The workaround has been to declare the content type in the
+ * <tt>&lt;param-name&gt;</tt> rather than in the <tt>&lt;param-value&gt;</tt>.
+ * </p>
+ * <h2>Designed for extension : the open/close principle</h2>
+ * <p>
+ * The <tt>ExpiresFilter</tt> has been designed for extension following the
+ * open/close principle.
+ * </p>
+ * <p>
+ * Key methods to override for extension are :
+ * <ul>
+ * <li>
+ * {@(protected))}
+ * </li>
+ * <li>
+ * {@(protected)>
+ * </ul>
+ * </p>
+ * <h1>Troubleshooting</h1>
+ * <p>
+ * To troubleshoot, enable logging on the
+ * <tt>org.apache.catalina.filters.ExpiresFilter</tt>.
+ * </p>
+ * <p>
+ * Extract of logging.properties
+ * </p>
+ *
+ * <code><pre>
+ * org.apache.catalina.filters.ExpiresFilter.level = FINE
+ * </pre></code>
+ * <p>
+ * Sample of initialization log message :
+ * </p>
+ *
+ * <code><pre>
+ * Mar 26, 2010 2:01:41 PM org.apache.catalina.filters.ExpiresFilter init
+ * FINE: Filter initialized with configuration ExpiresFilter[
+ *   active=true,
+ *   excludedResponseStatusCode=[304],
+ *   default=null,
+ *   byType={
+ *     image=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]],
+ *     text/css=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]],
+ *     text/javascript=ExpiresConfiguration[startingPoint=ACCESS_TIME, duration=[10 MINUTE]]}]
+ * </pre></code>
+ * <p>
+ * Sample of per-request log message where <tt>ExpiresFilter</tt> adds an
+ * expiration date
+ * </p>
+ *
+ * <code><pre>
+ * Mar 26, 2010 2:09:47 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody
+ * FINE: Request "/tomcat.gif" with response status "200" content-type "image/gif", set expiration date 3/26/10 2:19 PM
+ * </pre></code>
+ * <p>
+ * Sample of per-request log message where <tt>ExpiresFilter</tt> does not add
+ * an expiration date
+ * </p>
+ *
+ * <code><pre>
+ * Mar 26, 2010 2:10:27 PM org.apache.catalina.filters.ExpiresFilter onBeforeWriteResponseBody
+ * FINE: Request "/docs/config/manager.html" with response status "200" content-type "text/html", no expiration configured
+ * </pre></code>
+ *
+ */
+public class ExpiresFilter extends FilterBase {
+
+   /**
+   * Duration composed of an {@(protected)}
+   */
+   protected static class Duration {
+
+     public static Duration minutes(int amount) {
+        return new Duration(amount, DurationUnit.MINUTE);
+     }
+
+     public static Duration seconds(int amount) {
+        return new Duration(amount, DurationUnit.SECOND);
+     }
+
+     final protected int amount;
+
+     final protected DurationUnit unit;
+
+     public Duration(int amount, DurationUnit unit) {
+        super();
+        this.amount = amount;
+        this.unit = unit;
+     }
+
+     public int getAmount() {
+        return amount;
+     }
+
+     public DurationUnit getUnit() {
+        return unit;
+     }
+
+     @Override
+     public String toString() {
+        return amount + " " + unit;
+     }
+   }
+
+   /**
+   * Duration unit
+   */
+   protected enum DurationUnit {
+     DAY(Calendar.DAY_OF_YEAR), HOUR(Calendar.HOUR), MINUTE(Calendar.MINUTE), MONTH(
+           Calendar.MONTH), SECOND(Calendar.SECOND), WEEK(
+           Calendar.WEEK_OF_YEAR), YEAR(Calendar.YEAR);
+     private final int calendardField;
+
+     private DurationUnit(int calendardField) {
+        this.calendardField = calendardField;
+     }
+
+     public int getCalendardField() {
+        return calendardField;
+     }
+
+   }
+
+   /**
+   * <p>
+   * Main piece of configuration of the filter.
+   * </p>
+   * <p>
+   * Can be expressed like '<tt>access plus 1 month 15  days 2 hours</tt>'.
+   * </p>
+   */
+   protected static class ExpiresConfiguration {
+     /**
+      * List of duration elements.
+      */
+     private List<Duration> durations;
+
+     /**
+      * Starting point of the elaspse to set in the response.
+      */
+     private StartingPoint startingPoint;
+
+     public ExpiresConfiguration(StartingPoint startingPoint,
+           Duration... durations) {
+        this(startingPoint, Arrays.asList(durations));
+     }
+
+     public ExpiresConfiguration(StartingPoint startingPoint,
+           List<Duration> durations) {
+        super();
+        this.startingPoint = startingPoint;
+        this.durations = durations;
+     }
+
+     public List<Duration> getDurations() {
+        return durations;
+     }
+
+     public StartingPoint getStartingPoint() {
+        return startingPoint;
+     }
+
+     @Override
+     public String toString() {
+        return "ExpiresConfiguration[startingPoint=" + startingPoint +
+             ", duration=" + durations + "]";
+     }
+   }
+
+   /**
+   * Expiration configuration starting point. Either the time the
+   * html-page/servlet-response was served ({@(protected)})
+   * or the last time the html-page/servlet-response was modified (
+   * {@(protected)}).
+   */
+   protected enum StartingPoint {
+     ACCESS_TIME, LAST_MODIFICATION_TIME
+   }
+
+   /**
+   * <p>
+   * Wrapping extension of the {@(protected)
+   * "Start Write Response Body" event.
+   * </p>
+   * <p>
+   * For performance optimization : this extended response holds the
+   * {@(protected)
+   * to the slow {@(protected)>
+   * to <tt>date</tt> to <tt>long</tt> conversion.
+   * </p>
+   */
+   public class XHttpServletResponse extends HttpServletResponseWrapper {
+
+     /**
+      * Value of the <tt>Cache-Control/tt> http response header if it has
+      * been set.
+      */
+     private String cacheControlHeader;
+
+     /**
+      * Value of the <tt>Last-Modified</tt> http response header if it has
+      * been set.
+      */
+     private long lastModifiedHeader;
+
+     private boolean lastModifiedHeaderSet;
+
+     private PrintWriter printWriter;
+
+     private HttpServletRequest request;
+
+     private ServletOutputStream servletOutputStream;
+
+     /**
+      * Indicates whether calls to write methods (<tt>write(...)</tt>,
+      * <tt>print(...)</tt>, etc) of the response body have been called or
+      * not.
+      */
+     private boolean writeResponseBodyStarted;
+
+     public XHttpServletResponse(HttpServletRequest request,
+           HttpServletResponse response) {
+        super(response);
+        this.request = request;
+     }
+
+     @Override
+     public void addDateHeader(String name, long date) {
+        super.addDateHeader(name, date);
+        if (!lastModifiedHeaderSet) {
+           this.lastModifiedHeader = date;
+           this.lastModifiedHeaderSet = true;
+        }
+     }
+
+     @Override
+     public void addHeader(String name, String value) {
+        super.addHeader(name, value);
+        if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name) &&
+             cacheControlHeader == null) {
+           cacheControlHeader = value;
+        }
+     }
+
+     public String getCacheControlHeader() {
+        return cacheControlHeader;
+     }
+
+     public long getLastModifiedHeader() {
+        return lastModifiedHeader;
+     }
+
+     @Override
+     public ServletOutputStream getOutputStream() throws IOException {
+        if (servletOutputStream == null) {
+           servletOutputStream = new XServletOutputStream(
+                super.getOutputStream(), request, this);
+        }
+        return servletOutputStream;
+     }
+
+     @Override
+     public PrintWriter getWriter() throws IOException {
+        if (printWriter == null) {
+           printWriter = new XPrintWriter(super.getWriter(), request, this);
+        }
+        return printWriter;
+     }
+
+     public boolean isLastModifiedHeaderSet() {
+        return lastModifiedHeaderSet;
+     }
+
+     public boolean isWriteResponseBodyStarted() {
+        return writeResponseBodyStarted;
+     }
+
+     @Override
+     public void reset() {
+        super.reset();
+        this.lastModifiedHeader = 0;
+        this.lastModifiedHeaderSet = false;
+        this.cacheControlHeader = null;
+     }
+
+     @Override
+     public void setDateHeader(String name, long date) {
+        super.setDateHeader(name, date);
+        if (HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
+           this.lastModifiedHeader = date;
+           this.lastModifiedHeaderSet = true;
+        }
+     }
+
+     @Override
+     public void setHeader(String name, String value) {
+        super.setHeader(name, value);
+        if (HEADER_CACHE_CONTROL.equalsIgnoreCase(name)) {
+           this.cacheControlHeader = value;
+        }
+     }
+
+     public void setWriteResponseBodyStarted(boolean writeResponseBodyStarted) {
+        this.writeResponseBodyStarted = writeResponseBodyStarted;
+     }
+   }
+
+   /**
+   * Wrapping extension of {@(protected)
+   * "Start Write Response Body" event.
+   */
+   public class XPrintWriter extends PrintWriter {
+     private PrintWriter out;
+
+     private HttpServletRequest request;
+
+     private XHttpServletResponse response;
+
+     public XPrintWriter(PrintWriter out, HttpServletRequest request,
+           XHttpServletResponse response) {
+        super(out);
+        this.out = out;
+        this.request = request;
+        this.response = response;
+     }
+
+     @Override
+     public PrintWriter append(char c) {
+        fireBeforeWriteResponseBodyEvent();
+        return out.append(c);
+     }
+
+     @Override
+     public PrintWriter append(CharSequence csq) {
+        fireBeforeWriteResponseBodyEvent();
+        return out.append(csq);
+     }
+
+     @Override
+     public PrintWriter append(CharSequence csq, int start, int end) {
+        fireBeforeWriteResponseBodyEvent();
+        return out.append(csq, start, end);
+     }
+
+     @Override
+     public void close() {
+        fireBeforeWriteResponseBodyEvent();
+        out.close();
+     }
+
+     private void fireBeforeWriteResponseBodyEvent() {
+        if (!this.response.isWriteResponseBodyStarted()) {
+           this.response.setWriteResponseBodyStarted(true);
+           onBeforeWriteResponseBody(request, response);
+        }
+     }
+
+     @Override
+     public void flush() {
+        fireBeforeWriteResponseBodyEvent();
+        out.flush();
+     }
+
+     @Override
+     public void print(boolean b) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(b);
+     }
+
+     @Override
+     public void print(char c) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(c);
+     }
+
+     @Override
+     public void print(char[] s) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(s);
+     }
+
+     @Override
+     public void print(double d) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(d);
+     }
+
+     @Override
+     public void print(float f) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(f);
+     }
+
+     @Override
+     public void print(int i) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(i);
+     }
+
+     @Override
+     public void print(long l) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(l);
+     }
+
+     @Override
+     public void print(Object obj) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(obj);
+     }
+
+     @Override
+     public void print(String s) {
+        fireBeforeWriteResponseBodyEvent();
+        out.print(s);
+     }
+
+     @Override
+     public PrintWriter printf(Locale l, String format, Object... args) {
+        fireBeforeWriteResponseBodyEvent();
+        return out.printf(l, format, args);
+     }
+
+     @Override
+     public PrintWriter printf(String format, Object... args) {
+        fireBeforeWriteResponseBodyEvent();
+        return out.printf(format, args);
+     }
+
+     @Override
+     public void println() {
+        fireBeforeWriteResponseBodyEvent();
+        out.println();
+     }
+
+     @Override
+     public void println(boolean x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(char x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(char[] x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(double x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(float x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(int x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(long x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(Object x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void println(String x) {
+        fireBeforeWriteResponseBodyEvent();
+        out.println(x);
+     }
+
+     @Override
+     public void write(char[] buf) {
+        fireBeforeWriteResponseBodyEvent();
+        out.write(buf);
+     }
+
+     @Override
+     public void write(char[] buf, int off, int len) {
+        fireBeforeWriteResponseBodyEvent();
+        out.write(buf, off, len);
+     }
+
+     @Override
+     public void write(int c) {
+        fireBeforeWriteResponseBodyEvent();
+        out.write(c);
+     }
+
+     @Override
+     public void write(String s) {
+        fireBeforeWriteResponseBodyEvent();
+        out.write(s);
+     }
+
+     @Override
+     public void write(String s, int off, int len) {
+        fireBeforeWriteResponseBodyEvent();
+        out.write(s, off, len);
+     }
+
+   }
+
+   /**
+   * Wrapping extension of {@(protected)
+   * "Start Write Response Body" event.
+   */
+   public class XServletOutputStream extends ServletOutputStream {
+
+     private HttpServletRequest request;
+
+     private XHttpServletResponse response;
+
+     private ServletOutputStream servletOutputStream;
+
+     public XServletOutputStream(ServletOutputStream servletOutputStream,
+           HttpServletRequest request, XHttpServletResponse response) {
+        super();
+        this.servletOutputStream = servletOutputStream;
+        this.response = response;
+        this.request = request;
+     }
+
+     @Override
+     public void close() throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.close();
+     }
+
+     private void fireOnBeforeWriteResponseBodyEvent() {
+        if (!this.response.isWriteResponseBodyStarted()) {
+           this.response.setWriteResponseBodyStarted(true);
+           onBeforeWriteResponseBody(request, response);
+        }
+     }
+
+     @Override
+     public void flush() throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.flush();
+     }
+
+     @Override
+     public void print(boolean b) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(b);
+     }
+
+     @Override
+     public void print(char c) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(c);
+     }
+
+     @Override
+     public void print(double d) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(d);
+     }
+
+     @Override
+     public void print(float f) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(f);
+     }
+
+     @Override
+     public void print(int i) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(i);
+     }
+
+     @Override
+     public void print(long l) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(l);
+     }
+
+     @Override
+     public void print(String s) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.print(s);
+     }
+
+     @Override
+     public void println() throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println();
+     }
+
+     @Override
+     public void println(boolean b) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(b);
+     }
+
+     @Override
+     public void println(char c) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(c);
+     }
+
+     @Override
+     public void println(double d) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(d);
+     }
+
+     @Override
+     public void println(float f) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(f);
+     }
+
+     @Override
+     public void println(int i) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(i);
+     }
+
+     @Override
+     public void println(long l) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(l);
+     }
+
+     @Override
+     public void println(String s) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.println(s);
+     }
+
+     @Override
+     public void write(byte[] b) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.write(b);
+     }
+
+     @Override
+     public void write(byte[] b, int off, int len) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.write(b, off, len);
+     }
+
+     @Override
+     public void write(int b) throws IOException {
+        fireOnBeforeWriteResponseBodyEvent();
+        servletOutputStream.write(b);
+     }
+
+   }
+
+   /**
+   * {@(protected)
+   * characters
+   */
+   private static final Pattern commaSeparatedValuesPattern = Pattern.compile("\\s*,\\s*");
+
+   private static final String HEADER_CACHE_CONTROL = "Cache-Control";
+
+   private static final String HEADER_EXPIRES = "Expires";
+
+   private static final String HEADER_LAST_MODIFIED = "Last-Modified";
+
+   private static final Log log = LogFactory.getLog(ExpiresFilter.class);
+
+   private static final String PARAMETER_EXPIRES_ACTIVE = "ExpiresActive";
+
+   private static final String PARAMETER_EXPIRES_BY_TYPE = "ExpiresByType";
+
+   private static final String PARAMETER_EXPIRES_DEFAULT = "ExpiresDefault";
+
+   private static final String PARAMETER_EXPIRES_EXCLUDED_RESPONSE_STATUS_CODES = "ExpiresExcludedResponseStatusCodes";
+
+   /**
+   * Convert a comma delimited list of numbers into an <tt>int[]</tt>.
+   *
+   * @param commaDelimitedInts
+   *        can be <code>null</code>
+   * @return never <code>null</code> array
+   */
+   protected static int[] commaDelimitedListToIntArray(
+        String commaDelimitedInts) {
+     String[] intsAsStrings = commaDelimitedListToStringArray(commaDelimitedInts);
+     int[] ints = new int[intsAsStrings.length];
+     for (int i = 0; i < intsAsStrings.length; i++) {
+        String intAsString = intsAsStrings[i];
+        try {
+           ints[i] = Integer.parseInt(intAsString);
+        } catch (NumberFormatException e) {
+           throw new RuntimeException("Exception parsing number '" + i +
+                "' (zero based) of comma delimited list '" +
+                commaDelimitedInts + "'");
+        }
+     }
+     return ints;
+   }
+
+   /**
+   * Convert a given comma delimited list of strings into an array of String
+   *
+   * @return array of patterns (non <code>null</code>)
+   */
+   protected static String[] commaDelimitedListToStringArray(
+        String commaDelimitedStrings) {
+     return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0]
+           : commaSeparatedValuesPattern.split(commaDelimitedStrings);
+   }
+
+   /**
+   * Return <code>true</code> if the given <code>str</code> contains the given
+   * <code>searchStr</code>.
+   */
+   protected static boolean contains(String str, String searchStr) {
+     if (str == null || searchStr == null) {
+        return false;
+     }
+     return str.indexOf(searchStr) >= 0;
+   }
+
+   /**
+   * Convert an array of ints into a comma delimited string
+   */
+   protected static String intsToCommaDelimitedString(int[] ints) {
+     if (ints == null) {
+        return "";
+     }
+
+     StringBuilder result = new StringBuilder();
+
+     for (int i = 0; i < ints.length; i++) {
+        result.append(ints[i]);
+        if (i < (ints.length - 1)) {
+           result.append(", ");
+        }
+     }
+     return result.toString();
+   }
+
+   /**
+   * Return <code>true</code> if the given <code>str</code> is
+   * <code>null</code> or has a zero characters length.
+   */
+   protected static boolean isEmpty(String str) {
+     return str == null || str.length() == 0;
+   }
+
+   /**
+   * Return <code>true</code> if the given <code>str</code> has at least one
+   * character (can be a withespace).
+   */
+   protected static boolean isNotEmpty(String str) {
+     return !isEmpty(str);
+   }
+
+   /**
+   * Return <code>true</code> if the given <code>string</code> starts with the
+   * given <code>prefix</code> ignoring case.
+   *
+   * @param string
+   *        can be <code>null</code>
+   * @param prefix
+   *        can be <code>null</code>
+   */
+   protected static boolean startsWithIgnoreCase(String string, String prefix) {
+     if (string == null || prefix == null) {
+        return string == null && prefix == null;
+     }
+     if (prefix.length() > string.length()) {
+        return false;
+     }
+
+     return string.regionMatches(true, 0, prefix, 0, prefix.length());
+   }
+
+   /**
+   * Return the subset of the given <code>str</code> that is before the first
+   * occurence of the given <code>separator</code>. Return <code>null</code>
+   * if the given <code>str</code> or the given <code>separator</code> is
+   * null. Return and empty string if the <code>separator</code> is empty.
+   *
+   * @param str
+   *        can be <code>null</code>
+   * @param separator
+   *        can be <code>null</code>
+   * @return
+   */
+   protected static String substringBefore(String str, String separator) {
+     if (str == null || str.isEmpty() || separator == null) {
+        return null;
+     }
+
+     if (separator.isEmpty()) {
+        return "";
+     }
+
+     int separatorIndex = str.indexOf(separator);
+     if (separatorIndex == -1) {
+        return str;
+     }
+     return str.substring(0, separatorIndex);
+   }
+
+   /**
+   * @see #isActive()
+   */
+   private boolean active = true;
+
+   /**
+   * Default Expires configuration.
+   */
+   private ExpiresConfiguration defaultExpiresConfiguration;
+
+   /**
+   * list of response status code for which the {@(protected)
+   * generate expiration headers.
+   */
+   private int[] excludedResponseStatusCodes = new int[] { HttpServletResponse.SC_NOT_MODIFIED };
+
+   /**
+   * Expires configuration by content type. Visible for test.
+   */
+   private Map<String, ExpiresConfiguration> expiresConfigurationByContentType = new LinkedHashMap<String, ExpiresConfiguration>();
+
+   public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain chain) throws IOException, ServletException {
+     if (request instanceof HttpServletRequest &&
+           response instanceof HttpServletResponse) {
+        HttpServletRequest httpRequest = (HttpServletRequest) request;
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+        if (response.isCommitted()) {
+           if (log.isDebugEnabled()) {
+             log.debug(sm.getString(
+                   "expiresFilter.responseAlreadyCommited",
+                   httpRequest.getRequestURL()));
+           }
+           chain.doFilter(request, response);
+        } else if (active) {
+           XHttpServletResponse xResponse = new XHttpServletResponse(
+                httpRequest, httpResponse);
+           chain.doFilter(request, xResponse);
+           if (!xResponse.isWriteResponseBodyStarted()) {
+             // Empty response, manually trigger
+             // onBeforeWriteResponseBody()
+             onBeforeWriteResponseBody(httpRequest, xResponse);
+           }
+        } else {
+           if (log.isDebugEnabled()) {
+             log.debug(sm.getString("expiresFilter.filterNotActive",
+                   httpRequest.getRequestURL()));
+           }
+           chain.doFilter(request, response);
+        }
+     } else {
+        chain.doFilter(request, response);
+     }
+   }
+
+   public ExpiresConfiguration getDefaultExpiresConfiguration() {
+     return defaultExpiresConfiguration;
+   }
+
+   public String getExcludedResponseStatusCodes() {
+     return intsToCommaDelimitedString(excludedResponseStatusCodes);
+   }
+
+   public int[] getExcludedResponseStatusCodesAsInts() {
+     return excludedResponseStatusCodes;
+   }
+
+   /**
+   * <p>
+   * Returns the expiration date of the given {@(protected)
+   * <code>null</code> if no expiration date has been configured for the
+   * declared content type.
+   * </p>
+   * <p>
+   * <code>protected</code> for extension.
+   * </p>
+   *
+   * @see HttpServletResponse#getContentType()
+   */
+   protected Date getExpirationDate(XHttpServletResponse response) {
+     String contentType = response.getContentType();
+
+     // lookup exact content-type match (e.g.
+     // "text/html; charset=iso-8859-1")
+     ExpiresConfiguration configuration = expiresConfigurationByContentType.get(contentType);
+     if (configuration != null) {
+        Date result = getExpirationDate(configuration, response);
+        if (log.isErrorEnabled()) {
+           log.error(sm.getString(
+                "expiresFilter.useMatchingConfiguration",
+                configuration, contentType, contentType, result));
+        }
+        return result;
+     }
+
+     if (contains(contentType, ";")) {
+        // lookup content-type without charset match (e.g. "text/html")
+        String contentTypeWithoutCharset = substringBefore(contentType, ";").trim();
+        configuration = expiresConfigurationByContentType.get(contentTypeWithoutCharset);
+
+        if (configuration != null) {
+           Date result = getExpirationDate(configuration, response);
+           if (log.isErrorEnabled()) {
+             log.error(sm.getString(
+                   "expiresFilter.useMatchingConfiguration",
+                   configuration, contentTypeWithoutCharset,
+                   contentType, result));
+           }
+           return result;
+        }
+     }
+
+     if (contains(contentType, "/")) {
+        // lookup major type match (e.g. "text")
+        String majorType = substringBefore(contentType, "/");
+        configuration = expiresConfigurationByContentType.get(majorType);
+        if (configuration != null) {
+           Date result = getExpirationDate(configuration, response);
+           if (log.isErrorEnabled()) {
+             log.error(sm.getString(
+                   "expiresFilter.useMatchingConfiguration",
+                   configuration, majorType, contentType, result));
+           }
+           return result;
+        }
+     }
+
+     if (defaultExpiresConfiguration != null) {
+        Date result = getExpirationDate(defaultExpiresConfiguration,
+             response);
+        if (log.isErrorEnabled()) {
+           log.error(sm.getString("expiresFilter.useDefaultConfiguration",
+                defaultExpiresConfiguration, contentType, result));
+        }
+        return result;
+     }
+
+     if (log.isErrorEnabled()) {
+        log.error(sm.getString(
+             "expiresFilter.noExpirationConfiguredForContentType",
+             contentType));
+     }
+     return null;
+   }
+
+   /**
+   * <p>
+   * Returns the expiration date of the given {@(protected)},
+   * {@(protected)}.
+   * </p>
+   * <p>
+   * <code>protected</code> for extension.
+   * </p>
+   */
+   protected Date getExpirationDate(ExpiresConfiguration configuration,
+        XHttpServletResponse response) {
+     Calendar calendar;
+     switch (configuration.getStartingPoint()) {
+     case ACCESS_TIME:
+        calendar = Calendar.getInstance();
+        break;
+     case LAST_MODIFICATION_TIME:
+        if (response.isLastModifiedHeaderSet()) {
+           try {
+             long lastModified = response.getLastModifiedHeader();
+             calendar = Calendar.getInstance();
+             calendar.setTimeInMillis(lastModified);
+           } catch (NumberFormatException e) {
+             // default to now
+             calendar = Calendar.getInstance();
+           }
+        } else {
+           // Last-Modified header not found, use now
+           calendar = Calendar.getInstance();
+        }
+        break;
+     default:
+        throw new IllegalStateException(sm.getString(
+             "expiresFilter.unsupportedStartingPoint",
+             configuration.getStartingPoint()));
+     }
+     for (Duration duration : configuration.getDurations()) {
+        calendar.add(duration.getUnit().getCalendardField(),
+             duration.getAmount());
+     }
+
+     return calendar.getTime();
+   }
+
+   public Map<String, ExpiresConfiguration> getExpiresConfigurationByContentType() {
+     return expiresConfigurationByContentType;
+   }
+
+   @Override
+   protected Log getLogger() {
+     return log;
+   }
+
+   @Override
+   public void init(FilterConfig filterConfig) throws ServletException {
+     for (Enumeration<String> names = filterConfig.getInitParameterNames(); names.hasMoreElements();) {
+        String name = names.nextElement();
+        String value = filterConfig.getInitParameter(name);
+
+        try {
+           if (name.startsWith(PARAMETER_EXPIRES_BY_TYPE)) {
+             String contentType = name.substring(
+                   PARAMETER_EXPIRES_BY_TYPE.length()).trim();
+             ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value);
+             this.expiresConfigurationByContentType.put(contentType,
+                   expiresConfiguration);
+           } else if (name.equalsIgnoreCase(PARAMETER_EXPIRES_DEFAULT)) {
+             ExpiresConfiguration expiresConfiguration = parseExpiresConfiguration(value);
+             this.defaultExpiresConfiguration = expiresConfiguration;
+           } else if (name.equalsIgnoreCase(PARAMETER_EXPIRES_ACTIVE)) {
+             this.active = "On".equalsIgnoreCase(value) ||
+                   Boolean.valueOf(value).booleanValue();
+           } else if (name.equalsIgnoreCase(PARAMETER_EXPIRES_EXCLUDED_RESPONSE_STATUS_CODES)) {
+             this.excludedResponseStatusCodes = commaDelimitedListToIntArray(value);
+           } else {
+             log.warn(sm.getString(
+                   "expiresFilter.unknownParameterIgnored", name,
+                   value));
+           }
+        } catch (RuntimeException e) {
+           throw new ServletException(sm.getString(
+                "expiresFilter.exceptionProcessingParameter", name,
+                value), e);
+        }
+     }
+
+     log.debug(sm.getString("expiresFilter.filterInitialized",
+           this.toString()));
+   }
+
+   /**
+   * Indicates that the filter is active. If <code>false</code>, the filter is
+   * pass-through. Default is <code>true</code>.
+   */
+   public boolean isActive() {
+     return active;
+   }
+
+   /**
+   *
+   * <p>
+   * <code>protected</code> for extension.
+   * </p>
+   */
+   protected boolean isEligibleToExpirationHeaderGeneration(
+        HttpServletRequest request, XHttpServletResponse response) {
+     boolean expirationHeaderHasBeenSet = response.containsHeader(HEADER_EXPIRES) ||
+           contains(response.getCacheControlHeader(), "max-age");
+     if (expirationHeaderHasBeenSet) {
+        if (log.isDebugEnabled()) {
+           log.debug(sm.getString(
+                "expiresFilter.expirationHeaderAlreadyDefined",
+                request.getRequestURI(),
+                Integer.valueOf(response.getStatus()),
+                response.getContentType()));
+        }
+        return false;
+     }
+
+     for (int skippedStatusCode : this.excludedResponseStatusCodes) {
+        if (response.getStatus() == skippedStatusCode) {
+           if (log.isDebugEnabled()) {
+             log.debug(sm.getString("expiresFilter.skippedStatusCode",
+                   request.getRequestURI(),
+                   Integer.valueOf(response.getStatus()),
+                   response.getContentType()));
+           }
+           return false;
+        }
+     }
+
+     return true;
+   }
+
+   /**
+   * <p>
+   * If no expiration header has been set by the servlet and an expiration has
+   * been defined in the {@(protected) '
+   * <tt>Expires</tt>' header and the attribute '<tt>max-age</tt>' of the '
+   * <tt>Cache-Control</tt>' header.
+   * </p>
+   * <p>
+   * Must be called on the "Start Write Response Body" event.
+   * </p>
+   * <p>
+   * Invocations to <tt>Logger.debug(...)</tt> are guarded by
+   * {@(protected)
+   * {@(protected)
+   * {@(protected)>
+   * objects instantiations (as of Tomcat 7).
+   * </p>
+   */
+   public void onBeforeWriteResponseBody(HttpServletRequest request,
+        XHttpServletResponse response) {
+
+     if (!isEligibleToExpirationHeaderGeneration(request, response)) {
+        return;
+     }
+
+     Date expirationDate = getExpirationDate(response);
+     if (expirationDate == null) {
+        if (log.isDebugEnabled()) {
+           log.debug(sm.getString("expiresFilter.noExpirationConfigured",
+                request.getRequestURI(),
+                Integer.valueOf(response.getStatus()),
+                response.getContentType()));
+        }
+     } else {
+        if (log.isDebugEnabled()) {
+           log.debug(sm.getString("expiresFilter.setExpirationDate",
+                request.getRequestURI(),
+                Integer.valueOf(response.getStatus()),
+                response.getContentType(), expirationDate));
+        }
+
+        String maxAgeDirective = "max-age=" +
+             ((expirationDate.getTime() - System.currentTimeMillis()) / 1000);
+
+        String cacheControlHeader = response.getCacheControlHeader();
+        String newCacheControlHeader = (cacheControlHeader == null) ? maxAgeDirective
+             : cacheControlHeader + ", " + maxAgeDirective;
+        response.setHeader(HEADER_CACHE_CONTROL, newCacheControlHeader);
+        response.setDateHeader(HEADER_EXPIRES, expirationDate.getTime());
+     }
+
+   }
+
+   /**
+   * Parse configuration lines like '
+   * <tt>access plus 1 month 15 days 2 hours</tt>' or '
+   * <tt>modification 1 day 2 hours 5 seconds</tt>'
+   *
+   * @param inputLine
+   */
+   protected ExpiresConfiguration parseExpiresConfiguration(String inputLine) {
+     String line = inputLine.trim();
+
+     StringTokenizer tokenizer = new StringTokenizer(line, " ");
+
+     String currentToken;
+
+     try {
+        currentToken = tokenizer.nextToken();
+     } catch (NoSuchElementException e) {
+        throw new IllegalStateException(sm.getString(
+             "expiresFilter.startingPointNotFound", line));
+     }
+
+     StartingPoint startingPoint;
+     if ("access".equalsIgnoreCase(currentToken) ||
+           "now".equalsIgnoreCase(currentToken)) {
+        startingPoint = StartingPoint.ACCESS_TIME;
+     } else if ("modification".equalsIgnoreCase(currentToken)) {
+        startingPoint = StartingPoint.LAST_MODIFICATION_TIME;
+     } else if (!tokenizer.hasMoreTokens() &&
+           startsWithIgnoreCase(currentToken, "a")) {
+        startingPoint = StartingPoint.ACCESS_TIME;
+        // trick : convert duration configuration from old to new style
+        tokenizer = new StringTokenizer(currentToken.substring(1) +
+             " seconds", " ");
+     } else if (!tokenizer.hasMoreTokens() &&
+           startsWithIgnoreCase(currentToken, "m")) {
+        startingPoint = StartingPoint.LAST_MODIFICATION_TIME;
+        // trick : convert duration configuration from old to new style
+        tokenizer = new StringTokenizer(currentToken.substring(1) +
+             " seconds", " ");
+     } else {
+        throw new IllegalStateException(sm.getString(
+             "expiresFilter.startingPointInvalid", currentToken, line));
+     }
+
+     try {
+        currentToken = tokenizer.nextToken();
+     } catch (NoSuchElementException e) {
+        throw new IllegalStateException(sm.getString(
+             "Duration not found in directive '{}'", line));
+     }
+
+     if ("plus".equalsIgnoreCase(currentToken)) {
+        // skip
+        try {
+           currentToken = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+           throw new IllegalStateException(sm.getString(
+                "Duration not found in directive '{}'", line));
+        }
+     }
+
+     List<Duration> durations = new ArrayList<Duration>();
+
+     while (currentToken != null) {
+        int amount;
+        try {
+           amount = Integer.parseInt(currentToken);
+        } catch (NumberFormatException e) {
+           throw new IllegalStateException(sm.getString(
+                "Invalid duration (number) '{}' in directive '{}'",
+                currentToken, line));
+        }
+
+        try {
+           currentToken = tokenizer.nextToken();
+        } catch (NoSuchElementException e) {
+           throw new IllegalStateException(
+                sm.getString(
+                     "Duration unit not found after amount {} in directive '{}'",
+                     Integer.valueOf(amount), line));
+        }
+        DurationUnit durationUnit;
+        if ("years".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.YEAR;
+        } else if ("month".equalsIgnoreCase(currentToken) ||
+             "months".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.MONTH;
+        } else if ("week".equalsIgnoreCase(currentToken) ||
+             "weeks".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.WEEK;
+        } else if ("day".equalsIgnoreCase(currentToken) ||
+             "days".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.DAY;
+        } else if ("hour".equalsIgnoreCase(currentToken) ||
+             "hours".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.HOUR;
+        } else if ("minute".equalsIgnoreCase(currentToken) ||
+             "minutes".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.MINUTE;
+        } else if ("second".equalsIgnoreCase(currentToken) ||
+             "seconds".equalsIgnoreCase(currentToken)) {
+           durationUnit = DurationUnit.SECOND;
+        } else {
+           throw new IllegalStateException(
+                sm.getString(
+                     "Invalid duration unit (years|months|weeks|days|hours|minutes|seconds) '{}' in directive '{}'",
+                     currentToken, line));
+        }
+
+        Duration duration = new Duration(amount, durationUnit);
+        durations.add(duration);
+
+        if (tokenizer.hasMoreTokens()) {
+           currentToken = tokenizer.nextToken();
+        } else {
+           currentToken = null;
+        }
+     }
+
+     return new ExpiresConfiguration(startingPoint, durations);
+   }
+
+   public void setActive(boolean active) {
+     this.active = active;
+   }
+
+   public void setDefaultExpiresConfiguration(
+        ExpiresConfiguration defaultExpiresConfiguration) {
+     this.defaultExpiresConfiguration = defaultExpiresConfiguration;
+   }
+
+   public void setExcludedResponseStatusCodes(int[] excludedResponseStatusCodes) {
+     this.excludedResponseStatusCodes = excludedResponseStatusCodes;
+   }
+
+   public void setExpiresConfigurationByContentType(
+        Map<String, ExpiresConfiguration> expiresConfigurationByContentType) {
+     this.expiresConfigurationByContentType = expiresConfigurationByContentType;
+   }
+
+   @Override
+   public String toString() {
+     return getClass().getSimpleName() + "[active=" + this.active +
+           ", excludedResponseStatusCode=[" +
+           intsToCommaDelimitedString(this.excludedResponseStatusCodes) +
+           "], default=" + this.defaultExpiresConfiguration + ", byType=" +
+           this.expiresConfigurationByContentType + "]";
+   }
+}

Propchange: tomcat/trunk/java/org/apache/catalina/filters/ExpiresFilter.java
------------------------------------------------------------------------------
  svn:eol-style = native

Modified: tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties?rev=964219&r1=964218&r2=964219&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties (original)
+++ tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties Wed Jul 14 22:11:30 2010
@@(protected)
filterbase.noSuchProperty=The property "{0}" is not defined for filters of type "{1}"

http.403=Access to the specified resource ({0}) has been forbidden.
+
+expiresFilter.noExpirationConfigured=Request "{0}" with response status "{1}" content-type "{2}", no expiration configured
+expiresFilter.setExpirationDate=Request "{0}" with response status "{1}" content-type "{2}", set expiration date {3}
+expiresFilter.startingPointNotFound=Starting point (access|now|modification|a<seconds>|m<seconds>) not found in directive "{0}"
+expiresFilter.startingPointInvalid=Invalid starting point (access|now|modification|a<seconds>|m<seconds>) "{0}" in directive "{1}"
+expiresFilter.responseAlreadyCommited=Request "{0}", can not apply ExpiresFilter on already committed response.
+expiresFilter.filterNotActive=Request "{0}", ExpiresFilter is NOT active
+expiresFilter.noExpirationConfiguredForContentType=No Expires configuration found for content-type "{0}"
+expiresFilter.useMatchingConfiguration=Use {0} matching "{1}" for content-type "{2}" returns {3}
+expiresFilter.useDefaultConfiguration=Use default {0} for content-type "{1}" returns {2}
+expiresFilter.unsupportedStartingPoint=Unsupported startingPoint "{0}"
+expiresFilter.unknownParameterIgnored=Unknown parameter "{0}" with value "{1}" is ignored !
+expiresFilter.exceptionProcessingParameter=Exception processing configuration parameter "{0}":"{1}"
+expiresFilter.filterInitialized=Filter initialized with configuration {0}
+expiresFilter.expirationHeaderAlreadyDefined=Request "{0}" with response status "{1}" content-type "{2}", expiration header already defined
+expiresFilter.skippedStatusCode=Request "{0}" with response status "{1}" content-type "{1}", skip expiration header generation for given status
+

Added: tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java?rev=964219&view=auto
==============================================================================
--- tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java (added)
+++ tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java Wed Jul 14 22:11:30 2010
@@(protected) @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.catalina.filters;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Calendar;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+import java.util.Map.Entry;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.Assert;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.deploy.FilterDef;
+import org.apache.catalina.deploy.FilterMap;
+import org.apache.catalina.filters.ExpiresFilter.Duration;
+import org.apache.catalina.filters.ExpiresFilter.DurationUnit;
+import org.apache.catalina.filters.ExpiresFilter.ExpiresConfiguration;
+import org.apache.catalina.filters.ExpiresFilter.StartingPoint;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
+
+public class TestExpiresFilter extends TomcatBaseTest {
+   public static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
+
+   public void testConfiguration() throws Exception {
+
+     Tomcat tomcat = getTomcatInstance();
+     Context root = tomcat.addContext("", TEMP_DIR);
+
+     FilterDef filterDef = new FilterDef();
+     filterDef.addInitParameter("ExpiresDefault", "access plus 1 month");
+     filterDef.addInitParameter("ExpiresByType text/html",
+           "access plus 1 month 15 days 2 hours");
+     filterDef.addInitParameter("ExpiresByType image/gif",
+           "modification plus 5 hours 3 minutes");
+     filterDef.addInitParameter("ExpiresByType image/jpg", "A10000");
+     filterDef.addInitParameter("ExpiresByType video/mpeg", "M20000");
+     filterDef.addInitParameter("ExpiresActive", "Off");
+     filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes",
+           "304, 503");
+
+     ExpiresFilter expiresFilter = new ExpiresFilter();
+
+     filterDef.setFilter(expiresFilter);
+     filterDef.setFilterClass(ExpiresFilter.class.getName());
+     filterDef.setFilterName(ExpiresFilter.class.getName());
+
+     root.addFilterDef(filterDef);
+
+     FilterMap filterMap = new FilterMap();
+     filterMap.setFilterName(ExpiresFilter.class.getName());
+     filterMap.addURLPattern("*");
+
+     tomcat.start();
+     try {
+        Assert.assertEquals(false, expiresFilter.isActive());
+
+        // VERIFY EXCLUDED RESPONSE STATUS CODES
+        {
+           int[] excludedResponseStatusCodes = expiresFilter.getExcludedResponseStatusCodesAsInts();
+           Assert.assertEquals(2, excludedResponseStatusCodes.length);
+           Assert.assertEquals(304, excludedResponseStatusCodes[0]);
+           Assert.assertEquals(503, excludedResponseStatusCodes[1]);
+        }
+
+        // VERIFY DEFAULT CONFIGURATION
+        {
+           ExpiresConfiguration expiresConfiguration = expiresFilter.getDefaultExpiresConfiguration();
+           Assert.assertEquals(StartingPoint.ACCESS_TIME,
+                expiresConfiguration.getStartingPoint());
+           Assert.assertEquals(1,
+                expiresConfiguration.getDurations().size());
+           Assert.assertEquals(DurationUnit.MONTH,
+                expiresConfiguration.getDurations().get(0).getUnit());
+           Assert.assertEquals(1, expiresConfiguration.getDurations().get(
+                0).getAmount());
+        }
+
+        // VERIFY TEXT/HTML
+        {
+           ExpiresConfiguration expiresConfiguration = expiresFilter.getExpiresConfigurationByContentType().get(
+                "text/html");
+           Assert.assertEquals(StartingPoint.ACCESS_TIME,
+                expiresConfiguration.getStartingPoint());
+
+           Assert.assertEquals(3,
+                expiresConfiguration.getDurations().size());
+
+           Duration oneMonth = expiresConfiguration.getDurations().get(0);
+           Assert.assertEquals(DurationUnit.MONTH, oneMonth.getUnit());
+           Assert.assertEquals(1, oneMonth.getAmount());
+
+           Duration fifteenDays = expiresConfiguration.getDurations().get(
+                1);
+           Assert.assertEquals(DurationUnit.DAY, fifteenDays.getUnit());
+           Assert.assertEquals(15, fifteenDays.getAmount());
+
+           Duration twoHours = expiresConfiguration.getDurations().get(2);
+           Assert.assertEquals(DurationUnit.HOUR, twoHours.getUnit());
+           Assert.assertEquals(2, twoHours.getAmount());
+        }
+        // VERIFY IMAGE/GIF
+        {
+           ExpiresConfiguration expiresConfiguration = expiresFilter.getExpiresConfigurationByContentType().get(
+                "image/gif");
+           Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME,
+                expiresConfiguration.getStartingPoint());
+
+           Assert.assertEquals(2,
+                expiresConfiguration.getDurations().size());
+
+           Duration fiveHours = expiresConfiguration.getDurations().get(0);
+           Assert.assertEquals(DurationUnit.HOUR, fiveHours.getUnit());
+           Assert.assertEquals(5, fiveHours.getAmount());
+
+           Duration threeMinutes = expiresConfiguration.getDurations().get(
+                1);
+           Assert.assertEquals(DurationUnit.MINUTE, threeMinutes.getUnit());
+           Assert.assertEquals(3, threeMinutes.getAmount());
+
+        }
+        // VERIFY IMAGE/JPG
+        {
+           ExpiresConfiguration expiresConfiguration = expiresFilter.getExpiresConfigurationByContentType().get(
+                "image/jpg");
+           Assert.assertEquals(StartingPoint.ACCESS_TIME,
+                expiresConfiguration.getStartingPoint());
+
+           Assert.assertEquals(1,
+                expiresConfiguration.getDurations().size());
+
+           Duration tenThousandSeconds = expiresConfiguration.getDurations().get(
+                0);
+           Assert.assertEquals(DurationUnit.SECOND,
+                tenThousandSeconds.getUnit());
+           Assert.assertEquals(10000, tenThousandSeconds.getAmount());
+
+        }
+        // VERIFY VIDEO/MPEG
+        {
+           ExpiresConfiguration expiresConfiguration = expiresFilter.getExpiresConfigurationByContentType().get(
+                "video/mpeg");
+           Assert.assertEquals(StartingPoint.LAST_MODIFICATION_TIME,
+                expiresConfiguration.getStartingPoint());
+
+           Assert.assertEquals(1,
+                expiresConfiguration.getDurations().size());
+
+           Duration twentyThousandSeconds = expiresConfiguration.getDurations().get(
+                0);
+           Assert.assertEquals(DurationUnit.SECOND,
+                twentyThousandSeconds.getUnit());
+           Assert.assertEquals(20000, twentyThousandSeconds.getAmount());
+        }
+     } finally {
+        tomcat.stop();
+     }
+   }
+
+   /**
+   * Test that a resource with empty content is also processed
+   */
+
+   public void testEmptyContent() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/plain");
+           // no content is written in the response
+        }
+     };
+
+     validate(servlet, Integer.valueOf(7 * 60));
+   }
+
+   public void testParseExpiresConfigurationCombinedDuration() {
+     ExpiresFilter expiresFilter = new ExpiresFilter();
+     ExpiresConfiguration actualConfiguration = expiresFilter.parseExpiresConfiguration("access plus 1 month 15 days 2 hours");
+
+     Assert.assertEquals(StartingPoint.ACCESS_TIME,
+           actualConfiguration.getStartingPoint());
+
+     Assert.assertEquals(3, actualConfiguration.getDurations().size());
+
+   }
+
+   public void testParseExpiresConfigurationMonoDuration() {
+     ExpiresFilter expiresFilter = new ExpiresFilter();
+     ExpiresConfiguration actualConfiguration = expiresFilter.parseExpiresConfiguration("access plus 2 hours");
+
+     Assert.assertEquals(StartingPoint.ACCESS_TIME,
+           actualConfiguration.getStartingPoint());
+
+     Assert.assertEquals(1, actualConfiguration.getDurations().size());
+     Assert.assertEquals(2,
+           actualConfiguration.getDurations().get(0).getAmount());
+     Assert.assertEquals(DurationUnit.HOUR,
+           actualConfiguration.getDurations().get(0).getUnit());
+
+   }
+
+   public void testSkipBecauseCacheControlMaxAgeIsDefined() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/xml; charset=utf-8");
+           response.addHeader("Cache-Control", "private, max-age=232");
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(232));
+   }
+
+   public void testExcludedResponseStatusCode() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+           response.addHeader("ETag", "W/\"1934-1269208821000\"");
+           response.addDateHeader("Date", System.currentTimeMillis());
+        }
+     };
+
+     validate(servlet, null, HttpServletResponse.SC_NOT_MODIFIED);
+   }
+
+   public void testNullContentType() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType(null);
+        }
+     };
+
+     validate(servlet, Integer.valueOf(1 * 60));
+   }
+
+   public void testSkipBecauseExpiresIsDefined() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/xml; charset=utf-8");
+           response.addDateHeader("Expires", System.currentTimeMillis());
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, null);
+   }
+
+   public void testUseContentTypeExpiresConfiguration() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/xml; charset=utf-8");
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(3 * 60));
+   }
+
+   public void testUseContentTypeWithoutCharsetExpiresConfiguration()
+        throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/xml; charset=iso-8859-1");
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(5 * 60));
+   }
+
+   public void testUseDefaultConfiguration1() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("image/jpeg");
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(1 * 60));
+   }
+
+   public void testUseDefaultConfiguration2() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("image/jpeg");
+           response.addHeader("Cache-Control", "private");
+
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(1 * 60));
+   }
+
+   public void testUseMajorTypeExpiresConfiguration() throws Exception {
+     HttpServlet servlet = new HttpServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void service(HttpServletRequest request,
+             HttpServletResponse response) throws ServletException,
+             IOException {
+           response.setContentType("text/json; charset=iso-8859-1");
+           response.getWriter().print("Hello world");
+        }
+     };
+
+     validate(servlet, Integer.valueOf(7 * 60));
+   }
+
+   protected void validate(HttpServlet servlet, Integer expectedMaxAgeInSeconds)
+        throws Exception {
+     validate(servlet, expectedMaxAgeInSeconds, HttpURLConnection.HTTP_OK);
+   }
+
+   protected void validate(HttpServlet servlet,
+        Integer expectedMaxAgeInSeconds, int expectedResponseStatusCode)
+        throws Exception {
+     // SETUP
+
+     Tomcat tomcat = getTomcatInstance();
+     Context root = tomcat.addContext("", TEMP_DIR);
+
+     FilterDef filterDef = new FilterDef();
+     filterDef.addInitParameter("ExpiresDefault", "access plus 1 minute");
+     filterDef.addInitParameter("ExpiresByType text/xml;charset=utf-8",
+           "access plus 3 minutes");
+     filterDef.addInitParameter("ExpiresByType text/xml",
+           "access plus 5 minutes");
+     filterDef.addInitParameter("ExpiresByType text",
+           "access plus 7 minutes");
+     filterDef.addInitParameter("ExpiresExcludedResponseStatusCodes",
+           "304, 503");
+
+     filterDef.setFilterClass(ExpiresFilter.class.getName());
+     filterDef.setFilterName(ExpiresFilter.class.getName());
+
+     root.addFilterDef(filterDef);
+
+     FilterMap filterMap = new FilterMap();
+     filterMap.setFilterName(ExpiresFilter.class.getName());
+     filterMap.addURLPattern("*");
+     root.addFilterMap(filterMap);
+
+     Tomcat.addServlet(root, servlet.getClass().getName(), servlet);
+     root.addServletMapping("/test", servlet.getClass().getName());
+
+     tomcat.start();
+
+     try {
+        Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+        long timeBeforeInMillis = System.currentTimeMillis();
+
+        // TEST
+        HttpURLConnection httpURLConnection = (HttpURLConnection) new URL(
+             "http://localhost:" + tomcat.getConnector().getPort() +
+                   "/test").openConnection();
+
+        // VALIDATE
+        Assert.assertEquals(expectedResponseStatusCode,
+             httpURLConnection.getResponseCode());
+
+        StringBuilder msg = new StringBuilder();
+        for (Entry<String, List<String>> field : httpURLConnection.getHeaderFields().entrySet()) {
+           for (String value : field.getValue()) {
+             msg.append((field.getKey() == null ? "" : field.getKey() +
+                   ": ") +
+                   value + "\n");
+           }
+        }
+        System.out.println(msg);
+
+        Integer actualMaxAgeInSeconds;
+
+        String cacheControlHeader = httpURLConnection.getHeaderField("Cache-Control");
+        if (cacheControlHeader == null) {
+           actualMaxAgeInSeconds = null;
+        } else {
+           actualMaxAgeInSeconds = null;
+           StringTokenizer cacheControlTokenizer = new StringTokenizer(
+                cacheControlHeader, ",");
+           while (cacheControlTokenizer.hasMoreTokens() &&
+                actualMaxAgeInSeconds == null) {
+             String cacheDirective = cacheControlTokenizer.nextToken();
+             StringTokenizer cacheDirectiveTokenizer = new StringTokenizer(
+                   cacheDirective, "=");
+             if (cacheDirectiveTokenizer.countTokens() == 2) {
+                String key = cacheDirectiveTokenizer.nextToken().trim();
+                String value = cacheDirectiveTokenizer.nextToken().trim();
+                if (key.equalsIgnoreCase("max-age")) {
+                   actualMaxAgeInSeconds = Integer.valueOf(value);
+                }
+             }
+           }
+        }
+
+        if (expectedMaxAgeInSeconds == null) {
+           Assert.assertNull("actualMaxAgeInSeconds '" +
+                actualMaxAgeInSeconds + "' should be null",
+                actualMaxAgeInSeconds);
+           return;
+        }
+
+        Assert.assertNotNull(actualMaxAgeInSeconds);
+
+        @SuppressWarnings("null")
+        int deltaInSeconds = Math.abs(actualMaxAgeInSeconds.intValue() -
+             expectedMaxAgeInSeconds.intValue());
+        Assert.assertTrue("actualMaxAgeInSeconds: " +
+             actualMaxAgeInSeconds + ", expectedMaxAgeInSeconds: " +
+             expectedMaxAgeInSeconds + ", request time: " +
+             timeBeforeInMillis + " for content type " +
+             httpURLConnection.getContentType(), deltaInSeconds < 3);
+
+     } finally {
+        tomcat.stop();
+     }
+   }
+
+   public void testIntsToCommaDelimitedString() {
+     String actual = ExpiresFilter.intsToCommaDelimitedString(new int[] {
+           500, 503 });
+     String expected = "500, 503";
+
+     Assert.assertEquals(expected, actual);
+   }
+}

Propchange: tomcat/trunk/test/org/apache/catalina/filters/TestExpiresFilter.java
------------------------------------------------------------------------------
  svn:eol-style = native

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=964219&r1=964218&r2=964219&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 14 22:11:30 2010
@@(protected) @@
     allow the disabling of the <code>exec</code> command. This is now
     disabled by default. Based on a patch by Yair Lenga. (markt)
    </add>
+    <add>
+     <bug>48998</bug>: Add the ExpiresFilter, a port of the httpd mod_expires
+     module. Patch provided by Cyrille Le Clerc. (markt)
+    </add>
    <fix>
     <bug>49030</bug>: When initializing/starting/stopping connectors and
     one of them fails, do not ignore the others. (markt/kkolinko)



©2008 junlu.com - Jax Systems, LLC, U.S.A.