001    package org.rakeshv.filters;
002    
003    import java.io.IOException;
004    import java.util.zip.DeflaterOutputStream;
005    import java.util.zip.GZIPOutputStream;
006    import java.util.zip.ZipEntry;
007    import java.util.zip.ZipOutputStream;
008    import javax.servlet.ServletOutputStream;
009    import javax.servlet.http.HttpServletResponse;
010    
011    /**
012     * A sub-class of <code>ServletOutputStream</code> that is used to 
013     * compress the response sent by the server to a client request.  The
014     * only compression method supported are those that are available in the
015     * <code>java.util.zip</code> package.
016     *
017     * <p>&copy; Copyright 2003, Rakesh Vidyadharan</p>
018     * @author Rakesh Vidyadharan 2<sup><small>nd</small></sup> September 2003
019     *
020     * @version $Id: CompressionResponseStream.java,v 1.5 2005/10/15 01:10:20 rakesh Exp $
021     */
022    public class CompressionResponseStream extends ServletOutputStream 
023    {
024      /**
025       * The underlying gzip output stream to which we should write data.
026       */
027      protected DeflaterOutputStream deflaterStream = null;
028          
029      /**
030       * Has this stream been closed?
031       */
032      protected boolean closed = false;
033    
034      /**
035       * The response with which this servlet output stream is associated.
036       */
037      protected HttpServletResponse response = null;
038    
039      /**
040       * The underlying servlet output stream to which we should write data.
041       */
042      protected ServletOutputStream output = null;
043    
044      /**
045       * The type of compression algorithm to use.  The supported algorithms
046       * are <code>Deflate, GZIP, Zip</code>.
047       */
048      protected int compressionType = 0;
049    
050      /**
051       * Initialise a new instance of this class with the specified 
052       * servlet response.
053       *
054       * @param response The response to which the contents are to be
055       *   sent.
056       */
057      public CompressionResponseStream( HttpServletResponse response )
058        throws IOException
059      {
060        super();
061        this.response = response;
062        this.output = response.getOutputStream();
063      }
064    
065      /**
066       * Over-ridden form of the method.  Writes the specified int to the
067       * {@link #deflaterStream} if the stream has not been closed.  Uses
068       * the {@link #writeToDeflater( int )} method to write the specified
069       * value to the {@link #deflaterStream}.
070       *
071       * @param value - The integer value that is being written.
072       * @throws IOException - If the stream has already been closed.
073       */
074      public void write( int value ) throws IOException 
075      {
076        if ( closed ) 
077        {
078          throw new IOException("Cannot write to a closed output stream");
079        }
080    
081        writeToDeflater( value );
082      }
083    
084      /**
085       * Write the specified value to the {@link #deflaterStream}.  Checks
086       * to ensure that the {@link #deflaterStream} has been created, and
087       * if not, then creates the appropriate instance of the {@link
088       * #deflaterStream} using the {@link #compressionType} value.
089       *
090       * @param value - The value to write.
091       * @throws IOException - If errors are encountered while trying to
092       *   write to the stream.
093       */
094      protected void writeToDeflater( int value ) throws IOException 
095      {
096        if ( deflaterStream == null ) 
097        {
098          createDeflaterStream();
099        }
100    
101        deflaterStream.write( value );
102      }
103    
104      /**
105       * Over-ridden to ensure that the method just invokes the {@link
106       * #write( byte[], int, int )} method.
107       *
108       * @param buf - The byte buffer from which the data is to
109       *   read and written to the {@link #deflaterStream}.
110       * @throws IOException - If errors are encountered while writing
111       *   to the stream.
112       */
113      public void write( byte[] buf ) throws IOException 
114      {
115        write( buf, 0, buf.length );
116      }
117    
118      /**
119       * Over-ridden form of the method.  Writes the <code>byte array</code>
120       * to the {@link #deflaterStream} using {@link 
121       * #writeToDeflater( byte[], int, int)}.
122       *
123       * @param buf - The byte buffer from which the data is to
124       *   read and written to the {@link #deflaterStream}.
125       * @param off - The offset from the start of the buffer from 
126       *   which to start reading data.
127       * @param len - The length of data that is to be read from the
128       *   array.
129       * @throws IOException - If errors are encountered while writing
130       *   to the stream.
131       */
132      public void write( byte[] buf, int off, int len ) throws IOException 
133      {
134        if ( closed ) 
135        {
136          throw new IOException( "Cannot write to a closed output stream" );
137        }
138    
139        writeToDeflater( buf, off, len );
140      }
141    
142      /**
143       * Writes the date from the specified byte array to the {@link
144       * #deflaterStream}.  Checks to ensure that the {@link 
145       * #deflaterStream} has been created, and if not, then it creates
146       * the appropriate instance of the {@link #deflaterStream} based
147       * upon the value of {@link #compressionType}.
148       *
149       * @param buf - The byte buffer from which the data is to
150       *   read and written to the {@link #deflaterStream}.
151       * @param off - The offset from the start of the buffer from 
152       *   which to start reading data.
153       * @param len - The length of data that is to be read from the
154       *   array.
155       * @throws IOException - If errors are encountered while writing
156       *   to the stream.
157       */
158      protected void writeToDeflater( byte[] buf, int off, int len ) 
159        throws IOException 
160      {
161        if ( deflaterStream == null ) 
162        {
163          createDeflaterStream();
164        }
165    
166        deflaterStream.write( buf, off, len );
167      }
168    
169      /**
170       * Initialise the {@link #deflaterStream} with the appropriate type
171       * of compressed output stream based upon the value in the
172       * {@link #compressionType} instance variable.  If the value of
173       * {@link #compressionType} does not match one of the following,
174       * then <code>Deflate</code> compression is used.
175       *
176       * <ol>
177       *  <li>{@link CompressionFilter#GZIP_COMPRESSION}
178       *  <li>{@link CompressionFilter#ZIP_COMPRESSION}
179       * </ol>
180       *
181       * <p><b>Note:</b> If <code>zip</code> compression is requested, then
182       * the response will contain a <code>ZipEntry</code> object that
183       * contains the data as required by the <code>Zip</code> format.
184       * The <code>file name</code> for the <code>ZipEntry</code> object
185       * is arbitrarily set as the value of <code>System.currentTimeMillis()
186       * </code></p>.
187       *
188       * @throws IOException - If errors are encountered while creating
189       *   the stream, or setting its header.
190       */
191      protected void createDeflaterStream() throws IOException
192      {
193        if ( compressionType == CompressionFilter.GZIP_COMPRESSION )
194        {
195          deflaterStream = new GZIPOutputStream( output );
196          response.addHeader( "Content-Encoding", "gzip" );
197        }
198        else if ( compressionType == CompressionFilter.ZIP_COMPRESSION )
199        {
200          deflaterStream = new ZipOutputStream( output );
201          ( (ZipOutputStream) deflaterStream ).putNextEntry( new ZipEntry( String.valueOf( System.currentTimeMillis() ) ) );
202          response.addHeader( "Content-Encoding", "zip" );
203        }
204        else
205        {
206          deflaterStream = new DeflaterOutputStream( output );
207          response.addHeader( "Content-Encoding", "deflate" );
208        }
209      }
210    
211      /**
212       * Close this output stream, causing any buffered data to be flushed 
213       * and any further output data to throw an IOException.
214       *
215       * @throws IOException - If the stream has already been closed.
216       */
217      public void close() throws IOException 
218      {
219        if ( closed ) 
220        {
221          throw new IOException("This output stream has already been closed");
222        }
223    
224        flush();
225    
226        if ( deflaterStream != null ) 
227        {
228          finish();
229          deflaterStream.close();
230        }
231    
232        closed = true;
233      }
234    
235      /**
236       * Finishes writing compressed data to the output stream without 
237       * closing the underlying stream. Use this method when applying 
238       * multiple filters in succession to the same output stream.
239       *
240       * @throws IOException - If errors are encountered while writing
241       *   to the stream.  An exception will be thrown if this method is
242       *   invoked after the stream has been closed.
243       */
244      public void finish() throws IOException
245      {
246        if ( closed ) 
247        {
248          throw new IOException("This output stream has already been closed");
249        }
250    
251        if ( deflaterStream instanceof ZipOutputStream )
252        {
253          ( (ZipOutputStream) deflaterStream ).closeEntry();
254          deflaterStream.finish();
255        }
256        else
257        {
258          deflaterStream.finish();
259        }
260      }
261    
262      /**
263       * Flushes the underlying servlet output stream.  Flushes any
264       * unwritten content in the buffer to the servlet output 
265       * stream.
266       *
267       * @throws IOException - If the stream has already been closed.
268       */
269      public void flush() throws IOException 
270      {
271        if ( closed ) 
272        {
273          throw new IOException( "Cannot flush a closed output stream" );
274        }
275    
276        deflaterStream.flush();
277      }
278      
279      /**
280       * Returns {@link #closed}.
281       *
282       * @return boolean - The value/reference of/to closed.
283       */
284      public final boolean getClosed()
285      {
286        return closed;
287      }
288      
289      /**
290       * Returns {@link #compressionType}.
291       *
292       * @return int - The value/reference of/to compressionType.
293       */
294      public final int getCompressionType()
295      {
296        return compressionType;
297      }
298      
299      /**
300       * Set {@link #compressionType}.
301       *
302       * @param compressionType - The value to set.
303       */
304      public final void setCompressionType( int compressionType )
305      {
306        this.compressionType = compressionType;
307      }
308    }