/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.katari.core.web;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Calendar;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A base servlet used to serve static content (gif, png, css, etc) from the
* classpath.
*
* This class handles all the process related to sending the content to the
* client. The location of the content is delegated to the subclasses.
*
* Subclasses must implement findInputStream and getContentType.
*
* You must call this class init(ServletConfig) if you override it.
*
* This accepts the following configuration parameters:<br>
*
* requestCacheContent: whether to send the cache headers to the client with an
* expiration date one month in the future, or the headers that state that the
* content should not be cached (true / false). It is false by default.<br>
*
* debug: whether to enable debug mode or not. In debug mode, the servlet
* attempts to load the requested content directly from the file system. This
* makes it possible to edit the resources directly from disk and see the
* results inmediately without a redeploy. It is false by default.<br>
*
* All other initialization parameters are ignored, so subclasses can define
* adittional config parameters.
*/
public abstract class BaseStaticContentServlet extends HttpServlet {
/** The serialization version number.
*
* This number must change every time a new serialization incompatible change
* is introduced in the class.
*/
private static final long serialVersionUID = 1;
/** The buffer size used to transfer bytes to the client.
*/
private static final int BUFFER_SIZE = 4096;
/** The class logger.
*/
private static Logger log = LoggerFactory.getLogger(
BaseStaticContentServlet.class);
/** Provide a formatted date for setting heading information when caching
* static content.
*/
private final Calendar lastModified = Calendar.getInstance();
/** Whether to send the client the cache header asking to cache the content
* served by this servlet or not.
*/
private boolean requestCacheContent = false;
/** Whether debug mode is enabled.
*
* Initialized from the debug servlet parameter.
*/
private boolean debug = false;
/** Initializes the servlet.
*
* It sets the default packages for static resources.
*
* @param config The servlet configuration. It cannot be null.
*
* @throws ServletException in case of error.
*/
public void init(final ServletConfig config) throws ServletException {
log.trace("Entering init");
Validate.notNull(config, "The servlet config cannot be null.");
String applyCacheInfo = config.getInitParameter("requestCacheContent");
requestCacheContent = Boolean.valueOf(applyCacheInfo);
String debugValue = config.getInitParameter("debug");
debug = Boolean.valueOf(debugValue);
log.trace("Leaving init");
}
/** Serves a get request.
*
* @param request The request object.
*
* @param response The response object.
*
* @throws IOException in case of an io error.
*
* @throws ServletException in case of error.
*/
@Override
protected void doGet(final HttpServletRequest request, final
HttpServletResponse response) throws ServletException, IOException {
serveStaticContent(request, response);
}
/** Serves a post request.
*
* @param request The request object.
*
* @param response The response object.
*
* @throws IOException in case of an io error.
*
* @throws ServletException in case of error.
*/
@Override
protected void doPost(final HttpServletRequest request, final
HttpServletResponse response) throws ServletException, IOException {
serveStaticContent(request, response);
}
/** Serves some static content.
*
* @param request The request object.
*
* @param response The response object.
*
* @throws IOException in case of an io error.
*
* @throws ServletException in case of error.
*
* TODO See if it should use pathInfo instead of servletPath.
*/
private void serveStaticContent(final HttpServletRequest request, final
HttpServletResponse response) throws ServletException, IOException {
log.trace("Entering serveStaticContent");
String resourcePath = getServletPath(request);
findStaticResource(resourcePath, request, response);
log.trace("Leaving serveStaticContent");
}
/** Locate a static resource and copy directly to the response, setting the
* appropriate caching headers.
*
* A URL decoder is run on the resource path and it is configured to use the
* UTF-8 encoding because according to the World Wide Web Consortium
* Recommendation UTF-8 should be used and not doing so may introduce
* incompatibilites.
*
* @param theName The resource name
*
* @param request The request
*
* @param response The response
*
* @throws IOException If anything goes wrong
*/
private void findStaticResource(final String theName, final
HttpServletRequest request, final HttpServletResponse response) throws
IOException {
log.trace("Entering findStaticResource('{}', ...)", theName);
String name = URLDecoder.decode(theName, "UTF-8");
// Checks if the requested resource matches a recognized content type.
String contentType = getContentType(name);
if (contentType == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().write(
"<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN'"
+ " 'http://www.w3.org/TR/html4/strict.dtd'>"
+ "<html><head><title>404</title></head>"
+ "<body>Resource not found</body></html>");
log.trace("Leaving findStaticResource with SC_NOT_FOUND");
response.flushBuffer();
return;
}
// Looks for the resource.
InputStream is = findInputStream(name);
if (is == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
log.trace("Leaving findStaticResource with SC_NOT_FOUND");
response.getWriter().write(
"<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01//EN'"
+ " 'http://www.w3.org/TR/html4/strict.dtd'> "
+ "<html><head><title>404</title></head>"
+ "<body>Resource not found</body></html>");
log.trace("Leaving findStaticResource with SC_NOT_FOUND");
response.flushBuffer();
return;
}
// check for if-modified-since, prior to any other headers
long requestedOn = 0;
try {
requestedOn = request.getDateHeader("If-Modified-Since");
} catch (Exception e) {
log.warn("Invalid If-Modified-Since header value: '"
+ request.getHeader("If-Modified-Since") + "', ignoring");
}
// set the content-type header
response.setContentType(contentType);
Calendar cal = Calendar.getInstance();
long now = cal.getTimeInMillis();
response.setDateHeader("Date", now);
if (!debug && requestCacheContent) {
// set heading information for caching static content
cal.add(Calendar.MONTH, 1);
long expires = cal.getTimeInMillis();
// max-age is 1 month, in seconds.
response.setHeader("Cache-Control", "public, max-age=2592000");
response.setDateHeader("Expires", expires);
} else {
// By default, never cache the static content.
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "Thu, 01 Jan 1970 00:00:00 GMT");
}
long lastModifiedMillis = lastModified.getTimeInMillis();
boolean notModified;
notModified = 0 < requestedOn && requestedOn <= lastModifiedMillis;
if (!debug && notModified) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
log.trace("Leaving findStaticResource with SC_NOT_MODIFIED");
return;
}
try {
copy(is, response.getOutputStream());
} finally {
is.close();
}
log.trace("Leaving findStaticResource");
}
/**
* Determine the content type for the resource name.
*
* @param name The resource name. It cannot be null.
*
* @return The mime type, null if the resource name is not recognized.
*/
protected abstract String getContentType(final String name);
/**
* Copy bytes from the input stream to the output stream.
*
* @param input The input stream
* @param output The output stream
* @throws IOException If anytSrtringhing goes wrong
*/
private void copy(final InputStream input, final OutputStream output) throws
IOException {
final byte[] buffer = new byte[BUFFER_SIZE];
int n;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
}
output.flush();
}
/** Look for a static resource in the classpath.
*
* In debug mode, it looks for the resource in the file system, using
* debugPrefix as the base file name.
*
* @param name The resource name. It cannot be null.
*
* @return the inputstream of the resource, null if the resource could not be
* found.
*
* @throws IOException If there is a problem locating the resource.
*/
protected abstract InputStream findInputStream(final String name)
throws IOException;
/** Concatenates two path names.
*
* This is protected as an aid for subclasses.
*
* @param prefix The first component of the file name. It cannot be null.
*
* @param name The second component of the file name. It cannot be null.
*
* @return A file name of the form prefix/name with the correct number of /.
*/
protected String buildPath(final String prefix, final String name) {
Validate.notNull(prefix, "The file component prefix cannot be null.");
Validate.notNull(name, "The second file component cannot be null.");
if (prefix.endsWith("/") && name.startsWith("/")) {
return prefix + name.substring(1);
} else if (prefix.endsWith("/") || name.startsWith("/")) {
return prefix + name;
}
return prefix + "/" + name;
}
/** This is a convenience method to load a resource as a stream.
*
* The algorithm used to find the resource is given in getResource().
*
* @param resourceName The name of the resource to load. It cannot be null
* nor start with '/'.
*
* @return Returns an input stream representing the resource, null if not
* found.
*/
protected InputStream getResourceAsStream(final String resourceName) {
Validate.notNull(resourceName, "The resource name cannot be null.");
Validate.isTrue(!resourceName.startsWith("/"),
"The resource cannot start with /");
URL url = getResource(resourceName);
if (url == null) {
return null;
}
try {
return url.openStream();
} catch (IOException e) {
log.debug("Exception opening resource: " + resourceName, e);
return null;
}
}
/**
* Load a given resource.
* <p/>
* This method will try to load the resource using the following methods (in
* order):
*
* <ul>
*
* <li>From {@link Thread#getContextClassLoader()
* Thread.currentThread().getContextClassLoader()}
*
* <li>From the {@link Class#getClassLoader() getClass().getClassLoader() }
*
* </ul>
*
* @param resourceName The name of the resource to load
*
* @return Returns the url of the reesource, null if not found.
*/
protected URL getResource(final String resourceName) {
URL url = null;
// Try the context class loader.
ClassLoader contextClassLoader;
contextClassLoader = Thread.currentThread().getContextClassLoader();
if (null != contextClassLoader) {
url = contextClassLoader.getResource(resourceName);
}
// Try the current class class loader if the context class loader failed.
if (url == null) {
url = getClass().getClassLoader().getResource(resourceName);
}
return url;
}
/**
* Retrieves the current request servlet path.
* Deals with differences between servlet specs (2.2 vs 2.3+)
*
* @param request the request
* @return the servlet path
*/
private String getServletPath(final HttpServletRequest request) {
String servletPath = request.getServletPath();
if (null != servletPath && !"".equals(servletPath)) {
return servletPath;
}
String requestUri = request.getRequestURI();
int startIndex = request.getContextPath().length();
int endIndex = 0;
if (request.getPathInfo() == null) {
endIndex = requestUri.length();
} else {
endIndex = requestUri.lastIndexOf(request.getPathInfo());
}
if (startIndex > endIndex) { // this should not happen
endIndex = startIndex;
}
return requestUri.substring(startIndex, endIndex);
}
/** True if in debug mode.
*
* @return true in debug mode.
*/
public boolean isInDebugMode() {
return debug;
}
}