Java tutorial
// Copyright 2004 The Apache Software Foundation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package org.apache.tapestry.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; } }