View Javadoc

1   // ========================================================================
2   // Copyright (c) 2007-2009 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // All rights reserved. This program and the accompanying materials
5   // are made available under the terms of the Eclipse Public License v1.0
6   // and Apache License v2.0 which accompanies this distribution.
7   // The Eclipse Public License is available at 
8   // http://www.eclipse.org/legal/epl-v10.html
9   // The Apache License v2.0 is available at
10  // http://www.opensource.org/licenses/apache2.0.php
11  // You may elect to redistribute this code under either of these licenses. 
12  // ========================================================================
13  package org.eclipse.jetty.servlets;
14  
15  import java.io.IOException;
16  import java.io.OutputStream;
17  import java.io.OutputStreamWriter;
18  import java.io.PrintWriter;
19  import java.io.UnsupportedEncodingException;
20  import java.util.HashSet;
21  import java.util.Set;
22  import java.util.StringTokenizer;
23  import java.util.zip.GZIPOutputStream;
24  
25  import javax.servlet.FilterChain;
26  import javax.servlet.FilterConfig;
27  import javax.servlet.ServletException;
28  import javax.servlet.ServletOutputStream;
29  import javax.servlet.ServletRequest;
30  import javax.servlet.ServletResponse;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpServletResponse;
33  import javax.servlet.http.HttpServletResponseWrapper;
34  
35  import org.eclipse.jetty.continuation.Continuation;
36  import org.eclipse.jetty.continuation.ContinuationListener;
37  import org.eclipse.jetty.continuation.ContinuationSupport;
38  import org.eclipse.jetty.http.HttpMethods;
39  import org.eclipse.jetty.util.ByteArrayOutputStream2;
40  import org.eclipse.jetty.util.StringUtil;
41  import org.eclipse.jetty.util.log.Log;
42  
43  /* ------------------------------------------------------------ */
44  /** GZIP Filter
45   * This filter will gzip the content of a response iff: <ul>
46   * <li>The filter is mapped to a matching path</li>
47   * <li>The response status code is >=200 and <300
48   * <li>The content length is unknown or more than the <code>minGzipSize</code> initParameter or the minGzipSize is 0(default)</li>
49   * <li>The content-type is in the comma separated list of mimeTypes set in the <code>mimeTypes</code> initParameter or
50   * if no mimeTypes are defined the content-type is not "application/gzip"</li>
51   * <li>No content-encoding is specified by the resource</li>
52   * </ul>
53   * 
54   * <p>
55   * Compressing the content can greatly improve the network bandwidth usage, but at a cost of memory and
56   * CPU cycles.   If this filter is mapped for static content, then use of efficient direct NIO may be 
57   * prevented, thus use of the gzip mechanism of the {@link org.eclipse.jetty.servlet.DefaultServlet} is 
58   * advised instead.
59   * </p>
60   * <p>
61   * This filter extends {@link UserAgentFilter} and if the the initParameter <code>excludedAgents</code> 
62   * is set to a comma separated list of user agents, then these agents will be excluded from gzip content.
63   * </p>
64   *
65   */
66  public class GzipFilter extends UserAgentFilter
67  {
68      protected Set _mimeTypes;
69      protected int _bufferSize=8192;
70      protected int _minGzipSize=256;
71      protected Set _excluded;
72      
73      public void init(FilterConfig filterConfig) throws ServletException
74      {
75          super.init(filterConfig);
76          
77          String tmp=filterConfig.getInitParameter("bufferSize");
78          if (tmp!=null)
79              _bufferSize=Integer.parseInt(tmp);
80  
81          tmp=filterConfig.getInitParameter("minGzipSize");
82          if (tmp!=null)
83              _minGzipSize=Integer.parseInt(tmp);
84          
85          tmp=filterConfig.getInitParameter("mimeTypes");
86          if (tmp!=null)
87          {
88              _mimeTypes=new HashSet();
89              StringTokenizer tok = new StringTokenizer(tmp,",",false);
90              while (tok.hasMoreTokens())
91                  _mimeTypes.add(tok.nextToken());
92          }
93          
94          tmp=filterConfig.getInitParameter("excludedAgents");
95          if (tmp!=null)
96          {
97              _excluded=new HashSet();
98              StringTokenizer tok = new StringTokenizer(tmp,",",false);
99              while (tok.hasMoreTokens())
100                 _excluded.add(tok.nextToken());
101         }
102     }
103 
104     public void destroy()
105     {
106     }
107 
108     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
109         throws IOException, ServletException
110     {
111         HttpServletRequest request=(HttpServletRequest)req;
112         HttpServletResponse response=(HttpServletResponse)res;
113 
114         String ae = request.getHeader("accept-encoding");
115         if (ae != null && ae.indexOf("gzip")>=0 && !response.containsHeader("Content-Encoding")
116                 && !HttpMethods.HEAD.equalsIgnoreCase(request.getMethod()))
117         {
118             if (_excluded!=null)
119             {
120                 String ua=getUserAgent(request);
121                 if (_excluded.contains(ua))
122                 {
123                     super.doFilter(request,response,chain);
124                     return;
125                 }
126             }
127 
128             final GZIPResponseWrapper wrappedResponse=newGZIPResponseWrapper(request,response);
129             
130             boolean exceptional=true;
131             try
132             {
133                 super.doFilter(request,wrappedResponse,chain);
134                 exceptional=false;
135             }
136             finally
137             {
138                 Continuation continuation = ContinuationSupport.getContinuation(request);
139                 if (continuation.isSuspended() && continuation.isResponseWrapped())   
140                 {
141                     continuation.addContinuationListener(new ContinuationListener()
142                     {
143                         public void onComplete(Continuation continuation)
144                         {
145                             try
146                             {
147                                 wrappedResponse.finish();
148                             }
149                             catch(IOException e)
150                             {
151                                 Log.warn(e);
152                             }
153                         }
154 
155                         public void onTimeout(Continuation continuation)
156                         {}
157                     });
158                 }
159                 else if (exceptional && !response.isCommitted())
160                 {
161                     wrappedResponse.resetBuffer();
162                     wrappedResponse.noGzip();
163                 }
164                 else
165                     wrappedResponse.finish();
166             }
167         }
168         else
169         {
170             super.doFilter(request,response,chain);
171         }
172     }
173     
174     protected GZIPResponseWrapper newGZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
175     {
176         return new GZIPResponseWrapper(request,response);
177     }
178     
179     /*
180      * Allows derived implementations to replace PrintWriter implementation
181      */
182     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
183     {
184         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
185     }
186     
187     public class GZIPResponseWrapper extends HttpServletResponseWrapper
188     {
189         HttpServletRequest _request;
190         boolean _noGzip;
191         PrintWriter _writer;
192         GzipStream _gzStream;
193         long _contentLength=-1;
194 
195         public GZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
196         {
197             super(response);
198             _request=request;
199         }
200 
201         public void setContentType(String ct)
202         {
203             super.setContentType(ct);
204 
205             if (ct!=null)
206             {
207                 int colon=ct.indexOf(";");
208                 if (colon>0)
209                     ct=ct.substring(0,colon);
210             }
211 
212             if ((_gzStream==null || _gzStream._out==null) && 
213                 (_mimeTypes==null && "application/gzip".equalsIgnoreCase(ct) ||
214                  _mimeTypes!=null && (ct==null||!_mimeTypes.contains(StringUtil.asciiToLowerCase(ct)))))
215             {
216                 noGzip();
217             }
218         }
219 
220         public void setStatus(int sc, String sm)
221         {
222             super.setStatus(sc,sm);
223             if (sc<200||sc>=300)
224                 noGzip();
225         }
226 
227         public void setStatus(int sc)
228         {
229             super.setStatus(sc);
230             if (sc<200||sc>=300)
231                 noGzip();
232         }
233 
234         public void setContentLength(int length)
235         {
236             _contentLength=length;
237             if (_gzStream!=null)
238                 _gzStream.setContentLength(length);
239         }
240         
241         public void addHeader(String name, String value)
242         {
243             if ("content-length".equalsIgnoreCase(name))
244             {
245                 _contentLength=Long.parseLong(value);
246                 if (_gzStream!=null)
247                     _gzStream.setContentLength(_contentLength);
248             }
249             else if ("content-type".equalsIgnoreCase(name))
250             {   
251                 setContentType(value);
252             }
253             else if ("content-encoding".equalsIgnoreCase(name))
254             {   
255                 super.addHeader(name,value);
256                 if (!isCommitted())
257                 {
258                     noGzip();
259                 }
260             }
261             else
262                 super.addHeader(name,value);
263         }
264 
265         public void setHeader(String name, String value)
266         {
267             if ("content-length".equalsIgnoreCase(name))
268             {
269                 _contentLength=Long.parseLong(value);
270                 if (_gzStream!=null)
271                     _gzStream.setContentLength(_contentLength);
272             }
273             else if ("content-type".equalsIgnoreCase(name))
274             {   
275                 setContentType(value);
276             }
277             else if ("content-encoding".equalsIgnoreCase(name))
278             {   
279                 super.setHeader(name,value);
280                 if (!isCommitted())
281                 {
282                     noGzip();
283                 }
284             }
285             else
286                 super.setHeader(name,value);
287         }
288 
289         public void setIntHeader(String name, int value)
290         {
291             if ("content-length".equalsIgnoreCase(name))
292             {
293                 _contentLength=value;
294                 if (_gzStream!=null)
295                     _gzStream.setContentLength(_contentLength);
296             }
297             else
298                 super.setIntHeader(name,value);
299         }
300 
301         public void flushBuffer() throws IOException
302         {
303             if (_writer!=null)
304                 _writer.flush();
305             if (_gzStream!=null)
306                 _gzStream.finish();
307             else
308                 getResponse().flushBuffer();
309         }
310 
311         public void reset()
312         {
313             super.reset();
314             if (_gzStream!=null)
315                 _gzStream.resetBuffer();
316             _writer=null;
317             _gzStream=null;
318             _noGzip=false;
319             _contentLength=-1;
320         }
321         
322         public void resetBuffer()
323         {
324             super.resetBuffer();
325             if (_gzStream!=null)
326                 _gzStream.resetBuffer();
327             _writer=null;
328             _gzStream=null;
329         }
330         
331         public void sendError(int sc, String msg) throws IOException
332         {
333             resetBuffer();
334             super.sendError(sc,msg);
335         }
336         
337         public void sendError(int sc) throws IOException
338         {
339             resetBuffer();
340             super.sendError(sc);
341         }
342         
343         public void sendRedirect(String location) throws IOException
344         {
345             resetBuffer();
346             super.sendRedirect(location);
347         }
348 
349         public ServletOutputStream getOutputStream() throws IOException
350         {
351             if (_gzStream==null)
352             {
353                 if (getResponse().isCommitted() || _noGzip)
354                     return getResponse().getOutputStream();
355                 
356                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
357             }
358             else if (_writer!=null)
359                 throw new IllegalStateException("getWriter() called");
360             
361             return _gzStream;   
362         }
363 
364         public PrintWriter getWriter() throws IOException
365         {
366             if (_writer==null)
367             { 
368                 if (_gzStream!=null)
369                     throw new IllegalStateException("getOutputStream() called");
370                 
371                 if (getResponse().isCommitted() || _noGzip)
372                     return getResponse().getWriter();
373                 
374                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
375                 _writer=newWriter(_gzStream,getCharacterEncoding());
376             }
377             return _writer;   
378         }
379 
380         void noGzip()
381         {
382             _noGzip=true;
383             if (_gzStream!=null)
384             {
385                 try
386                 {
387                     _gzStream.doNotGzip();
388                 }
389                 catch (IOException e)
390                 {
391                     throw new IllegalStateException(e);
392                 }
393             }
394         }
395         
396         void finish() throws IOException
397         {
398             if (_writer!=null && !_gzStream._closed)
399                 _writer.flush();
400             if (_gzStream!=null)
401                 _gzStream.finish();
402         }
403      
404         protected GzipStream newGzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
405         {
406             return new GzipStream(request,response,contentLength,bufferSize,minGzipSize);
407         }
408     }
409 
410     
411     public static class GzipStream extends ServletOutputStream
412     {
413         protected HttpServletRequest _request;
414         protected HttpServletResponse _response;
415         protected OutputStream _out;
416         protected ByteArrayOutputStream2 _bOut;
417         protected GZIPOutputStream _gzOut;
418         protected boolean _closed;
419         protected int _bufferSize;
420         protected int _minGzipSize;
421         protected long _contentLength;
422 
423         public GzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
424         {
425             _request=request;
426             _response=response;
427             _contentLength=contentLength;
428             _bufferSize=bufferSize;
429             _minGzipSize=minGzipSize;
430             if (minGzipSize==0)
431                 doGzip();
432         }
433 
434         public void resetBuffer()
435         {
436             if (_response.isCommitted())
437                 throw new IllegalStateException("Committed");
438             _closed=false;
439             _out=null;
440             _bOut=null;
441             if (_gzOut!=null)
442                 _response.setHeader("Content-Encoding",null);
443             _gzOut=null;
444         }
445 
446         public void setContentLength(long length)
447         {
448             _contentLength=length;
449         }
450         
451         public void flush() throws IOException
452         {
453             if (_out==null || _bOut!=null)
454             {
455                 if (_contentLength>0 && _contentLength<_minGzipSize)
456                     doNotGzip();
457                 else
458                     doGzip();
459             }
460             
461             _out.flush();
462         }
463 
464         public void close() throws IOException
465         {
466             if (_closed)
467                 return;
468             
469             if (_request.getAttribute("javax.servlet.include.request_uri")!=null)            
470                 flush();
471             else
472             {
473                 if (_bOut!=null)
474                 {
475                     if (_contentLength<0)
476                         _contentLength=_bOut.getCount();
477                     if (_contentLength<_minGzipSize)
478                         doNotGzip();
479                     else
480                         doGzip();
481                 }
482                 else if (_out==null)
483                 {
484                     doNotGzip();
485                 }
486 
487                 if (_gzOut!=null)
488                     _gzOut.close();
489                 else
490                     _out.close();
491                 _closed=true;
492             }
493         }  
494 
495         public void finish() throws IOException
496         {
497             if (!_closed)
498             {
499                 if (_out==null || _bOut!=null)
500                 {
501                     if (_contentLength>0 && _contentLength<_minGzipSize)
502                         doNotGzip();
503                     else
504                         doGzip();
505                 }
506                 
507                 if (_gzOut!=null && !_closed)
508                 {
509                     _closed=true;
510                     _gzOut.close();
511                 }
512             }
513         }  
514 
515         public void write(int b) throws IOException
516         {    
517             checkOut(1);
518             _out.write(b);
519         }
520 
521         public void write(byte b[]) throws IOException
522         {
523             checkOut(b.length);
524             _out.write(b);
525         }
526 
527         public void write(byte b[], int off, int len) throws IOException
528         {
529             checkOut(len);
530             _out.write(b,off,len);
531         }
532         
533         protected boolean setContentEncodingGzip()
534         {
535             _response.setHeader("Content-Encoding", "gzip");
536             return _response.containsHeader("Content-Encoding");
537         }
538         
539         public void doGzip() throws IOException
540         {
541             if (_gzOut==null) 
542             {
543                 if (_response.isCommitted())
544                     throw new IllegalStateException();
545                 
546                 if (setContentEncodingGzip())
547                 {
548                     _out=_gzOut=new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
549 
550                     if (_bOut!=null)
551                     {
552                         _out.write(_bOut.getBuf(),0,_bOut.getCount());
553                         _bOut=null;
554                     }
555                 }
556                 else 
557                     doNotGzip();
558             }
559         }
560         
561         public void doNotGzip() throws IOException
562         {
563             if (_gzOut!=null) 
564                 throw new IllegalStateException();
565             if (_out==null || _bOut!=null )
566             {
567                 _out=_response.getOutputStream();
568                 if (_contentLength>=0)
569                 {
570                     if(_contentLength<Integer.MAX_VALUE)
571                         _response.setContentLength((int)_contentLength);
572                     else
573                         _response.setHeader("Content-Length",Long.toString(_contentLength));
574                 }
575 
576                 if (_bOut!=null)
577                     _out.write(_bOut.getBuf(),0,_bOut.getCount());
578                 _bOut=null;
579             }   
580         }
581         
582         private void checkOut(int length) throws IOException 
583         {
584             if (_closed) 
585                 throw new IOException("CLOSED");
586             
587             if (_out==null)
588             {
589                 if (_response.isCommitted() || (_contentLength>=0 && _contentLength<_minGzipSize))
590                     doNotGzip();
591                 else if (length>_minGzipSize)
592                     doGzip();
593                 else
594                     _out=_bOut=new ByteArrayOutputStream2(_bufferSize);
595             }
596             else if (_bOut!=null)
597             {
598                 if (_response.isCommitted() || (_contentLength>=0 && _contentLength<_minGzipSize))
599                     doNotGzip();
600                 else if (length>=(_bOut.getBuf().length -_bOut.getCount()))
601                     doGzip();
602             }
603         }
604     }
605     
606 }