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