com.samskivert.velocity.DispatcherServlet Maven / Gradle / Ivy
Show all versions of samskivert Show documentation
//
// samskivert library - useful routines for java programs
// Copyright (C) 2001-2012 Michael Bayne, et al.
// http://github.com/samskivert/samskivert/blob/master/COPYING
package com.samskivert.velocity;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Properties;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.io.VelocityWriter;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.util.SimplePool;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.event.EventCartridge;
import org.apache.velocity.app.event.MethodExceptionEventHandler;
import com.samskivert.servlet.HttpErrorException;
import com.samskivert.servlet.MessageManager;
import com.samskivert.servlet.RedirectException;
import com.samskivert.servlet.SiteIdentifier;
import com.samskivert.servlet.SiteResourceLoader;
import com.samskivert.servlet.util.FriendlyException;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.StringUtil;
import static com.samskivert.servlet.Log.log;
/**
* The dispatcher servlet builds upon Velocity's architecture. It does so in the following ways:
*
* - It defines the notion of a logic object which populates the context with data to be
* used to satisfy a particular request. The logic is not a servlet and is therefore limited in
* what it can do while populating data. Experience dictates that ultimate flexibility leads to bad
* design decisions and that this is a place where that sort of thing can be comfortably nipped in
* the bud.
*
* - It allows template files to be referenced directly in the URL while maintaining the ability
* to choose a cobranded template based on information in the request. The URI is mapped to a
* servlet based on some simple mapping rules. This provides template designers with a clearer
* understanding of the structure of a web application as well as with an easy way to test their
* templates in the absence of an associated servlet.
*
* - It provides a common error handling paradigm that simplifies the task of authoring web
* applications.
*
*
* URI to servlet mapping
The mapping process allows the Velocity framework to be
* invoked for all requests ending in a particular file extension (usually .wm
). It is
* necessary to instruct your servlet engine of choice to invoke the DispatcherServlet
* for all requests ending in that extension. For Apache/JServ this looks something like this:
*
*
* ApJServAction .wm /servlets/com.samskivert.velocity.Dispatcher
*
*
* The request URI then defines the path of the template that will be used to satisfy the
* request. To understand how code is selected to go along with the request, let's look at an
* example. Consider the following configuration:
*
*
* applications=whowhere
* whowhere.base_uri=/whowhere
* whowhere.base_pkg=whowhere.logic
*
*
* This defines an application identified as whowhere
. An application is defined by
* three parameters, the application identifier, the base_uri
, and the
* base_pkg
. The base_uri
defines the prefix shared by all pages served
* by the application and which serves to identify which application to invoke when processing a
* request. The base_pkg
is used to construct the logic classname based on the URI and
* the base_uri
parameter.
*
* Now let's look at a sample request to determine how the logic classname is
* resolved. Consider the following request URI:
*
*
* /whowhere/view/trips.wm
*
*
* It begins with /whowhere
which tells the dispatcher that it's part of the
* whowhere
application. That application's base_uri
is then stripped
* from the URI leaving /view/trips.wm
. The slashes are converted into periods to map
* directories to packages, giving us view.trips.wm
. Finally, the
* base_pkg
is prepended and the trailing .wm
extension removed.
*
* Thus the class invoked to populate the context for this request is
* whowhere.servlets.view.trips
(note that the classname is lowercase which
* is an intentional choice in resolving conflicting recommendations that classnames should always
* start with a capital letter and URLs should always be lowercase).
*
*
The template used to generate the result is loaded based on the full URI, essentially with a
* call to getTemplate("/whowhere/view/trips.wm")
in this example. This is the place
* where more sophisticated cobranding support could be inserted in the future (ie. if I ever want
* to use this to develop a cobranded web site).
*
* @see Logic
*/
public class DispatcherServlet extends HttpServlet
implements MethodExceptionEventHandler
{
/** The HTTP content type context key. */
public static final String CONTENT_TYPE = "default.contentType";
@Override
public void init (ServletConfig config)
throws ServletException
{
super.init(config);
// create and initialize our application
try {
_app = createApp(config);
_app.init(config, getServletContext(), getLogicPackage(config));
} catch (Throwable t) {
throw new ServletException("Error instantiating Application: " + t, t);
}
try {
Velocity.init(loadConfiguration(config));
} catch (Exception e) {
throw new ServletException("Error initializing Velocity: " + e, e);
}
_defaultContentType = RuntimeSingleton.getString(CONTENT_TYPE, DEFAULT_CONTENT_TYPE);
// determine the character set we'll use
_charset = config.getInitParameter(CHARSET_KEY);
if (_charset == null) {
_charset = "UTF-8";
}
}
/**
* Handles HTTP GET
requests by calling {@link #doRequest}.
*/
@Override
public void doGet (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doRequest(request, response);
}
/**
* Handles HTTP POST
requests by calling {@link #doRequest}.
*/
@Override
public void doPost (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
doRequest(request, response);
}
/**
* Clean up after ourselves and our application.
*/
@Override
public void destroy ()
{
super.destroy();
// shutdown our application
_app.shutdown();
}
protected Application createApp (ServletConfig config) throws Exception {
String appcl = config.getInitParameter(APP_CLASS_KEY);
if (StringUtil.isBlank(appcl)) {
return new Application();
} else {
Class> appclass = Class.forName(appcl);
return (Application)appclass.newInstance();
}
}
protected String getLogicPackage (ServletConfig config) {
String logicPkg = config.getInitParameter(LOGIC_PKG_KEY);
return StringUtil.isBlank(logicPkg) ? "" : logicPkg;
}
protected Properties loadVelocityProps (ServletConfig config) throws IOException {
String propsPath = config.getInitParameter(INIT_PROPS_KEY);
if (propsPath == null) {
throw new IOException(INIT_PROPS_KEY + " must point to the velocity properties file " +
"in the servlet configuration.");
}
// config util loads properties files from the classpath
Properties props = ConfigUtil.loadProperties(propsPath);
if (props == null) {
throw new IOException("Unable to load velocity properties " +
"from file '" + INIT_PROPS_KEY + "'.");
}
return props;
}
/**
* We load our velocity properties from the classpath rather than from a file.
*/
protected Properties loadConfiguration (ServletConfig config)
throws IOException
{
Properties props = loadVelocityProps(config);
// if we failed to create our application for whatever reason; bail
if (_app == null) {
return props;
}
// let the application set up velocity properties
_app.configureVelocity(config, props);
// if no file resource loader path has been set and a site-specific jar file path was
// provided, wire up our site resource manager
configureResourceManager(config, props);
// wire up our #import directive
props.setProperty("userdirective", ImportDirective.class.getName());
// configure the servlet context logger
props.put(RuntimeSingleton.RUNTIME_LOG_LOGSYSTEM_CLASS,
ServletContextLogger.class.getName());
// now return our augmented properties
return props;
}
protected void configureResourceManager (ServletConfig config, Properties props) {
if (props.getProperty("file.resource.loader.path") == null) {
SiteResourceLoader siteLoader = _app.getSiteResourceLoader();
if (siteLoader != null) {
log.info("Velocity loading templates from site loader.");
props.setProperty(RuntimeSingleton.RESOURCE_MANAGER_CLASS,
SiteResourceManager.class.getName());
_usingSiteLoading = true;
} else {
// otherwise use a servlet context resource loader
log.info("Velocity loading templates from servlet context.");
props.setProperty(RuntimeSingleton.RESOURCE_MANAGER_CLASS,
ServletContextResourceManager.class.getName());
}
}
}
/**
* Loads up the template appropriate for this request, locates and invokes any associated logic
* class and finally returns the prepared template which will be merged with the prepared
* context.
*/
public Template handleRequest (HttpServletRequest req, HttpServletResponse rsp, Context ctx)
throws Exception
{
InvocationContext ictx = (InvocationContext)ctx;
Logic logic = null;
// listen for exceptions so that we can report them
EventCartridge ec = ictx.getEventCartridge();
if (ec == null) {
ec = new EventCartridge();
ec.attachToContext(ictx);
ec.addEventHandler(this);
}
// if our application failed to initialize, fail with a 500 response
if (_app == null) {
rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return null;
}
// obtain the siteid for this request and stuff that into the context
int siteId = SiteIdentifier.DEFAULT_SITE_ID;
SiteIdentifier ident = _app.getSiteIdentifier();
if (ident != null) {
siteId = ident.identifySite(req);
}
if (_usingSiteLoading) {
ctx.put("__siteid__", Integer.valueOf(siteId));
}
// put the context path in the context as well to make it easier to
// construct full paths
ctx.put("context_path", req.getContextPath());
// then select the template
Template tmpl = null;
try {
tmpl = selectTemplate(siteId, ictx);
} catch (ResourceNotFoundException rnfe) {
// send up a 404. For some annoying reason, Jetty tells Apache
// that all is okay (200) when sending its own custom error pages,
// forcing us to use Jetty's custom error page handling code rather
// than passing it up the chain to be dealt with appropriately.
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
// assume the request is in the default character set unless it has
// actually been sensibly supplied by the browser
if (req.getCharacterEncoding() == null) {
req.setCharacterEncoding(_charset);
}
// assume an HTML response in the default character set unless
// otherwise massaged by the logic
rsp.setContentType("text/html; charset=" + _charset);
Exception error = null;
try {
// insert the application into the context in case the logic or a
// tool wishes to make use of it
ictx.put(APPLICATION_KEY, _app);
// if the application provides a message manager, we want create a
// translation tool and stuff that into the context
MessageManager msgmgr = _app.getMessageManager();
if (msgmgr != null) {
I18nTool i18n = new I18nTool(req, msgmgr);
ictx.put(I18NTOOL_KEY, i18n);
}
// create a form tool for use by the template
FormTool form = new FormTool(req);
ictx.put(FORMTOOL_KEY, form);
// create a new string tool for use by the template
StringTool string = new StringTool();
ictx.put(STRINGTOOL_KEY, string);
// create a new data tool for use by the tempate
DataTool datatool = new DataTool();
ictx.put(DATATOOL_KEY, datatool);
// create a curreny tool set up to use the correct locale
CurrencyTool ctool = new CurrencyTool(req.getLocale());
ictx.put(CURRENCYTOOL_KEY, ctool);
// allow the application to prepare the context
_app.prepareContext(ictx);
// allow the application to do global access control
_app.checkAccess(ictx);
// resolve the appropriate logic class for this URI and execute it
// if it exists
String path = req.getServletPath();
logic = resolveLogic(path);
if (logic != null) {
logic.invoke(_app, ictx);
}
} catch (Exception e) {
error = e;
}
// if an error occurred processing the template, allow the application
// to convert it to something more appropriate and then handle it
String errmsg = null;
try {
if (error != null) {
throw _app.translateException(error);
}
} catch (RedirectException re) {
rsp.sendRedirect(re.getRedirectURL());
return null;
} catch (HttpErrorException hee) {
String msg = hee.getErrorMessage();
if (msg != null) {
rsp.sendError(hee.getErrorCode(), msg);
} else {
rsp.sendError(hee.getErrorCode());
}
return null;
} catch (FriendlyException fe) {
// grab the error message, we'll deal with it shortly
errmsg = fe.getMessage();
} catch (Exception e) {
errmsg = _app.handleException(req, logic, e);
}
// if we have an error message, insert it into the template
if (errmsg != null) {
// try using the application to localize the error message
// before we insert it
MessageManager msgmgr = _app.getMessageManager();
if (msgmgr != null) {
errmsg = msgmgr.getMessage(req, errmsg);
}
ictx.put(ERROR_KEY, errmsg);
}
return tmpl;
}
/**
* Called when a method throws an exception during template evaluation.
*/
@SuppressWarnings("rawtypes") // our super class declares a bare Class
public Object methodException (Class clazz, String method, Exception e)
throws Exception
{
log.warning("Exception", "class", clazz.getName(), "method", method, e);
return "";
}
/**
* Returns the reference to the application that is handling this
* request.
*
* @return The application in effect for this request or null if no
* application was selected to handle the request.
*/
public static Application getApplication (InvocationContext context)
{
return (Application)context.get(APPLICATION_KEY);
}
/**
* Handles all requests (by default).
*
* @param request HttpServletRequest object containing client request.
* @param response HttpServletResponse object for the response.
*/
protected void doRequest (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
InvocationContext context = null;
try {
context = new InvocationContext(request, response);
setContentType(request, response);
Template template = handleRequest(request, response, context);
if (template != null) {
mergeTemplate(template, context);
}
} catch (Exception e) {
log.warning("doRequest failed", "uri", request.getRequestURI(), e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e.getMessage());
}
}
/**
* Sets the content type of the response, defaulting to {@link #_defaultContentType} if not
* overriden. Delegates to {@link #chooseCharacterEncoding(HttpServletRequest)} to select the
* appropriate character encoding.
*/
protected void setContentType (HttpServletRequest request, HttpServletResponse response)
{
String contentType = _defaultContentType;
int index = contentType.lastIndexOf(';') + 1;
if (index <= 0 || (index < contentType.length() &&
contentType.indexOf("charset", index) == -1)) {
// append the character encoding which we'd like to use
String encoding = chooseCharacterEncoding(request);
if (!DEFAULT_OUTPUT_ENCODING.equalsIgnoreCase(encoding)) {
contentType += "; charset=" + encoding;
}
}
response.setContentType(contentType);
}
/**
* Chooses the output character encoding to be used as the value for the "charset=" portion of
* the HTTP Content-Type header (and thus returned by
* response.getCharacterEncoding()
). Called by {@link #setContentType} if an
* encoding isn't already specified by Content-Type. By default, chooses the value of
* RuntimeSingleton's output.encoding
property.
*/
protected String chooseCharacterEncoding (HttpServletRequest request)
{
return RuntimeSingleton.getString(
RuntimeConstants.OUTPUT_ENCODING, DEFAULT_OUTPUT_ENCODING);
}
/**
* This method is called to select the appropriate template for this request. The default
* implementation simply loads the template using Velocity's default template loading services
* based on the URI provided in the request.
*
* @param ctx The context of this request.
*
* @return The template to be used in generating the response.
*/
protected Template selectTemplate (int siteId, InvocationContext ctx)
throws ResourceNotFoundException, ParseErrorException, Exception
{
String path = ctx.getRequest().getServletPath();
if (_usingSiteLoading) {
// if we're using site resource loading, we need to prefix the path with the site
// identifier
path = siteId + ":" + path;
}
// log.info("Loading template", "path", path);
return RuntimeSingleton.getTemplate(path);
}
/**
* Merges the template with the context.
*
* @param template template object returned by the {@link #handleRequest} method.
* @param context the context for this request.
*/
protected void mergeTemplate (Template template, InvocationContext context)
throws ResourceNotFoundException, ParseErrorException, MethodInvocationException,
UnsupportedEncodingException, IOException, Exception
{
HttpServletResponse response = context.getResponse();
ServletOutputStream output = response.getOutputStream();
// ASSUMPTION: response.setContentType() has been called.
String encoding = response.getCharacterEncoding();
VelocityWriter vw = null;
try {
vw = (VelocityWriter)_writerPool.get();
if (vw == null) {
vw = new VelocityWriter(new OutputStreamWriter(output, encoding), 4 * 1024, true);
} else {
vw.recycle(new OutputStreamWriter(output, encoding));
}
template.merge(context, vw);
} catch (IOException ioe) {
// the client probably crashed or aborted the connection ungracefully, so use log.info
log.info("Failed to write response", "uri", context.getRequest().getRequestURI(),
"error", ioe);
} finally {
if (vw != null) {
try {
// flush and put back into the pool don't close to allow us to play nicely with
// others.
vw.flush();
} catch (IOException e) {
// do nothing
}
// Clear the VelocityWriter's reference to its internal OutputStreamWriter to allow
// the latter to be GC'd while vw is pooled.
vw.recycle(null);
_writerPool.put(vw);
}
}
}
/**
* This method is called to select the appropriate logic for this request URI.
*
* @return The logic to be used in generating the response or null if no logic could be
* matched.
*/
protected Logic resolveLogic (String path)
{
// look for a cached logic instance
String lclass = _app.generateClass(path);
Logic logic = _logic.get(lclass);
if (logic == null) {
logic = instantiateLogic(path, lclass);
// if something failed, use a dummy in it's place so that we don't sit around all day
// freaking out about our inability to instantiate the proper logic class
if (logic == null) {
logic = new DummyLogic();
}
// cache the resolved logic instance
_logic.put(lclass, logic);
}
return logic;
}
/**
* Instantiates a logic instance with the supplied class name. May return null if no such class
* exists.
*/
protected Logic instantiateLogic (String path, String lclass) {
try {
Class> pcl = Class.forName(lclass);
return (Logic)pcl.newInstance();
} catch (ClassNotFoundException cnfe) {
// nothing interesting to report
} catch (Throwable t) {
log.warning("Unable to instantiate logic for application", "path", path,
"lclass", lclass, t);
}
return null;
}
/** The application being served by this dispatcher servlet. */
protected Application _app;
/** A table of resolved logic instances. */
protected HashMap _logic = new HashMap();
/** The character set in which serve our responses. */
protected String _charset;
/** Set to true if we're using the {@link SiteResourceLoader}. */
protected boolean _usingSiteLoading;
/** Our default content type. */
protected String _defaultContentType;
/** A pool of VelocityWriter instances. */
protected static SimplePool _writerPool = new SimplePool(40);
/** Describes the location of our properties. */
protected static final String INIT_PROPS_KEY = "org.apache.velocity.properties";
/** This is the key used in the context for error messages. */
protected static final String ERROR_KEY = "error";
/** This is the key used to store a reference back to the dispatcher servlet in our invocation
* context. */
protected static final String APPLICATION_KEY = "%_app_%";
/** The key used to store the translation tool in the context. */
protected static final String I18NTOOL_KEY = "i18n";
/** The key used to store the form tool in the context. */
protected static final String FORMTOOL_KEY = "form";
/** The key used to store the string tool in the context. */
protected static final String STRINGTOOL_KEY = "string";
/** The key used to store the data tool in the context. */
protected static final String DATATOOL_KEY = "data";
/** The key used to store the currency tool in the context. */
protected static final String CURRENCYTOOL_KEY = "cash";
/** The servlet parameter key specifying the application class. */
protected static final String APP_CLASS_KEY = "app_class";
/** The servlet parameter key specifying the base logic package. */
protected static final String LOGIC_PKG_KEY = "logic_package";
/** The servlet parameter key specifying the default character set. */
protected static final String CHARSET_KEY = "charset";
/** The default content type for responses. */
protected static final String DEFAULT_CONTENT_TYPE = "text/html";
/** The default encoding for the output stream. */
protected static final String DEFAULT_OUTPUT_ENCODING = "ISO-8859-1";
}