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
015package org.apache.tapestry.engine;
016
017import java.util.HashMap;
018import java.util.Iterator;
019import java.util.Map;
020
021import org.apache.commons.logging.Log;
022import org.apache.commons.logging.LogFactory;
023import org.apache.hivemind.ApplicationRuntimeException;
024import org.apache.hivemind.ErrorLog;
025import org.apache.hivemind.impl.ErrorLogImpl;
026import org.apache.hivemind.util.Defense;
027import org.apache.hivemind.util.ToStringBuilder;
028import org.apache.tapestry.IComponent;
029import org.apache.tapestry.IEngine;
030import org.apache.tapestry.IForm;
031import org.apache.tapestry.IMarkupWriter;
032import org.apache.tapestry.IPage;
033import org.apache.tapestry.IRequestCycle;
034import org.apache.tapestry.RedirectException;
035import org.apache.tapestry.RenderRewoundException;
036import org.apache.tapestry.StaleLinkException;
037import org.apache.tapestry.Tapestry;
038import org.apache.tapestry.record.PageRecorderImpl;
039import org.apache.tapestry.record.PropertyPersistenceStrategySource;
040import org.apache.tapestry.request.RequestContext;
041import org.apache.tapestry.services.AbsoluteURLBuilder;
042import org.apache.tapestry.services.Infrastructure;
043import org.apache.tapestry.util.IdAllocator;
044import 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
053public 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}&nbsp;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}