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 015package org.apache.tapestry.engine; 016 017import java.util.HashMap; 018import java.util.Iterator; 019import java.util.Map; 020 021import org.apache.commons.logging.Log; 022import org.apache.commons.logging.LogFactory; 023import org.apache.hivemind.ApplicationRuntimeException; 024import org.apache.hivemind.ErrorLog; 025import org.apache.hivemind.impl.ErrorLogImpl; 026import org.apache.hivemind.util.Defense; 027import org.apache.hivemind.util.ToStringBuilder; 028import org.apache.tapestry.IComponent; 029import org.apache.tapestry.IEngine; 030import org.apache.tapestry.IForm; 031import org.apache.tapestry.IMarkupWriter; 032import org.apache.tapestry.IPage; 033import org.apache.tapestry.IRequestCycle; 034import org.apache.tapestry.RedirectException; 035import org.apache.tapestry.RenderRewoundException; 036import org.apache.tapestry.StaleLinkException; 037import org.apache.tapestry.Tapestry; 038import org.apache.tapestry.record.PageRecorderImpl; 039import org.apache.tapestry.record.PropertyPersistenceStrategySource; 040import org.apache.tapestry.request.RequestContext; 041import org.apache.tapestry.services.AbsoluteURLBuilder; 042import org.apache.tapestry.services.Infrastructure; 043import org.apache.tapestry.util.IdAllocator; 044import 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 053public 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 * @param context 143 * Part of (partial) compatibility with Tapestry 3.0 144 */ 145 146 public RequestCycle(IEngine engine, QueryParameterMap parameters, String serviceName, 147 IMonitor monitor, RequestCycleEnvironment environment, RequestContext context) 148 { 149 // Variant from instance to instance 150 151 _engine = engine; 152 _parameters = parameters; 153 _serviceName = serviceName; 154 _monitor = monitor; 155 156 // Invariant from instance to instance 157 158 _infrastructure = environment.getInfrastructure(); 159 _pageSource = _infrastructure.getPageSource(); 160 _strategySource = environment.getStrategySource(); 161 _absoluteURLBuilder = environment.getAbsoluteURLBuilder(); 162 _requestContext = context; 163 _log = new ErrorLogImpl(environment.getErrorHandler(), LOG); 164 165 } 166 167 /** 168 * Alternate constructor used <strong>only for testing purposes</strong>. 169 * 170 * @since 4.0 171 */ 172 public RequestCycle() 173 { 174 } 175 176 /** 177 * Called at the end of the request cycle (i.e., after all responses have been sent back to the 178 * client), to release all pages loaded during the request cycle. 179 */ 180 181 public void cleanup() 182 { 183 if (_loadedPages == null) 184 return; 185 186 Iterator i = _loadedPages.values().iterator(); 187 188 while (i.hasNext()) 189 { 190 IPage page = (IPage) i.next(); 191 192 _pageSource.releasePage(page); 193 } 194 195 _loadedPages = null; 196 _pageRecorders = null; 197 198 } 199 200 public IEngineService getService() 201 { 202 return _infrastructure.getServiceMap().getService(_serviceName); 203 } 204 205 public String encodeURL(String URL) 206 { 207 return _infrastructure.getResponse().encodeURL(URL); 208 } 209 210 public IEngine getEngine() 211 { 212 return _engine; 213 } 214 215 public Object getAttribute(String name) 216 { 217 return _attributes.get(name); 218 } 219 220 public IMonitor getMonitor() 221 { 222 return _monitor; 223 } 224 225 /** @deprecated */ 226 public String getNextActionId() 227 { 228 return Integer.toHexString(++_actionId); 229 } 230 231 public IPage getPage() 232 { 233 return _page; 234 } 235 236 /** 237 * Gets the page from the engines's {@link IPageSource}. 238 */ 239 240 public IPage getPage(String name) 241 { 242 Defense.notNull(name, "name"); 243 244 IPage result = null; 245 246 if (_loadedPages != null) 247 result = (IPage) _loadedPages.get(name); 248 249 if (result == null) 250 { 251 result = loadPage(name); 252 253 if (_loadedPages == null) 254 _loadedPages = new HashMap(); 255 256 _loadedPages.put(name, result); 257 } 258 259 return result; 260 } 261 262 private IPage loadPage(String name) 263 { 264 try 265 { 266 _monitor.pageLoadBegin(name); 267 268 IPage result = _pageSource.getPage(this, name, _monitor); 269 270 // Get the recorder that will eventually observe and record 271 // changes to persistent properties of the page. 272 273 IPageRecorder recorder = getPageRecorder(name); 274 275 // Have it rollback the page to the prior state. Note that 276 // the page has a null observer at this time (which keeps 277 // these changes from being sent to the page recorder). 278 279 recorder.rollback(result); 280 281 // Now, have the page use the recorder for any future 282 // property changes. 283 284 result.setChangeObserver(recorder); 285 286 return result; 287 } 288 finally 289 { 290 _monitor.pageLoadEnd(name); 291 } 292 293 } 294 295 /** 296 * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are 297 * shortlived objects managed exclusively by the request cycle. 298 */ 299 300 protected IPageRecorder getPageRecorder(String name) 301 { 302 if (_pageRecorders == null) 303 _pageRecorders = new HashMap(); 304 305 IPageRecorder result = (IPageRecorder) _pageRecorders.get(name); 306 307 if (result == null) 308 { 309 result = new PageRecorderImpl(name, this, _strategySource, _log); 310 _pageRecorders.put(name, result); 311 } 312 313 return result; 314 } 315 316 public boolean isRewinding() 317 { 318 return _rewinding; 319 } 320 321 public boolean isRewound(IComponent component) throws StaleLinkException 322 { 323 // If not rewinding ... 324 325 if (!_rewinding) 326 return false; 327 328 if (_actionId != _targetActionId) 329 return false; 330 331 // OK, we're there, is the page is good order? 332 333 if (component == _targetComponent) 334 return true; 335 336 // Woops. Mismatch. 337 338 throw new StaleLinkException(component, Integer.toHexString(_targetActionId), 339 _targetComponent.getExtendedId()); 340 } 341 342 public void removeAttribute(String name) 343 { 344 if (LOG.isDebugEnabled()) 345 LOG.debug("Removing attribute " + name); 346 347 _attributes.remove(name); 348 } 349 350 /** 351 * Renders the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. This 352 * clears all attributes. 353 */ 354 355 public void renderPage(IMarkupWriter writer) 356 { 357 String pageName = _page.getPageName(); 358 _monitor.pageRenderBegin(pageName); 359 360 _rewinding = false; 361 _actionId = -1; 362 _targetActionId = 0; 363 364 try 365 { 366 _page.renderPage(writer, this); 367 368 } 369 catch (ApplicationRuntimeException ex) 370 { 371 // Nothing much to add here. 372 373 throw ex; 374 } 375 catch (Throwable ex) 376 { 377 // But wrap other exceptions in a RequestCycleException ... this 378 // will ensure that some of the context is available. 379 380 throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex); 381 } 382 finally 383 { 384 reset(); 385 } 386 387 _monitor.pageRenderEnd(pageName); 388 389 } 390 391 /** 392 * Resets all internal state after a render or a rewind. 393 */ 394 395 private void reset() 396 { 397 _actionId = 0; 398 _targetActionId = 0; 399 _attributes.clear(); 400 _idAllocator.clear(); 401 } 402 403 /** 404 * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}. 405 * <p> 406 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 407 * renderred without this exception being thrown, it means that the target action id was not 408 * valid, and a {@link ApplicationRuntimeException} is thrown. 409 * <p> 410 * This clears all attributes. 411 * 412 * @since 1.0.2 413 */ 414 415 public void rewindForm(IForm form) 416 { 417 IPage page = form.getPage(); 418 String pageName = page.getPageName(); 419 420 _rewinding = true; 421 422 _monitor.pageRewindBegin(pageName); 423 424 // Fake things a little for getNextActionId() / isRewound() 425 // This used to be more involved (and include service parameters, and a parameter 426 // to this method), when the actionId was part of the Form name. That's not longer 427 // necessary (no service parameters), and we can fake things here easily enough with 428 // fixed actionId of 0. 429 430 _targetActionId = 0; 431 _actionId = -1; 432 433 _targetComponent = form; 434 435 try 436 { 437 page.beginPageRender(); 438 439 form.rewind(NullWriter.getSharedInstance(), this); 440 441 // Shouldn't get this far, because the form should 442 // throw the RenderRewoundException. 443 444 throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form 445 .getExtendedId()), form); 446 } 447 catch (RenderRewoundException ex) 448 { 449 // This is acceptible and expected. 450 } 451 catch (ApplicationRuntimeException ex) 452 { 453 // RequestCycleExceptions don't need to be wrapped. 454 throw ex; 455 } 456 catch (Throwable ex) 457 { 458 // But wrap other exceptions in a ApplicationRuntimeException ... this 459 // will ensure that some of the context is available. 460 461 throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex); 462 } 463 finally 464 { 465 page.endPageRender(); 466 467 _monitor.pageRewindEnd(pageName); 468 469 reset(); 470 _rewinding = false; 471 } 472 } 473 474 /** 475 * Rewinds the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. 476 * <p> 477 * The process is expected to end with a {@link RenderRewoundException}. If the entire page is 478 * renderred without this exception being thrown, it means that the target action id was not 479 * valid, and a {@link ApplicationRuntimeException}is thrown. 480 * <p> 481 * This clears all attributes. 482 * 483 * @deprecated To be removed in 4.1 with no replacement. 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); 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}