001    /* DERIVED FROM APACHE CODE, modifications (c) Damon Hart-Davis 2001-2006, under same terms:
002     *
003     * CompressionResponseStream.java
004     * $Header: /rw/CVS-repository.s0.l/general/PG2Ksrc/javasrc/webSvr/org/hd/d/pg2k/webSvr/util/CompressionResponseStream.java,v 1.8 2004/12/21 19:03:34 dhd Exp $
005     * $Revision: 1.8 $
006     * $Date: 2004/12/21 19:03:34 $
007     *
008     * ====================================================================
009     *
010     * The Apache Software License, Version 1.1
011     *
012     * Copyright (c) 1999 The Apache Software Foundation.  All rights
013     * reserved.
014     *
015     * Redistribution and use in source and binary forms, with or without
016     * modification, are permitted provided that the following conditions
017     * are met:
018     *
019     * 1. Redistributions of source code must retain the above copyright
020     *    notice, this list of conditions and the following disclaimer.
021     *
022     * 2. Redistributions in binary form must reproduce the above copyright
023     *    notice, this list of conditions and the following disclaimer in
024     *    the documentation and/or other materials provided with the
025     *    distribution.
026     *
027     * 3. The end-user documentation included with the redistribution, if
028     *    any, must include the following acknowlegement:
029     *       "This product includes software developed by the
030     *        Apache Software Foundation (http://www.apache.org/)."
031     *    Alternately, this acknowlegement may appear in the software itself,
032     *    if and wherever such third-party acknowlegements normally appear.
033     *
034     * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
035     *    Foundation" must not be used to endorse or promote products derived
036     *    from this software without prior written permission. For written
037     *    permission, please contact apache@apache.org.
038     *
039     * 5. Products derived from this software may not be called "Apache"
040     *    nor may "Apache" appear in their names without prior written
041     *    permission of the Apache Group.
042     *
043     * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
044     * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
045     * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
046     * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
047     * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
048     * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
049     * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
050     * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
051     * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
052     * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
053     * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
054     * SUCH DAMAGE.
055     * ====================================================================
056     *
057     * This software consists of voluntary contributions made by many
058     * individuals on behalf of the Apache Software Foundation.  For more
059     * information on the Apache Software Foundation, please see
060     * <http://www.apache.org/>.
061     *
062     * [Additional notices, if required by prior licensing conditions]
063     *
064     */
065    
066    package org.hd.d.pg2k.webSvr.util;
067    
068    import java.io.ByteArrayOutputStream;
069    import java.io.IOException;
070    import java.io.OutputStream;
071    
072    import javax.servlet.ServletOutputStream;
073    import javax.servlet.http.HttpServletResponse;
074    
075    import ORG.hd.d.IsDebug;
076    
077    
078    /**Implementation of <b>ServletOutputStream</b> that works with CompressionServletResponseWrapper implementation.
079     * @author Amy Roh (original)
080     * @author Damon Hart-Davis
081     * @version $Revision: 1.8 $, $Date: 2004/12/21 19:03:34 $
082     */
083    
084    public final class CompressionResponseStream extends ServletOutputStream
085        {
086        // ----------------------------------------------------------- Constructors
087    
088    
089        /**Construct a servlet output stream associated with the specified Response.
090         * @param response  the associated response
091         */
092        public CompressionResponseStream(final CompressionServletResponseWrapper wrapper,
093                                         final HttpServletResponse response)
094            throws IOException
095            {
096            if((wrapper == null) || (response == null)) { throw new IllegalArgumentException(); }
097            this.response = response;
098            this.wrapper = wrapper;
099    
100            output = response.getOutputStream();
101            if(output == null) { throw new IllegalStateException(); }
102            }
103    
104    
105        // ----------------------------------------------------- Instance Variables
106    
107    
108        /**The threshold number which decides to compress or not; non-negative.
109         * Users can configure in web.xml to set it to fit their needs.
110         * <p>
111         * If positive, size of output below which we will not attempt
112         * to compress but will leave the output is it arrived.
113         */
114        private int compressionThreshold;
115    
116        /**Buffer in which we accumulate output before starting compressed stream.
117         * Initially null,
118         * created on demand if the the compressionThreshold is positive,
119         * flushed into the compression stream and destroyed
120         * if it overflows during a write() or if still extant and close().
121         */
122        private ByteArrayOutputStream buffer;
123    
124        /**The underlying gzip output stream to which we should write data.
125         * If this is non-null then we are committed to writing the
126         * stream compressed.
127         * <p>
128         * As an emergency swerve if we discover that someelse in the filter chain
129         * decided late to add a content encoding,
130         * this may in fact be a pass-through stream.
131         */
132        protected /*Deflater*/OutputStream compressedStream;
133    
134        /**Has this stream been closed?
135         */
136        protected boolean closed;
137    
138        /**The response with which this servlet output stream is associated; never null. */
139        private final HttpServletResponse response;
140    
141        /**The wrapper that created this; never null. */
142        private final CompressionServletResponseWrapper wrapper;
143    
144        /**The underlying servlet output stream to which we should write data; never null. */
145        private final ServletOutputStream output;
146    
147    
148        /**Minimum output buffer size to mop up output from GZIPOutputStream.
149         * Without this, the leading and trailing bytes are liable to
150         * end up as separate length==1 (HTTP/1.1) chunks because of the
151         * implementation of GZIPOutputStream up to JDK1.3.0.
152         * <p>
153         * Using a BufferedOutputStream helps ensure that the chunks are
154         * large and efficent both to represent and to transmit.
155         */
156        private static final int BUFSIZE = 8192;
157    
158        /**If true, use JazzLib's flush()able Deflator rather than the JDK's. */
159        private static final boolean USE_JAZZLIB = true;
160    
161        /**If false, suppress flushes of the GZIP output stream itself until the stream is closed.
162         * This is because a flush() generally cannot push out anything useful
163         * with Sun's GZIPOutputStream up to an including JDK 5,
164         * ie that the client could actually uncompress immediately,
165         * and so just represents a waste of network packets and bandwidth.
166         * <p>
167         * If we use JazzLib then flush() is useful.
168         */
169        private static final boolean FLUSH_GZIP = USE_JAZZLIB;
170    
171        // --------------------------------------------------------- Public Methods
172    
173    
174        /**Set the compressionThreshold; must not be negative.
175         * Zero means always compress.
176         * <p>
177         * This value may be ignored, ie treated as if zero.
178         */
179        protected synchronized void setBuffer(final int threshold)
180            {
181            if(threshold < 0) { throw new IllegalArgumentException(); }
182            compressionThreshold = threshold;
183            }
184    
185        /**Close this output stream.
186         * Cause any buffered data to be flushed and
187         * any further output data to throw an IOException.
188         */
189        @Override
190        public synchronized void close()
191            throws IOException
192            {
193            //System.out.println("close() @ CompressionResponseStream");
194            if(closed)
195                { throw new IOException("This output stream has already been closed"); }
196    
197            // In the case we didn't get to start compressing (too little or no data),
198            // then write out any initial data we collected directly downstream.
199            // Presumably we have just avoided wasted compression overhead.
200            // We can also re-apply some Content-XXX headers to help transfer integrity.
201            // Where we were compressing, correctly terminate the compression.
202            if(buffer != null)
203                {
204                assert(compressedStream == null);
205                reapplySuppledContentHeaders();
206    //if(IsDebug.isDebug) { System.out.println("INFO: close() pushing out initial buffer of size: "+buffer.size()); }
207                output.write(buffer.toByteArray());
208                buffer = null; // Allow GC.
209                output.close();
210                }
211            // If we actually create a compression stream, close it and downstream.
212            else if(compressedStream != null)
213                {
214                compressedStream.close();
215                }
216            // We seem to have written nothing at all.  Close downstream.
217            else
218                {
219                reapplySuppledContentHeaders();
220                output.close();
221                }
222    
223            closed = true;
224            }
225    
226        /**Flush any buffered data for this output stream, and force the response to be committed.
227         * This may resist attempting the flush() the compressed stream
228         * as it will have no effect other than to wastefully push out some
229         * header bytes at most.
230         * <p>
231         * This will always ensure that the underlying stream
232         * is directly or indirectly flushed so as to commit the response.
233         */
234        @Override
235        public synchronized void flush()
236            throws IOException
237            {
238            if(closed) { throw new IOException("Cannot flush a closed output stream"); }
239    
240    //if(WATCH_FLUSHES) { System.out.println("servlet.out.flush() called..."); }
241    //(new Throwable()).printStackTrace();
242    
243            if(FLUSH_GZIP)
244                {
245                // If we are not blocking all flush() operations entirely...
246                makeSureGZOSCreated(); // Commit to use compression or not right now...
247                if(buffer != null)
248                    {
249    //if(IsDebug.isDebug) { System.out.println("INFO: flush() pushing out initial buffer of size: "+buffer.size()); }
250                    writeToGZip(buffer.toByteArray(), 0, buffer.size());
251                    buffer = null; // Discard buffer and allow GC.
252                    }
253                compressedStream.flush(); // Flush through compressor and thus commit downstream too.
254                }
255            else
256                {
257                output.flush(); // Commit downstream.
258                }
259            }
260    
261        /**Create the GZIPOutputStream if necessary and advertise the Content-Encoding in the HTTP headers.
262         * It does nothing if the compressedStream is no longer null.
263         * <p>
264         * After this completes compressedStream will not be null.
265         * <p>
266         * Calling this commits us to whether we will use compression at all
267         * for this HTTP request.
268         * We <em>will</em> apply compression
269         * <em>unless</em> we find another Content-Encoding is being applied.
270         * <p>
271         * We wrap the output in a BufferedOutputStream first to
272         * soak up the single-byte leading and trailing writes that
273         * GZIPOutputStream up to JDK 1.4.0 does, and to avoid sending
274         * redundant blocks of data downstream (such as the gzip header)
275         * before we have to, since these will otherwise get sent as
276         * separate chunks/packets in some cases.
277         * <p>
278         * We also take this opportunity to add the "Content-Encoding" header
279         * to indicate that the output is GZIPed, and to add a "Vary" header
280         * to indicate that this content encoding was negotiated with the client
281         * so caches/proxies can handle differently-able clients properly.
282         * <p>
283         * We double-check that the a Content-Encoding has not now been applied
284         * since this might happen lazily.
285         * If it has then we duck and dive by using the raw underlying stream.
286         */
287        private synchronized void makeSureGZOSCreated()
288            throws IOException
289            {
290            if(compressedStream != null) { return; }
291    
292            // We cannot apply any encoding/compression if there is a Content-MD5 header,
293            // since it should be applied/recomputed after this compression,
294            // and the GZIP checksum will probably do to protect the content in transit.
295            // Last-ditch check that this chain/page/etc is not getting multiply ZIPped...
296            // We DO NOT re-apply deferred Content-XXX header values.
297            if((response).containsHeader("Content-MD5") ||
298               (response).containsHeader("Content-Encoding"))
299                {
300    if(IsDebug.isDebug) { System.err.println("WARNING: CompresionFilter saw Content-Encoding/Content-MD5: LATE abort/swerve..."); }
301                compressedStream = output;
302                return;
303                }
304    
305            // We buffer behind the compressor stream for network efficiency,
306            // eg to ensure that its headers and trailers don't get sent separately.
307            final int bufsize = Math.max(BUFSIZE, compressionThreshold);
308    
309            // Set our compressor.
310            // Note that this adjusts its compression on the fly to suit power availability.
311            compressedStream = new FlushableGZIPOutputStream(output, bufsize);
312    
313            // Note that we now have a content encoding in place.
314            response.addHeader("Content-Encoding", CompressionFilter.GZIP_TOKEN);
315    
316            // OK, compresion depends on content negotiation with the client,
317            // then we'd better add a Vary header to avoid mucking up any proxy/cache...
318            response.addHeader("Vary", "Accept-Encoding");
319    
320    //if(IsDebug.isDebug) { System.out.println("INFO: compression filter started..."); }
321            }
322    
323        /**Reapply user-supplied header values that would have conflicted with compression.
324         * We can reapply these Content-XXX headers to improve on-the-wire integrity.
325         * <p>
326         * We carefully avoid overwriting any values that have been set elsewhere.
327         */
328        private void reapplySuppledContentHeaders()
329            {
330            final HttpServletResponse httpServletResponse = (response);
331            // Cannot do anything if response already committed.
332            if(httpServletResponse.isCommitted())
333                {
334    if(IsDebug.isDebug) { System.out.println("CompressionResponseStream.reapplySuppledContentHeaders(): response already committed."); }
335                return;
336                }
337    
338            // Content-Length
339            if((wrapper.getContentLength() > 0) &&
340               !httpServletResponse.containsHeader("Content-Length"))
341                {
342    if(IsDebug.isDebug) { System.out.println("CompressionResponseStream.reapplySuppledContentHeaders(): re-applying Content-Length."); }
343                httpServletResponse.setContentLength(wrapper.getContentLength());
344                }
345    
346            // Content-MD5
347            if((wrapper.getContentMD5() != null) &&
348               !httpServletResponse.containsHeader("Content-MD5"))
349                {
350    if(IsDebug.isDebug) { System.out.println("CompressionResponseStream.reapplySuppledContentHeaders(): re-applying Content-MD5."); }
351                httpServletResponse.setHeader("Content-MD5", wrapper.getContentMD5());
352                }
353            }
354    
355        /**Private per-instance buffer for write(int); never null. */
356        private final byte buf1[] = new byte[1];
357    
358        /**Write the specified byte to our output stream.
359         * This is likely to be hideously inefficient compared to the multi-byte write() calls.
360         *
361         * @param b The byte to be written
362         * @exception IOException if an input/output error occurs
363         */
364        @Override
365        public synchronized void write(final int b) throws IOException
366            {
367            buf1[0] = (byte) b;
368            write(buf1, 0, 1);
369            }
370    
371        /**Write the specified byte array to our output stream.
372         * @param b The byte array to be written; never null
373         * @exception IOException if an input/output error occurs
374         */
375        @Override
376        public void write(final byte b[]) throws IOException
377            { write(b, 0, b.length); }
378    
379        /**Write the specified array portion to our output stream.
380         * All write()s to this output stream ultimately pass through this method.
381         *
382         * @param b  the byte array containing the bytes to be written
383         * @param off  zero-relative starting offset of the bytes to be written
384         * @param len  the number of bytes to be written
385         *
386         * @exception IOException if an input/output error occurs
387         */
388        @Override
389        public synchronized void write(final byte b[], final int off, final int len)
390            throws IOException
391            {
392            if(closed) {
393                throw new IOException("Cannot write to a closed output stream");
394                }
395    
396            if(len == 0) { return; } // Nothing to do...
397    
398    //System.out.println("servlet.out.write(b, off, "+len+") called...");
399    
400            // If the compressing output stream does not yet exist,
401            // and the threshold is positive,
402            // we buffer internally (creating the buffer if need be).
403            // Calling flush() may push this buffered data down the wire.
404            if((compressedStream == null) && (compressionThreshold > 0))
405                {
406                final int bufferedBytes = (buffer == null) ? 0 : buffer.size();
407    
408                // If data would not overflow buffer (ie not pass threshold)
409                // then create buffer if need be,
410                // append new data,
411                // and return.
412                if(len + bufferedBytes <= compressionThreshold)
413                    {
414                    if(buffer == null)
415                        { buffer = new ByteArrayOutputStream(compressionThreshold); }
416                    buffer.write(b, off, len);
417                    return;
418                    }
419    
420                // If buffer would overflow because we would pass our threshold;
421                // then write buffer content to the compressed stream (and discard buffer),
422                // then write the new data to the compressed stream.
423                if(buffer != null)
424                    {
425                    writeToGZip(buffer.toByteArray(), 0, buffer.size());
426                    buffer = null; // Allow GC.
427                    }
428    
429                // Fall through to write new data directly
430                // to compressed stream.
431                //
432                // This will ensure that compressedStream!= null,
433                // so we will not try to use the buffer again.
434                }
435    
436            // Write to the compressedStream.
437            writeToGZip(b, off, len);
438            }
439    
440        /**Write the specified array portion to our compressed GZIP output stream.
441         * @exception IOException if an input/output error occurs
442         */
443        private synchronized void writeToGZip(final byte b[], final int off, final int len) throws IOException
444            {
445            makeSureGZOSCreated();
446    //System.out.println("writeToGZip(b, off, "+len+")...");
447            compressedStream.write(b, off, len);
448            }
449        }