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 }