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 }