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  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 comma 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()) && !"HEAD".equalsIgnoreCase(request.getMethod()))
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                 if (!response.isCommitted())
138                     response.reset();
139                 throw e;
140             }
141             finally
142             {
143                 if (exceptional && !response.isCommitted())
144                 {
145                     wrappedResponse.resetBuffer();
146                     wrappedResponse.noGzip();
147                 }
148                 else
149                     wrappedResponse.finish();
150             }
151         }
152         else
153         {
154             super.doFilter(request,response,chain);
155         }
156     }
157 
158     protected GZIPResponseWrapper newGZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
159     {
160         return new GZIPResponseWrapper(request,response);
161     }
162 
163     /*
164      * Allows derived implementations to replace PrintWriter implementation
165      */
166     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
167     {
168         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
169     }
170 
171     public class GZIPResponseWrapper extends HttpServletResponseWrapper
172     {
173         HttpServletRequest _request;
174         boolean _noGzip;
175         PrintWriter _writer;
176         GzipStream _gzStream;
177         long _contentLength=-1;
178 
179         public GZIPResponseWrapper(HttpServletRequest request, HttpServletResponse response)
180         {
181             super(response);
182             _request=request;
183         }
184 
185         public void setContentType(String ct)
186         {
187             super.setContentType(ct);
188 
189             if (ct!=null)
190             {
191                 int colon=ct.indexOf(";");
192                 if (colon>0)
193                     ct=ct.substring(0,colon);
194             }
195 
196             if ((_gzStream==null || _gzStream._out==null) &&
197                 (_mimeTypes==null && "application/gzip".equalsIgnoreCase(ct) ||
198                  _mimeTypes!=null && (ct==null||!_mimeTypes.contains(StringUtil.asciiToLowerCase(ct)))))
199             {
200                 noGzip();
201             }
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                 _writer=newWriter(_gzStream,getCharacterEncoding());
361             }
362             return _writer;
363         }
364 
365         void noGzip()
366         {
367             _noGzip=true;
368             if (_gzStream!=null)
369             {
370                 try
371                 {
372                     _gzStream.doNotGzip();
373                 }
374                 catch (IOException e)
375                 {
376                     throw new IllegalStateException();
377                 }
378             }
379         }
380 
381         void finish() throws IOException
382         {
383             if (_writer!=null && !_gzStream._closed)
384                 _writer.flush();
385             if (_gzStream!=null)
386                 _gzStream.finish();
387         }
388 
389         protected GzipStream newGzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
390         {
391             return new GzipStream(request,response,contentLength,bufferSize,minGzipSize);
392         }
393     }
394 
395 
396     public static class GzipStream extends ServletOutputStream
397     {
398         protected HttpServletRequest _request;
399         protected HttpServletResponse _response;
400         protected OutputStream _out;
401         protected ByteArrayOutputStream2 _bOut;
402         protected GZIPOutputStream _gzOut;
403         protected boolean _closed;
404         protected int _bufferSize;
405         protected int _minGzipSize;
406         protected long _contentLength;
407 
408         public GzipStream(HttpServletRequest request,HttpServletResponse response,long contentLength,int bufferSize, int minGzipSize) throws IOException
409         {
410             _request=request;
411             _response=response;
412             _contentLength=contentLength;
413             _bufferSize=bufferSize;
414             _minGzipSize=minGzipSize;
415             if (minGzipSize==0)
416                 doGzip();
417         }
418 
419         public void resetBuffer()
420         {
421             _closed=false;
422             _out=null;
423             _bOut=null;
424             if (_gzOut!=null && !_response.isCommitted())
425                 _response.setHeader("Content-Encoding",null);
426             _gzOut=null;
427         }
428 
429         public void setContentLength(long length)
430         {
431             _contentLength=length;
432         }
433 
434         public void flush() throws IOException
435         {
436             if (_out==null || _bOut!=null)
437             {
438                 if (_contentLength>0 && _contentLength<_minGzipSize)
439                     doNotGzip();
440                 else
441                     doGzip();
442             }
443 
444             _out.flush();
445         }
446 
447         public void close() throws IOException
448         {
449             if (_request.getAttribute("javax.servlet.include.request_uri")!=null)
450                 flush();
451             else
452             {
453                 if (_bOut!=null)
454                 {
455                     if (_contentLength<0)
456                         _contentLength=_bOut.getCount();
457                     if (_contentLength<_minGzipSize)
458                         doNotGzip();
459                     else
460                         doGzip();
461                 }
462                 else if (_out==null)
463                 {
464                     doNotGzip();
465                 }
466 
467                 if (_gzOut!=null)
468                     _gzOut.close();
469                 else
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 && !_closed)
488                 {
489                     _closed=true;
490                     _gzOut.close();
491                 }
492             }
493         }
494 
495         public void write(int b) throws IOException
496         {
497             checkOut(1);
498             _out.write(b);
499         }
500 
501         public void write(byte b[]) throws IOException
502         {
503             checkOut(b.length);
504             _out.write(b);
505         }
506 
507         public void write(byte b[], int off, int len) throws IOException
508         {
509             checkOut(len);
510             _out.write(b,off,len);
511         }
512 
513         protected boolean setContentEncodingGzip()
514         {
515             _response.setHeader("Content-Encoding", "gzip");
516             return _response.containsHeader("Content-Encoding");
517         }
518 
519         public void doGzip() throws IOException
520         {
521             if (_gzOut==null)
522             {
523                 if (_response.isCommitted())
524                     throw new IllegalStateException();
525 
526                 if (setContentEncodingGzip())
527                 {
528                     _out=_gzOut=new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
529 
530                     if (_bOut!=null)
531                     {
532                         _out.write(_bOut.getBuf(),0,_bOut.getCount());
533                         _bOut=null;
534                     }
535                 }
536                 else
537                     doNotGzip();
538             }
539         }
540 
541         public void doNotGzip() throws IOException
542         {
543             if (_gzOut!=null)
544                 throw new IllegalStateException();
545             if (_out==null || _bOut!=null )
546             {
547                 _out=_response.getOutputStream();
548                 if (_contentLength>=0)
549                 {
550                     if(_contentLength<Integer.MAX_VALUE)
551                         _response.setContentLength((int)_contentLength);
552                     else
553                         _response.setHeader("Content-Length",Long.toString(_contentLength));
554                 }
555 
556                 if (_bOut!=null)
557                     _out.write(_bOut.getBuf(),0,_bOut.getCount());
558                 _bOut=null;
559             }
560         }
561 
562         private void checkOut(int length) throws IOException
563         {
564             if (_closed)
565                 throw new IOException("CLOSED");
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 }