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 }