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