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        public String getNextActionId()
224        {
225            return Integer.toHexString(++_actionId);
226        }
227    
228        public IPage getPage()
229        {
230            return _page;
231        }
232    
233        /**
234         * Gets the page from the engines's {@link IPageSource}.
235         */
236    
237        public IPage getPage(String name)
238        {
239            Defense.notNull(name, "name");
240    
241            IPage result = null;
242    
243            if (_loadedPages != null)
244                result = (IPage) _loadedPages.get(name);
245    
246            if (result == null)
247            {
248                result = loadPage(name);
249    
250                if (_loadedPages == null)
251                    _loadedPages = new HashMap();
252    
253                _loadedPages.put(name, result);
254            }
255    
256            return result;
257        }
258    
259        private IPage loadPage(String name)
260        {
261            try
262            {
263                _monitor.pageLoadBegin(name);
264    
265                IPage result = _pageSource.getPage(this, name, _monitor);
266    
267                // Get the recorder that will eventually observe and record
268                // changes to persistent properties of the page.
269    
270                IPageRecorder recorder = getPageRecorder(name);
271    
272                // Have it rollback the page to the prior state. Note that
273                // the page has a null observer at this time (which keeps
274                // these changes from being sent to the page recorder).
275    
276                recorder.rollback(result);
277    
278                // Now, have the page use the recorder for any future
279                // property changes.
280    
281                result.setChangeObserver(recorder);
282    
283                // Now that persistent properties have been restored, we can
284                // attach the page to this request.
285    
286                result.attach(_engine, this);
287    
288                return result;
289            }
290            finally
291            {
292                _monitor.pageLoadEnd(name);
293            }
294    
295        }
296    
297        /**
298         * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are
299         * shortlived objects managed exclusively by the request cycle.
300         */
301    
302        protected IPageRecorder getPageRecorder(String name)
303        {
304            if (_pageRecorders == null)
305                _pageRecorders = new HashMap();
306    
307            IPageRecorder result = (IPageRecorder) _pageRecorders.get(name);
308    
309            if (result == null)
310            {
311                result = new PageRecorderImpl(name, this, _strategySource, _log);
312                _pageRecorders.put(name, result);
313            }
314    
315            return result;
316        }
317    
318        public boolean isRewinding()
319        {
320            return _rewinding;
321        }
322    
323        public boolean isRewound(IComponent component) throws StaleLinkException
324        {
325            // If not rewinding ...
326    
327            if (!_rewinding)
328                return false;
329    
330            if (_actionId != _targetActionId)
331                return false;
332    
333            // OK, we're there, is the page is good order?
334    
335            if (component == _targetComponent)
336                return true;
337    
338            // Woops. Mismatch.
339    
340            throw new StaleLinkException(component, Integer.toHexString(_targetActionId),
341                    _targetComponent.getExtendedId());
342        }
343    
344        public void removeAttribute(String name)
345        {
346            if (LOG.isDebugEnabled())
347                LOG.debug("Removing attribute " + name);
348    
349            _attributes.remove(name);
350        }
351    
352        /**
353         * Renders the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}. This
354         * clears all attributes.
355         */
356    
357        public void renderPage(IMarkupWriter writer)
358        {
359            String pageName = _page.getPageName();
360            _monitor.pageRenderBegin(pageName);
361    
362            _rewinding = false;
363            _actionId = -1;
364            _targetActionId = 0;
365    
366            try
367            {
368                _page.renderPage(writer, this);
369    
370            }
371            catch (ApplicationRuntimeException ex)
372            {
373                // Nothing much to add here.
374    
375                throw ex;
376            }
377            catch (Throwable ex)
378            {
379                // But wrap other exceptions in a RequestCycleException ... this
380                // will ensure that some of the context is available.
381    
382                throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
383            }
384            finally
385            {
386                reset();
387            }
388    
389            _monitor.pageRenderEnd(pageName);
390    
391        }
392    
393        /**
394         * Resets all internal state after a render or a rewind.
395         */
396    
397        private void reset()
398        {
399            _actionId = 0;
400            _targetActionId = 0;
401            _attributes.clear();
402            _idAllocator.clear();
403        }
404    
405        /**
406         * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}.
407         * <p>
408         * The process is expected to end with a {@link RenderRewoundException}. If the entire page is
409         * renderred without this exception being thrown, it means that the target action id was not
410         * valid, and a {@link ApplicationRuntimeException}&nbsp;is thrown.
411         * <p>
412         * This clears all attributes.
413         * 
414         * @since 1.0.2
415         */
416    
417        public void rewindForm(IForm form)
418        {
419            IPage page = form.getPage();
420            String pageName = page.getPageName();
421    
422            _rewinding = true;
423    
424            _monitor.pageRewindBegin(pageName);
425    
426            // Fake things a little for getNextActionId() / isRewound()
427            // This used to be more involved (and include service parameters, and a parameter
428            // to this method), when the actionId was part of the Form name. That's not longer
429            // necessary (no service parameters), and we can fake things here easily enough with
430            // fixed actionId of 0.
431    
432            _targetActionId = 0;
433            _actionId = -1;
434    
435            _targetComponent = form;
436    
437            try
438            {
439                page.beginPageRender();
440    
441                form.rewind(NullWriter.getSharedInstance(), this);
442    
443                // Shouldn't get this far, because the form should
444                // throw the RenderRewoundException.
445    
446                throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form
447                        .getExtendedId()), form);
448            }
449            catch (RenderRewoundException ex)
450            {
451                // This is acceptible and expected.
452            }
453            catch (ApplicationRuntimeException ex)
454            {
455                // RequestCycleExceptions don't need to be wrapped.
456                throw ex;
457            }
458            catch (Throwable ex)
459            {
460                // But wrap other exceptions in a ApplicationRuntimeException ... this
461                // will ensure that some of the context is available.
462    
463                throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex);
464            }
465            finally
466            {
467                page.endPageRender();
468    
469                _monitor.pageRewindEnd(pageName);
470    
471                reset();
472                _rewinding = false;
473            }
474        }
475    
476        /**
477         * Rewinds the page by invoking {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.
478         * <p>
479         * The process is expected to end with a {@link RenderRewoundException}. If the entire page is
480         * renderred without this exception being thrown, it means that the target action id was not
481         * valid, and a {@link ApplicationRuntimeException}is thrown.
482         * <p>
483         * This clears all attributes.
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, this);
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    }