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