Java Enterpise Platform - MIME Servlet

Motivation

Java servlets can return more than just plain text. In fact, servlets can be used to return any of the media types specified by the MIME Internet standard. Rather than writing a new servlet for each possible MIME media type, it is useful to have a single servlet class that is capable of returning content from any of the MIME media types.

It's reasonable to ask the question "why would we want to serve MIME content through a servlet when most application containers are capable or serving such content without developers having to write any code?" The answer, in a word, is control. Application containers don't provide mechanisms to secure which users may access a particular MIME resource. Moreover, by serving MIME content through a servlet rather than through the application container, it becomes possible to dynamically change the MIME content generated based on, for example, the user making the request.

The Code

The servlet cataloged below is generic and can be used to return content from any of the MIME media types.


package com.bigohsoftware.common.servlets.mime;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public abstract class MimeServlet extends HttpServlet
{

	public static final String REQUESTED_RESOURCE_NAME = "requestedResourceName";

	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
	{
		doPost(req, resp);
	}

	public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException
	{
		logInfo(getServletConfig().getServletName() + " invoked.  Requested mime resource: " + req.getParameter(REQUESTED_RESOURCE_NAME));

		// Get the name of the requested resource
		String requestedResourceName = req.getParameter(REQUESTED_RESOURCE_NAME);

		if (requestedResourceName == null || requestedResourceName.equals(""))
		{
			logError("Called " + getServletConfig().getServletName() + " without providing a parameter for '" + REQUESTED_RESOURCE_NAME + "'");
			resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
			return;
		}
		
		// Ensure that the user is allowed to access the requested resource
		if (!isCanUserAccessRequestedResource(requestedResourceName, req.getSession(true))) {
			resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
			return;
		}

		// Get the MIM type for the resource
		String mimeType = getMimeType(requestedResourceName);

		if (mimeType == null || mimeType.equals(""))
		{
			logError("Failed to get MIME type for the requested resource.");
			resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			return;
		}

		// Set the content type for the response
		resp.setContentType(mimeType);

		// Get a byte array representation of the resource to be returned in the
		// response
		byte[] resourceBytes = getMimeResourceBytes(requestedResourceName);

		if (resourceBytes == null || resourceBytes.length == 0)
		{
			logError("No resource found under the name \"" + requestedResourceName + "\"");
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		// Set content length for the response
		resp.setContentLength(resourceBytes.length);

		// Control the HTTP caching of the response
		resp.setHeader("Cache-Control", "max-age=" + getMaxAgeInSeconds(requestedResourceName));

		// Use streams to return the requested resource 
		ByteArrayInputStream in = new ByteArrayInputStream(resourceBytes);
		OutputStream out = resp.getOutputStream();

		byte[] buf = new byte[1024];
		int count = 0;
		while ((count = in.read(buf)) >= 0)
		{
			out.write(buf, 0, count);
		}
		
		in.close();
		out.close();

	}
	
	/**
	 * Used to determine whether the user may access the requested resource.
	 * 
	 * @param requestedResourceName
	 * @param session
	 * @return
	 */
	protected boolean isCanUserAccessRequestedResource(String requestedResourceName, HttpSession session) {
		return true;
	}

	/**
	 * Returns the numbers of seconds after which the response should be
	 * considered stale by HTTP caches.
	 * 
	 * @param requestedImageName
	 * @return
	 */
	protected long getMaxAgeInSeconds(String requestedImageName)
	{
		// retun 1 hour by default
		return 60 * 60;
	}

	@Override
	protected long getLastModified(HttpServletRequest req)
	{
		String requestedResourceName = req.getParameter(REQUESTED_RESOURCE_NAME);

		if (requestedResourceName == null || requestedResourceName.equals(""))
		{
			return -1;
		}
		else
		{
			return getLastModified(requestedResourceName);
		}
	}

	/**
	 * Returns the system time when the requested resource was last modified.
	 * 
	 * A return value of -1 means that the resource is always "stale."
	 * 
	 * @param requestedResourceName
	 * @return
	 */
	protected abstract long getLastModified(String requestedResourceName);

	/**
	 * Returns a String representation of the resource's MIME type.
	 * 
	 * If you're returning a resource for which you know the original file name,
	 * this method cold easily be implemented by returning:
	 * 
	 * getServletContext().getMimeType(originalFileName.toLowerCase());
	 * 
	 * @param requestedImageName
	 * @return
	 */
	protected abstract String getMimeType(String requestedImageName);

	/**
	 * Returns the bytes for the requested resource.
	 * 
	 * @param requestedImageName
	 * @return
	 */
	protected abstract byte[] getMimeResourceBytes(String requestedImageName) throws IOException;

	/**
	 * Logs informational messages from the servlet.
	 * 
	 * @param info
	 */
	public abstract void logInfo(String info);

	/**
	 * Logs error messages from the servlet.
	 * 
	 * @param error
	 */
	public abstract void logError(String error);

}
				
Discussion

The generic MIME servlet cataloged above provides several important features that you can use for customization.

You can optionally secure the resources on your site by overriding the isCanUserAccessRequestedResource(..) method.


// Ensure that the user is allowed to access the requested resource
if (!isCanUserAccessRequestedResource(requestedResourceName, req.getSession(true))) {
	resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
	return;
}
				

You can control the length of time before your response is revalidated by HTTP caches by overriding the getMaxAgeInSeconds(..) methods.


// Control the HTTP caching of the response
resp.setHeader("Cache-Control", "max-age=" + getMaxAgeInSeconds(requestedResourceName));					
				
An Example: Image Servlet

By way of example, we can utilize the generic MIME servlet to quickly create a servlet that serves up an image. The ImagServlet is quite succinct. It trivially returns an image stored under he WEB-INF/ directory named exampleImage.jpg.


package com.bigohsoftware.common.servlets.mime;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class ImageServlet extends MimeServlet
{

	@Override
	protected byte[] getMimeResourceBytes(String requestedImageName) throws IOException
	{
		InputStream resourceInputStream = null;
		ByteArrayOutputStream baos = null;

		try
		{
			resourceInputStream = getServletContext().getResourceAsStream("/WEB-INF/exampleImage.jpg");
			
			baos = new ByteArrayOutputStream();
			
			byte[] buf = new byte[1024];
            int count = 0;
            while ((count = resourceInputStream.read(buf)) >= 0) {
                baos.write(buf, 0, count);
            }

            return baos.toByteArray();
			
		}
		finally
		{
			if (resourceInputStream != null)
			{
					resourceInputStream.close();
			}
			if (baos != null)
			{
					baos.close();
			}
		}
	}

	@Override
	public long getLastModified(String requestedResourceName)
	{
		return -1;
	}

	@Override
	protected String getMimeType(String requestedImageName)
	{
		return getServletContext().getMimeType("exampleImage.jpg");
	}

	@Override
	public void logError(String error)
	{
		System.err.println(error);
	}

	@Override
	public void logInfo(String info)
	{
		System.out.println(info);
	}

}
				

In order to use the example ImageServlet to return the exampleImage.jpg file, we need only declare the servlet in a web poject's deploymet descriptor (web.xml). Once the servlet is defined in the deployment descriptor, use your web bowser to request the exampleImage.jpg file at this address:
http://[ip_address]:[port]/[web_project_name]/ImageServlet?requestedResourceName=not_used