001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.flex; 029 030import org.opencms.jsp.util.CmsJspStandardContextBean; 031import org.opencms.main.CmsIllegalArgumentException; 032import org.opencms.main.CmsLog; 033import org.opencms.main.OpenCms; 034import org.opencms.util.CmsDateUtil; 035import org.opencms.util.CmsRequestUtil; 036 037import java.io.BufferedWriter; 038import java.io.ByteArrayOutputStream; 039import java.io.IOException; 040import java.io.OutputStreamWriter; 041import java.io.PrintWriter; 042import java.net.URI; 043import java.net.URISyntaxException; 044import java.util.ArrayList; 045import java.util.HashMap; 046import java.util.Iterator; 047import java.util.List; 048import java.util.Map; 049 050import javax.servlet.ServletOutputStream; 051import javax.servlet.http.Cookie; 052import javax.servlet.http.HttpServletResponse; 053import javax.servlet.http.HttpServletResponseWrapper; 054 055import org.apache.commons.logging.Log; 056 057/** 058 * Wrapper class for a HttpServletResponse, required in order to process JSPs from the OpenCms VFS.<p> 059 * 060 * This class wraps the standard HttpServletResponse so that it's output can be delivered to 061 * the CmsFlexCache.<p> 062 * 063 * @since 6.0.0 064 */ 065public class CmsFlexResponse extends HttpServletResponseWrapper { 066 067 /** 068 * Wrapped implementation of the ServletOutputStream.<p> 069 * 070 * This implementation writes to an internal buffer and optionally to another 071 * output stream at the same time.<p> 072 * 073 * It should be fully transparent to the standard ServletOutputStream.<p> 074 */ 075 private static class CmsServletOutputStream extends ServletOutputStream { 076 077 /** The optional output stream to write to. */ 078 private ServletOutputStream m_servletStream; 079 080 /** The internal stream buffer. */ 081 private ByteArrayOutputStream m_stream; 082 083 /** 084 * Constructor that must be used if the stream should write 085 * only to a buffer.<p> 086 */ 087 public CmsServletOutputStream() { 088 089 m_servletStream = null; 090 clear(); 091 } 092 093 /** 094 * Constructor that must be used if the stream should write 095 * to a buffer and to another stream at the same time.<p> 096 * 097 * @param servletStream The stream to write to 098 */ 099 public CmsServletOutputStream(ServletOutputStream servletStream) { 100 101 m_servletStream = servletStream; 102 clear(); 103 } 104 105 /** 106 * Clears the buffer by initializing the buffer with a new stream.<p> 107 */ 108 public void clear() { 109 110 m_stream = new java.io.ByteArrayOutputStream(1024); 111 } 112 113 /** 114 * @see java.io.OutputStream#close() 115 */ 116 @Override 117 public void close() throws IOException { 118 119 if (m_stream != null) { 120 m_stream.close(); 121 } 122 if (m_servletStream != null) { 123 m_servletStream.close(); 124 } 125 super.close(); 126 } 127 128 /** 129 * @see java.io.OutputStream#flush() 130 */ 131 @Override 132 public void flush() throws IOException { 133 134 if (LOG.isDebugEnabled()) { 135 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_FLUSHED_1, m_servletStream)); 136 } 137 if (m_servletStream != null) { 138 m_servletStream.flush(); 139 } 140 } 141 142 /** 143 * Provides access to the bytes cached in the buffer.<p> 144 * 145 * @return the cached bytes from the buffer 146 */ 147 public byte[] getBytes() { 148 149 return m_stream.toByteArray(); 150 } 151 152 /** 153 * @see java.io.OutputStream#write(byte[], int, int) 154 */ 155 @Override 156 public void write(byte[] b, int off, int len) throws IOException { 157 158 m_stream.write(b, off, len); 159 if (m_servletStream != null) { 160 m_servletStream.write(b, off, len); 161 } 162 } 163 164 /** 165 * @see java.io.OutputStream#write(int) 166 */ 167 @Override 168 public void write(int b) throws IOException { 169 170 m_stream.write(b); 171 if (m_servletStream != null) { 172 m_servletStream.write(b); 173 } 174 } 175 } 176 177 /** The cache delimiter char. */ 178 public static final char FLEX_CACHE_DELIMITER = (char)0; 179 180 /** Static string to indicate a header is "set" in the header maps. */ 181 public static final String SET_HEADER = "[setHeader]"; 182 183 /** The log object for this class. */ 184 protected static final Log LOG = CmsLog.getLog(CmsFlexResponse.class); 185 186 /** Map to save response headers belonging to a single include call in .*/ 187 private Map<String, List<String>> m_bufferHeaders; 188 189 /** String to hold a buffered redirect target. */ 190 private String m_bufferRedirect; 191 192 /** Byte array used for "cached leafs" optimization. */ 193 private byte[] m_cacheBytes; 194 195 /** The cached entry that is constructed from this response. */ 196 private CmsFlexCacheEntry m_cachedEntry; 197 198 /** Indicates if caching is required, will always be true if m_writeOnlyToBuffer is true. */ 199 private boolean m_cachingRequired; 200 201 /** The CmsFlexController for this response. */ 202 private CmsFlexController m_controller; 203 204 /** The encoding to use for the response. */ 205 private String m_encoding; 206 207 /** Map to save all response headers (including sub-elements) in. */ 208 private Map<String, List<String>> m_headers; 209 210 /** A list of include calls that origin from this page, i.e. these are sub elements of this element. */ 211 private List<String> m_includeList; 212 213 /** A list of attributes that belong to the include calls. */ 214 private List<Map<String, Object>> m_includeListAttributes; 215 216 /** A list of parameters that belong to the include calls. */ 217 private List<Map<String, String[]>> m_includeListParameters; 218 219 /** Indicates if this element is currently in include mode, i.e. processing a sub-element. */ 220 private boolean m_includeMode; 221 222 /** A list of results from the inclusions, needed because of JSP buffering. */ 223 private List<byte[]> m_includeResults; 224 225 /** Flag to indicate if this is the top level element or an included sub - element. */ 226 private boolean m_isTopElement; 227 228 /** The CmsFlexCacheKey for this response. */ 229 private CmsFlexCacheKey m_key; 230 231 /** A special wrapper class for a ServletOutputStream. */ 232 private CmsFlexResponse.CmsServletOutputStream m_out; 233 234 /** Indicates that parent stream is writing only in the buffer. */ 235 private boolean m_parentWritesOnlyToBuffer; 236 237 /** The wrapped ServletResponse. */ 238 private HttpServletResponse m_res; 239 240 /** Indicates if this response is suspended (probably because of a redirect). */ 241 private boolean m_suspended; 242 243 /** State bit indicating whether content type has been set, type may only be set once according to spec. */ 244 private boolean m_typeSet; 245 246 /** Indicates that the OutputStream m_out should write ONLY in the buffer. */ 247 private boolean m_writeOnlyToBuffer; 248 249 /** A print writer that writes in the m_out stream. */ 250 private java.io.PrintWriter m_writer; 251 252 /** 253 * Constructor for the CmsFlexResponse, 254 * this variation one is usually used to wrap responses for further include calls in OpenCms.<p> 255 * 256 * @param res the CmsFlexResponse to wrap 257 * @param controller the controller to use 258 */ 259 public CmsFlexResponse(HttpServletResponse res, CmsFlexController controller) { 260 261 super(res); 262 m_res = res; 263 m_controller = controller; 264 m_encoding = controller.getCurrentResponse().getEncoding(); 265 m_isTopElement = controller.getCurrentResponse().isTopElement(); 266 m_parentWritesOnlyToBuffer = controller.getCurrentResponse().hasIncludeList() && !controller.isForwardMode(); 267 setOnlyBuffering(m_parentWritesOnlyToBuffer); 268 m_headers = new HashMap<String, List<String>>(16); 269 m_bufferHeaders = new HashMap<String, List<String>>(8); 270 } 271 272 /** 273 * Constructor for the CmsFlexResponse, 274 * this variation is usually used for the "top" response.<p> 275 * 276 * @param res the HttpServletResponse to wrap 277 * @param controller the controller to use 278 * @param streaming indicates if streaming should be enabled or not 279 * @param isTopElement indicates if this is the top element of an include cascade 280 */ 281 public CmsFlexResponse( 282 HttpServletResponse res, 283 CmsFlexController controller, 284 boolean streaming, 285 boolean isTopElement) { 286 287 super(res); 288 m_res = res; 289 m_controller = controller; 290 m_encoding = controller.getCmsObject().getRequestContext().getEncoding(); 291 m_isTopElement = isTopElement; 292 m_parentWritesOnlyToBuffer = !streaming && !controller.isForwardMode(); 293 setOnlyBuffering(m_parentWritesOnlyToBuffer); 294 m_headers = new HashMap<String, List<String>>(16); 295 m_bufferHeaders = new HashMap<String, List<String>>(8); 296 } 297 298 /** 299 * Process the headers stored in the provided map and add them to the response.<p> 300 * 301 * @param headers the headers to add 302 * @param res the response to add the headers to 303 */ 304 public static void processHeaders(Map<String, List<String>> headers, HttpServletResponse res) { 305 306 if (headers != null) { 307 Iterator<Map.Entry<String, List<String>>> i = headers.entrySet().iterator(); 308 while (i.hasNext()) { 309 Map.Entry<String, List<String>> entry = i.next(); 310 String key = entry.getKey(); 311 List<String> l = entry.getValue(); 312 for (int j = 0; j < l.size(); j++) { 313 if ((j == 0) && ((l.get(0)).startsWith(SET_HEADER))) { 314 String s = l.get(0); 315 res.setHeader(key, s.substring(SET_HEADER.length())); 316 } else { 317 res.addHeader(key, l.get(j)); 318 } 319 } 320 } 321 } 322 } 323 324 /** 325 * Method overloaded from the standard HttpServletRequest API.<p> 326 * 327 * Cookies must be set directly as a header, otherwise they might not be set 328 * in the super class.<p> 329 * 330 * @see javax.servlet.http.HttpServletResponseWrapper#addCookie(javax.servlet.http.Cookie) 331 */ 332 @Override 333 public void addCookie(Cookie cookie) { 334 335 if (cookie == null) { 336 throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_ADD_COOKIE_0)); 337 } 338 339 StringBuffer header = new StringBuffer(128); 340 341 // name and value 342 header.append(cookie.getName()); 343 header.append('='); 344 header.append(cookie.getValue()); 345 346 // add version 1 / RFC 2109 specific information 347 if (cookie.getVersion() == 1) { 348 header.append("; Version=1"); 349 350 // comment 351 if (cookie.getComment() != null) { 352 header.append("; Comment="); 353 header.append(cookie.getComment()); 354 } 355 } 356 357 // domain 358 if (cookie.getDomain() != null) { 359 header.append("; Domain="); 360 header.append(cookie.getDomain()); 361 } 362 363 // max-age / expires 364 if (cookie.getMaxAge() >= 0) { 365 if (cookie.getVersion() == 0) { 366 // old Netscape format 367 header.append("; Expires="); 368 long time; 369 if (cookie.getMaxAge() == 0) { 370 time = 10000L; 371 } else { 372 time = System.currentTimeMillis() + (cookie.getMaxAge() * 1000L); 373 } 374 header.append(CmsDateUtil.getOldCookieDate(time)); 375 } else { 376 // new RFC 2109 format 377 header.append("; Max-Age="); 378 header.append(cookie.getMaxAge()); 379 } 380 } 381 382 // path 383 if (cookie.getPath() != null) { 384 header.append("; Path="); 385 header.append(cookie.getPath()); 386 } 387 388 // secure 389 if (cookie.getSecure()) { 390 header.append("; Secure"); 391 } 392 393 addHeader("Set-Cookie", header.toString()); 394 } 395 396 /** 397 * Method overload from the standard HttpServletRequest API.<p> 398 * 399 * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long) 400 */ 401 @Override 402 public void addDateHeader(String name, long date) { 403 404 addHeader(name, CmsDateUtil.getHeaderDate(date)); 405 } 406 407 /** 408 * Method overload from the standard HttpServletRequest API.<p> 409 * 410 * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String) 411 */ 412 @Override 413 public void addHeader(String name, String value) { 414 415 if (isSuspended()) { 416 return; 417 } 418 419 if (CmsRequestUtil.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) { 420 setContentType(value); 421 return; 422 } 423 424 if (m_cachingRequired && !m_includeMode) { 425 addHeaderList(m_bufferHeaders, name, value); 426 if (LOG.isDebugEnabled()) { 427 LOG.debug( 428 Messages.get().getBundle().key( 429 Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_ELEMENT_BUFFER_2, 430 name, 431 value)); 432 } 433 } 434 435 if (m_writeOnlyToBuffer) { 436 addHeaderList(m_headers, name, value); 437 if (LOG.isDebugEnabled()) { 438 LOG.debug( 439 Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_HEADERS_2, name, value)); 440 } 441 } else { 442 if (LOG.isDebugEnabled()) { 443 LOG.debug( 444 Messages.get().getBundle().key( 445 Messages.LOG_FLEXRESPONSE_ADDING_HEADER_TO_PARENT_RESPONSE_2, 446 name, 447 value)); 448 } 449 m_res.addHeader(name, value); 450 } 451 } 452 453 /** 454 * Method overload from the standard HttpServletRequest API.<p> 455 * 456 * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int) 457 */ 458 @Override 459 public void addIntHeader(String name, int value) { 460 461 addHeader(name, String.valueOf(value)); 462 } 463 464 /** 465 * Adds an inclusion target to the list of include results.<p> 466 * 467 * Should be used only in inclusion-scenarios 468 * like the JSP cms:include tag processing.<p> 469 * 470 * @param target the include target name to add 471 * @param parameterMap the map of parameters given with the include command 472 * @param attributeMap the map of attributes given with the include command 473 */ 474 public void addToIncludeList(String target, Map<String, String[]> parameterMap, Map<String, Object> attributeMap) { 475 476 if (m_includeList == null) { 477 m_includeList = new ArrayList<String>(10); 478 m_includeListParameters = new ArrayList<Map<String, String[]>>(10); 479 m_includeListAttributes = new ArrayList<Map<String, Object>>(10); 480 } 481 // never cache some request attributes, e.g. the Flex controller 482 m_controller.removeUncacheableAttributes(attributeMap); 483 // only cache a copy of the JSP standard context bean 484 CmsJspStandardContextBean bean = (CmsJspStandardContextBean)attributeMap.get( 485 CmsJspStandardContextBean.ATTRIBUTE_NAME); 486 if (bean != null) { 487 attributeMap.put(CmsJspStandardContextBean.ATTRIBUTE_NAME, bean.createCopy()); 488 } 489 490 m_includeListAttributes.add(attributeMap); 491 m_includeListParameters.add(parameterMap); 492 m_includeList.add(target); 493 } 494 495 /** 496 * @see javax.servlet.ServletResponseWrapper#flushBuffer() 497 */ 498 @Override 499 public void flushBuffer() throws IOException { 500 501 if (OpenCms.getSystemInfo().getServletContainerSettings().isPreventResponseFlush()) { 502 // Websphere does not allow to set headers afterwards, so we have to prevent this call 503 return; 504 } 505 super.flushBuffer(); 506 } 507 508 /** 509 * Returns the value of the encoding used for this response.<p> 510 * 511 * @return the value of the encoding used for this response 512 */ 513 public String getEncoding() { 514 515 return m_encoding; 516 } 517 518 /** 519 * Provides access to the header cache of the top wrapper.<p> 520 * 521 * @return the Map of cached headers 522 */ 523 public Map<String, List<String>> getHeaders() { 524 525 return m_headers; 526 } 527 528 /** 529 * Method overload from the standard HttpServletRequest API.<p> 530 * 531 * @see javax.servlet.ServletResponse#getOutputStream() 532 */ 533 @Override 534 public ServletOutputStream getOutputStream() throws IOException { 535 536 if (m_out == null) { 537 initStream(); 538 } 539 return m_out; 540 } 541 542 /** 543 * Method overload from the standard HttpServletRequest API.<p> 544 * 545 * @see javax.servlet.ServletResponse#getWriter() 546 */ 547 @Override 548 public PrintWriter getWriter() throws IOException { 549 550 if (m_writer == null) { 551 initStream(); 552 } 553 return m_writer; 554 } 555 556 /** 557 * Returns the bytes that have been written on the current writers output stream.<p> 558 * 559 * @return the bytes that have been written on the current writers output stream 560 */ 561 public byte[] getWriterBytes() { 562 563 if (isSuspended()) { 564 // No output whatsoever if the response is suspended 565 return new byte[0]; 566 } 567 if (m_cacheBytes != null) { 568 // Optimization for cached "leaf" nodes, here I re-use the array from the cache 569 return m_cacheBytes; 570 } 571 if (m_out == null) { 572 // No output was written so far, just return an empty array 573 return new byte[0]; 574 } 575 if (m_writer != null) { 576 // Flush the writer in case something was written on it 577 m_writer.flush(); 578 } 579 return m_out.getBytes(); 580 } 581 582 /** 583 * This flag indicates if the response is suspended or not.<p> 584 * 585 * A suspended response must not write further output to any stream or 586 * process a cache entry for itself.<p> 587 * 588 * Currently, a response is only suspended if it is redirected.<p> 589 * 590 * @return true if the response is suspended, false otherwise 591 */ 592 public boolean isSuspended() { 593 594 return m_suspended; 595 } 596 597 /** 598 * Returns <code>true</code> if this response has been constructed for the 599 * top level element of this request, <code>false</code> if it was 600 * constructed for an included sub-element.<p> 601 * 602 * @return <code>true</code> if this response has been constructed for the 603 * top level element of this request, <code>false</code> if it was 604 * constructed for an included sub-element. 605 */ 606 public boolean isTopElement() { 607 608 return m_isTopElement; 609 } 610 611 /** 612 * Method overload from the standard HttpServletRequest API.<p> 613 * 614 * @see javax.servlet.http.HttpServletResponse#sendRedirect(java.lang.String) 615 * 616 * @throws IllegalArgumentException In case of a malformed location string 617 */ 618 @SuppressWarnings("unused") 619 @Override 620 public void sendRedirect(String location) throws IOException { 621 622 // Ignore any redirects after the first one 623 if (isSuspended() && (!location.equals(m_bufferRedirect))) { 624 return; 625 } 626 if (LOG.isDebugEnabled()) { 627 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SENDREDIRECT_1, location)); 628 } 629 if (m_cachingRequired && !m_includeMode) { 630 m_bufferRedirect = location; 631 } 632 633 if (!m_cachingRequired) { 634 // If caching is required a cached entry will be constructed first and redirect will 635 // be called after this is completed and stored in the cache 636 if (LOG.isDebugEnabled()) { 637 LOG.debug( 638 Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_TOPRESPONSE_SENDREDIRECT_1, location)); 639 } 640 if (LOG.isWarnEnabled()) { 641 if (m_controller.getResponseStackSize() > 2) { 642 // sendRedirect in a stacked response scenario, this may cause issues in some app servers 643 LOG.warn( 644 Messages.get().getBundle().key( 645 Messages.LOG_FLEXRESPONSE_REDIRECTWARNING_3, 646 m_controller.getCmsResource().getRootPath(), 647 m_controller.getCurrentRequest().getElementUri(), 648 location)); 649 } 650 } 651 652 try { 653 // Checking for possible illegal characters (for example, XSS exploits) before sending the redirect 654 // The constructor is key here. That method will throw an URISyntaxException if the URL 655 // format is not according to standards (e.g. contains illegal characters, like spaces, < or >, etc). 656 new URI(location); 657 } catch (URISyntaxException e) { 658 // Deliberately NOT passing the original exception, since the URISyntaxException contains the full path, 659 // which may include the XSS attempt 660 LOG.error(Messages.get().getBundle().key(Messages.ERR_FLEXRESPONSE_URI_SYNTAX_EXCEPTION_0), e); 661 throw new IllegalArgumentException("Illegal or malformed characters found in path"); 662 } 663 664 // use top response for redirect 665 HttpServletResponse topRes = m_controller.getTopResponse(); 666 // add all headers found to make sure cookies can be set before redirect 667 processHeaders(getHeaders(), topRes); 668 topRes.sendRedirect(location); 669 } 670 671 m_controller.suspendFlexResponse(); 672 } 673 674 /** 675 * Method overload from the standard HttpServletRequest API.<p> 676 * 677 * @see javax.servlet.ServletResponse#setContentType(java.lang.String) 678 */ 679 @Override 680 public void setContentType(String type) { 681 682 if (LOG.isDebugEnabled()) { 683 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SETTING_CONTENTTYPE_1, type)); 684 } 685 // only if this is the "Top-Level" element, do set the content type 686 // otherwise an included JSP could reset the type with some unwanted defaults 687 if (!m_typeSet && m_isTopElement) { 688 // type must be set only once, otherwise some Servlet containers (not Tomcat) generate errors 689 m_typeSet = true; 690 super.setContentType(type); 691 return; 692 } 693 } 694 695 /** 696 * Method overload from the standard HttpServletRequest API.<p> 697 * 698 * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long) 699 */ 700 @Override 701 public void setDateHeader(String name, long date) { 702 703 setHeader(name, CmsDateUtil.getHeaderDate(date)); 704 } 705 706 /** 707 * Method overload from the standard HttpServletRequest API.<p> 708 * 709 * @see javax.servlet.http.HttpServletResponse#setHeader(java.lang.String, java.lang.String) 710 */ 711 @Override 712 public void setHeader(String name, String value) { 713 714 if (isSuspended()) { 715 return; 716 } 717 718 if (CmsRequestUtil.HEADER_CONTENT_TYPE.equalsIgnoreCase(name)) { 719 setContentType(value); 720 return; 721 } 722 723 if (m_cachingRequired && !m_includeMode) { 724 setHeaderList(m_bufferHeaders, name, value); 725 if (LOG.isDebugEnabled()) { 726 LOG.debug( 727 Messages.get().getBundle().key( 728 Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_ELEMENT_BUFFER_2, 729 name, 730 value)); 731 } 732 } 733 734 if (m_writeOnlyToBuffer) { 735 setHeaderList(m_headers, name, value); 736 if (LOG.isDebugEnabled()) { 737 LOG.debug( 738 Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_HEADERS_2, name, value)); 739 } 740 } else { 741 if (LOG.isDebugEnabled()) { 742 LOG.debug( 743 Messages.get().getBundle().key( 744 Messages.LOG_FLEXRESPONSE_SETTING_HEADER_IN_PARENT_RESPONSE_2, 745 name, 746 value)); 747 } 748 m_res.setHeader(name, value); 749 } 750 } 751 752 /** 753 * Method overload from the standard HttpServletRequest API.<p> 754 * 755 * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int) 756 */ 757 @Override 758 public void setIntHeader(String name, int value) { 759 760 setHeader(name, "" + value); 761 } 762 763 /** 764 * Sets buffering status of the response.<p> 765 * 766 * This must be done before the first output is written. 767 * Buffering is needed to process elements that can not be written 768 * directly to the output stream because their sub - elements have to 769 * be processed separately. Which is so far true only for JSP pages.<p> 770 * 771 * If buffering is on, nothing is written to the output stream 772 * even if streaming for this response is enabled.<p> 773 * 774 * @param value the value to set 775 */ 776 public void setOnlyBuffering(boolean value) { 777 778 m_writeOnlyToBuffer = value && !m_controller.isForwardMode(); 779 780 if (m_writeOnlyToBuffer) { 781 setCmsCachingRequired(true); 782 } 783 } 784 785 /** 786 * Adds some bytes to the list of include results.<p> 787 * 788 * Should be used only in inclusion-scenarios 789 * like the JSP cms:include tag processing.<p> 790 * 791 * @param result the byte array to add 792 */ 793 void addToIncludeResults(byte[] result) { 794 795 if (m_includeResults == null) { 796 m_includeResults = new ArrayList<byte[]>(10); 797 } 798 m_includeResults.add(result); 799 } 800 801 /** 802 * Returns the cache key for to this response.<p> 803 * 804 * @return the cache key for to this response 805 */ 806 CmsFlexCacheKey getCmsCacheKey() { 807 808 return m_key; 809 } 810 811 /** 812 * Is used to check if the response has an include list, 813 * which indicates a) it is probably processing a JSP element 814 * and b) it can never be streamed and always must be buffered.<p> 815 * 816 * @return true if this response has an include list, false otherwise 817 */ 818 boolean hasIncludeList() { 819 820 return m_includeList != null; 821 } 822 823 /** 824 * Generates a CmsFlexCacheEntry from the current response using the 825 * stored include results.<p> 826 * 827 * In case the results were written only to the buffer until now, 828 * they are now re-written on the output stream, with all included 829 * elements.<p> 830 * 831 * @throws IOException in case something goes wrong while writing to the output stream 832 * 833 * @return the generated cache entry 834 */ 835 CmsFlexCacheEntry processCacheEntry() throws IOException { 836 837 if (isSuspended() && (m_bufferRedirect == null)) { 838 // an included element redirected this response, no cache entry must be produced 839 return null; 840 } 841 if (m_cachingRequired) { 842 // cache entry must only be calculated if it's actually needed (always true if we write only to buffer) 843 m_cachedEntry = new CmsFlexCacheEntry(); 844 if (m_bufferRedirect != null) { 845 // only set et cached redirect target 846 m_cachedEntry.setRedirect(m_bufferRedirect); 847 } else { 848 // add cached headers 849 m_cachedEntry.addHeaders(m_bufferHeaders); 850 // add cached output 851 if (m_includeList != null) { 852 // probably JSP: we must analyze out stream for includes calls 853 // also, m_writeOnlyToBuffer must be "true" or m_includeList can not be != null 854 processIncludeList(); 855 } else { 856 // output is delivered directly, no include call parsing required 857 m_cachedEntry.add(getWriterBytes()); 858 } 859 } 860 // update the "last modified" date for the cache entry 861 m_cachedEntry.complete(); 862 } 863 // in case the output was only buffered we have to re-write it to the "right" stream 864 if (m_writeOnlyToBuffer) { 865 866 // since we are processing a cache entry caching is not required 867 m_cachingRequired = false; 868 869 if (m_bufferRedirect != null) { 870 // send buffered redirect, will trigger redirect of top response 871 sendRedirect(m_bufferRedirect); 872 } else { 873 // process the output 874 if (m_parentWritesOnlyToBuffer) { 875 // write results back to own stream, headers are already in buffer 876 if (m_out != null) { 877 try { 878 m_out.clear(); 879 } catch (Exception e) { 880 if (LOG.isDebugEnabled()) { 881 LOG.debug( 882 Messages.get().getBundle().key( 883 Messages.LOG_FLEXRESPONSE_ERROR_FLUSHING_OUTPUT_STREAM_1, 884 e)); 885 } 886 } 887 } else { 888 if (LOG.isDebugEnabled()) { 889 LOG.debug( 890 Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ERROR_OUTPUT_STREAM_NULL_0)); 891 } 892 } 893 writeCachedResultToStream(this); 894 } else { 895 // we can use the parent stream 896 processHeaders(m_headers, m_res); 897 writeCachedResultToStream(m_res); 898 } 899 } 900 } 901 return m_cachedEntry; 902 } 903 904 /** 905 * Sets the cache key for this response from 906 * a pre-calculated cache key.<p> 907 * 908 * @param value the cache key to set 909 */ 910 void setCmsCacheKey(CmsFlexCacheKey value) { 911 912 m_key = value; 913 } 914 915 /** 916 * Sets the cache key for this response, which is calculated 917 * from the provided parameters.<p> 918 * 919 * @param resourcename the target resource for which to create the cache key 920 * @param cacheDirectives the cache directives of the resource (value of the property "cache") 921 * @param online indicates if this resource is online or offline 922 * 923 * @return the generated cache key 924 * 925 * @throws CmsFlexCacheException in case the value String had a parse error 926 */ 927 CmsFlexCacheKey setCmsCacheKey(String resourcename, String cacheDirectives, boolean online) 928 throws CmsFlexCacheException { 929 930 m_key = new CmsFlexCacheKey(resourcename, cacheDirectives, online); 931 if (m_key.hadParseError()) { 932 // We throw the exception here to make sure this response has a valid key (cache=never) 933 throw new CmsFlexCacheException( 934 Messages.get().container( 935 Messages.LOG_FLEXRESPONSE_PARSE_ERROR_IN_CACHE_KEY_2, 936 cacheDirectives, 937 resourcename)); 938 } 939 return m_key; 940 } 941 942 /** 943 * Set caching status for this response.<p> 944 * 945 * Will always be set to <code>"true"</code> if setOnlyBuffering() is set to <code>"true"</code>. 946 * Currently this is an optimization for non - JSP elements that 947 * are known not to be cachable.<p> 948 * 949 * @param value the value to set 950 */ 951 void setCmsCachingRequired(boolean value) { 952 953 m_cachingRequired = (value || m_writeOnlyToBuffer) && !m_controller.isForwardMode(); 954 } 955 956 /** 957 * This flag indicates to the response if it is in "include mode" or not.<p> 958 * 959 * This is important in case a cache entry is constructed, 960 * since the cache entry must not consist of output or headers of the 961 * included elements.<p> 962 * 963 * @param value the value to set 964 */ 965 void setCmsIncludeMode(boolean value) { 966 967 m_includeMode = value; 968 } 969 970 /** 971 * Sets the suspended status of the response, and also sets 972 * the suspend status of all responses wrapping this response.<p> 973 * 974 * A suspended response must not write further output to any stream or 975 * process a cache entry for itself.<p> 976 * 977 * @param value the value to set 978 */ 979 void setSuspended(boolean value) { 980 981 m_suspended = value; 982 } 983 984 /** 985 * Writes some bytes to the current output stream, 986 * this method should be called from CmsFlexCacheEntry.service() only.<p> 987 * 988 * @param bytes an array of bytes 989 * @param useArray indicates that the byte array should be used directly 990 * 991 * @throws IOException in case something goes wrong while writing to the stream 992 */ 993 void writeToOutputStream(byte[] bytes, boolean useArray) throws IOException { 994 995 if (isSuspended()) { 996 return; 997 } 998 if (m_writeOnlyToBuffer) { 999 if (useArray) { 1000 // This cached entry has no sub-elements (it a "leaf") and so we can just use it's bytes 1001 m_cacheBytes = bytes; 1002 } else { 1003 if (m_out == null) { 1004 initStream(); 1005 } 1006 // In this case the buffer will not write to the servlet stream, but to it's internal buffer only 1007 m_out.write(bytes); 1008 } 1009 } else { 1010 if (LOG.isDebugEnabled()) { 1011 LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXRESPONSE_ERROR_WRITING_TO_OUTPUT_STREAM_0)); 1012 } 1013 // The request is not buffered, so we can write directly to it's parents output stream 1014 m_res.getOutputStream().write(bytes); 1015 m_res.getOutputStream().flush(); 1016 } 1017 } 1018 1019 /** 1020 * Helper method to add a value in the internal header list.<p> 1021 * 1022 * @param headers the headers to look up the value in 1023 * @param name the name to look up 1024 * @param value the value to set 1025 */ 1026 private void addHeaderList(Map<String, List<String>> headers, String name, String value) { 1027 1028 List<String> values = headers.get(name); 1029 if (values == null) { 1030 values = new ArrayList<String>(); 1031 headers.put(name, values); 1032 } 1033 values.add(value); 1034 } 1035 1036 /** 1037 * Initializes the current responses output stream 1038 * and the corresponding print writer.<p> 1039 * 1040 * @throws IOException in case something goes wrong while initializing 1041 */ 1042 private void initStream() throws IOException { 1043 1044 if (m_out == null) { 1045 if (!m_writeOnlyToBuffer) { 1046 // we can use the parents output stream 1047 if (m_cachingRequired || (m_controller.getResponseStackSize() > 1)) { 1048 // we are allowed to cache our results (probably to construct a new cache entry) 1049 m_out = new CmsFlexResponse.CmsServletOutputStream(m_res.getOutputStream()); 1050 } else { 1051 // we are not allowed to cache so we just use the parents output stream 1052 m_out = (CmsFlexResponse.CmsServletOutputStream)m_res.getOutputStream(); 1053 } 1054 } else { 1055 // construct a "buffer only" output stream 1056 m_out = new CmsFlexResponse.CmsServletOutputStream(); 1057 } 1058 } 1059 if (m_writer == null) { 1060 // create a PrintWriter that uses the encoding required for the request context 1061 m_writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(m_out, m_encoding)), false); 1062 } 1063 } 1064 1065 /** 1066 * This method is needed to process pages that can NOT be analyzed 1067 * directly during delivering (like JSP) because they write to 1068 * their own buffer.<p> 1069 * 1070 * In this case, we don't actually write output of include calls to the stream. 1071 * Where there are include calls we write a <code>{@link #FLEX_CACHE_DELIMITER}</code> char on the stream 1072 * to indicate that at this point the output of the include must be placed later. 1073 * The include targets (resource names) are then saved in the m_includeList.<p> 1074 * 1075 * This method must be called after the complete page has been processed. 1076 * It will contain the output of the page only (no includes), 1077 * with <code>{@link #FLEX_CACHE_DELIMITER}</code> chars were the include calls should be placed. 1078 * What we do here is analyze the output and cut it in parts 1079 * of <code>byte[]</code> arrays which then are saved in the resulting cache entry. 1080 * For the includes, we just save the name of the resource in 1081 * the cache entry.<p> 1082 * 1083 * If caching is disabled this method is just not called.<p> 1084 */ 1085 private void processIncludeList() { 1086 1087 byte[] result = getWriterBytes(); 1088 if (!hasIncludeList()) { 1089 // no include list, so no includes and we just use the bytes as they are in one block 1090 m_cachedEntry.add(result); 1091 } else { 1092 // process the include list 1093 int max = result.length; 1094 int pos = 0; 1095 int last = 0; 1096 int size = 0; 1097 int count = 0; 1098 1099 // work through result and split this with include list calls 1100 int i = 0; 1101 while ((i < m_includeList.size()) && (pos < max)) { 1102 // look for the first FLEX_CACHE_DELIMITER char 1103 while ((pos < max) && (result[pos] != FLEX_CACHE_DELIMITER)) { 1104 pos++; 1105 } 1106 if ((pos < max) && (result[pos] == FLEX_CACHE_DELIMITER)) { 1107 count++; 1108 // a byte value of C_FLEX_CACHE_DELIMITER in our (String) output list indicates 1109 // that the next include call must be placed here 1110 size = pos - last; 1111 if (size > 0) { 1112 // if not (it might be 0) there would be 2 include calls back 2 back 1113 byte[] piece = new byte[size]; 1114 System.arraycopy(result, last, piece, 0, size); 1115 // add the byte array to the cache entry 1116 m_cachedEntry.add(piece); 1117 piece = null; 1118 } 1119 last = ++pos; 1120 // add an include call to the cache entry 1121 m_cachedEntry.add( 1122 m_includeList.get(i), 1123 m_includeListParameters.get(i), 1124 m_includeListAttributes.get(i)); 1125 i++; 1126 } 1127 } 1128 if (pos < max) { 1129 // there is content behind the last include call 1130 size = max - pos; 1131 byte[] piece = new byte[size]; 1132 System.arraycopy(result, pos, piece, 0, size); 1133 m_cachedEntry.add(piece); 1134 piece = null; 1135 } 1136 if (i >= m_includeList.size()) { 1137 // clear the include list if all include calls are handled 1138 m_includeList = null; 1139 m_includeListParameters = null; 1140 m_includeListAttributes = null; 1141 } else { 1142 // if something is left, remove the processed entries 1143 m_includeList = m_includeList.subList(count, m_includeList.size()); 1144 m_includeListParameters = m_includeListParameters.subList(count, m_includeListParameters.size()); 1145 m_includeListAttributes = m_includeListAttributes.subList(count, m_includeListAttributes.size()); 1146 } 1147 } 1148 } 1149 1150 /** 1151 * Helper method to set a value in the internal header list. 1152 * 1153 * @param headers the headers to set the value in 1154 * @param name the name to set 1155 * @param value the value to set 1156 */ 1157 private void setHeaderList(Map<String, List<String>> headers, String name, String value) { 1158 1159 List<String> values = new ArrayList<String>(); 1160 values.add(SET_HEADER + value); 1161 headers.put(name, values); 1162 } 1163 1164 /** 1165 * This delivers cached sub-elements back to the stream. 1166 * Needed to overcome JSP buffering.<p> 1167 * 1168 * @param res the response to write the cached results to 1169 * 1170 * @throws IOException in case something goes wrong writing to the responses output stream 1171 */ 1172 private void writeCachedResultToStream(HttpServletResponse res) throws IOException { 1173 1174 List<Object> elements = m_cachedEntry.elements(); 1175 int count = 0; 1176 if (elements != null) { 1177 for (int i = 0; i < elements.size(); i++) { 1178 Object o = elements.get(i); 1179 if (o instanceof byte[]) { 1180 res.getOutputStream().write((byte[])o); 1181 } else { 1182 if ((m_includeResults != null) && (m_includeResults.size() > count)) { 1183 // make sure that we don't run behind end of list (should never happen, though) 1184 res.getOutputStream().write(m_includeResults.get(count)); 1185 count++; 1186 } 1187 // skip next entry, which is the parameter map for this include call 1188 i++; 1189 // skip next entry, which is the attribute map for this include call 1190 i++; 1191 } 1192 } 1193 } 1194 } 1195}