blob: df5f26628b7de2404d8d319e69de332484215746 [file] [log] [blame]
// Copyright 2004, 2008 The Apache Software Foundation
//
// 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 org.apache.tapestry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.engine.BaseEngine;
import org.apache.tapestry.engine.IPropertySource;
import org.apache.tapestry.parse.SpecificationParser;
import org.apache.tapestry.request.RequestContext;
import org.apache.tapestry.resource.ClasspathResourceLocation;
import org.apache.tapestry.resource.ContextResourceLocation;
import org.apache.tapestry.spec.ApplicationSpecification;
import org.apache.tapestry.spec.IApplicationSpecification;
import org.apache.tapestry.util.*;
import org.apache.tapestry.util.exception.ExceptionAnalyzer;
import org.apache.tapestry.util.pool.Pool;
import org.apache.tapestry.util.xml.DocumentParseException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
/**
* 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/>
* <p>The servlet init parameter <code>org.apache.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/>
* <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>org.apache.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/>
* <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}.
*
* @author Howard Lewis Ship
* @version $Id$
*/
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 =
"org.apache.tapestry.application-specification";
/**
* Name of the cookie written to the client web browser to identify the locale.
*/
private static final String LOCALE_COOKIE_NAME = "org.apache.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;
/**
* The resolved class name used to instantiate the engine.
*
* @since 3.0
*/
private String _engineClassName;
/**
* Used to search for configuration properties.
*
* @since 3.0
*/
private IPropertySource _propertySource;
/**
* 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.getMessage("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);
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 (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(
Tapestry.format(
"ApplicationServlet.engine-stateful-without-session",
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 = "org.apache.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/>
* <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(IResourceLocation)}.
* <p/>
* <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
{
IResourceLocation specLocation = getApplicationSpecificationLocation();
if (specLocation == null)
{
if (LOG.isDebugEnabled())
LOG.debug(Tapestry.getMessage("ApplicationServlet.no-application-specification"));
return constructStandinSpecification();
}
if (LOG.isDebugEnabled())
LOG.debug("Loading application specification from " + specLocation);
return parseApplicationSpecification(specLocation);
}
/**
* Gets the location of the application specification, if there is one.
* <p/>
* <ul> <li>Invokes {@link #getApplicationSpecificationPath()} to get the location of the application specification
* on the classpath. <li>If that return null, search for the application specification: <ul>
* <li><i>name</i>.application in /WEB-INF/<i>name</i>/ <li><i>name</i>.application in /WEB-INF/ </ul> </ul>
* <p/>
* <p>Returns the location of the application specification, or null if not found.
*
* @since 3.0
*/
protected IResourceLocation getApplicationSpecificationLocation() throws ServletException
{
String path = getApplicationSpecificationPath();
if (path != null)
return new ClasspathResourceLocation(_resolver, path);
ServletContext context = getServletContext();
String servletName = getServletName();
String expectedName = servletName + ".application";
IResourceLocation webInfLocation = new ContextResourceLocation(context, "/WEB-INF/");
IResourceLocation webInfAppLocation = webInfLocation.getRelativeLocation(servletName + "/");
IResourceLocation result = check(webInfAppLocation, expectedName);
if (result != null)
return result;
return check(webInfLocation, expectedName);
}
/**
* Checks for the application specification relative to the specified location.
*
* @since 3.0
*/
private IResourceLocation check(IResourceLocation location, String name)
{
IResourceLocation result = location.getRelativeLocation(name);
if (LOG.isDebugEnabled())
LOG.debug("Checking for existence of " + result);
if (result.getResourceURL() != null)
{
LOG.debug("Found.");
return result;
}
return null;
}
/**
* Invoked from {@link #constructApplicationSpecification()} when the application doesn't have an explicit
* specification. A simple specification is constructed and returned. This is useful for minimal applications and
* prototypes.
*
* @since 3.0
*/
protected IApplicationSpecification constructStandinSpecification()
{
ApplicationSpecification result = new ApplicationSpecification();
IResourceLocation virtualLocation =
new ContextResourceLocation(getServletContext(), "/WEB-INF/");
result.setSpecificationLocation(virtualLocation);
result.setName(getServletName());
result.setResourceResolver(_resolver);
return result;
}
/**
* 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(IResourceLocation location)
throws ServletException
{
try
{
SpecificationParser parser = new SpecificationParser(_resolver);
return parser.parseApplicationSpecification(location);
}
catch (DocumentParseException ex)
{
show(ex);
throw new ServletException(
Tapestry.format("ApplicationServlet.could-not-parse-spec", location),
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>org.apache.tapestry.application-specification</code>, which is the
* location, on the classpath, of the application specification.
* <p/>
* <p/>
* If the parameter is not set, this method returns null, and a search for the application specification within the
* servlet context will begin.
*
* @see #getApplicationSpecificationLocation()
*/
protected String getApplicationSpecificationPath() throws ServletException
{
return getInitParameter(APP_SPEC_PATH_PARAM);
}
/**
* Invoked by {@link #getEngine(RequestContext)} to create the {@link IEngine} instance specific to the application,
* if not already in the {@link HttpSession}.
* <p/>
* <p>The {@link IEngine} instance returned is stored into the {@link HttpSession}.
*
* @see #getEngineClassName()
*/
protected IEngine createEngine(RequestContext context) throws ServletException
{
try
{
String className = getEngineClassName();
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);
}
}
/**
* Returns the name of the class to use when instantiating an engine instance for this application. If the
* application specification provides a value, this is returned. Otherwise, a search for the configuration property
* <code>org.apache.tapestry.engine-class</code> occurs (see {@link #searchConfiguration(String)}).
* <p/>
* <p>If the search is still unsuccessful, then {@link org.apache.tapestry.engine.BaseEngine} is used.
*/
protected String getEngineClassName()
{
if (_engineClassName != null)
return _engineClassName;
_engineClassName = _specification.getEngineClassName();
if (_engineClassName == null)
_engineClassName = searchConfiguration("org.apache.tapestry.engine-class");
if (_engineClassName == null)
_engineClassName = BaseEngine.class.getName();
return _engineClassName;
}
/**
* Searches for a configuration property in: <ul> <li>The servlet's initial parameters <li>The servlet context's
* initial parameters <li>JVM system properties </ul>
*
* @see #createPropertySource()
* @since 3.0
*/
protected String searchConfiguration(String propertyName)
{
synchronized (this)
{
if (_propertySource == null)
_propertySource = createPropertySource();
}
return _propertySource.getPropertyValue(propertyName);
}
/**
* Creates an instance of {@link IPropertySource} used for searching of configuration values. Subclasses may
* override this method if they want to change the normal locations that properties are searched for within.
*
* @since 3.0
*/
protected IPropertySource createPropertySource()
{
DelegatingPropertySource result = new DelegatingPropertySource();
result.addSource(new ServletPropertySource(getServletConfig()));
result.addSource(new ServletContextPropertySource(getServletContext()));
result.addSource(SystemPropertiesPropertySource.getInstance());
return result;
}
/**
* 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/>
* <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;
}
/**
* Ensures that shared janitor thread is terminated.
*
* @see javax.servlet.Servlet#destroy()
* @since 3.0.3
*/
public void destroy()
{
try
{
JanitorThread.getSharedJanitorThread().interrupt();
}
finally
{
super.destroy();
}
}
}