RequestHandler.java

/***************************************************************************
   Copyright 2008 Emily Estes

   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 net.metanotion.web.servlets;


import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.io.Serializer;
import net.metanotion.util.Dispatcher;
import net.metanotion.util.Message;

import net.metanotion.web.Cookie;
import net.metanotion.web.HttpMethod;
import net.metanotion.web.HttpValues;
import net.metanotion.web.RequestObject;
import net.metanotion.web.SessionHandler;

/** This class is the main HTTP Request/Response processing loop for the web framework. It relies on the
Java Servlet classes for request and response handling. However, this class does not control how it is
situated inside a servlet context, and thus relies on other classes or dependency injection to create it.
It is currently only used by the {@link net.metanotion.web.servlets.EmbeddedServlet} class which is used
as the servlet for running servlet containers like Jetty in embedded/app server mode. It is relatively
straightforward to run this inside of a normal serlet on a servlet container like Tomcat, however due
to disuse the code was removed from the framework.
	@see net.metanotion.web.servlets.EmbeddedServlet
*/
public final class RequestHandler {
	private static final Logger logger = LoggerFactory.getLogger(RequestHandler.class);

	/** The default maximum size of in-memory file for file uploads inside an HTTP Request. */
	public static final int MAX_MEMORY_FILE = 10240;
	/** If an uploaded file is bigger than the maximum in-memory allowed size, this constant is the default maximum
		size of a file to store in the temp folder. */
	public static final int MAX_DISK_FILE = 1048576;
	/** The location of the temporary directory used for file uploads that overflow the maximum for in-memory files.
	This relies on the setting of the system property: "<code>java.io.tmpdir</code>". */
	public static final File DEFAULT_TEMP_DIR;
	static {
		File tempDir = null;
		try {
			tempDir = new File(System.getProperty("java.io.tmpdir"));
		} catch (Exception e) { tempDir = null; }
		DEFAULT_TEMP_DIR = tempDir;
	}

	/** The dispatcher provided by the application. */
	private final Dispatcher<? extends Object, RequestObject> dispatcher;
	/** The session handler provided by the application. */
	private final SessionHandler app;
	/** The temporary file forlder to process large file uploads. */
	private final File tempFolder;
	/** The maximum size of a file upload to cache in memoriy. */
	private final int maxMemFile;
	/** The absolute maximum size of a file upload. */
	private final long maxDiskFile;
	/** Whether to allow to cache file uploads on disk at all. */
	private final boolean allowDiskCache;

	/** Create a new HTTP request handler with a given dispatcher and an app provided session handler using defaults
			for handling file uploads.
		@param dispatcher The application provided dispatcher to convert {@link net.metanotion.web.RequestObject}'s
			into application specific messages.
		@param app The session handler for the app to process messages.
	*/
	public RequestHandler(final Dispatcher<? extends Object, RequestObject> dispatcher, final SessionHandler app) {
		this(dispatcher, app, DEFAULT_TEMP_DIR);
	}

	/** Create a new HTTP request handler with a given dispatcher and an app provided session handler using default
			sizes for file uploads.
		@param dispatcher The application provided dispatcher to convert {@link net.metanotion.web.RequestObject}'s
			into application specific messages.
		@param app The session handler for the app to process messages.
		@param tempFolder The temp folder to use for file-uploads bigger than maxMemFile and smaller than maxDiskFile.
	*/
	public RequestHandler(final Dispatcher<? extends Object, RequestObject> dispatcher,
			final SessionHandler app,
			final File tempFolder) {
		this(dispatcher, app, tempFolder, MAX_MEMORY_FILE, MAX_DISK_FILE);
	}

	/** Create a new HTTP request handler with a given dispatcher and an app provided session handler using the default
			temporary folder.
		@param dispatcher The application provided dispatcher to convert {@link net.metanotion.web.RequestObject}'s
			into application specific messages.
		@param app The session handler for the app to process messages.
		@param maxMemFile The maximum size an in-memory file is allowed to be for processing file uploads. Files bigger
			than this will be stored in the tempFolder.
		@param maxDiskFile The maximum size allowed for file uploads. File uploads smaller than this value, but bigger
			than maxMemFile will be temporarily cached in the tempFolder.
	*/
	public RequestHandler(final Dispatcher<? extends Object, RequestObject> dispatcher,
			final SessionHandler app,
			final int maxMemFile,
			final long maxDiskFile) {
		this(dispatcher, app, DEFAULT_TEMP_DIR, maxMemFile, maxDiskFile);
	}

