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    import java.util.Stack;
021    
022    import org.apache.commons.logging.Log;
023    import org.apache.commons.logging.LogFactory;
024    import org.apache.hivemind.ApplicationRuntimeException;
025    import org.apache.hivemind.ErrorLog;
026    import org.apache.hivemind.impl.ErrorLogImpl;
027    import org.apache.hivemind.util.Defense;
028    import org.apache.hivemind.util.ToStringBuilder;
029    import org.apache.tapestry.IComponent;
030    import org.apache.tapestry.IEngine;
031    import org.apache.tapestry.IForm;
032    import org.apache.tapestry.IPage;
033    import org.apache.tapestry.IRender;
034    import org.apache.tapestry.IRequestCycle;
035    import org.apache.tapestry.RedirectException;
036    import org.apache.tapestry.RenderRewoundException;
037    import org.apache.tapestry.StaleLinkException;
038    import org.apache.tapestry.Tapestry;
039    import org.apache.tapestry.record.PageRecorderImpl;
040    import org.apache.tapestry.record.PropertyPersistenceStrategySource;
041    import org.apache.tapestry.services.AbsoluteURLBuilder;
042    import org.apache.tapestry.services.Infrastructure;
043    import org.apache.tapestry.services.ResponseBuilder;
044    import org.apache.tapestry.util.IdAllocator;
045    import org.apache.tapestry.util.QueryParameterMap;
046    
047    /**
048     * Provides the logic for processing a single request cycle. Provides access to the
049     * {@link IEngine engine} and the {@link RequestContext}.
050     * 
051     * @author Howard Lewis Ship
052     */
053    
054    public class RequestCycle implements IRequestCycle
055    {
056        private static final Log LOG = LogFactory.getLog(RequestCycle.class);
057        
058        protected ResponseBuilder _responseBuilder;
059        
060        private IPage _page;
061    
062        private IEngine _engine;
063    
064        private String _serviceName;
065    
066        /** @since 4.0 */
067    
068        private PropertyPersistenceStrategySource _strategySource;
069    
070        /** @since 4.0 */
071    
072        private IPageSource _pageSource;
073    
074        /** @since 4.0 */
075    
076        private Infrastructure _infrastructure;
077    
078        /**
079         * Contains parameters extracted from the request context, plus any decoded by any
080         * {@link ServiceEncoder}s.
081         * 
082         * @since 4.0
083         */
084    
085        private QueryParameterMap _parameters;
086    
087        /** @since 4.0 */
088    
089        private AbsoluteURLBuilder _absoluteURLBuilder;
090    
091        /**
092         * A mapping of pages loaded during the current request cycle. Key is the page name, value is
093         * the {@link IPage}instance.
094         */
095    
096        private Map _loadedPages;
097    
098        /**
099         * A mapping of page recorders for the current request cycle. Key is the page name, value is the
100         * {@link IPageRecorder}instance.
101         */
102    
103        private Map _pageRecorders;
104    
105        private boolean _rewinding = false;
106    
107        private Map _attributes = new HashMap();
108    
109        private int _targetActionId;
110    
111        private IComponent _targetComponent;
112    
113        /** @since 2.0.3 * */
114    
115        private Object[] _listenerParameters;
116    
117        /** @since 4.0 */
118    
119        private ErrorLog _log;
120    
121        /** @since 4.0 */
122    
123        private IdAllocator _idAllocator = new IdAllocator();
124    
125        private Stack _renderStack = new Stack();
126        
127        /**
128         * Standard constructor used to render a response page.
129         * 
130         * @param engine
131         *            the current request's engine
132         * @param parameters
133         *            query parameters (possibly the result of {@link ServiceEncoder}s decoding path
134         *            information)
135         * @param serviceName
136         *            the name of engine service
137         * @param environment
138         *            additional invariant services and objects needed by each RequestCycle instance
139         */
140    
141        public RequestCycle(IEngine engine, QueryParameterMap parameters, String serviceName,
142                RequestCycleEnvironment environment)
143        {
144            // Variant from instance to instance
145    
146            _engine = engine;
147            _parameters = parameters;
148            _serviceName = serviceName;
149    
150            // Invariant from instance to instance
151    
152            _infrastructure = environment.getInfrastructure();
153            _pageSource = _infrastructure.getPageSource();
154            _strategySource = environment.getStrategySource();
155            _absoluteURLBuilder = environment.getAbsoluteURLBuilder();
156            _log = new ErrorLogImpl(environment.getErrorHandler(), LOG);
157        }
158        
159        /**
160         * Alternate constructor used <strong>only for testing purposes</strong>.
161         * 
162         * @since 4.0
163         */
164        public RequestCycle()
165        {
166        }
167    
168        /**
169         * Called at the end of the request cycle (i.e., after all responses have been sent back to the
170         * client), to release all pages loaded during the request cycle.
171         */
172    
173        public void cleanup()
174        {
175            if (_loadedPages == null)
176                return;
177    
178            Iterator i = _loadedPages.values().iterator();
179    
180            while (i.hasNext())
181            {
182                IPage page = (IPage) i.next();
183    
184                _pageSource.releasePage(page);
185            }
186    
187            _loadedPages = null;
188            _pageRecorders = null;
189            _renderStack.clear();
190            
191        }
192    
193        public IEngineService getService()
194        {
195            return _infrastructure.getServiceMap().getService(_serviceName);
196        }
197    
198        public String encodeURL(String URL)
199        {
200            return _infrastructure.getResponse().encodeURL(URL);
201        }
202    
203        public IEngine getEngine()
204        {
205            return _engine;
206        }
207    
208        public Object getAttribute(String name)
209        {
210            return _attributes.get(name);
211        }
212    
213        public IPage getPage()
214        {
215            return _page;
216        }
217    
218        /**
219         * Gets the page from the engines's {@link IPageSource}.
220         */
221    
222        public IPage getPage(String name)
223        {
224            Defense.notNull(name, "name");
225    
226            IPage result = null;
227    
228            if (_loadedPages != null)
229                result = (IPage) _loadedPages.get(name);
230    
231            if (result == null)
232            {
233                result = loadPage(name);
234    
235                if (_loadedPages == null)
236                    _loadedPages = new HashMap();
237    
238                _loadedPages.put(name, result);
239            }
240    
241            return result;
242        }
243    
244        private IPage loadPage(String name)
245        {
246            IPage result = _pageSource.getPage(this, name);
247    
248            // Get the recorder that will eventually observe and record
249            // changes to persistent properties of the page.
250    
251            IPageRecorder recorder = getPageRecorder(name);
252    
253            // Have it rollback the page to the prior state. Note that
254            // the page has a null observer at this time (which keeps
255            // these changes from being sent to the page recorder).
256    
257            recorder.rollback(result);
258    
259            // Now, have the page use the recorder for any future
260            // property changes.
261    
262            result.setChangeObserver(recorder);
263    
264            return result;
265    
266        }
267    
268        /**
269         * Returns the page recorder for the named page. Starting with Tapestry 4.0, page recorders are
270         * shortlived objects managed exclusively by the request cycle.
271         */
272    
273        protected IPageRecorder getPageRecorder(String name)
274        {
275            if (_pageRecorders == null)
276                _pageRecorders = new HashMap();
277    
278            IPageRecorder result = (IPageRecorder) _pageRecorders.get(name);
279    
280            if (result == null)
281            {
282                result = new PageRecorderImpl(name, _strategySource, _log);
283                _pageRecorders.put(name, result);
284            }
285    
286            return result;
287        }
288        
289        public void setResponseBuilder(ResponseBuilder builder)
290        {
291            // TODO: What scenerio requires setting the builder after the fact?
292            //if (_responseBuilder != null)
293              //  throw new IllegalArgumentException("A ResponseBuilder has already been set on this response.");
294            
295            _responseBuilder = builder;
296        }
297        
298        public ResponseBuilder getResponseBuilder()
299        {
300            return _responseBuilder;
301        }
302        
303        /** 
304         * {@inheritDoc}
305         */
306        public boolean renderStackEmpty()
307        {
308            return _renderStack.isEmpty();
309        }
310    
311        /** 
312         * {@inheritDoc}
313         */
314        public IRender renderStackPeek()
315        {
316            if (_renderStack.size() < 1)
317                return null;
318            
319            return (IRender)_renderStack.peek();
320        }
321    
322        /** 
323         * {@inheritDoc}
324         */
325        public IRender renderStackPop()
326        {
327            if (_renderStack.size() == 0)
328                return null;
329            
330            return (IRender)_renderStack.pop();
331        }
332    
333        /** 
334         * {@inheritDoc}
335         */
336        public IRender renderStackPush(IRender render)
337        {
338            if (_renderStack.size() > 0 && _renderStack.peek() == render)
339                return render;
340            
341            return (IRender)_renderStack.push(render);
342        }
343    
344        /** 
345         * {@inheritDoc}
346         */
347        public int renderStackSearch(IRender render)
348        {
349            return _renderStack.search(render);
350        }
351        
352        /**
353         * {@inheritDoc}
354         */
355        public Iterator renderStackIterator()
356        {
357            return _renderStack.iterator();
358        }
359        
360        public boolean isRewinding()
361        {
362            return _rewinding;
363        }
364    
365        public boolean isRewound(IComponent component)
366        {
367            // If not rewinding ...
368    
369            if (!_rewinding)
370                return false;
371    
372            // OK, we're there, is the page is good order?
373    
374            if (component == _targetComponent)
375                return true;
376    
377            // Woops. Mismatch.
378    
379            throw new StaleLinkException(component, Integer.toHexString(_targetActionId),
380                    _targetComponent.getExtendedId());
381        }
382    
383        public void removeAttribute(String name)
384        {
385            if (LOG.isDebugEnabled())
386                LOG.debug("Removing attribute " + name);
387    
388            _attributes.remove(name);
389        }
390    
391        /**
392         * Renders the page by invoking {@link IPage#renderPage(ResponseBuilder, IRequestCycle)}. This
393         * clears all attributes.
394         */
395    
396        public void renderPage(ResponseBuilder builder)
397        {
398            _rewinding = false;
399    
400            try
401            {
402                _page.renderPage(builder, this);
403                
404            }
405            catch (ApplicationRuntimeException ex)
406            {
407                // Nothing much to add here.
408    
409                throw ex;
410            }
411            catch (Throwable ex)
412            {
413                // But wrap other exceptions in a RequestCycleException ... this
414                // will ensure that some of the context is available.
415    
416                throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
417            }
418            finally
419            {
420                reset();
421            }
422    
423        }
424    
425        /**
426         * Resets all internal state after a render or a rewind.
427         */
428    
429        private void reset()
430        {
431            _attributes.clear();
432            _idAllocator.clear();
433        }
434    
435        /**
436         * Rewinds an individual form by invoking {@link IForm#rewind(IMarkupWriter, IRequestCycle)}.
437         * <p>
438         * The process is expected to end with a {@link RenderRewoundException}. If the entire page is
439         * renderred without this exception being thrown, it means that the target action id was not
440         * valid, and a {@link ApplicationRuntimeException}&nbsp;is thrown.
441         * <p>
442         * This clears all attributes.
443         * 
444         * @since 1.0.2
445         */
446    
447        public void rewindForm(IForm form)
448        {
449            IPage page = form.getPage();
450            _rewinding = true;
451            
452            _targetComponent = form;
453    
454            try
455            {
456                page.beginPageRender();
457    
458                form.rewind(NullWriter.getSharedInstance(), this);
459    
460                // Shouldn't get this far, because the form should
461                // throw the RenderRewoundException.
462    
463                throw new StaleLinkException(Tapestry.format("RequestCycle.form-rewind-failure", form
464                        .getExtendedId()), form);
465            }
466            catch (RenderRewoundException ex)
467            {
468                // This is acceptible and expected.
469            }
470            catch (ApplicationRuntimeException ex)
471            {
472                // RequestCycleExceptions don't need to be wrapped.
473                throw ex;
474            }
475            catch (Throwable ex)
476            {
477                // But wrap other exceptions in a ApplicationRuntimeException ... this
478                // will ensure that some of the context is available.
479    
480                throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex);
481            }
482            finally
483            {
484                page.endPageRender();
485    
486                reset();
487                _rewinding = false;
488            }
489        }
490    
491        public void setAttribute(String name, Object value)
492        {
493            if (LOG.isDebugEnabled())
494                LOG.debug("Set attribute " + name + " to " + value);
495    
496            _attributes.put(name, value);
497        }
498    
499        /**
500         * Invokes {@link IPageRecorder#commit()}on each page recorder loaded during the request cycle
501         * (even recorders marked for discard).
502         */
503    
504        public void commitPageChanges()
505        {
506            if (LOG.isDebugEnabled())
507                LOG.debug("Committing page changes");
508    
509            if (_pageRecorders == null || _pageRecorders.isEmpty())
510                return;
511    
512            Iterator i = _pageRecorders.values().iterator();
513    
514            while (i.hasNext())
515            {
516                IPageRecorder recorder = (IPageRecorder) i.next();
517    
518                recorder.commit();
519            }
520        }
521    
522        /**
523         * As of 4.0, just a synonym for {@link #forgetPage(String)}.
524         * 
525         * @since 2.0.2
526         */
527    
528        public void discardPage(String name)
529        {
530            forgetPage(name);
531        }
532    
533        /** @since 2.0.3 * */
534    
535        public Object[] getServiceParameters()
536        {
537            return getListenerParameters();
538        }
539    
540        /** @since 2.0.3 * */
541    
542        public void setServiceParameters(Object[] serviceParameters)
543        {
544            setListenerParameters(serviceParameters);
545        }
546    
547        /** @since 4.0 */
548        public Object[] getListenerParameters()
549        {
550            return _listenerParameters;
551        }
552    
553        /** @since 4.0 */
554        public void setListenerParameters(Object[] parameters)
555        {
556            _listenerParameters = parameters;
557        }
558    
559        /** @since 3.0 * */
560    
561        public void activate(String name)
562        {
563            IPage page = getPage(name);
564    
565            activate(page);
566        }
567        
568        /** @since 3.0 */
569    
570        public void activate(IPage page)
571        {
572            Defense.notNull(page, "page");
573    
574            if (LOG.isDebugEnabled())
575                LOG.debug("Activating page " + page);
576            
577            Tapestry.clearMethodInvocations();
578            
579            page.validate(this);
580            
581            Tapestry.checkMethodInvocation(Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID, "validate()", page);
582    
583            _page = page;
584        }
585        
586        /** @since 4.0 */
587        public String getParameter(String name)
588        {
589            return _parameters.getParameterValue(name);
590        }
591    
592        /** @since 4.0 */
593        public String[] getParameters(String name)
594        {
595            return _parameters.getParameterValues(name);
596        }
597    
598        /**
599         * @since 3.0
600         */
601        public String toString()
602        {
603            ToStringBuilder b = new ToStringBuilder(this);
604    
605            b.append("rewinding", _rewinding);
606    
607            b.append("serviceName", _serviceName);
608    
609            b.append("serviceParameters", _listenerParameters);
610    
611            if (_loadedPages != null)
612                b.append("loadedPages", _loadedPages.keySet());
613    
614            b.append("attributes", _attributes);
615            b.append("targetActionId", _targetActionId);
616            b.append("targetComponent", _targetComponent);
617    
618            return b.toString();
619        }
620    
621        /** @since 4.0 */
622    
623        public String getAbsoluteURL(String partialURL)
624        {
625            String contextPath = _infrastructure.getRequest().getContextPath();
626    
627            return _absoluteURLBuilder.constructURL(contextPath + partialURL);
628        }
629    
630        /** @since 4.0 */
631    
632        public void forgetPage(String pageName)
633        {
634            Defense.notNull(pageName, "pageName");
635    
636            _strategySource.discardAllStoredChanged(pageName);
637        }
638    
639        /** @since 4.0 */
640    
641        public Infrastructure getInfrastructure()
642        {
643            return _infrastructure;
644        }
645    
646        /** @since 4.0 */
647    
648        public String getUniqueId(String baseId)
649        {
650            return _idAllocator.allocateId(baseId);
651        }
652    
653        /** @since 4.0 */
654        public void sendRedirect(String URL)
655        {
656            throw new RedirectException(URL);
657        }
658    
659    }