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 * @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 }