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 ' '>" + 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 }