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