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