org.apache.tapestry.engine.AbstractEngine.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.tapestry.engine.AbstractEngine.java

Source

//  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.engine;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;

import org.apache.bsf.BSFManager;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.ApplicationServlet;
import org.apache.tapestry.IEngine;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.INamespace;
import org.apache.tapestry.IPage;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.IResourceResolver;
import org.apache.tapestry.PageRedirectException;
import org.apache.tapestry.RedirectException;
import org.apache.tapestry.StaleLinkException;
import org.apache.tapestry.StaleSessionException;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.asset.ResourceChecksumSource;
import org.apache.tapestry.asset.ResourceChecksumSourceImpl;
import org.apache.tapestry.enhance.DefaultComponentClassEnhancer;
import org.apache.tapestry.listener.ListenerMap;
import org.apache.tapestry.pageload.PageSource;
import org.apache.tapestry.request.RequestContext;
import org.apache.tapestry.request.ResponseOutputStream;
import org.apache.tapestry.spec.IApplicationSpecification;
import org.apache.tapestry.util.DelegatingPropertySource;
import org.apache.tapestry.util.PropertyHolderPropertySource;
import org.apache.tapestry.util.ResourceBundlePropertySource;
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.io.DataSqueezer;
import org.apache.tapestry.util.pool.Pool;

/**
 *  Basis for building real Tapestry applications.  Immediate subclasses
 *  provide different strategies for managing page state and other resources
 *  between request cycles.
 *
 *  Uses a shared instance of
 *  {@link ITemplateSource}, {@link ISpecificationSource},
 *  {@link IScriptSource} and {@link IComponentMessagesSource}
 *  stored as attributes of the  {@link ServletContext}
 *  (they will be shared by all sessions).
 *
 *  <p>An application is designed to be very lightweight.
 *  Particularily, it should <b>never</b> hold references to any
 *  {@link IPage} or {@link org.apache.tapestry.IComponent} objects.  The entire system is
 *  based upon being able to quickly rebuild the state of any page(s).
 *
 * <p>Where possible, instance variables should be transient.  They
 * can be restored inside {@link #setupForRequest(RequestContext)}.
 *
 *  <p>In practice, a subclass (usually {@link BaseEngine})
 *  is used without subclassing.  Instead, a
 *  visit object is specified.  To facilitate this, the application specification
 *  may include a property, <code>org.apache.tapestry.visit-class</code>
 *  which is the class name  to instantiate when a visit object is first needed.  See
 *  {@link #createVisit(IRequestCycle)} for more details.
 *
 * <p>Some of the classes' behavior is controlled by JVM system properties
 * (typically only used during development):
 *
 * <table border=1>
 *    <tr> <th>Property</th> <th>Description</th> </tr>
 *  <tr> <td>org.apache.tapestry.enable-reset-service</td>
 *      <td>If true, enabled an additional service, reset, that
 *      allow page, specification and template caches to be cleared on demand.
 *     See {@link #isResetServiceEnabled()}. </td>
 * </tr>
 * <tr>
 *      <td>org.apache.tapestry.disable-caching</td>
 *   <td>If true, then the page, specification, template and script caches
 *  will be cleared after each request. This slows things down,
 *  but ensures that the latest versions of such files are used.
 *  Care should be taken that the source directories for the files
 *  preceeds any versions of the files available in JARs or WARs. </td>
 * </tr>
 * </table>
 *
 *
 *  @author Howard Lewis Ship
 *  @version $Id: AbstractEngine.java,v 1.30.2.2 2005/03/19 00:46:51 pferraro Exp $
 *
 **/

