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 }