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 setHeader(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.setHeader(name,value);
221                 if (!isCommitted())
222                 {
223                     noGzip();
224                 }
225             }
226             else
227                 super.setHeader(name,value);
228         }
229 
230         public void setIntHeader(String name, int value)
231         {
232             if ("content-length".equalsIgnoreCase(name))
233             {
234                 _contentLength=value;
235                 if (_gzStream!=null)
236                     _gzStream.setContentLength(_contentLength);
237             }
238             else
239                 super.setIntHeader(name,value);
240         }
241 
242         public void flushBuffer() throws IOException
243         {
244             if (_writer!=null)
245                 _writer.flush();
246             if (_gzStream!=null)
247                 _gzStream.finish();
248             else
249                 getResponse().flushBuffer();
250         }
251 
252         public void reset()
253         {
254             super.reset();
255             if (_gzStream!=null)
256                 _gzStream.resetBuffer();
257             _writer=null;
258             _gzStream=null;
259             _noGzip=false;
260             _contentLength=-1;
261         }
262         
263         public void resetBuffer()
264         {
265             super.resetBuffer();
266             if (_gzStream!=null)
267                 _gzStream.resetBuffer();
268             _writer=null;
269             _gzStream=null;
270         }
271         
272         public void sendError(int sc, String msg) throws IOException
273         {
274             resetBuffer();
275             super.sendError(sc,msg);
276         }
277         
278         public void sendError(int sc) throws IOException
279         {
280             resetBuffer();
281             super.sendError(sc);
282         }
283         
284         public void sendRedirect(String location) throws IOException
285         {
286             resetBuffer();
287             super.sendRedirect(location);
288         }
289 
290         public ServletOutputStream getOutputStream() throws IOException
291         {
292             if (_gzStream==null)
293             {
294                 if (getResponse().isCommitted() || _noGzip)
295                     return getResponse().getOutputStream();
296                 
297                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
298             }
299             else if (_writer!=null)
300                 throw new IllegalStateException("getWriter() called");
301             
302             return _gzStream;   
303         }
304 
305         public PrintWriter getWriter() throws IOException
306         {
307             if (_writer==null)
308             { 
309                 if (_gzStream!=null)
310                     throw new IllegalStateException("getOutputStream() called");
311                 
312                 if (getResponse().isCommitted() || _noGzip)
313                     return getResponse().getWriter();
314                 
315                 _gzStream=newGzipStream(_request,(HttpServletResponse)getResponse(),_contentLength,_bufferSize,_minGzipSize);
316                 String encoding = getCharacterEncoding();
317                 _writer=encoding==null?new PrintWriter(_gzStream):new PrintWriter(new OutputStreamWriter(_gzStream,encoding));
318             }
319             return _writer;   
320         }
321 
322         void noGzip()
323         {
324             _noGzip=true;
325             if (_gzStream!=null)
326             {
327                 try
328                 {
329                     _gzStream.doNotGzip();
330                 }
331                 catch (IOException e)
332                 {
333                     throw new IllegalStateException();
334                 }
335             }
336         }
337         
338         void finish() throws IOException
339         {
340             if (_writer!=null)
341                 _writer.flush();
342             if (_gzStream!=null)
343                 _gzStream.finish();
344         }
345      
346         protected GzipStream newGzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
347         {
348             return new GzipStream(request,response,contentLength,bufferSize,minGzipSize);
349         }
350     }
351 
352     
353     public static class GzipStream extends ServletOutputStream
354     {
355         protected HttpServletRequest _request;
356         protected HttpServletResponse _response;
357         protected OutputStream _out;
358         protected ByteArrayOutputStream2 _bOut;
359         protected GZIPOutputStream _gzOut;
360         protected boolean _closed;
361         protected int _bufferSize;
362         protected int _minGzipSize;
363         protected long _contentLength;
364 
365         public GzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
366         {
367             _request=request;
368             _response=response;
369             _contentLength=contentLength;
370             _bufferSize=bufferSize;
371             _minGzipSize=minGzipSize;
372             if (minGzipSize==0)
373                 doGzip();
374         }
375 
376         public void resetBuffer()
377         {
378             _closed=false;
379             _out=null;
380             _bOut=null;
381             if (_gzOut!=null && !_response.isCommitted())
382                 _response.setHeader("Content-Encoding",null);
383             _gzOut=null;
384         }
385 
386         public void setContentLength(long length)
387         {
388             _contentLength=length;
389         }
390         
391         public void flush() throws IOException
392         {
393             if (_out==null || _bOut!=null)
394             {
395                 if (_contentLength>0 && _contentLength<_minGzipSize)
396                     doNotGzip();
397                 else
398                     doGzip();
399             }
400             
401             _out.flush();
402         }
403 
404         public void close() throws IOException
405         {
406             if (_request.getAttribute("javax.servlet.include.request_uri")!=null)            
407                 flush();
408             else
409             {
410                 if (_bOut!=null)
411                 {
412                     if (_contentLength<0)
413                         _contentLength=_bOut.getCount();
414                     if (_contentLength<_minGzipSize)
415                         doNotGzip();
416                     else
417                         doGzip();
418                 }
419                 else if (_out==null)
420                 {
421                     doNotGzip();
422                 }
423 
424                 if (_gzOut!=null)
425                     _gzOut.finish();
426                 _out.close();
427                 _closed=true;
428             }
429         }  
430 
431         public void finish() throws IOException
432         {
433             if (!_closed)
434             {
435                 if (_out==null || _bOut!=null)
436                 {
437                     if (_contentLength>0 && _contentLength<_minGzipSize)
438                         doNotGzip();
439                     else
440                         doGzip();
441                 }
442                 
443                 if (_gzOut!=null)
444                     _gzOut.finish();
445             }
446         }  
447 
448         public void write(int b) throws IOException
449         {    
450             checkOut(1);
451             _out.write(b);
452         }
453 
454         public void write(byte b[]) throws IOException
455         {
456             checkOut(b.length);
457             _out.write(b);
458         }
459 
460         public void write(byte b[], int off, int len) throws IOException
461         {
462             checkOut(len);
463             _out.write(b,off,len);
464         }
465         
466         protected boolean setContentEncodingGzip()
467         {
468             _response.setHeader("Content-Encoding", "gzip");
469             return _response.containsHeader("Content-Encoding");
470         }
471         
472         public void doGzip() throws IOException
473         {
474             if (_gzOut==null) 
475             {
476                 if (_response.isCommitted())
477                     throw new IllegalStateException();
478                 
479                 if (setContentEncodingGzip())
480                 {
481                     _out=_gzOut=new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
482 
483                     if (_bOut!=null)
484                     {
485                         _out.write(_bOut.getBuf(),0,_bOut.getCount());
486                         _bOut=null;
487                     }
488                 }
489                 else 
490                     doNotGzip();
491             }
492         }
493         
494         public void doNotGzip() throws IOException
495         {
496             if (_gzOut!=null) 
497                 throw new IllegalStateException();
498             if (_out==null || _bOut!=null )
499             {
500                 _out=_response.getOutputStream();
501                 if (_contentLength>=0)
502                 {
503                     if(_contentLength<Integer.MAX_VALUE)
504                         _response.setContentLength((int)_contentLength);
505                     else
506                         _response.setHeader("Content-Length",Long.toString(_contentLength));
507                 }
508 
509                 if (_bOut!=null)
510                     _out.write(_bOut.getBuf(),0,_bOut.getCount());
511                 _bOut=null;
512             }   
513         }
514         
515         private void checkOut(int length) throws IOException 
516         {
517             if (_closed) 
518             {
519                 new Throwable().printStackTrace();
520                 throw new IOException("CLOSED");
521             }
522             
523             if (_out==null)
524             {
525                 if (_response.isCommitted() || (_contentLength>=0 && _contentLength<_minGzipSize))
526                     doNotGzip();
527                 else if (length>_minGzipSize)
528                     doGzip();
529                 else
530                     _out=_bOut=new ByteArrayOutputStream2(_bufferSize);
531             }
532             else if (_bOut!=null)
533             {
534                 if (_response.isCommitted() || (_contentLength>=0 && _contentLength<_minGzipSize))
535                     doNotGzip();
536                 else if (length>=(_bOut.size()-_bOut.getCount()))
537                     doGzip();
538             }
539         }
540     }
541 }