001// Copyright 2009, 2012 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.internal.gzip;
016
017import org.apache.tapestry5.internal.InternalConstants;
018import org.apache.tapestry5.services.assets.CompressionAnalyzer;
019
020import javax.servlet.ServletOutputStream;
021import javax.servlet.http.HttpServletResponse;
022import java.io.BufferedOutputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.util.zip.GZIPOutputStream;
027
028/**
029 * A buffered output stream that, when a certain number of bytes is buffered (the cutover point) will open a compressed
030 * stream (via {@link org.apache.tapestry5.services.Response#getOutputStream(String)}
031 */
032public class BufferedGZipOutputStream extends ServletOutputStream
033{
034    private final String contentType;
035
036    private final HttpServletResponse response;
037
038    private final CompressionAnalyzer analyzer;
039
040    private final int cutover;
041
042    private ByteArrayOutputStream byteArrayOutputStream;
043
044    /**
045     * Initially the ByteArrayOutputStream, later the response output stream (possibly wrapped with a
046     * GZIPOutputStream).
047     */
048    private OutputStream currentOutputStream;
049
050    public BufferedGZipOutputStream(String contentType, HttpServletResponse response, int cutover,
051                                    CompressionAnalyzer analyzer)
052    {
053        this.contentType = contentType;
054        this.response = response;
055        this.cutover = cutover;
056        this.analyzer = analyzer;
057
058        byteArrayOutputStream = new ByteArrayOutputStream(cutover);
059
060        currentOutputStream = byteArrayOutputStream;
061    }
062
063    private void checkForCutover() throws IOException
064    {
065        if (byteArrayOutputStream == null) return;
066
067        if (byteArrayOutputStream.size() < cutover) return;
068
069        // Time to switch over to GZIP.
070        openResponseOutputStream(true);
071    }
072
073    private void openResponseOutputStream(boolean gzip) throws IOException
074    {
075        OutputStream responseOutputStream = response.getOutputStream();
076
077        boolean useCompression = gzip && analyzer.isCompressable(contentType);
078
079        OutputStream possiblyCompressed = useCompression
080                ? new GZIPOutputStream(responseOutputStream)
081                : responseOutputStream;
082
083        if (useCompression)
084        {
085            response.setHeader(InternalConstants.CONTENT_ENCODING_HEADER, InternalConstants.GZIP_CONTENT_ENCODING);
086        }
087
088        currentOutputStream =
089                new BufferedOutputStream(possiblyCompressed);
090
091        // Write what content we already have to the new stream.
092
093        byteArrayOutputStream.writeTo(currentOutputStream);
094
095        byteArrayOutputStream = null;
096    }
097
098    public void write(int b) throws IOException
099    {
100        currentOutputStream.write(b);
101
102        checkForCutover();
103    }
104
105    @Override
106    public void write(byte[] b) throws IOException
107    {
108        currentOutputStream.write(b);
109
110        checkForCutover();
111    }
112
113    @Override
114    public void write(byte[] b, int off, int len) throws IOException
115    {
116        currentOutputStream.write(b, off, len);
117
118        checkForCutover();
119    }
120
121    @Override
122    public void flush() throws IOException
123    {
124        forceOutputStream().flush();
125    }
126
127    @Override
128    public void close() throws IOException
129    {
130        // When closing, if we haven't accumulated enough output yet to start compressing,
131        // then send what we have, uncompressed.
132
133        forceOutputStream().close();
134    }
135
136    private OutputStream forceOutputStream() throws IOException
137    {
138        if (byteArrayOutputStream != null)
139            openResponseOutputStream(false);
140
141        return currentOutputStream;
142    }
143}