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