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