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