public abstract class AbstractEngine
        implements IEngine, IEngineServiceView, Externalizable, HttpSessionBindingListener {
    private static final Log LOG = LogFactory.getLog(AbstractEngine.class);

    /**
     *  @since 2.0.4
     *
     **/

    private static final long serialVersionUID = 6884834397673817117L;

    private transient String _contextPath;
    private transient String _servletPath;
    private transient String _clientAddress;
    private transient String _sessionId;
    private transient boolean _stateful;
    private transient ListenerMap _listeners;

    /** @since 2.2 **/

    private transient DataSqueezer _dataSqueezer;

    /**
     *  An object used to contain application-specific server side state.
     *
     **/

    private Object _visit;

    /**
     *  The globally shared application object.  Typically, this is created
     *  when first needed, shared between sessions and engines, and
     *  stored in the {@link ServletContext}.
     *
     *  @since 2.3
     *
     **/

    private transient Object _global;

    /**
     *  The base name for the servlet context key used to store
     *  the application-defined Global object, if any.
     *
     *  @since 2.3
     *
     **/

    public static final String GLOBAL_NAME = "org.apache.tapestry.global";

    /**
     *  The name of the application property that will be used to
     *  determine the encoding to use when generating the output
     *
     *  @since 3.0
     **/

    public static final String OUTPUT_ENCODING_PROPERTY_NAME = "org.apache.tapestry.output-encoding";

    /**
     *  The default encoding that will be used when generating the output.
     *  It is used if no output encoding property has been specified.
     *
     *  @since 3.0
     */

    public static final String DEFAULT_OUTPUT_ENCODING = "UTF-8";

    /**
     *  The curent locale for the engine, which may be changed at any time.
     *
     **/

    private Locale _locale;

    /**
     *  Set by {@link #setLocale(Locale)} when the locale is changed;
     *  this allows the locale cookie to be updated.
     *
     **/

    private boolean _localeChanged;

    /**
     *  The specification for the application, which
     *  lives in the {@link ServletContext}.  If the
     *  session (and application) moves to a different context (i.e.,
     *  a different JVM), then
     *  we want to reconnect to the specification in the new context.
     *  A check is made on every request
     *  cycle as needed.
     *
     **/

    protected transient IApplicationSpecification _specification;

    /**
     *  The source for template data. The template source is stored
     *  in the {@link ServletContext} as a named attribute.
     *  After de-serialization, the application can re-connect to
     *  the template source (or create a new one).
     *
     **/

    protected transient ITemplateSource _templateSource;

    /**
     *  The source for component specifications, stored in the
     *  {@link ServletContext} (like {@link #_templateSource}).
     *
     **/

    protected transient ISpecificationSource _specificationSource;

    /**
     *  The source for parsed scripts, again, stored in the
     *  {@link ServletContext}.
     *
     *  @since 1.0.2
     *
     **/

    private transient IScriptSource _scriptSource;

    /**
     *  The name of the context attribute for the {@link IScriptSource} instance.
     *  The application's name is appended.
     *
     *  @since 1.0.2
     *
     **/

    protected static final String SCRIPT_SOURCE_NAME = "org.apache.tapestry.ScriptSource";

    /**
     *  The name of the context attribute for the {@link IComponentMessagesSource}
     *  instance.  The application's name is appended.
     *
     *  @since 2.0.4
     *
     **/

    protected static final String STRINGS_SOURCE_NAME = "org.apache.tapestry.StringsSource";

    private transient IComponentMessagesSource _stringsSource;

    /**
     *  The name of the application specification property used to specify the
     *  class of the visit object.
     *
     **/

    public static final String VISIT_CLASS_PROPERTY_NAME = "org.apache.tapestry.visit-class";

    /**
     *  Servlet context attribute name for the default {@link ITemplateSource}
     *  instance.  The application's name is appended.
     *
     **/

    protected static final String TEMPLATE_SOURCE_NAME = "org.apache.tapestry.TemplateSource";

    /**
     *  Servlet context attribute name for the default {@link ISpecificationSource}
     *  instance.  The application's name is appended.
     *
     **/

    protected static final String SPECIFICATION_SOURCE_NAME = "org.apache.tapestry.SpecificationSource";

    /**
     *  Servlet context attribute name for the {@link IPageSource}
     *  instance.  The application's name is appended.
     *
     **/

    protected static final String PAGE_SOURCE_NAME = "org.apache.tapestry.PageSource";

    /**
     *  Servlet context attribute name for a shared instance
     *  of {@link DataSqueezer}.  The instance is actually shared
     *  between Tapestry applications within the same context
     *  (which will have the same ClassLoader).
     *
     *  @since 2.2
     *
     **/

    protected static final String DATA_SQUEEZER_NAME = "org.apache.tapestry.DataSqueezer";

    /**
     * Servlet context attribute name for a shared instance
     * of {@link ResourceChecksumSource}.
     * @since 3.0.3
     */
    protected static final String RESOURCE_CHECKSUM_SOURCE_NAME = "org.apache.tapestry.ResourceChecksumSource";

    /**
     *  The source for pages, which acts as a pool, but is capable of
     *  creating pages as needed.  Stored in the
     *  {@link ServletContext}, like {@link #_templateSource}.
     *
     **/

    private transient IPageSource _pageSource;

    /**
     *  If true (set from JVM system parameter
     *  <code>org.apache.tapestry.enable-reset-service</code>)
     *  then the reset service will be enabled, allowing
     *  the cache of pages, specifications and template
     *  to be cleared on demand.
     *
     **/

    private static final boolean _resetServiceEnabled = Boolean
            .getBoolean("org.apache.tapestry.enable-reset-service");

    /**
     * If true (set from the JVM system parameter
     * <code>org.apache.tapestry.disable-caching</code>)
     * then the cache of pages, specifications and template
     * will be cleared after each request.
     *
     **/

    private static final boolean _disableCaching = Boolean.getBoolean("org.apache.tapestry.disable-caching");

    private transient IResourceResolver _resolver;

    /**
     *  Constant used to store a {@link org.apache.tapestry.util.IPropertyHolder}
     *  in the servlet context.
     *
     *  @since 2.3
     *
     **/

    protected static final String PROPERTY_SOURCE_NAME = "org.apache.tapestry.PropertySource";

    /**
     *  A shared instance of {@link IPropertySource}
     *
     *  @since 3.0
     *  @see #createPropertySource(RequestContext)
     *
     **/

    private transient IPropertySource _propertySource;

    /**
     *  Map from service name to service instance.
     *
     *  @since 1.0.9
     *
     **/

    private transient Map _serviceMap;

    protected static final String SERVICE_MAP_NAME = "org.apache.tapestry.ServiceMap";

    /**
     *  A shared instance of {@link Pool}.
     *
     *  @since 3.0
     *  @see #createPool(RequestContext)
     *
     **/

    private transient Pool _pool;

    protected static final String POOL_NAME = "org.apache.tapestry.Pool";

    /**
     *  Name of a shared instance of {@link org.apache.tapestry.engine.IComponentClassEnhancer}
     *  stored in the {@link ServletContext}.
     *
     *  @since 3.0
     *
     **/

    protected static final String ENHANCER_NAME = "org.apache.tapestry.ComponentClassEnhancer";

    /**
     *  A shared instance of {@link org.apache.tapestry.engine.IComponentClassEnhancer}.
     *
     *  @since 3.0
     *  @see #createComponentClassEnhancer(RequestContext)
     *
     **/

    private transient IComponentClassEnhancer _enhancer;

    /**
     *  Set to true when there is a (potential)
     *  change to the internal state of the engine, set
     *  to false when the engine is stored into the
     *  {@link HttpSession}.
     *
     *  @since 3.0
     *
     **/

    private transient boolean _dirty;

    /**
     * The instance of {@link IMonitorFactory} used to create a monitor.
     *
     * @since 3.0
     */

    private transient IMonitorFactory _monitorFactory;

    /**
     * Used to obtain resource checksums for the asset service.
     * @since 3.0.3
     */
    private transient ResourceChecksumSource _resourceChecksumSource;

    /**
     *  Sets the Exception page's exception property, then renders the Exception page.
     *
     *  <p>If the render throws an exception, then copious output is sent to
     *  <code>System.err</code> and a {@link ServletException} is thrown.
     *
     **/

    protected void activateExceptionPage(IRequestCycle cycle, ResponseOutputStream output, Throwable cause)
            throws ServletException {
        try {
            IPage exceptionPage = cycle.getPage(getExceptionPageName());

            exceptionPage.setProperty("exception", cause);

            cycle.activate(exceptionPage);

            renderResponse(cycle, output);

        } catch (Throwable ex) {
            // Worst case scenario.  The exception page itself is broken, leaving
            // us with no option but to write the cause to the output.

            reportException(Tapestry.getMessage("AbstractEngine.unable-to-process-client-request"), cause);

            // Also, write the exception thrown when redendering the exception
            // page, so that can get fixed as well.

            reportException(Tapestry.getMessage("AbstractEngine.unable-to-present-exception-page"), ex);

            // And throw the exception.

            throw new ServletException(ex.getMessage(), ex);
        }
    }

    /**
     *  Writes a detailed report of the exception to <code>System.err</code>.
     *
     **/

    public void reportException(String reportTitle, Throwable ex) {
        LOG.warn(reportTitle, ex);

        System.err.println("\n\n**********************************************************\n\n");

        System.err.println(reportTitle);

        System.err.println("\n\n      Session id: " + _sessionId + "\n  Client address: " + _clientAddress
                + "\n\nExceptions:\n");

        new ExceptionAnalyzer().reportException(ex, System.err);

        System.err.println("\n**********************************************************\n");

    }

    /**
     *  Invoked at the end of the request cycle to release any resources specific
     *  to the request cycle.
     *
     **/

    protected abstract void cleanupAfterRequest(IRequestCycle cycle);

    /**
     *  Extends the description of the class generated by {@link #toString()}.
     *  If a subclass adds additional instance variables that should be described
     *  in the instance description, it may overide this method. This implementation
     *  does nothing.
     *
     *  @see #toString()
     *
     **/

    protected void extendDescription(ToStringBuilder builder) {

    }

    /**
     *  Returns the locale for the engine.  This is initially set
     *  by the {@link ApplicationServlet} but may be updated
     *  by the application.
     *
     **/

    public Locale getLocale() {
        return _locale;
    }

    /**
     * Overriden in subclasses that support monitoring.  Should create and return
     * an instance of {@link IMonitor} that is appropriate for the request cycle described
     * by the {@link RequestContext}.
     *
     * <p>The monitor is used to create a {@link RequestCycle}.
     *
     * <p>This implementation uses a {@link IMonitorFactory}
     * to create the monitor instance.  The factory
     * is provided as an application extension.  If the application
     * extension does not exist, {@link DefaultMonitorFactory} is used.
     *
     * <p>As of release 3.0, this method should <em>not</em> return null.
     *
     *
     */

    public IMonitor getMonitor(RequestContext context) {
        if (_monitorFactory == null) {
            if (_specification.checkExtension(Tapestry.MONITOR_FACTORY_EXTENSION_NAME))
                _monitorFactory = (IMonitorFactory) _specification
                        .getExtension(Tapestry.MONITOR_FACTORY_EXTENSION_NAME, IMonitorFactory.class);
            else
                _monitorFactory = DefaultMonitorFactory.SHARED;
        }

        return _monitorFactory.createMonitor(context);
    }

    public IPageSource getPageSource() {
        return _pageSource;
    }

    /**
     *  Returns a service with the given name.  Services are created by the
     *  first call to {@link #setupForRequest(RequestContext)}.
     **/

    public IEngineService getService(String name) {
        IEngineService result = (IEngineService) _serviceMap.get(name);

        if (result == null)
            throw new ApplicationRuntimeException(Tapestry.format("AbstractEngine.unknown-service", name));

        return result;
    }

    public String getServletPath() {
        return _servletPath;
    }

    /**
     * Returns the context path, the prefix to apply to any URLs so that they
     * are recognized as belonging to the Servlet 2.2 context.
     *
     *  @see org.apache.tapestry.asset.ContextAsset
     *
     **/

    public String getContextPath() {
        return _contextPath;
    }

    /**
     *  Returns the specification, if available, or null otherwise.
     *
     *  <p>To facilitate deployment across multiple servlet containers, the
     *  application is serializable.  However, the reference to the specification
     *  is transient.   When an application instance is deserialized, it reconnects
     *  with the application specification by locating it in the {@link ServletContext}
     *  or parsing it fresh.
     *
     **/

    public IApplicationSpecification getSpecification() {
        return _specification;
    }

    public ISpecificationSource getSpecificationSource() {
        return _specificationSource;
    }

    public ITemplateSource getTemplateSource() {
        return _templateSource;
    }

    /**
     *  Reads the state serialized by {@link #writeExternal(ObjectOutput)}.
     *
     *  <p>This always set the stateful flag.  By default, a deserialized
     *  session is stateful (else, it would not have been serialized).
     **/

    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        _stateful = true;

        String localeName = in.readUTF();
        _locale = Tapestry.getLocale(localeName);

        _visit = in.readObject();
    }

    /**
     *  Writes the following properties:
     *
     *  <ul>
     *  <li>locale name ({@link Locale#toString()})
     *  <li>visit
     *  </ul>
     *
     **/

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(_locale.toString());
        out.writeObject(_visit);
    }

    /**
     *  Invoked, typically, when an exception occurs while servicing the request.
     *  This method resets the output, sets the new page and renders it.
     *
     **/

    protected void redirect(String pageName, IRequestCycle cycle, ResponseOutputStream out,
            ApplicationRuntimeException exception) throws IOException, ServletException {
        // Discard any output from the previous page.

        out.reset();

        IPage page = cycle.getPage(pageName);

        cycle.activate(page);

        renderResponse(cycle, out);
    }

    public void renderResponse(IRequestCycle cycle, ResponseOutputStream output)
            throws ServletException, IOException {
        if (LOG.isDebugEnabled())
            LOG.debug("Begin render response.");

        // If the locale has changed during this request cycle then
        // do the work to propogate the locale change into
        // subsequent request cycles.

        if (_localeChanged) {
            _localeChanged = false;

            RequestContext context = cycle.getRequestContext();
            ApplicationServlet servlet = context.getServlet();

            servlet.writeLocaleCookie(_locale, this, context);
        }

        // Commit all changes and ignore further changes.

        IPage page = cycle.getPage();

        IMarkupWriter writer = page.getResponseWriter(output);

        output.setContentType(writer.getContentType());

        boolean discard = true;

        try {
            cycle.renderPage(writer);

            discard = false;
        } finally {
            // Closing the writer closes its PrintWriter and a whole stack of java.io objects,
            // which tend to stream a lot of output that eventually hits the
            // ResponseOutputStream.  If we are discarding output anyway (due to an exception
            // getting thrown during the render), we can save ourselves some trouble
            // by ignoring it.

            if (discard)
                output.setDiscard(true);

            writer.close();

            if (discard)
                output.setDiscard(false);
        }

    }

    /**
     * Invalidates the session, then redirects the client web browser to
     * the servlet's prefix, starting a new visit.
     *
     * <p>Subclasses should perform their own restart (if necessary, which is
     * rarely) before invoking this implementation.
     *
     **/

    public void restart(IRequestCycle cycle) throws IOException {
        RequestContext context = cycle.getRequestContext();

        HttpSession session = context.getSession();

        if (session != null) {
            try {
                session.invalidate();
            } catch (IllegalStateException ex) {
                if (LOG.isDebugEnabled())
                    LOG.debug("Exception thrown invalidating HttpSession.", ex);

                // Otherwise, ignore it.
            }
        }

        // Make isStateful() return false, so that the servlet doesn't
        // try to store the engine back into the (now invalid) session.

        _stateful = false;

        String url = context.getAbsoluteURL(_servletPath);

        context.redirect(url);
    }

    /**
     *  Delegate method for the servlet.  Services the request.
     *
     **/

    public boolean service(RequestContext context) throws ServletException, IOException {
        ApplicationServlet servlet = context.getServlet();
        IRequestCycle cycle = null;
        ResponseOutputStream output = null;
        IMonitor monitor = null;

        if (LOG.isDebugEnabled())
            LOG.debug("Begin service " + context.getRequestURI());

        if (_specification == null)
            _specification = servlet.getApplicationSpecification();

        // The servlet invokes setLocale() before invoking service().  We want
        // to ignore that setLocale() ... that is, not force a cookie to be
        // written.

        _localeChanged = false;

        if (_resolver == null)
            _resolver = servlet.getResourceResolver();

        try {
            setupForRequest(context);

            monitor = getMonitor(context);

            output = new ResponseOutputStream(context.getResponse());
        } catch (Exception ex) {
            reportException(Tapestry.getMessage("AbstractEngine.unable-to-begin-request"), ex);

            throw new ServletException(ex.getMessage(), ex);
        }

        IEngineService service = null;

        try {
            try {
                String serviceName;

                try {
                    serviceName = extractServiceName(context);

                    if (Tapestry.isBlank(serviceName))
                        serviceName = Tapestry.HOME_SERVICE;

                    // Must have a service to create the request cycle.
                    // Must have a request cycle to report an exception.

                    service = getService(serviceName);
                } catch (Exception ex) {
                    service = getService(Tapestry.HOME_SERVICE);
                    cycle = createRequestCycle(context, service, monitor);

                    throw ex;
                }

                cycle = createRequestCycle(context, service, monitor);

                monitor.serviceBegin(serviceName, context.getRequestURI());

                // Invoke the service, which returns true if it may have changed
                // the state of the engine (most do return true).

                service.service(this, cycle, output);

                // Return true only if the engine is actually dirty.  This cuts down
                // on the number of times the engine is stored into the
                // session unceccesarily.

                return _dirty;
            } catch (PageRedirectException ex) {
                handlePageRedirectException(ex, cycle, output);
            } catch (RedirectException ex) {
                handleRedirectException(cycle, ex);
            } catch (StaleLinkException ex) {
                handleStaleLinkException(ex, cycle, output);
            } catch (StaleSessionException ex) {
                handleStaleSessionException(ex, cycle, output);
            }
        } catch (Exception ex) {
            monitor.serviceException(ex);

            // Discard any output (if possible).  If output has already been sent to
            // the client, then things get dicey.  Note that this block
            // gets activated if the StaleLink or StaleSession pages throws
            // any kind of exception.

            // Attempt to switch to the exception page.  However, this may itself fail
            // for a number of reasons, in which case a ServletException is thrown.

            output.reset();

            if (LOG.isDebugEnabled())
                LOG.debug("Uncaught exception", ex);

            activateExceptionPage(cycle, output, ex);
        } finally {
            if (service != null)
                monitor.serviceEnd(service.getName());

            try {
                cycle.cleanup();

                // Closing the buffered output closes the underlying stream as well.

                if (output != null)
                    output.forceFlush();
            } catch (Exception ex) {
                reportException(Tapestry.getMessage("AbstractEngine.exception-during-cleanup"), ex);
            } finally {
                cleanupAfterRequest(cycle);
            }

            if (_disableCaching) {
                try {
                    clearCachedData();
                } catch (Exception ex) {
                    reportException(Tapestry.getMessage("AbstractEngine.exception-during-cache-clear"), ex);
                }
            }

            if (LOG.isDebugEnabled())
                LOG.debug("End service");

        }

        return _dirty;
    }

    /**
     *  Handles {@link PageRedirectException} which involves
     *  executing {@link IPage#validate(IRequestCycle)} on the target page
     *  (of the exception), until either a loop is found, or a page
     *  succesfully validates and can be activated.
     *
     *  <p>This should generally not be overriden in subclasses.
     *
     *  @since 3.0
     */

    protected void handlePageRedirectException(PageRedirectException ex, IRequestCycle cycle,
            ResponseOutputStream output) throws IOException, ServletException {
        List pageNames = new ArrayList();

        String pageName = ex.getTargetPageName();

        while (true) {
            if (pageNames.contains(pageName)) {
                // Add the offending page to pageNames so it shows in the
                // list.

                pageNames.add(pageName);

                StringBuffer buffer = new StringBuffer();
                int count = pageNames.size();

                for (int i = 0; i < count; i++) {
                    if (i > 0)
                        buffer.append("; ");

                    buffer.append(pageNames.get(i));
                }

                throw new ApplicationRuntimeException(
                        Tapestry.format("AbstractEngine.validate-cycle", buffer.toString()));
            }

            // Record that this page has been a target.

            pageNames.add(pageName);

            try {
                // Attempt to activate the new page.

                cycle.activate(pageName);

                break;
            } catch (PageRedirectException ex2) {
                pageName = ex2.getTargetPageName();
            }
        }

        // Discard any output from the previous page.

        output.reset();

        renderResponse(cycle, output);
    }

    /**
     *  Invoked from {@link #service(RequestContext)} to create an instance of
     *  {@link IRequestCycle} for the current request.  This implementation creates
     *  an returns an instance of {@link RequestCycle}.
     *
     *  @since 3.0
     *
     **/

    protected IRequestCycle createRequestCycle(RequestContext context, IEngineService service, IMonitor monitor) {
        return new RequestCycle(this, context, service, monitor);
    }

    /**
     *  Invoked by {@link #service(RequestContext)} if a {@link StaleLinkException}
     *  is thrown by the {@link IEngineService service}.  This implementation
     *  sets the message property of the StaleLink page to the
     *  message provided in the exception,
     *  then invokes
     *  {@link #redirect(String, IRequestCycle, ResponseOutputStream, ApplicationRuntimeException)}
     *  to render the StaleLink page.
     *
     *  <p>Subclasses may overide this method (without
     *  invoking this implementation).  A common practice
     *  is to present an error message on the application's
     *  Home page.
     *
     *  <p>Alternately, the application may provide its own version of
     *  the StaleLink page, overriding
     *  the framework's implementation (probably a good idea, because the
     *  default page hints at "application errors" and isn't localized).
     *  The overriding StaleLink implementation must
     *  implement a message property of type String.
     *
     *  @since 0.2.10
     *
     **/

    protected void handleStaleLinkException(StaleLinkException ex, IRequestCycle cycle, ResponseOutputStream output)
            throws IOException, ServletException {
        String staleLinkPageName = getStaleLinkPageName();
        IPage page = cycle.getPage(staleLinkPageName);

        page.setProperty("message", ex.getMessage());

        redirect(staleLinkPageName, cycle, output, ex);
    }

    /**
     *  Invoked by {@link #service(RequestContext)} if a {@link StaleSessionException}
     *  is thrown by the {@link IEngineService service}.  This implementation
     *  invokes
     *  {@link #redirect(String, IRequestCycle, ResponseOutputStream, ApplicationRuntimeException)}
     *  to render the StaleSession page.
     *
     *  <p>Subclasses may overide this method (without
     *  invoking this implementation).  A common practice
     *  is to present an eror message on the application's
     *  Home page.
     *
     *  @since 0.2.10
     **/

    protected void handleStaleSessionException(StaleSessionException ex, IRequestCycle cycle,
            ResponseOutputStream output) throws IOException, ServletException {
        redirect(getStaleSessionPageName(), cycle, output, ex);
    }

    /**
     *  Discards all cached pages, component specifications and templates.
     *  Subclasses who override this method should invoke this implementation
     *  as well.
     *
     *  @since 1.0.1
     *
     **/

    public void clearCachedData() {
        _pool.clear();
        _pageSource.reset();
        _specificationSource.reset();
        _templateSource.reset();
        _scriptSource.reset();
        _stringsSource.reset();
        _enhancer.reset();
        _resourceChecksumSource.reset();
    }

    /**
     *  Changes the locale for the engine.
     *
     **/

    public void setLocale(Locale value) {
        if (value == null)
            throw new IllegalArgumentException("May not change engine locale to null.");

        // Because locale changes are expensive (it involves writing a cookie and all that),
        // we're careful not to really change unless there's a true change in value.

        if (!value.equals(_locale)) {
            _locale = value;
            _localeChanged = true;
            markDirty();
        }
    }

    /**
     *  Invoked from {@link #service(RequestContext)} to ensure that the engine's
     *  instance variables are setup.  This allows the application a chance to
     *  restore transient variables that will not have survived deserialization.
     *
     *  Determines the servlet prefix:  this is the base URL used by
     *  {@link IEngineService services} to build URLs.  It consists
     *  of two parts:  the context path and the servlet path.
     *
     *  <p>The servlet path is retrieved from {@link HttpServletRequest#getServletPath()}.
     *
     *  <p>The context path is retrieved from {@link HttpServletRequest#getContextPath()}.
     *
     *  <p>The global object is retrieved from {@link IEngine#getGlobal()} method.
     *
     *  <p>The final path is available via the {@link #getServletPath()} method.
     *
     *  <p>In addition, this method locates and/or creates the:
     *  <ul>
     *  <li>{@link IComponentClassEnhancer}
     *  <li>{@link Pool}
     *  <li>{@link ITemplateSource}
     *  <li>{@link ISpecificationSource}
     *  <li>{@link IPageSource}
     *  <li>{@link IEngineService} {@link Map}
     *  <ll>{@link IScriptSource}
     *  <li>{@link IComponentMessagesSource}
     *  <li>{@link IPropertySource}
     *  </ul>
     *
     *  <p>This order is important, because some of the later shared objects
     *  depend on some of the earlier shared objects already having
     *  been located or created
     *  (especially {@link #getPool() pool}).
     *
     *  <p>Subclasses should invoke this implementation first, then perform their
     *  own setup.
     *
     **/

    protected void setupForRequest(RequestContext context) {
        HttpServlet servlet = context.getServlet();
        ServletContext servletContext = servlet.getServletContext();
        HttpServletRequest request = context.getRequest();
        HttpSession session = context.getSession();

        if (session != null)
            _sessionId = context.getSession().getId();
        else
            _sessionId = null;

        // Previously, this used getRemoteHost(), but that requires an
        // expensive reverse DNS lookup. Possibly, the host name lookup
        // should occur ... but only if there's an actual error message
        // to display.

        if (_clientAddress == null)
            _clientAddress = request.getRemoteAddr();

        // servletPath is null, so this means either we're doing the
        // first request in this session, or we're handling a subsequent
        // request in another JVM (i.e. another server in the cluster).
        // In any case, we have to do some late (re-)initialization.

        if (_servletPath == null) {
            // Get the path *within* the servlet context

            // In rare cases related to the tagsupport service, getServletPath() is wrong
            // (its a JSP, which invokes Tapestry as an include, thus muddling what
            // the real servlet and servlet path is).  In those cases, the JSP tag
            // will inform us.

            String path = (String) request.getAttribute(Tapestry.TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE);

            if (path == null)
                path = request.getServletPath();

            // Get the context path, which may be the empty string
            // (but won't be null).

            _contextPath = request.getContextPath();

            _servletPath = _contextPath + path;
        }

        String servletName = context.getServlet().getServletName();

        if (_propertySource == null) {
            String name = PROPERTY_SOURCE_NAME + ":" + servletName;

            _propertySource = (IPropertySource) servletContext.getAttribute(name);

            if (_propertySource == null) {
                _propertySource = createPropertySource(context);

                servletContext.setAttribute(name, _propertySource);
            }
        }

        if (_enhancer == null) {
            String name = ENHANCER_NAME + ":" + servletName;

            _enhancer = (IComponentClassEnhancer) servletContext.getAttribute(name);

            if (_enhancer == null) {
                _enhancer = createComponentClassEnhancer(context);

                servletContext.setAttribute(name, _enhancer);
            }
        }

        if (_pool == null) {
            String name = POOL_NAME + ":" + servletName;

            _pool = (Pool) servletContext.getAttribute(name);

            if (_pool == null) {
                _pool = createPool(context);

                servletContext.setAttribute(name, _pool);
            }
        }

        if (_templateSource == null) {
            String name = TEMPLATE_SOURCE_NAME + ":" + servletName;

            _templateSource = (ITemplateSource) servletContext.getAttribute(name);

            if (_templateSource == null) {
                _templateSource = createTemplateSource(context);

                servletContext.setAttribute(name, _templateSource);
            }
        }

        if (_specificationSource == null) {
            String name = SPECIFICATION_SOURCE_NAME + ":" + servletName;

            _specificationSource = (ISpecificationSource) servletContext.getAttribute(name);

            if (_specificationSource == null) {
                _specificationSource = createSpecificationSource(context);

                servletContext.setAttribute(name, _specificationSource);
            }
        }

        if (_pageSource == null) {
            String name = PAGE_SOURCE_NAME + ":" + servletName;

            _pageSource = (IPageSource) servletContext.getAttribute(name);

            if (_pageSource == null) {
                _pageSource = createPageSource(context);

                servletContext.setAttribute(name, _pageSource);
            }
        }

        if (_scriptSource == null) {
            String name = SCRIPT_SOURCE_NAME + ":" + servletName;

            _scriptSource = (IScriptSource) servletContext.getAttribute(name);

            if (_scriptSource == null) {
                _scriptSource = createScriptSource(context);

                servletContext.setAttribute(name, _scriptSource);
            }
        }

        if (_serviceMap == null) {
            String name = SERVICE_MAP_NAME + ":" + servletName;

            _serviceMap = (Map) servletContext.getAttribute(name);

            if (_serviceMap == null) {
                _serviceMap = createServiceMap();

                servletContext.setAttribute(name, _serviceMap);
            }
        }

        if (_stringsSource == null) {
            String name = STRINGS_SOURCE_NAME + ":" + servletName;

            _stringsSource = (IComponentMessagesSource) servletContext.getAttribute(name);

            if (_stringsSource == null) {
                _stringsSource = createComponentStringsSource(context);

                servletContext.setAttribute(name, _stringsSource);
            }
        }

        if (_dataSqueezer == null) {
            String name = DATA_SQUEEZER_NAME + ":" + servletName;

            _dataSqueezer = (DataSqueezer) servletContext.getAttribute(name);

            if (_dataSqueezer == null) {
                _dataSqueezer = createDataSqueezer();

                servletContext.setAttribute(name, _dataSqueezer);
            }
        }

        if (_global == null) {
            String name = GLOBAL_NAME + ":" + servletName;

            _global = servletContext.getAttribute(name);

            if (_global == null) {
                _global = createGlobal(context);

                servletContext.setAttribute(name, _global);
            }
        }

        if (_resourceChecksumSource == null) {
            String name = RESOURCE_CHECKSUM_SOURCE_NAME + ":" + servletName;

            _resourceChecksumSource = (ResourceChecksumSource) servletContext.getAttribute(name);

            if (_resourceChecksumSource == null) {
                _resourceChecksumSource = createResourceChecksumSource();

                servletContext.setAttribute(name, _resourceChecksumSource);
            }
        }

        String encoding = request.getCharacterEncoding();
        if (encoding == null) {
            encoding = getOutputEncoding();
            try {
                request.setCharacterEncoding(encoding);
            } catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException(Tapestry.format("illegal-encoding", encoding));
            } catch (NoSuchMethodError e) {
                // Servlet API 2.2 compatibility
                // Behave okay if the setCharacterEncoding() method is unavailable
            } catch (AbstractMethodError e) {
                // Servlet API 2.2 compatibility
                // Behave okay if the setCharacterEncoding() method is unavailable
            }
        }
    }

    /**
     *
     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
     *  a new instance of {@link IComponentMessagesSource}.
     *
     *  @return an instance of {@link DefaultComponentMessagesSource}
     *  @since 2.0.4
     *
     **/

    public IComponentMessagesSource createComponentStringsSource(RequestContext context) {
        return new DefaultComponentMessagesSource();
    }

    /**
     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
     *  an instance of {@link IScriptSource} that will be stored into
     *  the {@link ServletContext}.  Subclasses may override this method
     *  to provide a different implementation.
     *
     *
     *  @return an instance of {@link DefaultScriptSource}
     *  @since 1.0.9
     *
     **/

    protected IScriptSource createScriptSource(RequestContext context) {
        return new DefaultScriptSource(getResourceResolver());
    }

    /**
     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
     *  an instance of {@link IPageSource} that will be stored into
     *  the {@link ServletContext}.  Subclasses may override this method
     *  to provide a different implementation.
     *
     *  @return an instance of {@link PageSource}
     *  @since 1.0.9
     *
     **/

    protected IPageSource createPageSource(RequestContext context) {
        return new PageSource(this);
    }

    /**
     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
     *  an instance of {@link ISpecificationSource} that will be stored into
     *  the {@link ServletContext}.  Subclasses may override this method
     *  to provide a different implementation.
     *
     *  @return an instance of {@link DefaultSpecificationSource}
     *  @since 1.0.9
     **/

    protected ISpecificationSource createSpecificationSource(RequestContext context) {
        return new DefaultSpecificationSource(getResourceResolver(), _specification, _pool);
    }

    /**
     *  Invoked from {@link #setupForRequest(RequestContext)} to provide
     *  an instance of {@link ITemplateSource} that will be stored into
     *  the {@link ServletContext}.  Subclasses may override this method
     *  to provide a different implementation.
     *
     *  @return an instance of {@link DefaultTemplateSource}
     *  @since 1.0.9
     *
     **/

    protected ITemplateSource createTemplateSource(RequestContext context) {
        return new DefaultTemplateSource();
    }

    /**
     * Invoked from {@link #setupForRequest(RequestContext)} to provide
     * an instance of {@link ResourceChecksumSource} that will be stored into
     * the {@link ServletContext}.  Subclasses may override this method
     * to provide a different implementation.
     * @return an instance of {@link ResourceChecksumSourceImpl} that uses MD5 and Hex encoding.
     * @since 3.0.3
     */
    protected ResourceChecksumSource createResourceChecksumSource() {
        return new ResourceChecksumSourceImpl("MD5", new Hex());
    }

    /**
     *  Returns an object which can find resources and classes.
     *
     **/

    public IResourceResolver getResourceResolver() {
        return _resolver;
    }

    /**
     *  Generates a description of the instance.
     *  Invokes {@link #extendDescription(ToStringBuilder)}
     *  to fill in details about the instance.
     *
     *  @see #extendDescription(ToStringBuilder)
     *
     **/

    public String toString() {
        ToStringBuilder builder = new ToStringBuilder(this);

        builder.append("name", _specification == null ? Tapestry.getMessage("AbstractEngine.unknown-specification")
                : _specification.getName());

        builder.append("dirty", _dirty);
        builder.append("locale", _locale);
        builder.append("stateful", _stateful);
        builder.append("visit", _visit);

        extendDescription(builder);

        return builder.toString();
    }

    /**
     *  Returns true if the reset service is curently enabled.
     *
     **/

    public boolean isResetServiceEnabled() {
        return _resetServiceEnabled;
    }

    /**
     *  Implemented by subclasses to return the names of the active pages
     *  (pages for which recorders exist).  May return the empty list,
     *  but should not return null.
     *
     **/

    abstract public Collection getActivePageNames();

    /**
     *  Gets the visit object, if it has been created already.
     *
     *  <p>
     *  If the visit is non-null then
     *  the {@link #isDirty()} flag is set (because
     *  the engine can't tell what the caller will
     *  <i>do</i> with the visit).
     *
     **/

    public Object getVisit() {
        if (_visit != null)
            markDirty();

        return _visit;
    }

    /**
     *  Gets the visit object, invoking {@link #createVisit(IRequestCycle)} to create
     *  it lazily if needed.  If cycle is null, the visit will not be lazily created.
     *
     *  <p>
     *  After creating the visit, but before returning,
     *  the {@link HttpSession} will be created, and
     *  {@link #setStateful()} will be invoked.
     *
     *  <p>
     *  Sets the {@link #isDirty()} flag, if the return value
     *  is not null.
     *
     *
     **/

    public Object getVisit(IRequestCycle cycle) {
        if (_visit == null && cycle != null) {
            _visit = createVisit(cycle);

            // Now that a visit object exists, we need to force the creation
            // of a HttpSession.

            cycle.getRequestContext().createSession();

            setStateful();
        }

        if (_visit != null)
            markDirty();

        return _visit;
    }

    /**
     *  Updates the visit object and
     *  sets the {@link #isDirty() dirty flag}.
     *
     **/

    public void setVisit(Object value) {
        _visit = value;

        markDirty();
    }

    public boolean getHasVisit() {
        return _visit != null;
    }

    /**
     *  Invoked to lazily create a new visit object when it is first
     *  referenced (by {@link #getVisit(IRequestCycle)}).  This implementation works
     *  by looking up the name of the class to instantiate
     *  in the {@link #getPropertySource() configuration}.
     *
     *  <p>Subclasses may want to overide this method if some other means
     *  of instantiating a visit object is required.
     **/

    protected Object createVisit(IRequestCycle cycle) {
        String visitClassName;
        Class visitClass;
        Object result = null;

        visitClassName = _propertySource.getPropertyValue(VISIT_CLASS_PROPERTY_NAME);

        if (LOG.isDebugEnabled())
            LOG.debug("Creating visit object as instance of " + visitClassName);

        visitClass = _resolver.findClass(visitClassName);

        try {
            result = visitClass.newInstance();
        } catch (Throwable t) {
            throw new ApplicationRuntimeException(
                    Tapestry.format("AbstractEngine.unable-to-instantiate-visit", visitClassName), t);
        }

        return result;
    }

    /**
     *  Returns the global object for the application.  The global object is created at the start
     *  of the request ({@link #setupForRequest(RequestContext)} invokes
     *  {@link #createGlobal(RequestContext)} if needed),
     *  and is stored into the {@link ServletContext}.  All instances of the engine for
     *  the application share
     *  the global object; however, the global object is explicitly <em>not</em>
     *  replicated to other servers within
     *  a cluster.
     *
     *  @since 2.3
     *
     **/

    public Object getGlobal() {
        return _global;
    }

    public IScriptSource getScriptSource() {
        return _scriptSource;
    }

    public boolean isStateful() {
        return _stateful;
    }

    /**
     *  Invoked by subclasses to indicate that some state must now be stored
     *  in the engine (and that the engine should now be stored in the
     *  HttpSession).  The caller is responsible for actually creating
     *  the HttpSession (it will have access to the {@link RequestContext}).
     *
     *  @since 1.0.2
     *
     **/

    protected void setStateful() {
        _stateful = true;
    }

    /**
     *  Allows subclasses to include listener methods easily.
     *
     * @since 1.0.2
     **/

    public ListenerMap getListeners() {
        if (_listeners == null)
            _listeners = new ListenerMap(this);

        return _listeners;
    }

    private static class RedirectAnalyzer {
        private boolean _internal;
        private String _location;

        private RedirectAnalyzer(String location) {
            if (Tapestry.isBlank(location)) {
                _location = "";
                _internal = true;

                return;
            }

            _location = location;

            _internal = !(location.startsWith("/") || location.indexOf("://") > 0);
        }

        public void process(IRequestCycle cycle) {
            RequestContext context = cycle.getRequestContext();

            if (_internal)
                forward(context);
            else
                redirect(context);
        }

        private void forward(RequestContext context) {
            HttpServletRequest request = context.getRequest();
            HttpServletResponse response = context.getResponse();

            RequestDispatcher dispatcher = request.getRequestDispatcher("/" + _location);

            if (dispatcher == null)
                throw new ApplicationRuntimeException(
                        Tapestry.format("AbstractEngine.unable-to-find-dispatcher", _location));

            try {
                dispatcher.forward(request, response);
            } catch (ServletException ex) {
                throw new ApplicationRuntimeException(
                        Tapestry.format("AbstractEngine.unable-to-forward", _location), ex);
            } catch (IOException ex) {
                throw new ApplicationRuntimeException(
                        Tapestry.format("AbstractEngine.unable-to-forward", _location), ex);
            }
        }

        private void redirect(RequestContext context) {
            HttpServletResponse response = context.getResponse();

            String finalURL = response.encodeRedirectURL(_location);

            try {
                response.sendRedirect(finalURL);
            } catch (IOException ex) {
                throw new ApplicationRuntimeException(
                        Tapestry.format("AbstractEngine.unable-to-redirect", _location), ex);
            }
        }

    }

    /**
     *  Invoked when a {@link RedirectException} is thrown during the processing of a request.
     *
     *  @throws ApplicationRuntimeException if an {@link IOException},
     *  {@link ServletException} is thrown by the redirect, or if no
     *  {@link RequestDispatcher} can be found for local resource.
     *
     *  @since 2.2
     *
     **/

    protected void handleRedirectException(IRequestCycle cycle, RedirectException ex) {
        String location = ex.getRedirectLocation();

        if (LOG.isDebugEnabled())
            LOG.debug("Redirecting to: " + location);

        RedirectAnalyzer analyzer = new RedirectAnalyzer(location);

        analyzer.process(cycle);
    }

    /**
     *  Creates a Map of all the services available to the application.
     *
     *  <p>Note: the Map returned is not synchronized, on the theory that returned
     *  map is not further modified and therefore threadsafe.
     *
     **/

    private Map createServiceMap() {
        if (LOG.isDebugEnabled())
            LOG.debug("Creating service map.");

        ISpecificationSource source = getSpecificationSource();

        // Build the initial version of the result map,
        // where each value is the *name* of a class.

        Map result = new HashMap();

        // Do the framework first.

        addServices(source.getFrameworkNamespace(), result);

        // And allow the application to override the framework.

        addServices(source.getApplicationNamespace(), result);

        IResourceResolver resolver = getResourceResolver();

        Iterator i = result.entrySet().iterator();

        while (i.hasNext()) {
            Map.Entry entry = (Map.Entry) i.next();

            String name = (String) entry.getKey();
            String className = (String) entry.getValue();

            if (LOG.isDebugEnabled())
                LOG.debug("Creating service " + name + " as instance of " + className);

            Class serviceClass = resolver.findClass(className);

            try {
                IEngineService service = (IEngineService) serviceClass.newInstance();
                String serviceName = service.getName();

                if (!service.getName().equals(name))
                    throw new ApplicationRuntimeException(
                            Tapestry.format("AbstractEngine.service-name-mismatch", name, className, serviceName));

                // Replace the class name with an instance
                // of the named class.

                entry.setValue(service);
            } catch (InstantiationException ex) {
                String message = Tapestry.format("AbstractEngine.unable-to-instantiate-service", name, className);

                LOG.error(message, ex);

                throw new ApplicationRuntimeException(message, ex);
            } catch (IllegalAccessException ex) {
                String message = Tapestry.format("AbstractEngine.unable-to-instantiate-service", name, className);

                LOG.error(message, ex);

                throw new ApplicationRuntimeException(message, ex);
            }
        }

        // Result should not be modified after this point, for threadsafety issues.
        // We could wrap it in an unmodifiable, but for efficiency we don't.

        return result;
    }

    /**
     *  Locates all services in the namespace and adds key/value
     *  pairs to the map (name and class name).  Then recursively
     *  descendends into child namespaces to collect more
     *  service names.
     *
     *  @since 2.2
     *
     **/

    private void addServices(INamespace namespace, Map map) {
        List names = namespace.getServiceNames();
        int count = names.size();

        for (int i = 0; i < count; i++) {
            String name = (String) names.get(i);

            map.put(name, namespace.getServiceClassName(name));
        }

        List namespaceIds = namespace.getChildIds();
        count = namespaceIds.size();

        for (int i = 0; i < count; i++) {
            String id = (String) namespaceIds.get(i);

            addServices(namespace.getChildNamespace(id), map);
        }
    }

    /**
     *  @since 2.0.4
     *
     **/

    public IComponentMessagesSource getComponentMessagesSource() {
        return _stringsSource;
    }

    /**
     *  @since 2.2
     *
     **/

    public DataSqueezer getDataSqueezer() {
        return _dataSqueezer;
    }

    /**
     *
     *  Invoked from {@link #setupForRequest(RequestContext)} to create
     *  a {@link DataSqueezer} when needed (typically, just the very first time).
     *  This implementation returns a standard, new instance.
     *
     *  @since 2.2
     *
     **/

    public DataSqueezer createDataSqueezer() {
        return new DataSqueezer(_resolver);
    }

    /**
     *  Invoked from {@link #service(RequestContext)} to extract, from the URL,
     *  the name of the service.  The current implementation expects the first
     *  pathInfo element to be the service name.  At some point in the future,
     *  the method of constructing and parsing URLs may be abstracted into
     *  a developer-selected class.
     *
     *  <p>Subclasses may override this method if the application defines
     *  specific services with unusual URL encoding rules.
     *
     *  <p>This implementation simply extracts the value for
     *  query parameter {@link Tapestry#SERVICE_QUERY_PARAMETER_NAME}
     *  and extracts the service name from that.
     *
     *  <p>
     *  For supporting the JSP tags, this method first
     *  checks for attribute {@link Tapestry#TAG_SUPPORT_SERVICE_ATTRIBUTE}.  If non-null,
     *  then {@link Tapestry#TAGSUPPORT_SERVICE} is returned.
     *
     *  @since 2.2
     *
     **/

    protected String extractServiceName(RequestContext context) {
        if (context.getRequest().getAttribute(Tapestry.TAG_SUPPORT_SERVICE_ATTRIBUTE) != null)
            return Tapestry.TAGSUPPORT_SERVICE;

        String serviceData = context.getParameter(Tapestry.SERVICE_QUERY_PARAMETER_NAME);

        if (serviceData == null)
            return Tapestry.HOME_SERVICE;

        // The service name is anything before the first slash,
        // if there is one.

        int slashx = serviceData.indexOf('/');

        if (slashx < 0)
            return serviceData;

        return serviceData.substring(0, slashx);
    }

    /** @since 2.3 **/

    public IPropertySource getPropertySource() {
        return _propertySource;
    }

    /** @since 3.0.3 */

    public ResourceChecksumSource getResourceChecksumSource() {
        return _resourceChecksumSource;
    }

    /** @since 3.0 **/

    protected String getExceptionPageName() {
        return EXCEPTION_PAGE;
    }

    /** @since 3.0 **/

    protected String getStaleLinkPageName() {
        return STALE_LINK_PAGE;
    }

    /** @since 3.0 **/

    protected String getStaleSessionPageName() {
        return STALE_SESSION_PAGE;
    }

    /**
     *  Name of an application extension that can provide configuration properties.
     *
     *  @see #createPropertySource(RequestContext)
     *  @since 2.3
     *
     **/

    private static final String EXTENSION_PROPERTY_SOURCE_NAME = "org.apache.tapestry.property-source";

    /**
     *  Creates a shared property source that will be stored into
     *  the servlet context.
     *  Subclasses may override this method to build thier
     *  own search path.
     *
     *  <p>If the application specification contains an extension
     *  named "org.apache.tapestry.property-source" it is inserted
     *  in the search path just before
     *  the property source for JVM System Properties.  This is a simple
     *  hook at allow application-specific methods of obtaining
     *  configuration values (typically, from a database or from JMX,
     *  in some way).  Alternately, subclasses may
     *  override this method to provide whatever search path
     *  is appropriate.
     *
     *
     *  @since 2.3
     *
     **/

    protected IPropertySource createPropertySource(RequestContext context) {
        DelegatingPropertySource result = new DelegatingPropertySource();

        ApplicationServlet servlet = context.getServlet();
        IApplicationSpecification spec = servlet.getApplicationSpecification();

        result.addSource(new PropertyHolderPropertySource(spec));
        result.addSource(new ServletPropertySource(servlet.getServletConfig()));
        result.addSource(new ServletContextPropertySource(servlet.getServletContext()));

        if (spec.checkExtension(EXTENSION_PROPERTY_SOURCE_NAME)) {
            IPropertySource source = (IPropertySource) spec.getExtension(EXTENSION_PROPERTY_SOURCE_NAME,
                    IPropertySource.class);

            result.addSource(source);
        }

        result.addSource(SystemPropertiesPropertySource.getInstance());

        // Lastly, add a final source to handle "factory defaults".

        ResourceBundle bundle = ResourceBundle.getBundle("org.apache.tapestry.ConfigurationDefaults");

        result.addSource(new ResourceBundlePropertySource(bundle));

        return result;
    }

    /**
     *  Creates the shared Global object.  This implementation looks for an configuration
     *  property, <code>org.apache.tapestry.global-class</code>, and instantiates that class
     *  using a no-arguments
     *  constructor.  If the property is not defined, a synchronized
     *  {@link java.util.HashMap} is created.
     *
     *  @since 2.3
     *
     **/

    protected Object createGlobal(RequestContext context) {
        String className = _propertySource.getPropertyValue("org.apache.tapestry.global-class");

        if (className == null)
            return Collections.synchronizedMap(new HashMap());

        Class globalClass = _resolver.findClass(className);

        try {
            return globalClass.newInstance();
        } catch (Exception ex) {
            throw new ApplicationRuntimeException(
                    Tapestry.format("AbstractEngine.unable-to-instantiate-global", className), ex);
        }
    }

    /**
     *  Returns an new instance of {@link Pool}, with the standard
     *  set of adaptors, plus {@link BSFManagerPoolableAdaptor} for
     *  {@link BSFManager}.
     *
     *  <p>Subclasses may override this
     *  method to configure the Pool differently.
     *
     *  @since 3.0
     *
     **/

    protected Pool createPool(RequestContext context) {
        Pool result = new Pool();

        result.registerAdaptor(BSFManager.class, new BSFManagerPoolableAdaptor());

        return result;
    }

    /** @since 3.0 **/

    public Pool getPool() {
        return _pool;
    }

    /**
     *
     * Invoked from {@link #setupForRequest(RequestContext)}.  Creates
     * a new instance of {@link DefaultComponentClassEnhancer}.  Subclasses
     * may override to return a different object.
     * 
     * <p>
     * Check the property <code>org.apache.tapestry.enhance.disable-abstract-method-validation</code>
     * and, if true, disables abstract method validation. This is used  in some
     * errant JDK's (such as IBM's 1.3.1) that incorrectly report concrete methods from
     * abstract classes as abstract.
     *
     * @since 3.0
     */

    protected IComponentClassEnhancer createComponentClassEnhancer(RequestContext context) {
        boolean disableValidation = "true".equals(
                _propertySource.getPropertyValue("org.apache.tapestry.enhance.disable-abstract-method-validation"));

        return new DefaultComponentClassEnhancer(_resolver, disableValidation);
    }

    /** @since 3.0 **/

    public IComponentClassEnhancer getComponentClassEnhancer() {
        return _enhancer;
    }

    /**
     *  Returns true if the engine has (potentially) changed
     *  state since the last time it was stored
     *  into the {@link javax.servlet.http.HttpSession}.  Various
     *  events set this property to true.
     *
     *  @since 3.0
     *
     **/

    public boolean isDirty() {
        return _dirty;
    }

    /**
     *  Invoked to set the dirty flag, indicating that the
     *  engine should be stored into the
     *  {@link javax.servlet.http.HttpSession}.
     *
     *
     *  @since 3.0
     *
     **/

    protected void markDirty() {
        if (!_dirty)
            LOG.debug("Setting dirty flag.");

        _dirty = true;
    }

    /**
     *
     *  Clears the dirty flag when a engine is stored into the
     *  {@link HttpSession}.
     *
     *
     *  @since 3.0
     *
     **/

    public void valueBound(HttpSessionBindingEvent arg0) {
        LOG.debug(_dirty ? "Clearing dirty flag." : "Dirty flag already cleared.");

        _dirty = false;
    }

    /**
     *  Does nothing.
     *
     *  @since 3.0
     *
     **/

    public void valueUnbound(HttpSessionBindingEvent arg0) {
    }

    /**
     *
     *  The encoding to be used if none has been defined using the output encoding property.
     *  Override this method to change the default.
     *
     *  @return the default output encoding
     *  @since 3.0
     *
     **/
    protected String getDefaultOutputEncoding() {
        return DEFAULT_OUTPUT_ENCODING;
    }

    /**
     *
     *  Returns the encoding to be used to generate the servlet responses and
     *  accept the servlet requests.
     *
     *  The encoding is defined using the org.apache.tapestry.output-encoding
     *  and is UTF-8 by default
     *
     *  @since 3.0
     *  @see org.apache.tapestry.IEngine#getOutputEncoding()
     *
     **/
    public String getOutputEncoding() {
        IPropertySource source = getPropertySource();

        String encoding = source.getPropertyValue(OUTPUT_ENCODING_PROPERTY_NAME);
        if (encoding == null)
            encoding = getDefaultOutputEncoding();

        return encoding;
    }

}