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 ' '>\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 }