001// Copyright 2009 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.ResponseCompressionAnalyzer;
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 ResponseCompressionAnalyzer 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                                    ResponseCompressionAnalyzer 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            response.setHeader(InternalConstants.CONTENT_ENCODING_HEADER, InternalConstants.GZIP_CONTENT_ENCODING);
085
086        currentOutputStream =
087                new BufferedOutputStream(possiblyCompressed);
088
089        // Write what content we already have to the new stream.
090
091        byteArrayOutputStream.writeTo(currentOutputStream);
092
093        byteArrayOutputStream = null;
094    }
095
096    public void write(int b) throws IOException
097    {
098        currentOutputStream.write(b);
099
100        checkForCutover();
101    }
102
103    @Override
104    public void write(byte[] b) throws IOException
105    {
106        currentOutputStream.write(b);
107
108        checkForCutover();
109    }
110
111    @Override
112    public void write(byte[] b, int off, int len) throws IOException
113    {
114        currentOutputStream.write(b, off, len);
115
116        checkForCutover();
117    }
118
119    @Override
120    public void flush() throws IOException
121    {
122        forceOutputStream().flush();
123    }
124
125    @Override
126    public void close() throws IOException
127    {
128        // When closing, if we haven't accumulated enough output yet to start compressing,
129        // then send what we have, uncompressed.
130
131        forceOutputStream().close();
132    }
133
134    private OutputStream forceOutputStream() throws IOException
135    {
136        if (byteArrayOutputStream != null)
137            openResponseOutputStream(false);
138
139        return currentOutputStream;
140    }
141}