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         */
143    
144        public RequestCycle(IEngine engine, QueryParameterMap parameters, String serviceName,
145                IMonitor monitor, RequestCycleEnvironment environment)
146        {
147            // Variant from instance to instance
148    
149            _engine = engine;
150            _parameters = parameters;
151            _serviceName = serviceName;
152            _monitor = monitor;
153    
154            // Invariant from instance to instance
155    
156            _infrastructure = environment.getInfrastructure();
157            _pageSource = _infrastructure.getPageSource();
158            _strategySource = environment.getStrategySource();
159            _absoluteURLBuilder = environment.getAbsoluteURLBuilder();
160            _requestContext = environment.getRequestContext();
161            _log = new ErrorLogImpl(environment.getErrorHandler(), LOG);
162    
163        }
164    
165        /**
166         * Alternate constructor used <strong>only for testing purposes</strong>.
167         * 
168         * @since 4.0
169         */
170        public RequestCycle()
171        {
172        }
173    
174        /**
175         * Called at the end of the request cycle (i.e., after all responses have been sent back to the
176         * client), to release all pages loaded during the request cycle.
177         */
178    
179        public void cleanup()
180        {
181            if (_loadedPages == null)
182                return;
183    
184            Iterator i = _loadedPages.values().iterator();
185    
186            while (i.hasNext())
187            {
188                IPage page = (IPage) i.next();
189    
190                _pageSource.releasePage(page);
191            }
192    
193            _loadedPages = null;
194            _pageRecorders = null;
195    
196        }
197    
198        public IEngineService getService()
199        {
200            return _infrastructure.getServiceMap().getService(_serviceName);
201        }
202    
203        public String encodeURL(String URL)
204        {
205            return _infrastructure.getResponse().encodeURL(URL);
206        }
207    
208        public IEngine getEngine()
209        {
210            return _engine;
211        }
212    
213        public Object getAttribute(String name)
214        {
215            return _attributes.get(name);
216        }
217    
218        public IMonitor getMonitor()
219        {
220            return _monitor;
221        }
222    
223        /** @deprecated */
224        public String getNextActionId()
225        {
226            return Integer.toHexString(++_actionId);
227        }
228    
229        public IPage getPage()
230        {
231            return _page;
232        }
233    
234        /**
235         * Gets the page from the engines's {@link IPageSource}.
236         */
237    
238        public IPage getPage(String name)
239        {
240            Defense.notNull(name, "name");
241    
242            IPage result = null;
243    
244            if (_loadedPages != null)
245                result = (IPage) _loadedPages.get(name);
246    
247            if (result == null)
248            {
249                result = loadPage(name);
250    
251                if (_loadedPages == null)
252                    _loadedPages = new HashMap();
253    
254                _loadedPages.put(name, result);
255            }
256    
257            return result;
258        }
259    
260        private IPage loadPage(String name)
261        {
262            try
263            {
264                _monitor.pageLoadBegin(name);
265    
266                IPage result = _pageSource.getPage(this, name, _monitor);
267    
268                // Get the recorder that will eventually observe and record
269                // changes to persistent properties of the page.
270    
271                IPageRecorder recorder = getPageRecorder(name);
272    
273                // Have it rollback the page to the prior state. Note that
274                // the page has a null observer at this time (which keeps
275                // these changes from being sent to the page recorder).
276    
277                recorder.rollback(result);
278    
279                // Now, have the page use the recorder for any future
280                // property changes.
281    
282                result.setChangeObserver(recorder);
283    
284                // Now that persistent properties have been restored, we can
285                // attach the page to this request.
286    
287                result.attach(_engine, this);
288    
289                return result;
290            }
291            finally
292            {
293                _monitor.pageLoadEnd(name);
294            }
295    
296        }
297    
298        /**
299         * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are
300         * shortlived objects managed exclusively by the request cycle.
301         */
302    
303        protected IPageRecorder getPageRecorder(String name)
304        {
305            if (_pageRecorders == null)
306                _pageRecorders = new HashMap();
307    
308            IPageRecorder result = (IPageRecorder) _pageRecorders.get(name);
309    
310            if (result == null)
311            {
312                result = new PageRecorderImpl(name, this, _strategySource, _log);
313                _pageRecorders.put(name, result);
314            }
315    
316            return result;
317        }
318    
319        public boolean isRewinding()
320        {
321            return _rewinding;
322        }
323    
324        public boolean isRewound(IComponent component) throws StaleLinkException
325        {
326            // If not rewinding ...
327    
328            if (!_rewinding)
329                return false;
330    
331            if (_actionId != _targetActionId)
332                return false;
333    
334            // OK, we're there, is the page is good order?
335    
336            if (component == _targetComponent)
337                return true;
338    
339            // Woops. Mismatch.
340    
341            throw new StaleLinkException(component, Integer.toHexString(_targetActionId),
342                    _targetComponent.getExtendedId());
343        }
344    
345        public void removeAttribute(String name)
346        {
347            if (LOG.isDebugEnabled())
348                LOG.debug("Removing attribute " + name);
349    
350            _attributes.remove(name);
351        }
352    
353        /**
354         * Renders the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. This
355         * clears all attributes.
356         */
357    
358        public void renderPage(IMarkupWriter writer)
359        {
360            String pageName = _page.getPageName();
361            _monitor.pageRenderBegin(pageName);
362    
363            _rewinding = false;
364            _actionId = -1;
365            _targetActionId = 0;
366    
367            try
368            {
369                _page.renderPage(writer, this);
370    
371            }
372            catch (ApplicationRuntimeException ex)
373            {
374                // Nothing much to add here.
375    
376                throw ex;
377            }
378            catch (Throwable ex)
379            {
380                // But wrap other exceptions in a RequestCycleException ... this
381                // will ensure that some of the context is available.
382    
383                throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
384            }
385            finally
386            {
387                reset();
388            }
389    
390            _monitor.pageRenderEnd(pageName);
391    
392        }
393    
394        /**
395         * Resets all internal state after a render or a rewind.
396         */
397    
398        private void reset()
399        {
400            _actionId = 0;
401            _targetActionId = 0;
402            _attributes.clear();
403            _idAllocator.clear();
404        }
405    
406        /**
407         * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}.
408         * <p>
409         * The process is expected to end with a {@link RenderRewoundException}. If the entire page is
410         * renderred without this exception being thrown, it means that the target action id was not
411         * valid, and a {@link ApplicationRuntimeException}&nbsp;is thrown.
412         * <p>
413         * This clears all attributes.
414         * 
415         * @since 1.0.2
416         */
417    
418        public void rewindForm(IForm form)
419        {
420            IPage page = form.getPage();
421            String pageName = page.getPageName();
422    
423            _rewinding = true;
424    
425            _monitor.pageRewindBegin(pageName);
426    
427            // Fake things a little for getNextActionId() / isRewound()
428            // This used to be more involved (and include service parameters, and a parameter
429            // to this method), when the actionId was part of the Form name. That's not longer
430            // necessary (no service parameters), and we can fake things here easily enough with
431            // fixed actionId of 0.
432    
433            _targetActionId = 0;
434            _actionId = -1;
435    
436            _targetComponent = form;
437    
438            try
439            {
440                page.beginPageRender();
441    
442                form.rewind(NullWriter.getSharedInstance(), this);
443    
444                // Shouldn't get this far, because the form should
445                // throw the RenderRewoundException.
446    
447                throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form
448                        .getExtendedId()), form);
449            }
450            catch (RenderRewoundException ex)
451            {
452                // This is acceptible and expected.
453            }
454            catch (ApplicationRuntimeException ex)
455            {
456                // RequestCycleExceptions don't need to be wrapped.
457                throw ex;
458            }
459            catch (Throwable ex)
460            {
461                // But wrap other exceptions in a ApplicationRuntimeException ... this
462                // will ensure that some of the context is available.
463    
464                throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex);
465            }
466            finally
467            {
468                page.endPageRender();
469    
470                _monitor.pageRewindEnd(pageName);
471    
472                reset();
473                _rewinding = false;
474            }
475        }
476    
477        /**
478         * Rewinds the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.
479         * <p>
480         * The process is expected to end with a {@link RenderRewoundException}. If the entire page is
481         * renderred without this exception being thrown, it means that the target action id was not
482         * valid, and a {@link ApplicationRuntimeException}is thrown.
483         * <p>
484         * This clears all attributes.
485         * 
486         * @deprecated To be removed in 4.1 with no replacement.
487         */
488    
489        public void rewindPage(String targetActionId, IComponent targetComponent)
490        {
491            String pageName = _page.getPageName();
492    
493            _rewinding = true;
494    
495            _monitor.pageRewindBegin(pageName);
496    
497            _actionId = -1;
498    
499            // Parse the action Id as hex since that's whats generated
500            // by getNextActionId()
501            _targetActionId = Integer.parseInt(targetActionId, 16);
502            _targetComponent = targetComponent;
503    
504            try
505            {
506                _page.renderPage(NullWriter.getSharedInstance(), this);
507    
508                // Shouldn't get this far, because the target component should
509                // throw the RenderRewoundException.
510    
511                throw new StaleLinkException(_page, targetActionId, targetComponent.getExtendedId());
512            }
513            catch (RenderRewoundException ex)
514            {
515                // This is acceptible and expected.
516            }
517            catch (ApplicationRuntimeException ex)
518            {
519                // ApplicationRuntimeExceptions don't need to be wrapped.
520                throw ex;
521            }
522            catch (Throwable ex)
523            {
524                // But wrap other exceptions in a RequestCycleException ... this
525                // will ensure that some of the context is available.
526    
527                throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
528            }
529            finally
530            {
531                _monitor.pageRewindEnd(pageName);
532    
533                _rewinding = false;
534    
535                reset();
536            }
537    
538        }
539    
540        public void setAttribute(String name, Object value)
541        {
542            if (LOG.isDebugEnabled())
543                LOG.debug("Set attribute " + name + " to " + value);
544    
545            _attributes.put(name, value);
546        }
547    
548        /**
549         * Invokes {@link IPageRecorder#commit()}on each page recorder loaded during the request cycle
550         * (even recorders marked for discard).
551         */
552    
553        public void commitPageChanges()
554        {
555            if (LOG.isDebugEnabled())
556                LOG.debug("Committing page changes");
557    
558            if (_pageRecorders == null || _pageRecorders.isEmpty())
559                return;
560    
561            Iterator i = _pageRecorders.values().iterator();
562    
563            while (i.hasNext())
564            {
565                IPageRecorder recorder = (IPageRecorder) i.next();
566    
567                recorder.commit();
568            }
569        }
570    
571        /**
572         * As of 4.0, just a synonym for {@link #forgetPage(String)}.
573         * 
574         * @since 2.0.2
575         */
576    
577        public void discardPage(String name)
578        {
579            forgetPage(name);
580        }
581    
582        /** @since 2.0.3 * */
583    
584        public Object[] getServiceParameters()
585        {
586            return getListenerParameters();
587        }
588    
589        /** @since 2.0.3 * */
590    
591        public void setServiceParameters(Object[] serviceParameters)
592        {
593            setListenerParameters(serviceParameters);
594        }
595    
596        /** @since 4.0 */
597        public Object[] getListenerParameters()
598        {
599            return _listenerParameters;
600        }
601    
602        /** @since 4.0 */
603        public void setListenerParameters(Object[] parameters)
604        {
605            _listenerParameters = parameters;
606        }
607    
608        /** @since 3.0 * */
609    
610        public void activate(String name)
611        {
612            IPage page = getPage(name);
613    
614            activate(page);
615        }
616    
617        /** @since 3.0 */
618    
619        public void activate(IPage page)
620        {
621            Defense.notNull(page, "page");
622    
623            if (LOG.isDebugEnabled())
624                LOG.debug("Activating page " + page);
625    
626            Tapestry.clearMethodInvocations();
627    
628            page.validate(this);
629    
630            Tapestry
631                    .checkMethodInvocation(Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID, "validate()", page);
632    
633            _page = page;
634        }
635    
636        /** @since 4.0 */
637        public String getParameter(String name)
638        {
639            return _parameters.getParameterValue(name);
640        }
641    
642        /** @since 4.0 */
643        public String[] getParameters(String name)
644        {
645            return _parameters.getParameterValues(name);
646        }
647    
648        /**
649         * @since 3.0
650         */
651        public String toString()
652        {
653            ToStringBuilder b = new ToStringBuilder(this);
654    
655            b.append("rewinding", _rewinding);
656    
657            b.append("serviceName", _serviceName);
658    
659            b.append("serviceParameters", _listenerParameters);
660    
661            if (_loadedPages != null)
662                b.append("loadedPages", _loadedPages.keySet());
663    
664            b.append("attributes", _attributes);
665            b.append("targetActionId", _targetActionId);
666            b.append("targetComponent", _targetComponent);
667    
668            return b.toString();
669        }
670    
671        /** @since 4.0 */
672    
673        public String getAbsoluteURL(String partialURL)
674        {
675            String contextPath = _infrastructure.getRequest().getContextPath();
676    
677            return _absoluteURLBuilder.constructURL(contextPath + partialURL);
678        }
679    
680        /** @since 4.0 */
681    
682        public void forgetPage(String pageName)
683        {
684            Defense.notNull(pageName, "pageName");
685    
686            _strategySource.discardAllStoredChanged(pageName, this);
687        }
688    
689        /** @since 4.0 */
690    
691        public Infrastructure getInfrastructure()
692        {
693            return _infrastructure;
694        }
695    
696        public RequestContext getRequestContext()
697        {
698            return _requestContext;
699        }
700    
701        /** @since 4.0 */
702    
703        public String getUniqueId(String baseId)
704        {
705            return _idAllocator.allocateId(baseId);
706        }
707    
708        /** @since 4.0 */
709        public void sendRedirect(String URL)
710        {
711            throw new RedirectException(URL);
712        }
713    
714    }