package de.torisan.util.servletFilters;
import java.io.File;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** A Filter implementation that can be used to set certain (HTTP 1.1 as well as HTTP 1.0) header of the response
* which can improve cacheability of delivered content in the client's browser cache as well as in some proxy servers on the way.
* Improving cacheability means decreasing load on the server an minimising bandwidth consumption. But it also means that an outdated
* version of a resource from a server might (intentfully in his case) be displayed to the client!
*
* @author Toralf Richter
*/
public class SimpleBrowserCacheInfluencingFilter implements Filter {
/** The filter configuration to read from */
private FilterConfig fc;
/** Flag indication to use / to not use ETags */
private boolean useETag = false;
/** Value holder needed in calculation of time to set on the Expires header */
private int expiresCurrentPlusHours = 0;
/** This method performs the actual filtering / modifications to the response headers. It will perfom the actions configured
* in the deployment descriptor of the context this filter implementation is used in on the resources indicated by the
* filter-mapping properties configured in the deployment descriptor.
*/
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
String file = this.fc.getServletContext().getRealPath(
request.getRequestURI().substring( request.getContextPath().length() )
);
try {
File resFile = new File(file);
if (resFile.exists()) {
response.setDateHeader("Last-Modified", resFile.getAbsoluteFile().lastModified());
}
if( this.expiresCurrentPlusHours>0 ) {
response.setDateHeader("Expires", (System.currentTimeMillis()+((this.expiresCurrentPlusHours*3600)*1000L) ));
}
if( this.useETag ) {
if( !response.containsHeader("ETag") ) {
response.setHeader("ETag", "W/\""+resFile.length()+"-"+resFile.getAbsoluteFile().lastModified()+"\"" );
}
}
} catch (Exception e) {
// response.setHeader("X-Exception", e.getClass().getName() );
}
chain.doFilter(request, response);
}
/** The init method reads the FilterConfig, which is set up for the desired type of operation
* using init params in the web.xml deployment descriptor of the servlet context. In the following doc section you will find
* some explanations on how to configure the filter properly via the web.xml deployment descriptor as well as some considerations
* on the headers that can be used / configured.
* <br/>
* Known init params for this sample implementation are "useETag" and "expires". "useETag"
* accepts pseudo-boolean values of textual "true" and "false". "useETag" can as well be omitted (which maps it's value to "false").
* If "useETag" is set to true the filter will try so set an ETag header. If a previously set ETag header (generated by the container itself)
* is already present, this will not be changed.
* If an *ETag* header is set / used it will be set as a composition of the resource's modification date (millis since epoch)
* and it's byte size on disk.
* <p/>
* Considerations with ETag:<br/>
* 1. Documentation says: Entity tags are normally "strong validators," but the protocol provides a mechanism to tag an entity tag
* as "weak." One can think of a strong validator as one that changes whenever the bits of an entity changes, while a weak value changes
* whenever the meaning of an entity changes. Alternatively, one can think of a strong validator as part of an identifier for a specific
* entity, while a weak validator is part of an identifier for a set of semantically equivalent entities.<br/>
* 2. Tomcat itself when setting ETags set them as weak validator (prefixed with W/). This filter also sets ETags als weak validators because
* it seems more standard compliant. Consider the example: The bit order in an image file on disk changed, it's modifications time has been
* kept constant (e.g. by setting the mtime back to it's old value with a system tool). Since the the ETag here is composed of bit size
* (not a bit order hash) and mtime in millis since epoch, this would make the same ETag as before the change to the image file - although a
* change to the bit order in the file has occured. In most cases using a strong or weak validator ETag would not make much of a
* difference - yet in some it might.<br/>
* 3. Some more issues: Even if the ETag is set to a certain value with this filter, they final response sent to the client might contain
* a different value. This is due to the container itself overwriting any previously set ETag at a later stage (after this filter has been
* applied).
* <p/>
* "expires" takes a positive integer number as it's value. It can be omitted (which maps it's value to
* 0 and thereby instructs the filter to not set an *Expires* header. Any existing headers of the same name will
* remain in place though). The integer value of "expires" is used to set the Date value of the *Expires* header
* to the date value of the the current date plus x hours. <br/>
* So setting expires=36 would set an *Expires* header on the response which equals the current date + 36 hours.
* <p/>
* Considerations with Expires header: Keep in mind that Expires is a HTTP 1.0 header.
* Sample filter configuration section for web.xml
* <p/>
* <pre>
* <filter>
* <filter-name>SimpleBrowserCacheInfluencingFilter</filter-name>
* <filter-class>SimpleBrowserCacheInfluencingFilter</filter-class>
* <init-param>
* <param-name>useETag</param-name>
* <param-value>true</param-value>
* </init-param>
* <init-param>
* <param-name>expires</param-name>
* <param-value>36</param-value>
* </init-param>
* </filter>
* </pre>
*/
public void init(FilterConfig filterConfig) {
this.fc = filterConfig;
this.useETag = new Boolean( this.fc.getInitParameter( "useETag" ) ).booleanValue();
if ( this.fc.getInitParameter( "expires" )!=null && this.fc.getInitParameter( "expires" ).length()>0 ) {
try {
this.expiresCurrentPlusHours = Integer.parseInt( this.fc.getInitParameter( "expires" ), 10 );
} catch( Exception e ) {
}
}
}
/** Simple implementation of the destroy method prescribed by the
* Filter interface.
*/
public void destroy() {
this.fc = null;
}
}