	/** Create an instance of an Http Request handler to process HTTP requests for a given application.
		@param dispatcher The application provided dispatcher to convert {@link net.metanotion.web.RequestObject}'s
			into application specific messages.
		@param app The session handler for the app to process messages.
		@param tempFolder The temp folder to use for file-uploads bigger than maxMemFile and smaller than maxDiskFile.
		@param maxMemFile The maximum size an in-memory file is allowed to be for processing file uploads. Files bigger
			than this will be stored in the tempFolder.
		@param maxDiskFile The maximum size allowed for file uploads. File uploads smaller than this value, but bigger
			than maxMemFile will be temporarily cached in the tempFolder.
	*/
	public RequestHandler(final Dispatcher<? extends Object, RequestObject> dispatcher,
			final SessionHandler app,
			final File tempFolder,
			final int maxMemFile,
			final long maxDiskFile) {
		this.dispatcher = dispatcher;
		this.app = app;
		this.tempFolder = tempFolder;
		this.maxMemFile = maxMemFile;
		this.maxDiskFile = maxDiskFile;
		this.allowDiskCache = (tempFolder != null);
	}

	/** This is the core HTTP request/response processing loop of the web framework.
		The servlet HTTP request object is wrapped in a RequestObject implementation and then the request is sent
		through the application provided dispatcher. The message produced and the requestobject are passed through the
		application's session handler to produce a response. The response is then serialized the servlet response
		object. If at any point an exception occurs, it is logged and an HTTP "internal server error"(500) response is
		generated instead.
		@param method The HTTP verb associated with this HTTP request.
		@param req The Servlet generated HTTP request object.
		@param resp The Servlet response object used to write the response.
		@throws ServletException if calls against the servlet request or servlet response generate one.
		@throws IOException if the generation of the response or reading the request generate one.
	*/
	public void request(final HttpMethod method, final HttpServletRequest req, final HttpServletResponse resp)
			throws ServletException, IOException {
		try {
			logger.debug("Request");
			logger.debug("Setting up RequestObject");
			final RequestObject ro = new SimpleRequestObject(method, req, tempFolder, maxDiskFile, maxMemFile, allowDiskCache);
			logger.debug("Decoding request");
			final Message m = dispatcher.dispatch(ro);
			logger.debug("Dispatching request");
			final Object response = app.dispatch(m, ro);
			if(response == null) { return; }
			logger.info(response.toString());
			if(response instanceof HttpValues) {
				logger.info("HttpValues");
				writeHttpValues((HttpValues) response, resp, Charset.forName(resp.getCharacterEncoding()));
			} else {
				logger.info(response.getClass().toString());
				Serializer.write(response, resp.getOutputStream(), Charset.forName(resp.getCharacterEncoding()));
			}
		} catch (final Exception e) {
			logger.debug("Failure in Request", e);
			resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	/** Serialize an HttpValues instance through a servlet response object using the specified character encoding.
		@param hv The HttpValues instance to serialize
		@param resp The Servlet Response objet to write out the HTTP response through.
		@param charEncode The character encoding to use for the response, see {@link java.nio.charset.Charset}
		@throws An IOException if writing out the servlet response generates one.
	*/
	private void writeHttpValues(final HttpValues hv,
			final HttpServletResponse resp,
			final Charset charEncode) throws IOException {
		Iterator<Map.Entry<String,Object>> i = hv.listHeaders();
		while(i.hasNext()) {
			final Map.Entry<String,Object> header = i.next();
			final String k = header.getKey();
			final Object v = header.getValue();
			logger.info("Header {} - {}", k, v);
			if(v instanceof java.util.Date) {
				resp.setDateHeader(k,((java.util.Date) v).getTime());
			} else if(v instanceof String) {
				resp.setHeader(k, (String) v);
			} else {
				resp.setHeader(k, v.toString());
			}
		}
		i = hv.listCookies();
		while(i.hasNext()) {
			final Map.Entry<String,Object> cookie = i.next();
			final String k = cookie.getKey();
			final Object v = cookie.getValue();
			logger.info("Cookie {} - {}", k, v);
			javax.servlet.http.Cookie c;
			if(v instanceof String) {
				c = new javax.servlet.http.Cookie(k, (String) v);
			} else if (v instanceof Cookie) {
				c = browserToServletCookie((Cookie) v);
			} else {
				c = new javax.servlet.http.Cookie(k, v.toString());
			}
			resp.addCookie(c);
		}
		final Object r = hv.unwrap();
		logger.info("Result " + (r==null));
		logger.info(Integer.toString(hv.getHttpStatus()));
		resp.setStatus(hv.getHttpStatus());
		if(r != null) { Serializer.write(r, resp.getOutputStream(), charEncode); }
	}

	/** Convert an instance of the Cookie interface into a Servlet Cookie instance.
		@param bc The Web Framework Cookie instance.
		@return a Java Servlet Cookie equivalent to the Framework Cookie instance.
	*/
	private javax.servlet.http.Cookie browserToServletCookie(final Cookie bc) {
		final javax.servlet.http.Cookie c = new javax.servlet.http.Cookie(bc.getName(), bc.getValue());
		c.setComment(bc.getComment());
		c.setDomain(bc.getDomain());
		c.setMaxAge(bc.getMaxAge());
		c.setPath(bc.getPath());
		c.setSecure(bc.getSecure() == Cookie.Security.SSL ? true : false);
		c.setHttpOnly(bc.getHttpOnly() == Cookie.Access.ALL ? true : false);
		c.setVersion(bc.getVersion() == Cookie.Version.NETSCAPE ? 0 : 1);
		return c;
	}
}