| // 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.engine; |
| |
| 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.*; |
| import org.apache.tapestry.asset.ResourceChecksumSource; |
| import org.apache.tapestry.asset.ResourceChecksumSourceImpl; |
| import org.apache.tapestry.enhance.DefaultComponentClassEnhancer; |
| import org.apache.tapestry.enhance.IEnhancedClassFactory; |
| import org.apache.tapestry.enhance.javassist.EnhancedClassFactory; |
| 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.*; |
| import org.apache.tapestry.util.exception.ExceptionAnalyzer; |
| import org.apache.tapestry.util.io.DataSqueezer; |
| import org.apache.tapestry.util.pool.Pool; |
| |
| import javax.servlet.RequestDispatcher; |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.*; |
| import java.io.*; |
| import java.util.*; |
| |
| /** |
| * Basis for building real Tapestry applications. Immediate subclasses provide different strategies for managing page |
| * state and other resources between request cycles. |
| * <p/> |
| * 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/> |
| * <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/> |
| * <p>Where possible, instance variables should be transient. They can be restored inside {@link |
| * #setupForRequest(RequestContext)}. |
| * <p/> |
| * <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/> |
| * <p>Some of the classes' behavior is controlled by JVM system properties (typically only used during development): |
| * <p/> |
| * <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$ |
| */ |
| |
| 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} |
| * |
| * @see #createPropertySource(RequestContext) |
| * @since 3.0 |
| */ |
| |
| 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}. |
| * |
| * @see #createPool(RequestContext) |
| * @since 3.0 |
| */ |
| |
| 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}. |
| * |
| * @see #createComponentClassEnhancer(RequestContext) |
| * @since 3.0 |
| */ |
| |
| private transient IComponentClassEnhancer _enhancer; |
| |
| protected static final String CLASS_FACTORY_NAME = "org.apache.tapestry.enhance.javassist.EnhancedClassFactory"; |
| |
| private transient IEnhancedClassFactory _classFactory; |
| |
| /** |
| * 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; |
| |
| /** |
| * The name of the context attribute for the {@link ExpressionEvaluator} instance. The application's name is |
| * appended. |
| */ |
| protected static final String EXPRESSION_EVALUATOR_NAME = "org.apache.tapestry.engine.ExpressionEvaluator"; |
| |
| private transient ExpressionEvaluator _expressionEvaluator; |
| |
| private transient ExpressionCache _expressionCache; |
| |
| private transient String _outputEncoding; |
| |
| /** |
| * Sets the Exception page's exception property, then renders the Exception page. |
| * <p/> |
| * <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/> |
| * <p>The monitor is used to create a {@link RequestCycle}. |
| * <p/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <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: |
| * <p/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <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(); |
| _expressionCache.reset(); |
| _expressionEvaluator.reset(); |
| _outputEncoding = null; |
| } |
| |
| /** |
| * 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. |
| * <p/> |
| * 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/> |
| * <p>The servlet path is retrieved from {@link HttpServletRequest#getServletPath()}. |
| * <p/> |
| * <p>The context path is retrieved from {@link HttpServletRequest#getContextPath()}. |
| * <p/> |
| * <p>The global object is retrieved from {@link IEngine#getGlobal()} method. |
| * <p/> |
| * <p>The final path is available via the {@link #getServletPath()} method. |
| * <p/> |
| * <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/> |
| * <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/> |
| * <p>Subclasses should invoke this implementation first, then perform their own setup. They must also synchronize |
| * on the {@link org.apache.tapestry.request.RequestContext#getServlet() application servlet}. |
| */ |
| |
| 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(); |
| |
| // Synchronize on the servlet to make sure that two different IEngine instances |
| // do not try ot create some of the common dependencies simulataneously. We enclose |
| // all the code that uses the ServletContext as a cache. This is the motivation for |
| // adding an IoC container to Tapestry 4. |
| |
| // Note: by borrowing ConcurrentBarrier from Tapestry 5, we could perhaps optimize this |
| // a bit ... but in most cases, we'll "flit" into the synchronized block, see non-nulls |
| // for all the fields, and "flit" back out with virtually no chance of contention. |
| |
| synchronized (servlet) |
| { |
| 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 (_classFactory == null) |
| { |
| String name = CLASS_FACTORY_NAME + ":" + servletName; |
| |
| _classFactory = (IEnhancedClassFactory) servletContext.getAttribute(name); |
| |
| if (_classFactory == null) |
| { |
| _classFactory = createClassFactory(); |
| |
| servletContext.setAttribute(name, _classFactory); |
| } |
| } |
| |
| 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); |
| |
| _pool = createPool(context); |
| |
| } |
| |
| 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); |
| } |
| } |
| |
| if (_expressionEvaluator == null) |
| { |
| String name = EXPRESSION_EVALUATOR_NAME + ":" + servletName; |
| |
| _expressionEvaluator = (ExpressionEvaluator) servletContext.getAttribute(name); |
| |
| if (_expressionEvaluator == null) |
| { |
| _expressionEvaluator = createExpressionEvaluator(); |
| |
| servletContext.setAttribute(name, _expressionEvaluator); |
| } |
| } |
| |
| // That's the end of reading to and from the servlet context, so we can leave the block |
| // synchronized against the servlet. |
| } |
| |
| 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()); |
| } |
| |
| protected ExpressionEvaluator createExpressionEvaluator() |
| { |
| _expressionCache = (ExpressionCacheImpl) createExpressionCache(); |
| |
| return new ExpressionEvaluatorImpl(_resolver, _classFactory, _expressionCache, _specification); |
| } |
| |
| protected ExpressionCache createExpressionCache() |
| { |
| return new ExpressionCacheImpl(); |
| } |
| |
| /** |
| * Returns an object which can find resources and classes. |
| */ |
| |
| public IResourceResolver getResourceResolver() |
| { |
| return _resolver; |
| } |
| |
| public ExpressionEvaluator getExpressionEvaluator() |
| { |
| return _expressionEvaluator; |
| } |
| |
| /** |
| * 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/> |
| * <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/> |
| * <p/> |
| * After creating the visit, but before returning, the {@link HttpSession} will be created, and {@link |
| * #setStateful()} will be invoked. |
| * <p/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <p>Subclasses may override this method if the application defines specific services with unusual URL encoding |
| * rules. |
| * <p/> |
| * <p>This implementation simply extracts the value for query parameter {@link Tapestry#SERVICE_QUERY_PARAMETER_NAME} |
| * and extracts the service name from that. |
| * <p/> |
| * <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/> |
| * <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/> |
| * <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/> |
| * <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, _classFactory); |
| } |
| |
| protected IEnhancedClassFactory createClassFactory() |
| { |
| return new EnhancedClassFactory(_resolver); |
| } |
| |
| /** |
| * @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. |
| * <p/> |
| * The encoding is defined using the org.apache.tapestry.output-encoding and is UTF-8 by default |
| * |
| * @see org.apache.tapestry.IEngine#getOutputEncoding() |
| * @since 3.0 |
| */ |
| public String getOutputEncoding() |
| { |
| if (_outputEncoding != null) |
| return _outputEncoding; |
| |
| IPropertySource source = getPropertySource(); |
| |
| String encoding = source.getPropertyValue(OUTPUT_ENCODING_PROPERTY_NAME); |
| if (encoding == null) |
| encoding = getDefaultOutputEncoding(); |
| |
| _outputEncoding = encoding; |
| |
| return _outputEncoding; |
| } |
| |
| } |