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 }