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