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