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}