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  public class GzipFilter extends UserAgentFilter
65  {
66      protected Set _mimeTypes;
67      protected int _bufferSize=8192;
68      protected int _minGzipSize=256;
69      protected Set _excluded;
70      
71      public void init(FilterConfig filterConfig) throws ServletException
72      {
73          super.init(filterConfig);
74          
75          String tmp=filterConfig.getInitParameter("bufferSize");
76          if (tmp!=null)
77              _bufferSize=Integer.parseInt(tmp);
78  
79          tmp=filterConfig.getInitParameter("minGzipSize");
80          if (tmp!=null)
81              _minGzipSize=Integer.parseInt(tmp);
82          
83          tmp=filterConfig.getInitParameter("mimeTypes");
84          if (tmp!=null)
85          {
86              _mimeTypes=new HashSet();
87              StringTokenizer tok = new StringTokenizer(tmp,",",false);
88              while (tok.hasMoreTokens())
89                  _mimeTypes.add(tok.nextToken());
90          }
91          
92          tmp=filterConfig.getInitParameter("excludedAgents");
93          if (tmp!=null)
94          {
95              _excluded=new HashSet();
96              StringTokenizer tok = new StringTokenizer(tmp,",",false);
97              while (tok.hasMoreTokens())
98                  _excluded.add(tok.nextToken());
99          }
100     }
101 
102     public void destroy()
103     {
104     }
105 
106     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
107         throws IOException, ServletException
108     {
109         HttpServletRequest request=(HttpServletRequest)req;
110         HttpServletResponse response=(HttpServletResponse)res;
111 
112         String ae = request.getHeader("accept-encoding");
113         if (ae != null && ae.indexOf("gzip")>=0 && !response.containsHeader("Content-Encoding"))
114         {
115             if (_excluded!=null)
116             {
117                 String ua=getUserAgent(request);
118                 if (_excluded.contains(ua))
119                 {
120                     super.doFilter(request,response,chain);
121                     return;
122                 }
123             }
124 
125             final GZIPResponseWrapper wrappedResponse=newGZIPResponseWrapper(request,response);
126             
127             boolean exceptional=true;
128             try
129             {
130                 super.doFilter(request,wrappedResponse,chain);
131                 exceptional=false;
132             }
133             finally
134             {
135                 Continuation continuation = ContinuationSupport.getContinuation(request,response);
136                 if (continuation.isSuspended() && continuation.isResponseWrapped())   
137                 {
138                     continuation.addContinuationListener(new ContinuationListener()
139                     {
140                         public void onComplete(Continuation continuation)
141                         {
142                             try
143                             {
144                                 wrappedResponse.finish();
145                             }
146                             catch(IOException e)
147                             {
148                                 Log.warn(e);
149                             }
150                         }
151 
152                         public void onTimeout(Continuation continuation)
153                         {}
154                     });
155                 }
156                 else if (exceptional && !response.isCommitted())
157                 {
158                     wrappedResponse.resetBuffer();
159                     wrappedResponse.noGzip();
160                 }
161                 else
162                     wrappedResponse.finish();
163             }
164         }
165         else
166         {
167             super.doFilter(request,response,chain);
168         }
169     }
170     
171     protected GZIPResponseWrapper newGZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
172     {
173         return new GZIPResponseWrapper(request,response);
174     }
175 
176     public class GZIPResponseWrapper extends HttpServletResponseWrapper
177     {
178         HttpServletRequest _request;
179         boolean _noGzip;
180         PrintWriter _writer;
181         GzipStream _gzStream;
182         long _contentLength=-1;
183 
184         public GZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
185         {
186             super(response);
187             _request=request;
188         }
189 
190         public void setContentType(String ct)
191         {
192             super.setContentType(ct);
193             int colon=ct.indexOf(";");
194             if (colon>0)
195                 ct=ct.substring(0,colon);
196 
197             if ((_gzStream==null || _gzStream._out==null) && 
198                 (_mimeTypes==null && "application/gzip".equalsIgnoreCase(ct) ||
199                  _mimeTypes!=null && !_mimeTypes.contains(StringUtil.asciiToLowerCase(ct))))
200             {
201                 noGzip();
202             }
203         }
204 
205         public void setStatus(int sc, String sm)
206         {
207             super.setStatus(sc,sm);
208             if (sc<200||sc>=300)
209                 noGzip();
210         }
211 
212         public void setStatus(int sc)
213         {
214             super.setStatus(sc);
215             if (sc<200||sc>=300)
216                 noGzip();
217         }
218 
219         public void setContentLength(int length)
220         {
221             _contentLength=length;
222             if (_gzStream!=null)
223                 _gzStream.setContentLength(length);
224         }
225         
226         public void addHeader(String name, String value)
227         {
228             if ("content-length".equalsIgnoreCase(name))
229             {
230                 _contentLength=Long.parseLong(value);
231                 if (_gzStream!=null)
232                     _gzStream.setContentLength(_contentLength);
233             }
234             else if ("content-type".equalsIgnoreCase(name))
235             {   
236                 setContentType(value);
237             }
238             else if ("content-encoding".equalsIgnoreCase(name))
239             {   
240                 super.addHeader(name,value);
241                 if (!isCommitted())
242                 {
243                     noGzip();
244                 }
245             }
246             else
247                 super.addHeader(name,value);
248         }
249 
250         public void setHeader(String name, String value)
251         {
252             if ("content-length".equalsIgnoreCase(name))
253             {
254                 _contentLength=Long.parseLong(value);
255                 if (_gzStream!=null)
256                     _gzStream.setContentLength(_contentLength);
257             }
258             else if ("content-type".equalsIgnoreCase(name))
259             {   
260                 setContentType(value);
261             }
262             else if ("content-encoding".equalsIgnoreCase(name))
263             {   
264                 super.setHeader(name,value);
265                 if (!isCommitted())
266                 {
267                     noGzip();
268                 }
269             }
270             else
271                 super.setHeader(name,value);
272         }
273 
274         public void setIntHeader(String name, int value)
275         {
276             if ("content-length".equalsIgnoreCase(name))
277             {
278                 _contentLength=value;
279                 if (_gzStream!=null)
280                     _gzStream.setContentLength(_contentLength);
281             }
282             else
283                 super.setIntHeader(name,value);
284         }
285 
286         public void flushBuffer() throws IOException
287         {
288             if (_writer!=null)
289                 _writer.flush();
290             if (_gzStream!=null)
291                 _gzStream.finish();
292             else
293                 getResponse().flushBuffer();
294         }
295 
296         public void reset()
297         {
298             super.reset();
299             if (_gzStream!=null)
300                 _gzStream.resetBuffer();
301             _writer=null;
302             _gzStream=null;
303             _noGzip=false;
304             _contentLength=-1;
305         }
306         
307         public void resetBuffer()
308         {
309             super.resetBuffer();
310             if (_gzStream!=null)
311                 _gzStream.resetBuffer();
312             _writer=null;
313             _gzStream=null;
314         }
315         
316         public void sendError(int sc, String msg) throws IOException
317         {
318             resetBuffer();
319             super.sendError(sc,msg);
320         }
321         
322         public void sendError(int sc) throws IOException
323         {
324             resetBuffer();
325             super.sendError(sc);
326         }
327         
328         public void sendRedirect(String location) throws IOException
329         {
330             resetBuffer();
331             super.sendRedirect(location);
332         }
333 
334         public ServletOutputStream getOutputStream() throws IOException
335         {
336             if (_gzStream==null)
337             {
338                 if (getResponse().isCommitted() || _noGzip)
339                     return getResponse().getOutputStream();
340                 
341                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
342             }
343             else if (_writer!=null)
344                 throw new IllegalStateException("getWriter() called");
345             
346             return _gzStream;   
347         }
348 
349         public PrintWriter getWriter() throws IOException
350         {
351             if (_writer==null)
352             { 
353                 if (_gzStream!=null)
354                     throw new IllegalStateException("getOutputStream() called");
355                 
356                 if (getResponse().isCommitted() || _noGzip)
357                     return getResponse().getWriter();
358                 
359                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
360                 String encoding = getCharacterEncoding();
361                 _writer=encoding==null?new PrintWriter(_gzStream):new PrintWriter(new OutputStreamWriter(_gzStream,encoding));
362             }
363             return _writer;   
364         }
365 
366         void noGzip()
367         {
368             _noGzip=true;
369             if (_gzStream!=null)
370             {
371                 try
372                 {
373                     _gzStream.doNotGzip();
374                 }
375                 catch (IOException e)
376                 {
377                     throw new IllegalStateException(e);
378                 }
379             }
380         }
381         
382         void finish() throws IOException
383         {
384             if (_writer!=null)
385                 _writer.flush();
386             if (_gzStream!=null)
387                 _gzStream.finish();
388         }
389      
390         protected GzipStream newGzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
391         {
392             return new GzipStream(request,response,contentLength,bufferSize,minGzipSize);
393         }
394     }
395 
396     
397     public static class GzipStream extends ServletOutputStream
398     {
399         protected HttpServletRequest _request;
400         protected HttpServletResponse _response;
401         protected OutputStream _out;
402         protected ByteArrayOutputStream2 _bOut;
403         protected GZIPOutputStream _gzOut;
404         protected boolean _closed;
405         protected int _bufferSize;
406         protected int _minGzipSize;
407         protected long _contentLength;
408 
409         public GzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
410         {
411             _request=request;
412             _response=response;
413             _contentLength=contentLength;
414             _bufferSize=bufferSize;
415             _minGzipSize=minGzipSize;
416             if (minGzipSize==0)
417                 doGzip();
418         }
419 
420         public void resetBuffer()
421         {
422             if (_response.isCommitted())
423                 throw new IllegalStateException("Committed");
424             _closed=false;
425             _out=null;
426             _bOut=null;
427             if (_gzOut!=null)
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 (_closed)
453                 return;
454             
455             if (_request.getAttribute("javax.servlet.include.request_uri")!=null)            
456                 flush();
457             else
458             {
459                 if (_bOut!=null)
460                 {
461                     if (_contentLength<0)
462                         _contentLength=_bOut.getCount();
463                     if (_contentLength<_minGzipSize)
464                         doNotGzip();
465                     else
466                         doGzip();
467                 }
468                 else if (_out==null)
469                 {
470                     doNotGzip();
471                 }
472 
473                 if (_gzOut!=null)
474                     _gzOut.finish();
475                 _out.close();
476                 _closed=true;
477             }
478         }  
479 
480         public void finish() throws IOException
481         {
482             if (!_closed)
483             {
484                 if (_out==null || _bOut!=null)
485                 {
486                     if (_contentLength>0 && _contentLength<_minGzipSize)
487                         doNotGzip();
488                     else
489                         doGzip();
490                 }
491                 
492                 if (_gzOut!=null)
493                     _gzOut.finish();
494             }
495         }  
496 
497         public void write(int b) throws IOException
498         {    
499             checkOut(1);
500             _out.write(b);
501         }
502 
503         public void write(byte b[]) throws IOException
504         {
505             checkOut(b.length);
506             _out.write(b);
507         }
508 
509         public void write(byte b[], int off, int len) throws IOException
510         {
511             checkOut(len);
512             _out.write(b,off,len);
513         }
514         
515         protected boolean setContentEncodingGzip()
516         {
517             _response.setHeader("Content-Encoding", "gzip");
518             return _response.containsHeader("Content-Encoding");
519         }
520         
521         public void doGzip() throws IOException
522         {
523             if (_gzOut==null) 
524             {
525                 if (_response.isCommitted())
526                     throw new IllegalStateException();
527                 
528                 if (setContentEncodingGzip())
529                 {
530                     _out=_gzOut=new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
531 
532                     if (_bOut!=null)
533                     {
534                         _out.write(_bOut.getBuf(),0,_bOut.getCount());
535                         _bOut=null;
536                     }
537                 }
538                 else 
539                     doNotGzip();
540             }
541         }
542         
543         public void doNotGzip() throws IOException
544         {
545             if (_gzOut!=null) 
546                 throw new IllegalStateException();
547             if (_out==null || _bOut!=null )
548             {
549                 _out=_response.getOutputStream();
550                 if (_contentLength>=0)
551                 {
552                     if(_contentLength<Integer.MAX_VALUE)
553                         _response.setContentLength((int)_contentLength);
554                     else
555                         _response.setHeader("Content-Length",Long.toString(_contentLength));
556                 }
557 
558                 if (_bOut!=null)
559                     _out.write(_bOut.getBuf(),0,_bOut.getCount());
560                 _bOut=null;
561             }   
562         }
563         
564         private void checkOut(int length) throws IOException 
565         {
566             if (_closed) 
567                 throw new IOException("CLOSED");
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.getBuf().length -_bOut.getCount()))
583                     doGzip();
584             }
585         }
586     }
587     
588 }