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 { 369 _infrastructure.getStaleLinkExceptionPresenter() 370 .presentStaleLinkException(cycle, exception); 371 } 372 373 /** 374 * Invoked by {@link #service(WebRequest, WebResponse)} if a {@link StaleSessionException} is 375 * thrown by the {@link IEngineService service}. This implementation uses the 376 * {@link org.apache.tapestry.error.StaleSessionExceptionPresenter} to render the StaleSession 377 * page. 378 * <p> 379 * Subclasses may overide this method (without invoking this implementation), but it is better 380 * to override the tapestry.error.StaleSessionExceptionReporter service instead (or contribute a 381 * replacement to the tapestry.InfrastructureOverrides configuration point). 382 * 383 * @since 0.2.10 384 */ 385 386 protected void handleStaleSessionException(IRequestCycle cycle, StaleSessionException exception) 387 { 388 _infrastructure.getStaleSessionExceptionPresenter().presentStaleSessionException( 389 cycle, 390 exception); 391 } 392 393 /** 394 * Changes the locale for the engine. 395 */ 396 397 public void setLocale(Locale value) 398 { 399 Defense.notNull(value, "locale"); 400 401 _locale = value; 402 403 // The locale may be set before the engine is initialized with the Infrastructure. 404 405 if (_infrastructure != null) 406 _infrastructure.setLocale(value); 407 } 408 409 /** 410 * @see Infrastructure#getClassResolver() 411 */ 412 413 public ClassResolver getClassResolver() 414 { 415 return _infrastructure.getClassResolver(); 416 } 417 418 /** 419 * Generates a description of the instance. Invokes {@link #extendDescription(ToStringBuilder)} 420 * to fill in details about the instance. 421 * 422 * @see #extendDescription(ToStringBuilder) 423 */ 424 425 public String toString() 426 { 427 ToStringBuilder builder = new ToStringBuilder(this); 428 429 builder.append("locale", _locale); 430 431 return builder.toString(); 432 } 433 434 /** 435 * Gets the visit object from the 436 * {@link org.apache.tapestry.engine.state.ApplicationStateManager}, creating it if it does not 437 * already exist. 438 * <p> 439 * As of Tapestry 4.0, this will always create the visit object, possibly creating a new session 440 * in the process. 441 */ 442 443 public Object getVisit() 444 { 445 return _infrastructure.getApplicationStateManager().get("visit"); 446 } 447 448 public void setVisit(Object visit) 449 { 450 _infrastructure.getApplicationStateManager().store("visit", visit); 451 } 452 453 /** 454 * Gets the visit object from the 455 * {@link org.apache.tapestry.engine.state.ApplicationStateManager}, which will create it as 456 * necessary. 457 */ 458 459 public Object getVisit(IRequestCycle cycle) 460 { 461 return getVisit(); 462 } 463 464 public boolean getHasVisit() 465 { 466 return _infrastructure.getApplicationStateManager().exists("visit"); 467 } 468 469 /** 470 * Returns the global object for the application. The global object is created at the start of 471 * the request ({@link #setupForRequest(RequestContext)}invokes 472 * {@link #createGlobal(RequestContext)}if needed), and is stored into the 473 * {@link ServletContext}. All instances of the engine for the application share the global 474 * object; however, the global object is explicitly <em>not</em> replicated to other servers 475 * within a cluster. 476 * 477 * @since 2.3 478 */ 479 480 public Object getGlobal() 481 { 482 return _infrastructure.getApplicationStateManager().get("global"); 483 } 484 485 public IScriptSource getScriptSource() 486 { 487 return _infrastructure.getScriptSource(); 488 } 489 490 /** 491 * Allows subclasses to include listener methods easily. 492 * 493 * @since 1.0.2 494 */ 495 496 public ListenerMap getListeners() 497 { 498 if (_listeners == null) 499 _listeners = _infrastructure.getListenerMapSource().getListenerMapForObject(this); 500 501 return _listeners; 502 } 503 504 /** 505 * Invoked when a {@link RedirectException} is thrown during the processing of a request. 506 * 507 * @throws ApplicationRuntimeException 508 * if an {@link IOException},{@link ServletException}is thrown by the redirect, 509 * or if no {@link RequestDispatcher}can be found for local resource. 510 * @since 2.2 511 */ 512 513 protected void handleRedirectException(IRequestCycle cycle, RedirectException redirectException) 514 { 515 String location = redirectException.getRedirectLocation(); 516 517 if (LOG.isDebugEnabled()) 518 LOG.debug("Redirecting to: " + location); 519 520 _infrastructure.getRequest().forward(location); 521 } 522 523 /** 524 * @see Infrastructure#getDataSqueezer() 525 */ 526 527 public DataSqueezer getDataSqueezer() 528 { 529 return _infrastructure.getDataSqueezer(); 530 } 531 532 /** @since 2.3 */ 533 534 public IPropertySource getPropertySource() 535 { 536 return _infrastructure.getApplicationPropertySource(); 537 } 538 539 /** @since 4.0 */ 540 public Infrastructure getInfrastructure() 541 { 542 return _infrastructure; 543 } 544 545 public String getOutputEncoding() 546 { 547 return _infrastructure.getOutputEncoding(); 548 } 549 }