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