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.http.internal.gzip;
016
017import java.io.BufferedOutputStream;
018import java.io.ByteArrayOutputStream;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.util.zip.GZIPOutputStream;
022
023import javax.servlet.ServletOutputStream;
024import javax.servlet.http.HttpServletResponse;
025
026import org.apache.tapestry5.http.internal.TapestryHttpInternalConstants;
027import org.apache.tapestry5.http.services.CompressionAnalyzer;
028
029/**
030 * A buffered output stream that, when a certain number of bytes is buffered (the cutover point) will open a compressed
031 * stream (via {@link org.apache.tapestry5.http.services.Response#getOutputStream(String)}
032 */
033public class BufferedGZipOutputStream extends ServletOutputStream
034{
035    private final String contentType;
036
037    private final HttpServletResponse response;
038
039    private final CompressionAnalyzer analyzer;
040
041    private final int cutover;
042
043    private ByteArrayOutputStream byteArrayOutputStream;
044
045    /**
046     * Initially the ByteArrayOutputStream, later the response output stream (possibly wrapped with a
047     * GZIPOutputStream).
048     */
049    private OutputStream currentOutputStream;
050
051    public BufferedGZipOutputStream(String contentType, HttpServletResponse response, int cutover,
052                                    CompressionAnalyzer analyzer)
053    {
054        this.contentType = contentType;
055        this.response = response;
056        this.cutover = cutover;
057        this.analyzer = analyzer;
058
059        byteArrayOutputStream = new ByteArrayOutputStream(cutover);
060
061        currentOutputStream = byteArrayOutputStream;
062    }
063
064    private void checkForCutover() throws IOException
065    {
066        if (byteArrayOutputStream == null) return;
067
068        if (byteArrayOutputStream.size() < cutover) return;
069
070        // Time to switch over to GZIP.
071        openResponseOutputStream(true);
072    }
073
074    private void openResponseOutputStream(boolean gzip) throws IOException
075    {
076        OutputStream responseOutputStream = response.getOutputStream();
077
078        boolean useCompression = gzip && analyzer.isCompressable(contentType);
079
080        OutputStream possiblyCompressed = useCompression
081                ? new GZIPOutputStream(responseOutputStream)
082                : responseOutputStream;
083
084        if (useCompression)
085        {
086            response.setHeader(
087                    TapestryHttpInternalConstants.CONTENT_ENCODING_HEADER, 
088                    TapestryHttpInternalConstants.GZIP_CONTENT_ENCODING);
089        }
090
091        currentOutputStream =
092                new BufferedOutputStream(possiblyCompressed);
093
094        // Write what content we already have to the new stream.
095
096        byteArrayOutputStream.writeTo(currentOutputStream);
097
098        byteArrayOutputStream = null;
099    }
100
101    public void write(int b) throws IOException
102    {
103        currentOutputStream.write(b);
104
105        checkForCutover();
106    }
107
108    @Override
109    public void write(byte[] b) throws IOException
110    {
111        currentOutputStream.write(b);
112
113        checkForCutover();
114    }
115
116    @Override
117    public void write(byte[] b, int off, int len) throws IOException
118    {
119        currentOutputStream.write(b, off, len);
120
121        checkForCutover();
122    }
123
124    @Override
125    public void flush() throws IOException
126    {
127        forceOutputStream().flush();
128    }
129
130    @Override
131    public void close() throws IOException
132    {
133        // When closing, if we haven't accumulated enough output yet to start compressing,
134        // then send what we have, uncompressed.
135
136        forceOutputStream().close();
137    }
138
139    private OutputStream forceOutputStream() throws IOException
140    {
141        if (byteArrayOutputStream != null)
142            openResponseOutputStream(false);
143
144        return currentOutputStream;
145    }
146}