001// Copyright 2004, 2005 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry.engine;
016
017import java.io.IOException;
018import java.util.ArrayList;
019import java.util.List;
020import java.util.Locale;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.apache.hivemind.ApplicationRuntimeException;
025import org.apache.hivemind.ClassResolver;
026import org.apache.hivemind.util.Defense;
027import org.apache.hivemind.util.ToStringBuilder;
028import org.apache.tapestry.Constants;
029import org.apache.tapestry.IEngine;
030import org.apache.tapestry.IPage;
031import org.apache.tapestry.IRequestCycle;
032import org.apache.tapestry.PageRedirectException;
033import org.apache.tapestry.RedirectException;
034import org.apache.tapestry.StaleLinkException;
035import org.apache.tapestry.StaleSessionException;
036import org.apache.tapestry.listener.ListenerMap;
037import org.apache.tapestry.services.DataSqueezer;
038import org.apache.tapestry.services.Infrastructure;
039import org.apache.tapestry.spec.IApplicationSpecification;
040import org.apache.tapestry.web.WebRequest;
041import org.apache.tapestry.web.WebResponse;
042
043/**
044 * Basis for building real Tapestry applications. Immediate subclasses provide different strategies
045 * for managing page state and other resources between request cycles.
046 * <p>
047 * Note: much of this description is <em>in transition</em> as part of Tapestry 4.0. All ad-hoc
048 * singletons and such are being replaced with HiveMind services.
049 * <p>
050 * Uses a shared instance of {@link ITemplateSource},{@link ISpecificationSource},
051 * {@link IScriptSource}and {@link IComponentMessagesSource}stored as attributes of the
052 * {@link ServletContext}(they will be shared by all sessions).
053 * <p>
054 * An engine is designed to be very lightweight. Particularily, it should <b>never </b> hold
055 * references to any {@link IPage}or {@link org.apache.tapestry.IComponent}objects. The entire
056 * system is based upon being able to quickly rebuild the state of any page(s).
057 * <p>
058 * Where possible, instance variables should be transient. They can be restored inside
059 * {@link #setupForRequest(RequestContext)}.
060 * <p>
061 * In practice, a subclass (usually {@link BaseEngine}) is used without subclassing. Instead, a
062 * visit object is specified. To facilitate this, the application specification may include a
063 * property, <code>org.apache.tapestry.visit-class</code> which is the class name to instantiate
064 * when a visit object is first needed. See {@link #createVisit(IRequestCycle)}for more details.
065 * <p>
066 * Some of the classes' behavior is controlled by JVM system properties (typically only used during
067 * development): <table border=1>
068 * <tr>
069 * <th>Property</th>
070 * <th>Description</th>
071 * </tr>
072 * <tr>
073 * <td>org.apache.tapestry.enable-reset-service</td>
074 * <td>If true, enabled an additional service, reset, that allow page, specification and template
075 * caches to be cleared on demand. See {@link #isResetServiceEnabled()}.</td>
076 * </tr>
077 * <tr>
078 * <td>org.apache.tapestry.disable-caching</td>
079 * <td>If true, then the page, specification, template and script caches will be cleared after each
080 * request. This slows things down, but ensures that the latest versions of such files are used.
081 * Care should be taken that the source directories for the files preceeds any versions of the files
082 * available in JARs or WARs.</td>
083 * </tr>
084 * </table>
085 * 
086 * @author Howard Lewis Ship
087 */
088
089public abstract class AbstractEngine implements IEngine
090{
091    private static final Log LOG = LogFactory.getLog(AbstractEngine.class);
092
093    /**
094     * The link to the world of HiveMind services.
095     * 
096     * @since 4.0
097     */
098    private Infrastructure _infrastructure;
099
100    private ListenerMap _listeners;
101
102    /**
103     * The curent locale for the engine, which may be changed at any time.
104     */
105
106    private Locale _locale;
107
108    /**
109     * The name of the application specification property used to specify the class of the visit
110     * object.
111     */
112
113    public static final String VISIT_CLASS_PROPERTY_NAME = "org.apache.tapestry.visit-class";
114
115    /**
116     * @see org.apache.tapestry.error.ExceptionPresenter
117     */
118
119    protected void activateExceptionPage(IRequestCycle cycle, Throwable cause)
120    {
121        _infrastructure.getExceptionPresenter().presentException(cycle, cause);
122    }
123
124    /**
125     * Writes a detailed report of the exception to <code>System.err</code>.
126     * 
127     * @see org.apache.tapestry.error.RequestExceptionReporter
128     */
129
130    public void reportException(String reportTitle, Throwable ex)
131    {
132        _infrastructure.getRequestExceptionReporter().reportRequestException(reportTitle, ex);
133    }
134
135    /**
136     * Invoked at the end of the request cycle to release any resources specific to the request
137     * cycle. This implementation does nothing and may be overriden freely.
138     */
139
140    protected void cleanupAfterRequest(IRequestCycle cycle)
141    {
142
143    }
144
145    /**
146     * Returns the locale for the engine. This is initially set by the {@link ApplicationServlet}
147     * but may be updated by the application.
148     */
149
150    public Locale getLocale()
151    {
152        return _locale;
153    }
154
155    /**
156     * Returns a service with the given name.
157     * 
158     * @see Infrastructure#getServiceMap()
159     * @see org.apache.tapestry.services.ServiceMap
160     */
161
162    public IEngineService getService(String name)
163    {
164        return _infrastructure.getServiceMap().getService(name);
165    }
166
167    /** @see Infrastructure#getApplicationSpecification() */
168
169    public IApplicationSpecification getSpecification()
170    {
171        return _infrastructure.getApplicationSpecification();
172    }
173
174    /** @see Infrastructure#getSpecificationSource() */
175
176    public ISpecificationSource getSpecificationSource()
177    {
178        return _infrastructure.getSpecificationSource();
179    }
180
181    /**
182     * Invoked, typically, when an exception occurs while servicing the request. This method resets
183     * the output, sets the new page and renders it.
184     */
185
186    protected void redirect(String pageName, IRequestCycle cycle,
187            ApplicationRuntimeException exception) throws IOException
188    {
189        IPage page = cycle.getPage(pageName);
190
191        cycle.activate(page);
192
193        renderResponse(cycle);
194    }
195
196    /**
197     * Delegates to
198     * {@link org.apache.tapestry.services.ResponseRenderer#renderResponse(IRequestCycle)}.
199     */
200
201    public void renderResponse(IRequestCycle cycle) throws IOException
202    {
203        _infrastructure.getResponseRenderer().renderResponse(cycle);
204    }
205
206    /**
207     * Delegate method for the servlet. Services the request.
208     */
209
210    public void service(WebRequest request, WebResponse response) throws IOException
211    {
212        IRequestCycle cycle = null;
213        IMonitor monitor = null;
214        IEngineService service = null;
215
216        if (_infrastructure == null)
217            _infrastructure = (Infrastructure) request.getAttribute(Constants.INFRASTRUCTURE_KEY);
218
219        // Create the request cycle; if this fails, there's not much that can be done ... everything
220        // else in Tapestry relies on the RequestCycle.
221
222        try
223        {
224            cycle = _infrastructure.getRequestCycleFactory().newRequestCycle(this);
225        }
226        catch (RuntimeException ex)
227        {
228            throw ex;
229        }
230        catch (Exception ex)
231        {
232            throw new IOException(ex.getMessage());
233        }
234
235        try
236        {
237            try
238            {
239                monitor = cycle.getMonitor();
240
241                service = cycle.getService();
242
243                monitor.serviceBegin(service.getName(), _infrastructure.getRequest()
244                        .getRequestURI());
245
246                // Let the service handle the rest of the request.
247
248                service.service(cycle);
249
250                return;
251            }
252            catch (PageRedirectException ex)
253            {
254                handlePageRedirectException(cycle, ex);
255            }
256            catch (RedirectException ex)
257            {
258                handleRedirectException(cycle, ex);
259            }
260            catch (StaleLinkException ex)
261            {
262                handleStaleLinkException(cycle, ex);
263            }
264            catch (StaleSessionException ex)
265            {
266                handleStaleSessionException(cycle, ex);
267            }
268        }
269        catch (Exception ex)
270        {
271            monitor.serviceException(ex);
272
273            // Attempt to switch to the exception page. However, this may itself
274            // fail for a number of reasons, in which case an ApplicationRuntimeException is
275            // thrown.
276
277            if (LOG.isDebugEnabled())
278                LOG.debug("Uncaught exception", ex);
279
280            activateExceptionPage(cycle, ex);
281        }
282        finally
283        {
284            if (service != null)
285                monitor.serviceEnd(service.getName());
286
287            try
288            {
289                cycle.cleanup();
290                _infrastructure.getApplicationStateManager().flush();
291            }
292            catch (Exception ex)
293            {
294                reportException(EngineMessages.exceptionDuringCleanup(ex), ex);
295            }
296        }
297    }
298
299    /**
300     * Handles {@link PageRedirectException} which involves executing
301     * {@link IRequestCycle#activate(IPage)} on the target page (of the exception), until either a
302     * loop is found, or a page succesfully activates.
303     * <p>
304     * This should generally not be overriden in subclasses.
305     * 
306     * @since 3.0
307     */
308
309    protected void handlePageRedirectException(IRequestCycle cycle, PageRedirectException exception)
310            throws IOException
311    {
312        List pageNames = new ArrayList();
313
314        String pageName = exception.getTargetPageName();
315
316        while (true)
317        {
318            if (pageNames.contains(pageName))
319            {
320                pageNames.add(pageName);
321
322                throw new ApplicationRuntimeException(EngineMessages.validateCycle(pageNames));
323            }
324
325            // Record that this page has been a target.
326
327            pageNames.add(pageName);
328
329            try
330            {
331                // Attempt to activate the new page.
332
333                cycle.activate(pageName);
334
335                break;
336            }
337            catch (PageRedirectException secondRedirectException)
338            {
339                pageName = secondRedirectException.getTargetPageName();
340            }
341        }
342
343        renderResponse(cycle);
344    }
345
346    /**
347     * Invoked by {@link #service(WebRequest, WebResponse)} if a {@link StaleLinkException} is
348     * thrown by the {@link IEngineService service}. This implementation sets the message property
349     * of the StaleLink page to the message provided in the exception, then invokes
350     * {@link #redirect(String, IRequestCycle, ApplicationRuntimeException)} to render the StaleLink
351     * page.
352     * <p>
353     * Subclasses may overide this method (without invoking this implementation). A better practice
354     * is to contribute an alternative implementation of
355     * {@link org.apache.tapestry.error.StaleLinkExceptionPresenter} to the
356     * tapestry.InfrastructureOverrides configuration point.
357     * <p>
358     * A common practice is to present an error message on the application's Home page. Alternately,
359     * the application may provide its own version of the StaleLink page, overriding the framework's
360     * implementation (probably a good idea, because the default page hints at "application errors"
361     * and isn't localized). The overriding StaleLink implementation must implement a message
362     * property of type String.
363     * 
364     * @since 0.2.10
365     */
366
367    protected void handleStaleLinkException(IRequestCycle cycle, StaleLinkException exception)
368            throws IOException
369    {
370        _infrastructure.getStaleLinkExceptionPresenter()
371                .presentStaleLinkException(cycle, exception);
372    }
373
374    /**
375     * Invoked by {@link #service(WebRequest, WebResponse)} if a {@link StaleSessionException} is
376     * thrown by the {@link IEngineService service}. This implementation uses the
377     * {@link org.apache.tapestry.error.StaleSessionExceptionPresenter} to render the StaleSession
378     * page.
379     * <p>
380     * Subclasses may overide this method (without invoking this implementation), but it is better
381     * to override the tapestry.error.StaleSessionExceptionReporter service instead (or contribute a
382     * replacement to the tapestry.InfrastructureOverrides configuration point).
383     * 
384     * @since 0.2.10
385     */
386
387    protected void handleStaleSessionException(IRequestCycle cycle, StaleSessionException exception)
388            throws IOException
389    {
390        _infrastructure.getStaleSessionExceptionPresenter().presentStaleSessionException(
391                cycle,
392                exception);
393    }
394
395    /**
396     * Changes the locale for the engine.
397     */
398
399    public void setLocale(Locale value)
400    {
401        Defense.notNull(value, "locale");
402
403        _locale = value;
404
405        // The locale may be set before the engine is initialized with the Infrastructure.
406
407        if (_infrastructure != null)
408            _infrastructure.setLocale(value);
409    }
410
411    /**
412     * @see Infrastructure#getClassResolver()
413     */
414
415    public ClassResolver getClassResolver()
416    {
417        return _infrastructure.getClassResolver();
418    }
419
420    /**
421     * Generates a description of the instance. Invokes {@link #extendDescription(ToStringBuilder)}
422     * to fill in details about the instance.
423     * 
424     * @see #extendDescription(ToStringBuilder)
425     */
426
427    public String toString()
428    {
429        ToStringBuilder builder = new ToStringBuilder(this);
430
431        builder.append("locale", _locale);
432
433        return builder.toString();
434    }
435
436    /**
437     * Gets the visit object from the
438     * {@link org.apache.tapestry.engine.state.ApplicationStateManager}, creating it if it does not
439     * already exist.
440     * <p>
441     * As of Tapestry 4.0, this will always create the visit object, possibly creating a new session
442     * in the process.
443     */
444
445    public Object getVisit()
446    {
447        return _infrastructure.getApplicationStateManager().get("visit");
448    }
449
450    public void setVisit(Object visit)
451    {
452        _infrastructure.getApplicationStateManager().store("visit", visit);
453    }
454
455    /**
456     * Gets the visit object from the
457     * {@link org.apache.tapestry.engine.state.ApplicationStateManager}, which will create it as
458     * necessary.
459     */
460
461    public Object getVisit(IRequestCycle cycle)
462    {
463        return getVisit();
464    }
465
466    public boolean getHasVisit()
467    {
468        return _infrastructure.getApplicationStateManager().exists("visit");
469    }
470
471    /**
472     * Returns the global object for the application. The global object is created at the start of
473     * the request ({@link #setupForRequest(RequestContext)}invokes
474     * {@link #createGlobal(RequestContext)}if needed), and is stored into the
475     * {@link ServletContext}. All instances of the engine for the application share the global
476     * object; however, the global object is explicitly <em>not</em> replicated to other servers
477     * within a cluster.
478     * 
479     * @since 2.3
480     */
481
482    public Object getGlobal()
483    {
484        return _infrastructure.getApplicationStateManager().get("global");
485    }
486
487    public IScriptSource getScriptSource()
488    {
489        return _infrastructure.getScriptSource();
490    }
491
492    /**
493     * Allows subclasses to include listener methods easily.
494     * 
495     * @since 1.0.2
496     */
497
498    public ListenerMap getListeners()
499    {
500        if (_listeners == null)
501            _listeners = _infrastructure.getListenerMapSource().getListenerMapForObject(this);
502
503        return _listeners;
504    }
505
506    /**
507     * Invoked when a {@link RedirectException} is thrown during the processing of a request.
508     * 
509     * @throws ApplicationRuntimeException
510     *             if an {@link IOException},{@link ServletException}is thrown by the redirect,
511     *             or if no {@link RequestDispatcher}can be found for local resource.
512     * @since 2.2
513     */
514
515    protected void handleRedirectException(IRequestCycle cycle, RedirectException redirectException)
516    {
517        String location = redirectException.getRedirectLocation();
518
519        if (LOG.isDebugEnabled())
520            LOG.debug("Redirecting to: " + location);
521
522        _infrastructure.getRequest().forward(location);
523    }
524
525    /**
526     * @see Infrastructure#getDataSqueezer()
527     */
528
529    public DataSqueezer getDataSqueezer()
530    {
531        return _infrastructure.getDataSqueezer();
532    }
533
534    /** @since 2.3 */
535
536    public IPropertySource getPropertySource()
537    {
538        return _infrastructure.getApplicationPropertySource();
539    }
540
541    /** @since 4.0 */
542    public Infrastructure getInfrastructure()
543    {
544        return _infrastructure;
545    }
546
547    public String getOutputEncoding()
548    {
549        return _infrastructure.getOutputEncoding();
550    }
551}