001    // Copyright May 8, 2006 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    package org.apache.tapestry.services.impl;
015    
016    import org.apache.commons.logging.Log;
017    import org.apache.commons.logging.LogFactory;
018    import org.apache.hivemind.Resource;
019    import org.apache.hivemind.util.Defense;
020    import org.apache.tapestry.*;
021    import org.apache.tapestry.asset.AssetFactory;
022    import org.apache.tapestry.engine.IEngineService;
023    import org.apache.tapestry.engine.NullWriter;
024    import org.apache.tapestry.markup.MarkupWriterSource;
025    import org.apache.tapestry.markup.NestedMarkupWriterImpl;
026    import org.apache.tapestry.services.RequestLocaleManager;
027    import org.apache.tapestry.services.ResponseBuilder;
028    import org.apache.tapestry.services.ServiceConstants;
029    import org.apache.tapestry.util.ContentType;
030    import org.apache.tapestry.util.PageRenderSupportImpl;
031    import org.apache.tapestry.util.ScriptUtils;
032    import org.apache.tapestry.web.WebResponse;
033    
034    import java.io.IOException;
035    import java.io.PrintWriter;
036    import java.util.*;
037    
038    
039    /**
040     * Main class that handles dojo based ajax responses. These responses are wrapped
041     * by an xml document format that segments off invididual component/javascript response
042     * types into easy to manage xml elements that can then be interpreted and managed by 
043     * running client-side javascript.
044     *
045     */
046    public class DojoAjaxResponseBuilder implements ResponseBuilder
047    {
048        private static final Log _log = LogFactory.getLog(DojoAjaxResponseBuilder.class);
049    
050        private static final String NEWLINE = System.getProperty("line.separator");
051    
052        private final AssetFactory _assetFactory;
053    
054        private final String _namespace;
055    
056        private PageRenderSupportImpl _prs;
057    
058        // used to create IMarkupWriter
059        private RequestLocaleManager _localeManager;
060        private MarkupWriterSource _markupWriterSource;
061        private WebResponse _response;
062    
063        private List _errorPages;
064    
065        private ContentType _contentType;
066    
067        // our response writer
068        private IMarkupWriter _writer;
069        // Parts that will be updated.
070        private List _parts = new ArrayList();
071        // Map of specialized writers, like scripts
072        private Map _writers = new HashMap();
073        // List of status messages.
074        private List _statusMessages;
075    
076        private IRequestCycle _cycle;
077    
078        private IEngineService _pageService;
079    
080        /**
081         * Keeps track of renders involving a whole page response, such 
082         * as exception pages or pages activated via {@link IRequestCycle#activate(IPage)}.
083         */
084        private boolean _pageRender = false;
085    
086        /**
087         * Used to keep track of whether or not the appropriate xml response start
088         * block has been started.
089         */
090        private boolean _responseStarted = false;
091    
092        /**
093         * Creates a builder with a pre-configured {@link IMarkupWriter}.
094         * Currently only used for testing.
095         *
096         * @param cycle
097         *          The current cycle.
098         * @param writer
099         *          The markup writer to render all "good" content to.
100         * @param parts
101         *          A set of string ids of the components that may have
102         *          their responses rendered.
103         * @param errorPages
104         *          List of page names known to be exception pages.
105         */
106        public DojoAjaxResponseBuilder(IRequestCycle cycle, IMarkupWriter writer, List parts, List errorPages)
107        {
108            Defense.notNull(cycle, "cycle");
109            Defense.notNull(writer, "writer");
110    
111            _writer = writer;
112            _cycle = cycle;
113    
114            if (parts != null)
115                _parts.addAll(parts);
116    
117            _namespace = null;
118            _assetFactory = null;
119            _errorPages = errorPages;
120        }
121    
122        /**
123         * Creates a builder with a pre-configured {@link IMarkupWriter}. 
124         * Currently only used for testing.
125         *
126         * @param cycle
127         *          Current request.
128         * @param writer
129         *          The markup writer to render all "good" content to.
130         * @param parts
131         *          A set of string ids of the components that may have 
132         *          their responses rendered.
133         */
134        public DojoAjaxResponseBuilder(IRequestCycle cycle, IMarkupWriter writer, List parts)
135        {
136            this(cycle, writer, parts, null);
137        }
138    
139        /**
140         * Creates a new response builder with the required services it needs
141         * to render the response when {@link #renderResponse(IRequestCycle)} is called.
142         *
143         * @param cycle
144         *          The current request.
145         * @param localeManager
146         *          Used to set the locale on the response.
147         * @param markupWriterSource
148         *          Creates IJSONWriter instance to be used.
149         * @param webResponse
150         *          Web response for output stream.
151         * @param errorPages
152         *          List of page names known to be exception pages.
153         * @param assetFactory
154         *          Used to manage asset source inclusions.
155         * @param namespace
156         *          The core namespace to use for javascript/client side operations.
157         * @param pageService
158         *          {@link org.apache.tapestry.engine.PageService} used to generate page urls.
159         */
160        public DojoAjaxResponseBuilder(IRequestCycle cycle,
161                                       RequestLocaleManager localeManager,
162                                       MarkupWriterSource markupWriterSource,
163                                       WebResponse webResponse, List errorPages,
164                                       AssetFactory assetFactory, String namespace, IEngineService pageService)
165        {
166            Defense.notNull(cycle, "cycle");
167            Defense.notNull(assetFactory, "assetService");
168    
169            _cycle = cycle;
170            _localeManager = localeManager;
171            _markupWriterSource = markupWriterSource;
172            _response = webResponse;
173            _errorPages = errorPages;
174            _pageService = pageService;
175    
176            // Used by PageRenderSupport
177    
178            _assetFactory = assetFactory;
179            _namespace = namespace;
180    
181            _prs = new PageRenderSupportImpl(_assetFactory, _namespace, this, cycle);
182        }
183    
184        /**
185         *
186         * {@inheritDoc}
187         */
188        public boolean isDynamic()
189        {
190            return true;
191        }
192    
193        /**
194         * {@inheritDoc}
195         */
196        public void renderResponse(IRequestCycle cycle)
197          throws IOException
198        {
199            // if response was already started
200    
201            if (_responseStarted)
202            {
203                // clear out any previous input
204                clearPartialWriters();
205    
206                cycle.renderPage(this);
207    
208                TapestryUtils.removePageRenderSupport(cycle);
209                
210                endResponse();
211    
212                _writer.close();
213    
214                return;
215            }
216    
217            _localeManager.persistLocale();
218            _contentType = new ContentType(CONTENT_TYPE + ";charset=" + cycle.getInfrastructure().getOutputEncoding());
219    
220            String encoding = _contentType.getParameter(ENCODING_KEY);
221    
222            if (encoding == null)
223            {
224                encoding = cycle.getEngine().getOutputEncoding();
225    
226                _contentType.setParameter(ENCODING_KEY, encoding);
227            }
228    
229            if (_writer == null)
230            {
231                parseParameters(cycle);
232    
233                PrintWriter printWriter = _response.getPrintWriter(_contentType);
234                _writer = _markupWriterSource.newMarkupWriter(printWriter, _contentType);
235            }
236    
237            // render response
238    
239            TapestryUtils.storePageRenderSupport(cycle, _prs);
240    
241            cycle.renderPage(this);
242    
243            TapestryUtils.removePageRenderSupport(cycle);
244    
245            endResponse();
246    
247            _writer.close();
248        }
249    
250        public void flush()
251          throws IOException
252        {
253            // Important - causes any cookies stored to properly be written out before the
254            // rest of the response starts being written - see TAPESTRY-825
255    
256            _writer.flush();
257    
258            if (!_responseStarted)
259                beginResponse();
260        }
261    
262        /**
263         * {@inheritDoc}
264         */
265        public void updateComponent(String id)
266        {
267            if (!_parts.contains(id))
268                _parts.add(id);
269        }
270    
271        /**
272         * {@inheritDoc}
273         */
274        public IMarkupWriter getWriter()
275        {
276            return _writer;
277        }
278    
279        void setWriter(IMarkupWriter writer)
280        {
281            _writer = writer;
282        }
283    
284        /**
285         * {@inheritDoc}
286         */
287        public boolean isBodyScriptAllowed(IComponent target)
288        {
289            if (_pageRender)
290                return true;
291    
292            if (target != null
293                && IPage.class.isInstance(target)
294                || (IForm.class.isInstance(target)
295                    && ((IForm)target).isFormFieldUpdating()))
296                return true;
297    
298            return contains(target);
299        }
300    
301        /**
302         * {@inheritDoc}
303         */
304        public boolean isExternalScriptAllowed(IComponent target)
305        {
306            if (_pageRender)
307                return true;
308    
309            if (target != null
310                && IPage.class.isInstance(target)
311                || (IForm.class.isInstance(target)
312                    && ((IForm)target).isFormFieldUpdating()))
313                return true;
314    
315            return contains(target);
316        }
317    
318        /**
319         * {@inheritDoc}
320         */
321        public boolean isInitializationScriptAllowed(IComponent target)
322        {
323            if (_log.isDebugEnabled())
324            {
325                _log.debug("isInitializationScriptAllowed(" + target + ") contains?: " + contains(target) + " _pageRender: " + _pageRender);
326            }
327    
328            if (_pageRender)
329                return true;
330    
331            if (target != null
332                && IPage.class.isInstance(target)
333                || (IForm.class.isInstance(target)
334                    && ((IForm)target).isFormFieldUpdating()))
335                return true;
336    
337            return contains(target);
338        }
339    
340        /**
341         * {@inheritDoc}
342         */
343        public boolean isImageInitializationAllowed(IComponent target)
344        {
345            if (_pageRender)
346                return true;
347    
348            if (target != null
349                && IPage.class.isInstance(target)
350                || (IForm.class.isInstance(target)
351                    && ((IForm)target).isFormFieldUpdating()))
352                return true;
353    
354            return contains(target);
355        }
356    
357        /**
358         * {@inheritDoc}
359         */
360        public String getPreloadedImageReference(IComponent target, IAsset source)
361        {
362            return _prs.getPreloadedImageReference(target, source);
363        }
364    
365        /**
366         * {@inheritDoc}
367         */
368        public String getPreloadedImageReference(IComponent target, String url)
369        {
370            return _prs.getPreloadedImageReference(target, url);
371        }
372    
373        /**
374         * {@inheritDoc}
375         */
376        public String getPreloadedImageReference(String url)
377        {
378            return _prs.getPreloadedImageReference(url);
379        }
380    
381        /**
382         * {@inheritDoc}
383         */
384        public void addBodyScript(IComponent target, String script)
385        {
386            _prs.addBodyScript(target, script);
387        }
388    
389        /**
390         * {@inheritDoc}
391         */
392        public void addBodyScript(String script)
393        {
394            _prs.addBodyScript(script);
395        }
396    
397        /**
398         * {@inheritDoc}
399         */
400        public void addExternalScript(IComponent target, Resource resource)
401        {
402            _prs.addExternalScript(target, resource);
403        }
404    
405        /**
406         * {@inheritDoc}
407         */
408        public void addExternalScript(Resource resource)
409        {
410            _prs.addExternalScript(resource);
411        }
412    
413        /**
414         * {@inheritDoc}
415         */
416        public void addInitializationScript(IComponent target, String script)
417        {
418            _prs.addInitializationScript(target, script);
419        }
420    
421        /**
422         * {@inheritDoc}
423         */
424        public void addInitializationScript(String script)
425        {
426            _prs.addInitializationScript(script);
427        }
428    
429        public void addScriptAfterInitialization(IComponent target, String script)
430        {
431            _prs.addScriptAfterInitialization(target, script);
432        }
433    
434        /**
435         * {@inheritDoc}
436         */
437        public String getUniqueString(String baseValue)
438        {
439            return _prs.getUniqueString(baseValue);
440        }
441    
442        /**
443         * {@inheritDoc}
444         */
445        public void writeBodyScript(IMarkupWriter writer, IRequestCycle cycle)
446        {
447            _prs.writeBodyScript(writer, cycle);
448        }
449    
450        /**
451         * {@inheritDoc}
452         */
453        public void writeInitializationScript(IMarkupWriter writer)
454        {
455            _prs.writeInitializationScript(writer);
456        }
457    
458        /**
459         * {@inheritDoc}
460         */
461        public void beginBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
462        {
463            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
464    
465            writer.begin("script");
466            writer.printRaw(NEWLINE + "//<![CDATA[" + NEWLINE);
467        }
468    
469        /**
470         * {@inheritDoc}
471         */
472        public void endBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
473        {
474            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
475    
476            writer.printRaw(NEWLINE + "//]]>" + NEWLINE);
477            writer.end();
478        }
479    
480        /**
481         * {@inheritDoc}
482         */
483        public void writeBodyScript(IMarkupWriter normalWriter, String script, IRequestCycle cycle)
484        {
485            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
486    
487            writer.printRaw(script);
488        }
489    
490        /**
491         * {@inheritDoc}
492         */
493        public void writeExternalScript(IMarkupWriter normalWriter, String url, IRequestCycle cycle)
494        {
495            IMarkupWriter writer = getWriter(ResponseBuilder.INCLUDE_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
496    
497            // causes asset includes to be loaded dynamically into document head
498            writer.beginEmpty("include");
499            writer.attribute("url", url);
500        }
501    
502        /**
503         * {@inheritDoc}
504         */
505        public void writeImageInitializations(IMarkupWriter normalWriter, String script, String preloadName, IRequestCycle cycle)
506        {
507            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
508    
509            writer.printRaw(NEWLINE + preloadName + " = [];" + NEWLINE);
510            writer.printRaw("if (document.images) {" + NEWLINE);
511    
512            writer.printRaw(script);
513    
514            writer.printRaw("}" + NEWLINE);
515        }
516    
517        /**
518         * {@inheritDoc}
519         */
520        public void writeInitializationScript(IMarkupWriter normalWriter, String script)
521        {
522            IMarkupWriter writer = getWriter(ResponseBuilder.INITIALIZATION_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
523    
524            writer.begin("script");
525    
526            // return is in XML so must escape any potentially non-xml compliant content
527            writer.printRaw(NEWLINE + "//<![CDATA[" + NEWLINE);
528    
529            writer.printRaw(script);
530    
531            writer.printRaw(NEWLINE + "//]]>" + NEWLINE);
532    
533            writer.end();
534        }
535    
536        public void addStatus(IMarkupWriter normalWriter, String text)
537        {
538            addStatusMessage(normalWriter, "info", text);
539        }
540    
541        /**
542         * Adds a status message to the current response. This implementation keeps track
543         * of all messages and appends them to the XHR response. On the client side, 
544         * the default behavior is to publish the message to a topic matching the category name
545         * using <code>dojo.event.topic.publish(category,text);</code>.
546         *
547         * @param normalWriter
548         *          The markup writer to use, this may be ignored or swapped
549         *          out for a different writer depending on the implementation being used.
550         * @param category
551         *          Allows setting a category that best describes the type of the status message,
552         *          i.e. info, error, e.t.c.
553         * @param text
554         *          The status message. 
555         */
556        public void addStatusMessage(IMarkupWriter normalWriter, String category, String text)
557        {
558            if (_statusMessages==null)
559            {
560                _statusMessages = new ArrayList();
561            }
562    
563            _statusMessages.add(category);
564            _statusMessages.add(text);
565        }
566    
567        void writeStatusMessages() {
568    
569            for (int i=0; i < _statusMessages.size(); i+=2)
570            {
571                IMarkupWriter writer = getWriter((String) _statusMessages.get(i), "status");
572    
573                writer.printRaw((String) _statusMessages.get(i+1));
574            }
575    
576            _statusMessages = null;
577        }
578    
579        /**
580         * {@inheritDoc}
581         */
582        public void render(IMarkupWriter writer, IRender render, IRequestCycle cycle)
583        {
584            // must be a valid writer already
585    
586            if (NestedMarkupWriterImpl.class.isInstance(writer) && !NullWriter.class.isInstance(writer))
587            {
588                render.render(writer, cycle);
589                return;
590            }
591    
592            // check for page exception renders and write content to writer so client can display them
593    
594            if (IPage.class.isInstance(render))
595            {
596                IPage page = (IPage)render;
597                String errorPage = getErrorPage(page.getPageName());
598    
599                if (errorPage != null)
600                {
601                    _pageRender = true;
602    
603                    clearPartialWriters();
604                    render.render(getWriter(errorPage, EXCEPTION_TYPE), cycle);
605                    return;
606                }
607    
608                // If a page other than the active page originally requested is rendered
609                // it means someone activated a new page, so we need to tell the client to handle
610                // this appropriately. (usually by replacing the current dom with whatever this renders)
611    
612                if (_cycle.getParameter(ServiceConstants.PAGE) != null
613                    && !page.getPageName().equals(_cycle.getParameter(ServiceConstants.PAGE)))
614                {
615                    IMarkupWriter urlwriter = _writer.getNestedWriter();
616    
617                    urlwriter.begin("response");
618                    urlwriter.attribute("type", PAGE_TYPE);
619                    urlwriter.attribute("url", _pageService.getLink(true, page.getPageName()).getAbsoluteURL());
620    
621                    _writers.put(PAGE_TYPE, urlwriter);
622                    return;
623                }
624            }
625    
626            if (IComponent.class.isInstance(render)
627                && contains((IComponent)render, ((IComponent)render).peekClientId()))
628            {
629                render.render(getComponentWriter( ((IComponent)render).peekClientId() ), cycle);
630                return;
631            }
632    
633            // Nothing else found, throw out response
634    
635            render.render(NullWriter.getSharedInstance(), cycle);
636        }
637    
638        private String getErrorPage(String pageName)
639        {
640            for (int i=0; i < _errorPages.size(); i++)
641            {
642                String page = (String)_errorPages.get(i);
643    
644                if (pageName.indexOf(page) > -1)
645                    return page;
646            }
647    
648            return null;
649        }
650    
651        IMarkupWriter getComponentWriter(String id)
652        {
653            return getWriter(id, ELEMENT_TYPE);
654        }
655    
656        /**
657         *
658         * {@inheritDoc}
659         */
660        public IMarkupWriter getWriter(String id, String type)
661        {
662            Defense.notNull(id, "id can't be null");
663    
664            if (!_responseStarted)
665                beginResponse();
666    
667            IMarkupWriter w = (IMarkupWriter)_writers.get(id);
668            if (w != null)
669                return w;
670    
671            // Make component write to a "nested" writer
672            // so that element begin/ends don't conflict
673            // with xml element response begin/ends. This is very
674            // important.
675    
676            IMarkupWriter nestedWriter = _writer.getNestedWriter();
677            nestedWriter.begin("response");
678            nestedWriter.attribute("id", id);
679            if (type != null)
680                nestedWriter.attribute("type", type);
681    
682            _writers.put(id, nestedWriter);
683    
684            return nestedWriter;
685        }
686    
687        /**
688         * Called to start an ajax response. Writes xml doctype and starts
689         * the <code>ajax-response</code> element that will contain all of
690         * the returned content.
691         */
692        void beginResponse()
693        {
694            _responseStarted = true;
695    
696            _writer.printRaw("<?xml version=\"1.0\" encoding=\"" + _cycle.getInfrastructure().getOutputEncoding() + "\"?>");
697            _writer.printRaw("<!DOCTYPE html "
698                             + "PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
699                             + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\" [" + NEWLINE
700                             + "<!ENTITY nbsp '&#160;'>" + NEWLINE
701                             + "]>" + NEWLINE);
702            
703            _writer.printRaw("<ajax-response>");
704        }
705    
706        /**
707         * Invoked to clear out tempoary partial writer buffers before rendering exception
708         * page.
709         */
710        void clearPartialWriters()
711        {
712            _writers.clear();
713        }
714    
715        /**
716         * Called after the entire response has been captured. Causes
717         * the writer buffer output captured to be segmented and written
718         * out to the right response elements for the client libraries to parse.
719         */
720        void endResponse()
721        {
722            if (!_responseStarted)
723            {
724                beginResponse();
725            }
726    
727            // write out captured content
728    
729            if (_statusMessages != null)
730                writeStatusMessages();
731    
732            Iterator keys = _writers.keySet().iterator();
733            String buffer;
734    
735            while (keys.hasNext())
736            {
737                String key = (String)keys.next();
738                NestedMarkupWriter nw = (NestedMarkupWriter)_writers.get(key);
739    
740                buffer = nw.getBuffer();
741    
742                if (_log.isDebugEnabled())
743                {
744                    _log.debug("Ajax markup buffer for key <" + key + " contains: " + buffer);
745                }
746    
747                if (!isScriptWriter(key))
748                    _writer.printRaw(ScriptUtils.ensureValidScriptTags(buffer));
749                else
750                    _writer.printRaw(buffer);
751            }
752    
753            // end response
754    
755            _writer.printRaw("</ajax-response>");
756            _writer.flush();
757        }
758    
759        /**
760         * Determines if the specified markup writer key is one of
761         * the pre-defined script keys from ResponseBuilder.
762         *
763         * @param key
764         *          The key to check.
765         * @return True, if key is one of the ResponseBuilder keys. 
766         *         (BODY_SCRIPT,INCLUDE_SCRIPT,INITIALIZATION_SCRIPT)
767         */
768        boolean isScriptWriter(String key)
769        {
770            if (key == null)
771                return false;
772    
773            if (ResponseBuilder.BODY_SCRIPT.equals(key)
774                || ResponseBuilder.INCLUDE_SCRIPT.equals(key)
775                || ResponseBuilder.INITIALIZATION_SCRIPT.equals(key))
776                return true;
777    
778            return false;
779        }
780    
781        /**
782         * Grabs the incoming parameters needed for json responses, most notable the
783         * {@link ServiceConstants#UPDATE_PARTS} parameter.
784         *
785         * @param cycle
786         *            The request cycle to parse from
787         */
788        void parseParameters(IRequestCycle cycle)
789        {
790            Object[] updateParts = cycle.getParameters(ServiceConstants.UPDATE_PARTS);
791    
792            if (updateParts == null)
793                return;
794    
795            for(int i = 0; i < updateParts.length; i++)
796                _parts.add(updateParts[i].toString());
797        }
798    
799        /**
800         * Determines if the specified component is contained in the 
801         * responses requested update parts.
802         * @param target
803         *          The component to check for.
804         * @return True if the request should capture the components output.
805         */
806        public boolean contains(IComponent target)
807        {
808            if (target == null)
809                return false;
810    
811            String id = target.getClientId();
812    
813            return contains(target, id);
814        }
815    
816        boolean contains(IComponent target, String id)
817        {
818            if (_parts.contains(id))
819                return true;
820    
821            Iterator it = _cycle.renderStackIterator();
822            while (it.hasNext())
823            {
824                IComponent comp = (IComponent)it.next();
825                String compId = comp.getClientId();
826    
827                if (comp != target && _parts.contains(compId))
828                    return true;
829            }
830    
831            return false;
832        }
833    
834        /**
835         * {@inheritDoc}
836         */
837        public boolean explicitlyContains(IComponent target)
838        {
839            if (target == null)
840                return false;
841    
842            return _parts.contains(target.getId());
843        }
844    }