View Javadoc

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