Java tutorial
// Copyright 2004 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 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.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.DefaultResourceResolver; import org.apache.tapestry.util.DelegatingPropertySource; import org.apache.tapestry.util.JanitorThread; import org.apache.tapestry.util.ServletContextPropertySource; import org.apache.tapestry.util.ServletPropertySource; import org.apache.tapestry.util.SystemPropertiesPropertySource; import org.apache.tapestry.util.exception.ExceptionAnalyzer; import org.apache.tapestry.util.pool.Pool; import org.apache.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>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>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>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}. * * @version $Id: ApplicationServlet.java,v 1.8.2.1 2005/03/17 05:18:32 pferraro Exp $ * @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 = "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>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> * 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. * * <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>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> * 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>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>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) { 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>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(); } } }