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         * @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    }