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    
015    package org.apache.tapestry.engine;
016    
017    import java.io.IOException;
018    import java.util.ArrayList;
019    import java.util.List;
020    import java.util.Locale;
021    
022    import org.apache.commons.logging.Log;
023    import org.apache.commons.logging.LogFactory;
024    import org.apache.hivemind.ApplicationRuntimeException;
025    import org.apache.hivemind.ClassResolver;
026    import org.apache.hivemind.util.Defense;
027    import org.apache.hivemind.util.ToStringBuilder;
028    import org.apache.tapestry.Constants;
029    import org.apache.tapestry.IEngine;
030    import org.apache.tapestry.IPage;
031    import org.apache.tapestry.IRequestCycle;
032    import org.apache.tapestry.PageRedirectException;
033    import org.apache.tapestry.RedirectException;
034    import org.apache.tapestry.StaleLinkException;
035    import org.apache.tapestry.StaleSessionException;
036    import org.apache.tapestry.listener.ListenerMap;
037    import org.apache.tapestry.services.DataSqueezer;
038    import org.apache.tapestry.services.Infrastructure;
039    import org.apache.tapestry.spec.IApplicationSpecification;
040    import org.apache.tapestry.web.WebRequest;
041    import 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    
089    public 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    }