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