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 java.io.IOException;
017    import java.io.PrintWriter;
018    import java.util.ArrayList;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.List;
022    import java.util.Map;
023    
024    import org.apache.hivemind.util.Defense;
025    import org.apache.tapestry.IComponent;
026    import org.apache.tapestry.IForm;
027    import org.apache.tapestry.IMarkupWriter;
028    import org.apache.tapestry.IPage;
029    import org.apache.tapestry.IRender;
030    import org.apache.tapestry.IRequestCycle;
031    import org.apache.tapestry.NestedMarkupWriter;
032    import org.apache.tapestry.engine.NullWriter;
033    import org.apache.tapestry.markup.MarkupWriterSource;
034    import org.apache.tapestry.markup.NestedMarkupWriterImpl;
035    import org.apache.tapestry.services.RequestLocaleManager;
036    import org.apache.tapestry.services.ResponseBuilder;
037    import org.apache.tapestry.services.ServiceConstants;
038    import org.apache.tapestry.util.ContentType;
039    import org.apache.tapestry.util.ScriptUtils;
040    import org.apache.tapestry.web.WebResponse;
041    
042    
043    /**
044     * Main class that handles dojo based ajax responses. These responses are wrapped
045     * by an xml document format that segments off invididual component/javascript response
046     * types into easy to manage xml elements that can then be interpreted and managed by 
047     * running client-side javascript.
048     * 
049     * @author jkuhnert
050     */
051    public class DojoAjaxResponseBuilder implements ResponseBuilder
052    {
053        // used to create IMarkupWriter
054        private RequestLocaleManager _localeManager;
055        private MarkupWriterSource _markupWriterSource;
056        private WebResponse _webResponse;
057        private List _errorPages;
058        
059        // our response writer
060        private IMarkupWriter _writer;
061        // Parts that will be updated.
062        private List _parts = new ArrayList();
063        // Map of specialized writers, like scripts
064        private Map _writers = new HashMap();
065        
066        private IRequestCycle _cycle;
067        
068        /**
069         * Creates a builder with a pre-configured {@link IMarkupWriter}. 
070         * Currently only used for testing.
071         * 
072         * @param writer
073         *          The markup writer to render all "good" content to.
074         * @param parts
075         *          A set of string ids of the components that may have 
076         *          their responses rendered.
077         */
078        public DojoAjaxResponseBuilder(IRequestCycle cycle, IMarkupWriter writer, List parts)
079        {
080            Defense.notNull(cycle, "cycle");
081            Defense.notNull(writer, "writer");
082            
083            _writer = writer;
084            _cycle = cycle;
085            
086            if (parts != null) 
087                _parts.addAll(parts);
088        }
089        
090        /**
091         * Creates a new response builder with the required services it needs
092         * to render the response when {@link #renderResponse(IRequestCycle)} is called.
093         * 
094         * @param localeManager 
095         *          Used to set the locale on the response.
096         * @param markupWriterSource
097         *          Creates IJSONWriter instance to be used.
098         * @param webResponse
099         *          Web response for output stream.
100         */
101        public DojoAjaxResponseBuilder(IRequestCycle cycle, 
102                RequestLocaleManager localeManager, 
103                MarkupWriterSource markupWriterSource,
104                WebResponse webResponse, List errorPages)
105        {
106            Defense.notNull(cycle, "cycle");
107            
108            _cycle = cycle;
109            _localeManager = localeManager;
110            _markupWriterSource = markupWriterSource;
111            _webResponse = webResponse;
112            _errorPages = errorPages;
113        }
114        
115        /**
116         * 
117         * {@inheritDoc}
118         */
119        public boolean isDynamic()
120        {
121            return Boolean.TRUE;
122        }
123        
124        /** 
125         * {@inheritDoc}
126         */
127        public void renderResponse(IRequestCycle cycle)
128            throws IOException
129        {
130            _localeManager.persistLocale();
131            
132            ContentType contentType = new ContentType(CONTENT_TYPE
133                    + ";charset=" + cycle.getInfrastructure().getOutputEncoding());
134            
135            String encoding = contentType.getParameter(ENCODING_KEY);
136            
137            if (encoding == null)
138            {
139                encoding = cycle.getEngine().getOutputEncoding();
140                
141                contentType.setParameter(ENCODING_KEY, encoding);
142            }
143            
144            PrintWriter printWriter = _webResponse.getPrintWriter(contentType);
145            
146            _writer = _markupWriterSource.newMarkupWriter(printWriter, contentType);
147            
148            parseParameters(cycle);
149            
150            beginResponse();
151            
152            // render response
153            cycle.renderPage(this);
154            
155            endResponse();
156            
157            _writer.close();
158        }
159        
160        /** 
161         * {@inheritDoc}
162         */
163        public void updateComponent(String id)
164        {
165            if (!_parts.contains(id))
166                _parts.add(id);
167        }
168        
169        /** 
170         * {@inheritDoc}
171         */
172        public IMarkupWriter getWriter()
173        {
174            return _writer;
175        }
176        
177        /** 
178         * {@inheritDoc}
179         */
180        public boolean isBodyScriptAllowed(IComponent target)
181        {
182            if (target != null 
183                    && IForm.class.isInstance(target)
184                    && ((IForm)target).isFormFieldUpdating())
185                return true;
186            
187            return contains(target);
188        }
189        
190        /** 
191         * {@inheritDoc}
192         */
193        public boolean isExternalScriptAllowed(IComponent target)
194        {
195            if (target != null 
196                    && IForm.class.isInstance(target)
197                    && ((IForm)target).isFormFieldUpdating())
198                return true;
199            
200            return contains(target);
201        }
202        
203        /** 
204         * {@inheritDoc}
205         */
206        public boolean isInitializationScriptAllowed(IComponent target)
207        {
208            if (target != null 
209                    && IForm.class.isInstance(target)
210                    && ((IForm)target).isFormFieldUpdating())
211                return true;
212            
213            return contains(target);
214        }
215        
216        /**
217         * {@inheritDoc}
218         */
219        public boolean isImageInitializationAllowed(IComponent target)
220        {
221            if (target != null 
222                    && IForm.class.isInstance(target)
223                    && ((IForm)target).isFormFieldUpdating())
224                return true;
225            
226            return contains(target);
227        }
228        
229        /** 
230         * {@inheritDoc}
231         */
232        public void beginBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
233        {
234            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
235            
236            writer.begin("script");
237            writer.printRaw("\n//<![CDATA[\n");
238        }
239        
240        /** 
241         * {@inheritDoc}
242         */
243        public void endBodyScript(IMarkupWriter normalWriter, IRequestCycle cycle)
244        {
245            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
246            
247            writer.printRaw("\n//]]>\n");
248            writer.end();
249        }
250        
251        /** 
252         * {@inheritDoc}
253         */
254        public void writeBodyScript(IMarkupWriter normalWriter, String script, IRequestCycle cycle)
255        {
256            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
257            
258            writer.printRaw(script);
259        }
260        
261        /** 
262         * {@inheritDoc}
263         */
264        public void writeExternalScript(IMarkupWriter normalWriter, String url, IRequestCycle cycle)
265        {
266            IMarkupWriter writer = getWriter(ResponseBuilder.INCLUDE_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
267            
268            // causes asset includes to be loaded dynamically into document head
269            writer.printRaw("tapestry.loadScriptFromUrl(\"");
270            writer.print(url);
271            writer.printRaw("\");");
272            
273            writer.println();
274        }
275        
276        /** 
277         * {@inheritDoc}
278         */
279        public void writeImageInitializations(IMarkupWriter normalWriter, String script, String preloadName, IRequestCycle cycle)
280        {
281            IMarkupWriter writer = getWriter(ResponseBuilder.BODY_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
282            
283            writer.printRaw("\n\nvar " + preloadName + " = new Array();\n");
284            writer.printRaw("if (document.images)\n");
285            writer.printRaw("{\n");
286            
287            writer.printRaw(script);
288            
289            writer.printRaw("}\n");
290        }
291        
292        /** 
293         * {@inheritDoc}
294         */
295        public void writeInitializationScript(IMarkupWriter normalWriter, String script)
296        {
297            IMarkupWriter writer = getWriter(ResponseBuilder.INITIALIZATION_SCRIPT, ResponseBuilder.SCRIPT_TYPE);
298            
299            writer.begin("script");
300            
301            // return is in XML so must escape any potentially non-xml compliant content
302            writer.printRaw("\n//<![CDATA[\n");
303            
304            writer.printRaw(script);
305            
306            writer.printRaw("\n//]]>\n");
307            
308            writer.end();
309        }
310        
311        /** 
312         * {@inheritDoc}
313         */
314        public void render(IMarkupWriter writer, IRender render, IRequestCycle cycle)
315        {
316            // must be a valid writer already
317            if (NestedMarkupWriterImpl.class.isInstance(writer)) {
318                render.render(writer, cycle);
319                return;
320            }
321            
322            // check for page exception renders and write content to writer so client can display them
323            if (IPage.class.isInstance(render)) {
324                String errorPage = getErrorPage(((IPage)render).getPageName());
325                if (errorPage != null) {
326                    render.render(getWriter(errorPage, EXCEPTION_TYPE), cycle);
327                    return;
328                }
329            }
330            
331            if (IComponent.class.isInstance(render)
332                    && contains((IComponent)render))
333            {
334                render.render(getComponentWriter((IComponent)render), cycle);
335                return;
336            }
337            
338            // Nothing else found, throw out response
339            render.render(NullWriter.getSharedInstance(), cycle);
340        }
341        
342        private String getErrorPage(String pageName)
343        {
344            for (int i=0; i < _errorPages.size(); i++) {
345                String page = (String)_errorPages.get(i);
346                
347                if (pageName.indexOf(page) > -1)
348                    return page;
349            }
350            
351            return null;
352        }
353        
354        /**
355         * Gets a {@link NestedMarkupWriter} for the specified
356         * component to write to and caches the buffer for later
357         * writing.
358         * 
359         * @param target
360         *          The component to get a writer for.
361         * @return An IMarkuPWriter instance.
362         */
363        IMarkupWriter getComponentWriter(IComponent target)
364        {
365            String id = getComponentId(target);
366            
367            return getWriter(id, ELEMENT_TYPE);
368        }
369        
370        /**
371         * 
372         * {@inheritDoc}
373         */
374        public IMarkupWriter getWriter(String id, String type)
375        {
376            Defense.notNull(id, "id can't be null");
377            
378            IMarkupWriter w = (IMarkupWriter)_writers.get(id);
379            if (w != null) 
380                return w;
381            
382            // Make component write to a "nested" writer
383            // so that element begin/ends don't conflict
384            // with xml element response begin/ends. This is very
385            // important.
386            
387            IMarkupWriter nestedWriter = _writer.getNestedWriter();
388            nestedWriter.begin("response");
389            nestedWriter.attribute("id", id);
390            if (type != null)
391                nestedWriter.attribute("type", type);
392            
393            _writers.put(id, nestedWriter);
394            
395            return nestedWriter;
396        }
397        
398        /**
399         * Called to start an ajax response. Writes xml doctype and starts
400         * the <code>ajax-response</code> element that will contain all of
401         * the returned content.
402         */
403        void beginResponse()
404        {
405            _writer.printRaw("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
406            _writer.printRaw("<!DOCTYPE html "
407                    + "PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" "
408                    + "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\" [\n"
409                    + "<!ENTITY nbsp '&#160;'>\n"
410                    + "]>\n");
411            _writer.printRaw("<ajax-response>");
412        }
413        
414        /**
415         * Called after the entire response has been captured. Causes
416         * the writer buffer output captured to be segmented and written
417         * out to the right response elements for the client libraries to parse.
418         */
419        void endResponse()
420        {
421            // write out captured content
422            Iterator keys = _writers.keySet().iterator();
423            while (keys.hasNext()) {
424                
425                String key = (String)keys.next();
426                NestedMarkupWriter nw = (NestedMarkupWriter)_writers.get(key);
427                
428                nw.end();
429                
430                if (!isScriptWriter(key))
431                    _writer.printRaw(ScriptUtils.ensureValidScriptTags(nw.getBuffer()));
432                else
433                    _writer.printRaw(nw.getBuffer());
434            }
435            
436            //end response
437            _writer.printRaw("</ajax-response>");
438            _writer.flush();
439        }
440        
441        /**
442         * Determines if the specified markup writer key is one of
443         * the pre-defined script keys from ResponseBuilder.
444         * 
445         * @param key
446         *          The key to check.
447         * @return True, if key is one of the ResponseBuilder keys. 
448         *         (BODY_SCRIPT,INCLUDE_SCRIPT,INITIALIZATION_SCRIPT)
449         */
450        boolean isScriptWriter(String key)
451        {
452            if (key == null) 
453                return false;
454            
455            if (ResponseBuilder.BODY_SCRIPT.equals(key)
456                    || ResponseBuilder.INCLUDE_SCRIPT.equals(key)
457                    || ResponseBuilder.INITIALIZATION_SCRIPT.equals(key))
458                return true;
459            
460            return false;
461        }
462        
463        /**
464         * Grabs the incoming parameters needed for json responses, most notable the
465         * {@link ServiceConstants#UPDATE_PARTS} parameter.
466         * 
467         * @param cycle
468         *            The request cycle to parse from
469         */
470        void parseParameters(IRequestCycle cycle)
471        {
472            Object[] updateParts = cycle.getParameters(ServiceConstants.UPDATE_PARTS);
473            
474            if (updateParts == null)
475                return;
476            
477            for(int i = 0; i < updateParts.length; i++)
478                _parts.add(updateParts[i].toString());
479        }
480        
481        /**
482         * Determines if the specified component is contained in the 
483         * responses requested update parts.
484         * @param target
485         *          The component to check for.
486         * @return True if the request should capture the components output.
487         */
488        public boolean contains(IComponent target)
489        {
490            if (target == null) 
491                return false;
492            
493            String id = getComponentId(target);
494            
495            if (_parts.contains(id))
496                return true;
497            
498            Iterator it = _cycle.renderStackIterator();
499            while (it.hasNext()) {
500                
501                IComponent comp = (IComponent)it.next();
502                String compId = getComponentId(comp);
503                
504                if (comp != target && _parts.contains(compId))
505                    return true;
506            }
507            
508            return false;
509        }
510        
511        /**
512         * Gets the id of the specified component, choosing the "id" element
513         * binding over any other id.
514         * @param comp
515         * @return The id of the component.
516         */
517        String getComponentId(IComponent comp)
518        {
519            return comp.getClientId();
520        }
521    }