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.util.HashMap; 018 import java.util.Iterator; 019 import java.util.Map; 020 021 import org.apache.commons.logging.Log; 022 import org.apache.commons.logging.LogFactory; 023 import org.apache.hivemind.ApplicationRuntimeException; 024 import org.apache.hivemind.ErrorLog; 025 import org.apache.hivemind.impl.ErrorLogImpl; 026 import org.apache.hivemind.util.Defense; 027 import org.apache.hivemind.util.ToStringBuilder; 028 import org.apache.tapestry.IComponent; 029 import org.apache.tapestry.IEngine; 030 import org.apache.tapestry.IForm; 031 import org.apache.tapestry.IMarkupWriter; 032 import org.apache.tapestry.IPage; 033 import org.apache.tapestry.IRequestCycle; 034 import org.apache.tapestry.RedirectException; 035 import org.apache.tapestry.RenderRewoundException; 036 import org.apache.tapestry.StaleLinkException; 037 import org.apache.tapestry.Tapestry; 038 import org.apache.tapestry.record.PageRecorderImpl; 039 import org.apache.tapestry.record.PropertyPersistenceStrategySource; 040 import org.apache.tapestry.request.RequestContext; 041 import org.apache.tapestry.services.AbsoluteURLBuilder; 042 import org.apache.tapestry.services.Infrastructure; 043 import org.apache.tapestry.util.IdAllocator; 044 import org.apache.tapestry.util.QueryParameterMap; 045 046 /** 047 * Provides the logic for processing a single request cycle. Provides access to the 048 * {@link IEngine engine} and the {@link RequestContext}. 049 * 050 * @author Howard Lewis Ship 051 */ 052 053 public class RequestCycle implements IRequestCycle 054 { 055 private static final Log LOG = LogFactory.getLog(RequestCycle.class); 056 057 private IPage _page; 058 059 private IEngine _engine; 060 061 private String _serviceName; 062 063 private IMonitor _monitor; 064 065 /** @since 4.0 */ 066 067 private PropertyPersistenceStrategySource _strategySource; 068 069 /** @since 4.0 */ 070 071 private IPageSource _pageSource; 072 073 /** @since 4.0 */ 074 075 private Infrastructure _infrastructure; 076 077 /** 078 * Contains parameters extracted from the request context, plus any decoded by any 079 * {@link ServiceEncoder}s. 080 * 081 * @since 4.0 082 */ 083 084 private QueryParameterMap _parameters; 085 086 /** @since 4.0 */ 087 088 private AbsoluteURLBuilder _absoluteURLBuilder; 089 090 /** 091 * A mapping of pages loaded during the current request cycle. Key is the page name, value is 092 * the {@link IPage}instance. 093 */ 094 095 private Map _loadedPages; 096 097 /** 098 * A mapping of page recorders for the current request cycle. Key is the page name, value is the 099 * {@link IPageRecorder}instance. 100 */ 101 102 private Map _pageRecorders; 103 104 private boolean _rewinding = false; 105 106 private Map _attributes = new HashMap(); 107 108 private int _actionId; 109 110 private int _targetActionId; 111 112 private IComponent _targetComponent; 113 114 /** @since 2.0.3 * */ 115 116 private Object[] _listenerParameters; 117 118 /** @since 4.0 */ 119 120 private ErrorLog _log; 121 122 private RequestContext _requestContext; 123 124 /** @since 4.0 */ 125 126 private IdAllocator _idAllocator = new IdAllocator(); 127 128 /** 129 * Standard constructor used to render a response page. 130 * 131 * @param engine 132 * the current request's engine 133 * @param parameters 134 * query parameters (possibly the result of {@link ServiceEncoder}s decoding path 135 * information) 136 * @param serviceName 137 * the name of engine service 138 * @param monitor 139 * informed of various events during the processing of the request 140 * @param environment 141 * additional invariant services and objects needed by each RequestCycle instance 142 */ 143 144 public RequestCycle(IEngine engine, QueryParameterMap parameters, String serviceName, 145 IMonitor monitor, RequestCycleEnvironment environment) 146 { 147 // Variant from instance to instance 148 149 _engine = engine; 150 _parameters = parameters; 151 _serviceName = serviceName; 152 _monitor = monitor; 153 154 // Invariant from instance to instance 155 156 _infrastructure = environment.getInfrastructure(); 157 _pageSource = _infrastructure.getPageSource(); 158 _strategySource = environment.getStrategySource(); 159 _absoluteURLBuilder = environment.getAbsoluteURLBuilder(); 160 _requestContext = environment.getRequestContext(); 161 _log = new ErrorLogImpl(environment.getErrorHandler(), LOG); 162 163 } 164 165 /** 166 * Alternate constructor used <strong>only for testing purposes</strong>. 167 * 168 * @since 4.0 169 */ 170 public RequestCycle() 171 { 172 } 173 174 /** 175 * Called at the end of the request cycle (i.e., after all responses have been sent back to the 176 * client), to release all pages loaded during the request cycle. 177 */ 178 179 public void cleanup() 180 { 181 if (_loadedPages == null) 182 return; 183 184 Iterator i = _loadedPages.values().iterator(); 185 186 while (i.hasNext()) 187 { 188 IPage page = (IPage) i.next(); 189 190 _pageSource.releasePage(page); 191 } 192 193 _loadedPages = null; 194 _pageRecorders = null; 195 196 } 197 198 public IEngineService getService() 199 { 200 return _infrastructure.getServiceMap().getService(_serviceName); 201 } 202 203 public String encodeURL(String URL) 204 { 205 return _infrastructure.getResponse().encodeURL(URL); 206 } 207 208 public IEngine getEngine() 209 { 210 return _engine; 211 } 212 213 public Object getAttribute(String name) 214 { 215 return _attributes.get(name); 216 } 217 218 public IMonitor getMonitor() 219 { 220 return _monitor; 221 } 222 223 public String getNextActionId() 224 { 225 return Integer.toHexString(++_actionId); 226 } 227 228 public IPage getPage() 229 { 230 return _page; 231 } 232 233 /** 234 * Gets the page from the engines's {@link IPageSource}. 235 */ 236 237 public IPage getPage(String name) 238 { 239 Defense.notNull(name, "name"); 240 241 IPage result = null; 242 243 if (_loadedPages != null) 244 result = (IPage) _loadedPages.get(name); 245 246 if (result == null) 247 { 248 result = loadPage(name); 249 250 if (_loadedPages == null) 251 _loadedPages = new HashMap(); 252 253 _loadedPages.put(name, result); 254 } 255 256 return result; 257 } 258 259 private IPage loadPage(String name) 260 { 261 try 262 { 263 _monitor.pageLoadBegin(name); 264 265 IPage result = _pageSource.getPage(this, name, _monitor); 266 267 // Get the recorder that will eventually observe and record 268 // changes to persistent properties of the page. 269 270 IPageRecorder recorder = getPageRecorder(name); 271 272 // Have it rollback the page to the prior state. Note that 273 // the page has a null observer at this time (which keeps 274 // these changes from being sent to the page recorder). 275 276 recorder.rollback(result); 277 278 // Now, have the page use the recorder for any future 279 // property changes. 280 281 result.setChangeObserver(recorder); 282 283 // Now that persistent properties have been restored, we can 284 // attach the page to this request. 285 286 result.attach(_engine, this); 287 288 return result; 289 } 290 finally 291 { 292 _monitor.pageLoadEnd(name); 293 } 294 295 } 296 297 /** 298 * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are 299 * shortlived objects managed exclusively by the request cycle. 300 */ 301 302 protected IPageRecorder getPageRecorder(String name) 303 { 304 if (_pageRecorders == null) 305 _pageRecorders = new HashMap(); 306 307 IPageRecorder result = (IPageRecorder) _pageRecorders.get(name); 308 309 if (result == null) 310 { 311 result = new PageRecorderImpl(name, this, _strategySource, _log); 312 _pageRecorders.put(name, result); 313 } 314 315 return result; 316 } 317 318 public boolean isRewinding() 319 { 320 return _rewinding; 321 } 322 323 public boolean isRewound(IComponent component) throws StaleLinkException 324 { 325 // If not rewinding ... 326 327 if (!_rewinding) 328 return false; 329 330 if (_actionId != _targetActionId) 331 return false; 332 333 // OK, we're there, is the page is good order? 334 335 if (component == _targetComponent) 336 return true; 337 338 // Woops. Mismatch. 339 340 throw new StaleLinkException(component, Integer.toHexString(_targetActionId), 341 _targetComponent.getExtendedId()); 342 } 343 344 public void removeAttribute(String name) 345 { 346 if (LOG.isDebugEnabled()) 347 LOG.debug("Removing attribute " + name); 348 349 _attributes.remove(name); 350 } 351 352 /** 353 * Renders the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. This 354 * clears all attributes. 355 */ 356 357 public void renderPage(IMarkupWriter writer) 358 { 359 String pageName = _page.getPageName(); 360 _monitor.pageRenderBegin(pageName); 361 362 _rewinding = false; 363 _actionId = -1; 364 _targetActionId = 0; 365 366 try 367 { 368 _page.renderPage(writer, this); 369 370 } 371 catch (ApplicationRuntimeException ex) 372 { 373 // Nothing much to add here. 374 375 throw ex; 376 } 377 catch (Throwable ex) 378 { 379 // But wrap other exceptions in a RequestCycleException ... this 380 // will ensure that some of the context is available. 381 382 throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex); 383 } 384 finally 385 { 386 reset(); 387 } 388 389 _monitor.pageRenderEnd(pageName); 390 391 } 392 393 /** 394 * Resets all internal state after a render or a rewind. 395 */ 396 397 private void reset() 398 { 399 _actionId = 0; 400 _targetActionId = 0; 401 _attributes.clear(); 402 _idAllocator.clear(); 403 } 404 405 /** 406 * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}. 407 * <p> 408 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 409 * renderred without this exception being thrown, it means that the target action id was not 410 * valid, and a {@link ApplicationRuntimeException} is thrown. 411 * <p> 412 * This clears all attributes. 413 * 414 * @since 1.0.2 415 */ 416 417 public void rewindForm(IForm form) 418 { 419 IPage page = form.getPage(); 420 String pageName = page.getPageName(); 421 422 _rewinding = true; 423 424 _monitor.pageRewindBegin(pageName); 425 426 // Fake things a little for getNextActionId() / isRewound() 427 // This used to be more involved (and include service parameters, and a parameter 428 // to this method), when the actionId was part of the Form name. That's not longer 429 // necessary (no service parameters), and we can fake things here easily enough with 430 // fixed actionId of 0. 431 432 _targetActionId = 0; 433 _actionId = -1; 434 435 _targetComponent = form; 436 437 try 438 { 439 page.beginPageRender(); 440 441 form.rewind(NullWriter.getSharedInstance(), this); 442 443 // Shouldn't get this far, because the form should 444 // throw the RenderRewoundException. 445 446 throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form 447 .getExtendedId()), form); 448 } 449 catch (RenderRewoundException ex) 450 { 451 // This is acceptible and expected. 452 } 453 catch (ApplicationRuntimeException ex) 454 { 455 // RequestCycleExceptions don't need to be wrapped. 456 throw ex; 457 } 458 catch (Throwable ex) 459 { 460 // But wrap other exceptions in a ApplicationRuntimeException ... this 461 // will ensure that some of the context is available. 462 463 throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex); 464 } 465 finally 466 { 467 page.endPageRender(); 468 469 _monitor.pageRewindEnd(pageName); 470 471 reset(); 472 _rewinding = false; 473 } 474 } 475 476 /** 477 * Rewinds the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. 478 * <p> 479 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 480 * renderred without this exception being thrown, it means that the target action id was not 481 * valid, and a {@link ApplicationRuntimeException}is thrown. 482 * <p> 483 * This clears all attributes. 484 */ 485 486 public void rewindPage(String targetActionId, IComponent targetComponent) 487 { 488 String pageName = _page.getPageName(); 489 490 _rewinding = true; 491 492 _monitor.pageRewindBegin(pageName); 493 494 _actionId = -1; 495 496 // Parse the action Id as hex since that's whats generated 497 // by getNextActionId() 498 _targetActionId = Integer.parseInt(targetActionId, 16); 499 _targetComponent = targetComponent; 500 501 try 502 { 503 _page.renderPage(NullWriter.getSharedInstance(), this); 504 505 // Shouldn't get this far, because the target component should 506 // throw the RenderRewoundException. 507 508 throw new StaleLinkException(_page, targetActionId, targetComponent.getExtendedId()); 509 } 510 catch (RenderRewoundException ex) 511 { 512 // This is acceptible and expected. 513 } 514 catch (ApplicationRuntimeException ex) 515 { 516 // ApplicationRuntimeExceptions don't need to be wrapped. 517 throw ex; 518 } 519 catch (Throwable ex) 520 { 521 // But wrap other exceptions in a RequestCycleException ... this 522 // will ensure that some of the context is available. 523 524 throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex); 525 } 526 finally 527 { 528 _monitor.pageRewindEnd(pageName); 529 530 _rewinding = false; 531 532 reset(); 533 } 534 535 } 536 537 public void setAttribute(String name, Object value) 538 { 539 if (LOG.isDebugEnabled()) 540 LOG.debug("Set attribute " + name + " to " + value); 541 542 _attributes.put(name, value); 543 } 544 545 /** 546 * Invokes {@link IPageRecorder#commit()}on each page recorder loaded during the request cycle 547 * (even recorders marked for discard). 548 */ 549 550 public void commitPageChanges() 551 { 552 if (LOG.isDebugEnabled()) 553 LOG.debug("Committing page changes"); 554 555 if (_pageRecorders == null || _pageRecorders.isEmpty()) 556 return; 557 558 Iterator i = _pageRecorders.values().iterator(); 559 560 while (i.hasNext()) 561 { 562 IPageRecorder recorder = (IPageRecorder) i.next(); 563 564 recorder.commit(); 565 } 566 } 567 568 /** 569 * As of 4.0, just a synonym for {@link #forgetPage(String)}. 570 * 571 * @since 2.0.2 572 */ 573 574 public void discardPage(String name) 575 { 576 forgetPage(name); 577 } 578 579 /** @since 2.0.3 * */ 580 581 public Object[] getServiceParameters() 582 { 583 return getListenerParameters(); 584 } 585 586 /** @since 2.0.3 * */ 587 588 public void setServiceParameters(Object[] serviceParameters) 589 { 590 setListenerParameters(serviceParameters); 591 } 592 593 /** @since 4.0 */ 594 public Object[] getListenerParameters() 595 { 596 return _listenerParameters; 597 } 598 599 /** @since 4.0 */ 600 public void setListenerParameters(Object[] parameters) 601 { 602 _listenerParameters = parameters; 603 } 604 605 /** @since 3.0 * */ 606 607 public void activate(String name) 608 { 609 IPage page = getPage(name); 610 611 activate(page); 612 } 613 614 /** @since 3.0 */ 615 616 public void activate(IPage page) 617 { 618 Defense.notNull(page, "page"); 619 620 if (LOG.isDebugEnabled()) 621 LOG.debug("Activating page " + page); 622 623 Tapestry.clearMethodInvocations(); 624 625 page.validate(this); 626 627 Tapestry 628 .checkMethodInvocation(Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID, "validate()", page); 629 630 _page = page; 631 } 632 633 /** @since 4.0 */ 634 public String getParameter(String name) 635 { 636 return _parameters.getParameterValue(name); 637 } 638 639 /** @since 4.0 */ 640 public String[] getParameters(String name) 641 { 642 return _parameters.getParameterValues(name); 643 } 644 645 /** 646 * @since 3.0 647 */ 648 public String toString() 649 { 650 ToStringBuilder b = new ToStringBuilder(this); 651 652 b.append("rewinding", _rewinding); 653 654 b.append("serviceName", _serviceName); 655 656 b.append("serviceParameters", _listenerParameters); 657 658 if (_loadedPages != null) 659 b.append("loadedPages", _loadedPages.keySet()); 660 661 b.append("attributes", _attributes); 662 b.append("targetActionId", _targetActionId); 663 b.append("targetComponent", _targetComponent); 664 665 return b.toString(); 666 } 667 668 /** @since 4.0 */ 669 670 public String getAbsoluteURL(String partialURL) 671 { 672 String contextPath = _infrastructure.getRequest().getContextPath(); 673 674 return _absoluteURLBuilder.constructURL(contextPath + partialURL); 675 } 676 677 /** @since 4.0 */ 678 679 public void forgetPage(String pageName) 680 { 681 Defense.notNull(pageName, "pageName"); 682 683 _strategySource.discardAllStoredChanged(pageName, this); 684 } 685 686 /** @since 4.0 */ 687 688 public Infrastructure getInfrastructure() 689 { 690 return _infrastructure; 691 } 692 693 public RequestContext getRequestContext() 694 { 695 return _requestContext; 696 } 697 698 /** @since 4.0 */ 699 700 public String getUniqueId(String baseId) 701 { 702 return _idAllocator.allocateId(baseId); 703 } 704 705 /** @since 4.0 */ 706 public void sendRedirect(String URL) 707 { 708 throw new RedirectException(URL); 709 } 710 711 }