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