blob: 980d3eaa9f7e65130c00847d2110e3b1616cde2f [file]
//
// Tapestry Web Application Framework
// Copyright (c) 2000-2002 by Howard Lewis Ship
//
// Howard Lewis Ship
// http://sf.net/projects/tapestry
// mailto:hship@users.sf.net
//
// This library is free software.
//
// You may redistribute it and/or modify it under the terms of the GNU
// Lesser General Public License as published by the Free Software Foundation.
//
// Version 2.1 of the license should be included with this distribution in
// the file LICENSE, as well as License.html. If the license is not
// included with this distribution, you may find a copy at the FSF web
// site at 'www.gnu.org' or 'www.fsf.org', or you may write to the
// Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied waranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
package net.sf.tapestry;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Category;
import org.apache.log4j.Priority;
import net.sf.tapestry.parse.SpecificationParser;
import net.sf.tapestry.spec.ApplicationSpecification;
import net.sf.tapestry.util.StringSplitter;
import net.sf.tapestry.util.exception.ExceptionAnalyzer;
import net.sf.tapestry.util.pool.Pool;
import net.sf.tapestry.util.xml.DocumentParseException;
/**
* Links a servlet container with a Tapestry application. The servlet has some
* responsibilities related to bootstrapping the application (in terms of
* logging, reading the {@link ApplicationSpecification specification}, etc.).
* It is also responsible for creating or locating the {@link IEngine} and delegating
* incoming requests to it.
*
* <p>In some servlet containers (notably
* <a href="www.bea.com"/>WebLogic</a>)
* it is necessary to invoke {@link HttpSession#setAttribute(String,Object)}
* in order to force a persistent value to be replicated to the other
* servers in the cluster. Tapestry applications usually only have a single
* persistent value, the {@link IEngine engine}. For persistence to
* work in such an environment, the
* JVM system property <code>net.sf.tapestry.store-engine</code>
* must be set to <code>true</code>. This will force the application
* servlet to restore the engine into the {@link HttpSession} at the
* end of each request cycle.
*
* <p>The application servlet also has a default implementation of
* {@link #setupLogging} that configures logging for
* <a href="http://jakarta.apache.org/log4j">Log4J</a>. Subclasses
* with more sophisticated logging needs will need to overide this
* method.
*
* <p>As of release 1.0.1, it is no longer necessary for a {@link HttpSession}
* to be created on the first request cycle. Instead, the HttpSession is created
* as needed by the {@link IEngine} ... that is, when a visit object is created,
* or when persistent page state is required. Otherwise, for sessionless requests,
* an {@link IEngine} from a {@link Pool} is used. Additional work must be done
* so that the {@link IEngine} can change locale <em>without</em> forcing
* the creation of a session; this involves the servlet and the engine storing
* locale information in a {@link Cookie}.
*
* <p>This class is derived from the original class
* <code>com.primix.servlet.GatewayServlet</code>,
* part of the <b>ServletUtils</b> framework available from
* <a href="http://www.gjt.org/servlets/JCVSlet/list/gjt/com/primix/servlet">The Giant
* Java Tree</a>.
*
* @version $Id$
* @author Howard Lewis Ship
**/
abstract public class ApplicationServlet extends HttpServlet
{
private static final Category CAT =
Category.getInstance(ApplicationServlet.class);
/**
* Name of the cookie written to the client web browser to
* identify the locale.
*
**/
private static final String LOCALE_COOKIE_NAME = "net.sf.tapestry.locale";
/**
* A {@link Pool} used to store {@link IEngine engine}s that are not currently
* in use. The key is on {@link Locale}.
*
**/
private Pool enginePool = new Pool();
/**
* The application specification, which is read once and kept in memory
* thereafter.
*
**/
private ApplicationSpecification specification;
/**
* The name under which the {@link IEngine engine} is stored within the
* {@link HttpSession}.
*
**/
private String attributeName;
/**
* Gets the class loader for the servlet. Generally, this class (ApplicationServlet)
* is loaded by the system class loader, but the servlet and other classes in the
* application are loaded by a child class loader. Only the child has the full view
* of all classes and package resources.
*
**/
private ClassLoader classLoader = getClass().getClassLoader();
/**
* If true, then the engine is stored as an attribute of the {@link HttpSession}
* after every request.
*
* @since 0.2.12
*
**/
private static boolean storeEngine =
Boolean.getBoolean("net.sf.tapestry.store-engine");
/**
* Invokes {@link #doService(HttpServletRequest, HttpServletResponse)}.
*
* @since 1.0.6
*
**/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
doService(request, response);
}
/**
* Handles the GET and POST requests. Performs the following:
* <ul>
* <li>Construct a {@link RequestContext}
* <li>Invoke {@link #getEngine(RequestContext)} to get or create the {@link IEngine}
* <li>Invoke {@link IEngine#service(RequestContext)} on the application
* </ul>
**/
protected void doService(
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
RequestContext context = null;
IEngine engine;
try
{
// Create a context from the various bits and pieces.
context = new RequestContext(this, request, response);
// The subclass provides the engine.
engine = getEngine(context);
if (engine == null)
throw new ServletException(
Tapestry.getString("ApplicationServlet.could-not-locate-engine"));
boolean dirty = engine.service(context);
HttpSession session = context.getSession();
// When there's an active session, we *may* store it into
// the HttpSession and we *will not* store the engine
// back into the engine pool.
if (session != null)
{
// If the service may have changed the engine and the
// special storeEngine flag is on, then re-save the engine
// into the session. Otherwise, we only save the engine
// into the session when the session is first created (is new).
try
{
boolean forceStore =
engine.isStateful() && (session.getAttribute(attributeName) == null);
if (forceStore || (dirty && storeEngine))
{
if (CAT.isDebugEnabled())
CAT.debug("Storing " + engine + " into session as " + attributeName);
session.setAttribute(attributeName, engine);
}
}
catch (IllegalStateException ex)
{
// Ignore because the session been's invalidated.
// Allow the engine (which has state particular to the client)
// to be reclaimed by the garbage collector.
if (CAT.isDebugEnabled())
CAT.debug("Session invalidated.");
}
return;
}
if (engine.isStateful())
{
CAT.error(
"Engine "
+ engine
+ " is stateful even though there is no session. Discarding the engine.");
return;
}
// No session; the engine contains no state particular to
// the client (except for locale). Don't throw it away,
// instead save it in a pool for later reuse (by this, or another
// client in the same locale).
if (CAT.isDebugEnabled())
CAT.debug("Returning " + engine + " to pool.");
enginePool.store(engine.getLocale(), engine);
}
catch (ServletException ex)
{
log("ServletException", ex);
show(ex);
// Rethrow it.
throw ex;
}
catch (IOException ex)
{
log("IOException", ex);
show(ex);
// Rethrow it.
throw ex;
}
finally
{
if (context != null)
context.cleanup();
}
}
protected void show(Exception ex)
{
System.err.println(
"\n\n**********************************************************\n\n");
new ExceptionAnalyzer().reportException(ex, System.err);
System.err.println(
"\n**********************************************************\n");
}
/**
* Invokes {@link #doService(HttpServletRequest, HttpServletResponse)}.
*
*
**/
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
doService(request, response);
}
/**
* Returns the application specification, which is read
* by the {@link #init(ServletConfig)} method.
*
**/
public ApplicationSpecification getApplicationSpecification()
{
return specification;
}
/**
* Retrieves the {@link IEngine engine} that will process this
* request. This comes from one of the following places:
* <ul>
* <li>The {@link HttpSession}, if the there is one.
* <li>From the pool of available engines
* <li>Freshly created
* </ul>
*
**/
protected IEngine getEngine(RequestContext context) throws ServletException
{
IEngine engine = null;
HttpSession session = context.getSession();
// If there's a session, then find the engine within it.
if (session != null)
{
engine = (IEngine) session.getAttribute(attributeName);
if (engine != null)
{
if (CAT.isDebugEnabled())
CAT.debug("Retrieved " + engine + " from session " + session.getId() + ".");
return engine;
}
if (CAT.isDebugEnabled())
CAT.debug("Session exists, but doesn't contain an engine.");
}
Locale locale = getLocaleFromRequest(context);
engine = (IEngine) enginePool.retrieve(locale);
if (engine == null)
{
engine = createEngine(context);
engine.setLocale(locale);
}
else
{
if (CAT.isDebugEnabled())
CAT.debug("Using pooled engine " + engine + " (from locale " + locale + ").");
}
return engine;
}
/**
* Determines the {@link Locale} for the incoming request.
* This is determined from the locale cookie or, if not set,
* from the request itself. This may return null
* if no locale is determined.
*
**/
protected Locale getLocaleFromRequest(RequestContext context)
throws ServletException
{
Cookie cookie = context.getCookie(LOCALE_COOKIE_NAME);
if (cookie != null)
return Tapestry.getLocale(cookie.getValue());
return context.getRequest().getLocale();
}
/**
* Reads the application specification when the servlet is
* first initialized. All {@link IEngine engine instances}
* will have access to the specification via the servlet.
*
**/
public void init(ServletConfig config) throws ServletException
{
String path;
ServletContext servletContext;
String resource;
InputStream stream;
SpecificationParser parser;
super.init(config);
setupLogging();
path = getApplicationSpecificationPath();
// Make sure we locate the specification using our
// own class loader.
stream = getClass().getResourceAsStream(path);
if (stream == null)
throw new ServletException(
Tapestry.getString("ApplicationServlet.could-not-load-spec", path));
parser = new SpecificationParser();
try
{
if (CAT.isDebugEnabled())
CAT.debug("Loading application specification from " + path);
specification = parser.parseApplicationSpecification(stream, path);
}
catch (DocumentParseException ex)
{
show(ex);
throw new ServletException(
Tapestry.getString("ApplicationServlet.could-not-parse-spec", path),
ex);
}
attributeName = "net.sf.tapestry.engine." + specification.getName();
}
/**
* Invoked from {@link #init(ServletConfig)} before the specification is loaded to
* setup log4j logging. This implemention is sufficient for testing, but should
* be overiden in production applications.
*
* <p>Gets the JVM system property <code>net.sf.tapestry.root-logging-priority</code>,
* and (if non-null), converts it to a {@link Priority} and assigns it to the root
* {@link Category}.
*
* <p>For each priority, a check is made for a JVM system property
* <code>net.sf.tapestry.log4j.<em>priority</em></code> (i.e. <code>...log4j.DEBUG</code>).
* The value is a list of categories seperated by semicolons. Each of these
* categories will be assigned that priority.
*
* <p>Prior to Tapestry release 2.0.3, this method would also set the pattern
* for root category appender. That is better done using a <code>log4j.properties</code>
* file. This method exists to make it easy to augment or override that
* configuration using command line options.
*
* @since 0.2.9
**/
protected void setupLogging() throws ServletException
{
Priority priority = Priority.ERROR;
String value = System.getProperty("net.sf.tapestry.root-logging-priority");
if (value != null)
{
priority = Priority.toPriority(value, Priority.ERROR);
Category root = Category.getRoot();
root.setPriority(priority);
}
Priority[] priorities = Priority.getAllPossiblePriorities();
StringSplitter splitter = new StringSplitter(';');
for (int i = 0; i < priorities.length; i++)
{
priority = priorities[i];
String key = "net.sf.tapestry.log4j." + priority.toString();
String categoryList = System.getProperty(key);
if (categoryList != null)
{
String[] categories = splitter.splitToArray(categoryList);
for (int j = 0; j < categories.length; j++)
{
Category cat = Category.getInstance(categories[j]);
cat.setPriority(priority);
}
}
}
}
/**
* Implemented in subclasses to identify the resource path
* of the application specification.
*
**/
abstract protected String getApplicationSpecificationPath();
/**
* Invoked by {@link #getEngine(RequestContext)} to create
* the {@link IEngine} instance specific to the
* application, if not already in the
* {@link HttpSession}.
*
* <p>The {@link IEngine} instance returned is stored into the
* {@link HttpSession}.
*
* <p>This implementation instantiates a new engine as specified
* by {@link ApplicationSpecification#getEngineClassName()}.
*
**/
protected IEngine createEngine(RequestContext context) throws ServletException
{
try
{
String className = specification.getEngineClassName();
if (className == null)
throw new ServletException(
Tapestry.getString("ApplicationServlet.no-engine-class"));
if (CAT.isDebugEnabled())
CAT.debug("Creating engine from class " + className);
Class engineClass = Class.forName(className, true, classLoader);
IEngine result = (IEngine) engineClass.newInstance();
if (CAT.isDebugEnabled())
CAT.debug("Created engine " + result);
return result;
}
catch (Exception ex)
{
throw new ServletException(ex);
}
}
/**
* Invoked from the {@link IEngine engine}, just prior to starting to
* render a response, when the locale has changed. The servlet writes a
* {@link Cookie} so that, on subsequent request cycles, an engine localized
* to the selected locale is chosen.
*
* <p>At this time, the cookie is <em>not</em> persistent. That may
* change in subsequent releases.
*
* @since 1.0.1
**/
public void writeLocaleCookie(
Locale locale,
IEngine engine,
RequestContext cycle)
{
if (CAT.isDebugEnabled())
CAT.debug("Writing locale cookie " + locale);
Cookie cookie = new Cookie(LOCALE_COOKIE_NAME, locale.toString());
cookie.setPath(engine.getServletPath());
cycle.addCookie(cookie);
}
}