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