blob: fd299836112fe4a5cdd1afaf673bf5e87bf4b9bc [file] [log] [blame]
/*
* ====================================================================
* The Apache Software License, Version 1.1
*
* Copyright (c) 2002 The Apache Software Foundation. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution,
* if any, must include the following acknowledgment:
* "This product includes software developed by the
* Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself,
* if and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Apache" and "Apache Software Foundation" and
* "Apache Tapestry" must not be used to endorse or promote products
* derived from this software without prior written permission. For
* written permission, please contact apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache",
* "Apache Tapestry", nor may "Apache" appear in their name, without
* prior written permission of the Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*/
package net.sf.tapestry;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Locale;
import javax.servlet.ServletConfig;
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.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import net.sf.tapestry.parse.SpecificationParser;
import net.sf.tapestry.spec.IApplicationSpecification;
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>The servlet init parameter
* <code>net.sf.tapestry.specification-path</code>
* should be set to the complete resource path (within the classpath)
* to the application specification, i.e.,
* <code>/com/foo/bar/MyApp.application</code>.
*
* <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>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
**/
public class ApplicationServlet extends HttpServlet
{
private static final Log LOG = LogFactory.getLog(ApplicationServlet.class);
/** @since 2.3 **/
private static final String APP_SPEC_PATH_PARAM = "net.sf.tapestry.application-specification";
/**
* 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 IApplicationSpecification _specification;
/**
* The name under which the {@link IEngine engine} is stored within the
* {@link HttpSession}.
*
**/
private String _attributeName;
/**
* Invokes {@link #doService(HttpServletRequest, HttpServletResponse)}.
*
* @since 1.0.6
*
**/
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
doService(request, response);
}
/**
* @since 2.3
*
**/
private IResourceResolver _resolver;
/**
* 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;
try
{
// Create a context from the various bits and pieces.
context = createRequestContext(request, response);
// The subclass provides the engine.
IEngine 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)
{
if (LOG.isDebugEnabled())
LOG.debug("Storing " + engine + " into session as " + _attributeName);
// Some servlet container invoke valueUnbound(), then valueBound()
// when "refreshing" this way, so we tell the engine it is being
// refreshed.
engine.setRefreshing(true);
session.setAttribute(_attributeName, engine);
engine.setRefreshing(false);
}
}
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 (LOG.isDebugEnabled())
LOG.debug("Session invalidated.");
}
// The engine is stateful and stored in a session. Even if it started
// the request cycle in the pool, it doesn't go back.
return;
}
if (engine.isStateful())
{
LOG.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 (LOG.isDebugEnabled())
LOG.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();
}
}
/**
* Invoked by {@link #doService(HttpServletRequest, HttpServletResponse)} to create
* the {@link RequestContext} for this request cycle. Some applications may need to
* replace the default RequestContext with a subclass for particular behavior.
*
* @since 2.3
*
**/
protected RequestContext createRequestContext(HttpServletRequest request, HttpServletResponse response)
throws IOException
{
return new RequestContext(this, request, response);
}
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 IApplicationSpecification 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 (LOG.isDebugEnabled())
LOG.debug("Retrieved " + engine + " from session " + session.getId() + ".");
return engine;
}
if (LOG.isDebugEnabled())
LOG.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 (LOG.isDebugEnabled())
LOG.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.
*
* @see #getApplicationSpecification()
* @see #constructApplicationSpecification()
* @see #createResourceResolver()
*
**/
public void init(ServletConfig config) throws ServletException
{
super.init(config);
_resolver = createResourceResolver();
_specification = constructApplicationSpecification();
_attributeName = "net.sf.tapestry.engine." + config.getServletName();
}
/**
* Invoked from {@link #init(ServletConfig)} to create a resource resolver
* for the servlet (which will utlimately be shared and used through the
* application).
*
* <p>This implementation constructs a {@link DefaultResourceResolver}, subclasses
* may provide a different implementation.
*
* @see #getResourceResolver()
* @since 2.3
*
**/
protected IResourceResolver createResourceResolver() throws ServletException
{
return new DefaultResourceResolver();
}
/**
* Invoked from {@link #init(ServletConfig)} to read and construct
* the {@link ApplicationSpecification} for this servlet.
* Invokes {@link #getApplicationSpecificationPath()}, opens
* the resource as a stream, then invokes
* {@link #parseApplicationSpecification(InputStream, String)}.
*
* <p>
* This method exists to be overriden in
* applications where the application specification cannot be
* loaded from the classpath. Alternately, a subclass
* could override this method, invoke this implementation,
* and then add additional data to it (for example, an application
* where some of the pages are defined in an external source
* such as a database).
*
* @since 2.2
*
**/
protected IApplicationSpecification constructApplicationSpecification() throws ServletException
{
String path = getApplicationSpecificationPath();
URL specificationURL = _resolver.getResource(path);
InputStream stream = null;
try
{
if (specificationURL != null)
stream = specificationURL.openStream();
if (stream == null)
throw new ServletException(Tapestry.getString("ApplicationServlet.could-not-load-spec", path));
if (LOG.isDebugEnabled())
LOG.debug("Loading application specification from " + path);
IApplicationSpecification result = parseApplicationSpecification(stream, path);
stream.close();
stream = null;
return result;
}
catch (IOException ex)
{
throw new ServletException(Tapestry.getString("ApplicationServlet.could-not-open-spec", path), ex);
}
finally
{
close(stream);
}
}
/**
* Invoked from {@link #constructApplicationSpecification()} to
* actually parse the stream (with content provided from the path)
* and convert it into an {@link ApplicationSpecification}.
*
* @since 2.2
*
**/
protected IApplicationSpecification parseApplicationSpecification(InputStream stream, String path)
throws ServletException
{
try
{
SpecificationParser parser = new SpecificationParser();
return parser.parseApplicationSpecification(stream, path, _resolver);
}
catch (DocumentParseException ex)
{
show(ex);
throw new ServletException(Tapestry.getString("ApplicationServlet.could-not-parse-spec", path), ex);
}
}
/**
* Closes the stream, ignoring any exceptions.
*
**/
protected void close(InputStream stream)
{
try
{
if (stream != null)
stream.close();
}
catch (IOException ex)
{
// Ignore it.
}
}
/**
* Reads the servlet init parameter
* <code>net.sf.tapestry.application-specification</code> and
* throws {@link ServletException} if it is null.
*
**/
protected String getApplicationSpecificationPath() throws ServletException
{
String result = getInitParameter("net.sf.tapestry.application-specification");
if (result == null)
throw new ServletException(
Tapestry.getString("ApplicationServlet.app-spec-path-not-provided", APP_SPEC_PATH_PARAM));
return result;
}
/**
* 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 (LOG.isDebugEnabled())
LOG.debug("Creating engine from class " + className);
Class engineClass = getResourceResolver().findClass(className);
IEngine result = (IEngine) engineClass.newInstance();
if (LOG.isDebugEnabled())
LOG.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 (LOG.isDebugEnabled())
LOG.debug("Writing locale cookie " + locale);
Cookie cookie = new Cookie(LOCALE_COOKIE_NAME, locale.toString());
cookie.setPath(engine.getServletPath());
cycle.addCookie(cookie);
}
/**
* Returns a resource resolver that can access classes and resources related
* to the current web application context. Relies on
* {@link java.lang.Thread#getContextClassLoader()}, which is set by
* most modern servlet containers.
*
* @since 2.3
*
**/
public IResourceResolver getResourceResolver()
{
return _resolver;
}
}