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