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 /** @deprecated */ 224 public String getNextActionId() 225 { 226 return Integer.toHexString(++_actionId); 227 } 228 229 public IPage getPage() 230 { 231 return _page; 232 } 233 234 /** 235 * Gets the page from the engines's {@link IPageSource}. 236 */ 237 238 public IPage getPage(String name) 239 { 240 Defense.notNull(name, "name"); 241 242 IPage result = null; 243 244 if (_loadedPages != null) 245 result = (IPage) _loadedPages.get(name); 246 247 if (result == null) 248 { 249 result = loadPage(name); 250 251 if (_loadedPages == null) 252 _loadedPages = new HashMap(); 253 254 _loadedPages.put(name, result); 255 } 256 257 return result; 258 } 259 260 private IPage loadPage(String name) 261 { 262 try 263 { 264 _monitor.pageLoadBegin(name); 265 266 IPage result = _pageSource.getPage(this, name, _monitor); 267 268 // Get the recorder that will eventually observe and record 269 // changes to persistent properties of the page. 270 271 IPageRecorder recorder = getPageRecorder(name); 272 273 // Have it rollback the page to the prior state. Note that 274 // the page has a null observer at this time (which keeps 275 // these changes from being sent to the page recorder). 276 277 recorder.rollback(result); 278 279 // Now, have the page use the recorder for any future 280 // property changes. 281 282 result.setChangeObserver(recorder); 283 284 // Now that persistent properties have been restored, we can 285 // attach the page to this request. 286 287 result.attach(_engine, this); 288 289 return result; 290 } 291 finally 292 { 293 _monitor.pageLoadEnd(name); 294 } 295 296 } 297 298 /** 299 * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are 300 * shortlived objects managed exclusively by the request cycle. 301 */ 302 303 protected IPageRecorder getPageRecorder(String name) 304 { 305 if (_pageRecorders == null) 306 _pageRecorders = new HashMap(); 307 308 IPageRecorder result = (IPageRecorder) _pageRecorders.get(name); 309 310 if (result == null) 311 { 312 result = new PageRecorderImpl(name, this, _strategySource, _log); 313 _pageRecorders.put(name, result); 314 } 315 316 return result; 317 } 318 319 public boolean isRewinding() 320 { 321 return _rewinding; 322 } 323 324 public boolean isRewound(IComponent component) throws StaleLinkException 325 { 326 // If not rewinding ... 327 328 if (!_rewinding) 329 return false; 330 331 if (_actionId != _targetActionId) 332 return false; 333 334 // OK, we're there, is the page is good order? 335 336 if (component == _targetComponent) 337 return true; 338 339 // Woops. Mismatch. 340 341 throw new StaleLinkException(component, Integer.toHexString(_targetActionId), 342 _targetComponent.getExtendedId()); 343 } 344 345 public void removeAttribute(String name) 346 { 347 if (LOG.isDebugEnabled()) 348 LOG.debug("Removing attribute " + name); 349 350 _attributes.remove(name); 351 } 352 353 /** 354 * Renders the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. This 355 * clears all attributes. 356 */ 357 358 public void renderPage(IMarkupWriter writer) 359 { 360 String pageName = _page.getPageName(); 361 _monitor.pageRenderBegin(pageName); 362 363 _rewinding = false; 364 _actionId = -1; 365 _targetActionId = 0; 366 367 try 368 { 369 _page.renderPage(writer, this); 370 371 } 372 catch (ApplicationRuntimeException ex) 373 { 374 // Nothing much to add here. 375 376 throw ex; 377 } 378 catch (Throwable ex) 379 { 380 // But wrap other exceptions in a RequestCycleException ... this 381 // will ensure that some of the context is available. 382 383 throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex); 384 } 385 finally 386 { 387 reset(); 388 } 389 390 _monitor.pageRenderEnd(pageName); 391 392 } 393 394 /** 395 * Resets all internal state after a render or a rewind. 396 */ 397 398 private void reset() 399 { 400 _actionId = 0; 401 _targetActionId = 0; 402 _attributes.clear(); 403 _idAllocator.clear(); 404 } 405 406 /** 407 * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}. 408 * <p> 409 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 410 * renderred without this exception being thrown, it means that the target action id was not 411 * valid, and a {@link ApplicationRuntimeException} is thrown. 412 * <p> 413 * This clears all attributes. 414 * 415 * @since 1.0.2 416 */ 417 418 public void rewindForm(IForm form) 419 { 420 IPage page = form.getPage(); 421 String pageName = page.getPageName(); 422 423 _rewinding = true; 424 425 _monitor.pageRewindBegin(pageName); 426 427 // Fake things a little for getNextActionId() / isRewound() 428 // This used to be more involved (and include service parameters, and a parameter 429 // to this method), when the actionId was part of the Form name. That's not longer 430 // necessary (no service parameters), and we can fake things here easily enough with 431 // fixed actionId of 0. 432 433 _targetActionId = 0; 434 _actionId = -1; 435 436 _targetComponent = form; 437 438 try 439 { 440 page.beginPageRender(); 441 442 form.rewind(NullWriter.getSharedInstance(), this); 443 444 // Shouldn't get this far, because the form should 445 // throw the RenderRewoundException. 446 447 throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form 448 .getExtendedId()), form); 449 } 450 catch (RenderRewoundException ex) 451 { 452 // This is acceptible and expected. 453 } 454 catch (ApplicationRuntimeException ex) 455 { 456 // RequestCycleExceptions don't need to be wrapped. 457 throw ex; 458 } 459 catch (Throwable ex) 460 { 461 // But wrap other exceptions in a ApplicationRuntimeException ... this 462 // will ensure that some of the context is available. 463 464 throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex); 465 } 466 finally 467 { 468 page.endPageRender(); 469 470 _monitor.pageRewindEnd(pageName); 471 472 reset(); 473 _rewinding = false; 474 } 475 } 476 477 /** 478 * Rewinds the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. 479 * <p> 480 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 481 * renderred without this exception being thrown, it means that the target action id was not 482 * valid, and a {@link ApplicationRuntimeException}is thrown. 483 * <p> 484 * This clears all attributes. 485 * 486 * @deprecated To be removed in 4.1 with no replacement. 487 */ 488 489 public void rewindPage(String targetActionId, IComponent targetComponent) 490 { 491 String pageName = _page.getPageName(); 492 493 _rewinding = true; 494 495 _monitor.pageRewindBegin(pageName); 496 497 _actionId = -1; 498 499 // Parse the action Id as hex since that's whats generated 500 // by getNextActionId() 501 _targetActionId = Integer.parseInt(targetActionId, 16); 502 _targetComponent = targetComponent; 503 504 try 505 { 506 _page.renderPage(NullWriter.getSharedInstance(), this); 507 508 // Shouldn't get this far, because the target component should 509 // throw the RenderRewoundException. 510 511 throw new StaleLinkException(_page, targetActionId, targetComponent.getExtendedId()); 512 } 513 catch (RenderRewoundException ex) 514 { 515 // This is acceptible and expected. 516 } 517 catch (ApplicationRuntimeException ex) 518 { 519 // ApplicationRuntimeExceptions don't need to be wrapped. 520 throw ex; 521 } 522 catch (Throwable ex) 523 { 524 // But wrap other exceptions in a RequestCycleException ... this 525 // will ensure that some of the context is available. 526 527 throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex); 528 } 529 finally 530 { 531 _monitor.pageRewindEnd(pageName); 532 533 _rewinding = false; 534 535 reset(); 536 } 537 538 } 539 540 public void setAttribute(String name, Object value) 541 { 542 if (LOG.isDebugEnabled()) 543 LOG.debug("Set attribute " + name + " to " + value); 544 545 _attributes.put(name, value); 546 } 547 548 /** 549 * Invokes {@link IPageRecorder#commit()}on each page recorder loaded during the request cycle 550 * (even recorders marked for discard). 551 */ 552 553 public void commitPageChanges() 554 { 555 if (LOG.isDebugEnabled()) 556 LOG.debug("Committing page changes"); 557 558 if (_pageRecorders == null || _pageRecorders.isEmpty()) 559 return; 560 561 Iterator i = _pageRecorders.values().iterator(); 562 563 while (i.hasNext()) 564 { 565 IPageRecorder recorder = (IPageRecorder) i.next(); 566 567 recorder.commit(); 568 } 569 } 570 571 /** 572 * As of 4.0, just a synonym for {@link #forgetPage(String)}. 573 * 574 * @since 2.0.2 575 */ 576 577 public void discardPage(String name) 578 { 579 forgetPage(name); 580 } 581 582 /** @since 2.0.3 * */ 583 584 public Object[] getServiceParameters() 585 { 586 return getListenerParameters(); 587 } 588 589 /** @since 2.0.3 * */ 590 591 public void setServiceParameters(Object[] serviceParameters) 592 { 593 setListenerParameters(serviceParameters); 594 } 595 596 /** @since 4.0 */ 597 public Object[] getListenerParameters() 598 { 599 return _listenerParameters; 600 } 601 602 /** @since 4.0 */ 603 public void setListenerParameters(Object[] parameters) 604 { 605 _listenerParameters = parameters; 606 } 607 608 /** @since 3.0 * */ 609 610 public void activate(String name) 611 { 612 IPage page = getPage(name); 613 614 activate(page); 615 } 616 617 /** @since 3.0 */ 618 619 public void activate(IPage page) 620 { 621 Defense.notNull(page, "page"); 622 623 if (LOG.isDebugEnabled()) 624 LOG.debug("Activating page " + page); 625 626 Tapestry.clearMethodInvocations(); 627 628 page.validate(this); 629 630 Tapestry 631 .checkMethodInvocation(Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID, "validate()", page); 632 633 _page = page; 634 } 635 636 /** @since 4.0 */ 637 public String getParameter(String name) 638 { 639 return _parameters.getParameterValue(name); 640 } 641 642 /** @since 4.0 */ 643 public String[] getParameters(String name) 644 { 645 return _parameters.getParameterValues(name); 646 } 647 648 /** 649 * @since 3.0 650 */ 651 public String toString() 652 { 653 ToStringBuilder b = new ToStringBuilder(this); 654 655 b.append("rewinding", _rewinding); 656 657 b.append("serviceName", _serviceName); 658 659 b.append("serviceParameters", _listenerParameters); 660 661 if (_loadedPages != null) 662 b.append("loadedPages", _loadedPages.keySet()); 663 664 b.append("attributes", _attributes); 665 b.append("targetActionId", _targetActionId); 666 b.append("targetComponent", _targetComponent); 667 668 return b.toString(); 669 } 670 671 /** @since 4.0 */ 672 673 public String getAbsoluteURL(String partialURL) 674 { 675 String contextPath = _infrastructure.getRequest().getContextPath(); 676 677 return _absoluteURLBuilder.constructURL(contextPath + partialURL); 678 } 679 680 /** @since 4.0 */ 681 682 public void forgetPage(String pageName) 683 { 684 Defense.notNull(pageName, "pageName"); 685 686 _strategySource.discardAllStoredChanged(pageName, this); 687 } 688 689 /** @since 4.0 */ 690 691 public Infrastructure getInfrastructure() 692 { 693 return _infrastructure; 694 } 695 696 public RequestContext getRequestContext() 697 { 698 return _requestContext; 699 } 700 701 /** @since 4.0 */ 702 703 public String getUniqueId(String baseId) 704 { 705 return _idAllocator.allocateId(baseId); 706 } 707 708 /** @since 4.0 */ 709 public void sendRedirect(String URL) 710 { 711 throw new RedirectException(URL); 712 } 713 714 }