View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.servlets.gzip;
20  
21  import java.io.IOException;
22  import java.io.OutputStream;
23  import java.io.OutputStreamWriter;
24  import java.io.PrintWriter;
25  import java.io.UnsupportedEncodingException;
26  import java.util.HashSet;
27  import java.util.Set;
28  import java.util.StringTokenizer;
29  import java.util.zip.DeflaterOutputStream;
30  import java.util.zip.GZIPOutputStream;
31  
32  import javax.servlet.AsyncEvent;
33  import javax.servlet.AsyncListener;
34  import javax.servlet.ServletException;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  
38  import org.eclipse.jetty.http.HttpMethod;
39  import org.eclipse.jetty.http.MimeTypes;
40  import org.eclipse.jetty.server.Request;
41  import org.eclipse.jetty.server.handler.HandlerWrapper;
42  import org.eclipse.jetty.util.log.Log;
43  import org.eclipse.jetty.util.log.Logger;
44  
45  /* ------------------------------------------------------------ */
46  /**
47   * GZIP Handler This handler will gzip the content of a response if:
48   * <ul>
49   * <li>The filter is mapped to a matching path</li>
50   * <li>The response status code is >=200 and <300
51   * <li>The content length is unknown or more than the <code>minGzipSize</code> initParameter or the minGzipSize is 0(default)</li>
52   * <li>The content-type is in the comma separated list of mimeTypes set in the <code>mimeTypes</code> initParameter or if no mimeTypes are defined the
53   * content-type is not "application/gzip"</li>
54   * <li>No content-encoding is specified by the resource</li>
55   * </ul>
56   *
57   * <p>
58   * Compressing the content can greatly improve the network bandwidth usage, but at a cost of memory and CPU cycles. If this handler is used for static content,
59   * then use of efficient direct NIO may be prevented, thus use of the gzip mechanism of the <code>org.eclipse.jetty.servlet.DefaultServlet</code> is advised instead.
60   * </p>
61   */
62  public class GzipHandler extends HandlerWrapper
63  {
64      private static final Logger LOG = Log.getLogger(GzipHandler.class);
65  
66      final protected Set<String> _mimeTypes=new HashSet<>();
67      protected boolean _excludeMimeTypes=false;
68      protected Set<String> _excludedUA;
69      protected int _bufferSize = 8192;
70      protected int _minGzipSize = 256;
71      protected String _vary = "Accept-Encoding, User-Agent";
72  
73      /* ------------------------------------------------------------ */
74      /**
75       * Instantiates a new gzip handler.
76       */
77      public GzipHandler()
78      {
79      }
80  
81      /* ------------------------------------------------------------ */
82      /**
83       * Get the mime types.
84       *
85       * @return mime types to set
86       */
87      public Set<String> getMimeTypes()
88      {
89          return _mimeTypes;
90      }
91  
92      /* ------------------------------------------------------------ */
93      /**
94       * Set the mime types.
95       *
96       * @param mimeTypes
97       *            the mime types to set
98       */
99      public void setMimeTypes(Set<String> mimeTypes)
100     {
101         _excludeMimeTypes=false;
102         _mimeTypes.clear();
103         _mimeTypes.addAll(mimeTypes);
104     }
105 
106     /* ------------------------------------------------------------ */
107     /**
108      * Set the mime types.
109      *
110      * @param mimeTypes
111      *            the mime types to set
112      */
113     public void setMimeTypes(String mimeTypes)
114     {
115         if (mimeTypes != null)
116         {
117             _excludeMimeTypes=false;
118             _mimeTypes.clear();
119             StringTokenizer tok = new StringTokenizer(mimeTypes,",",false);
120             while (tok.hasMoreTokens())
121             {
122                 _mimeTypes.add(tok.nextToken());
123             }
124         }
125     }
126 
127     /* ------------------------------------------------------------ */
128     /**
129      * Set the mime types.
130      */
131     public void setExcludeMimeTypes(boolean exclude)
132     {
133         _excludeMimeTypes=exclude;
134     }
135 
136     /* ------------------------------------------------------------ */
137     /**
138      * Get the excluded user agents.
139      *
140      * @return excluded user agents
141      */
142     public Set<String> getExcluded()
143     {
144         return _excludedUA;
145     }
146 
147     /* ------------------------------------------------------------ */
148     /**
149      * Set the excluded user agents.
150      *
151      * @param excluded
152      *            excluded user agents to set
153      */
154     public void setExcluded(Set<String> excluded)
155     {
156         _excludedUA = excluded;
157     }
158 
159     /* ------------------------------------------------------------ */
160     /**
161      * Set the excluded user agents.
162      *
163      * @param excluded
164      *            excluded user agents to set
165      */
166     public void setExcluded(String excluded)
167     {
168         if (excluded != null)
169         {
170             _excludedUA = new HashSet<String>();
171             StringTokenizer tok = new StringTokenizer(excluded,",",false);
172             while (tok.hasMoreTokens())
173                 _excludedUA.add(tok.nextToken());
174         }
175     }
176 
177     /* ------------------------------------------------------------ */
178     /**
179      * @return The value of the Vary header set if a response can be compressed.
180      */
181     public String getVary()
182     {
183         return _vary;
184     }
185 
186     /* ------------------------------------------------------------ */
187     /**
188      * Set the value of the Vary header sent with responses that could be compressed.  
189      * <p>
190      * By default it is set to 'Accept-Encoding, User-Agent' since IE6 is excluded by 
191      * default from the excludedAgents. If user-agents are not to be excluded, then 
192      * this can be set to 'Accept-Encoding'.  Note also that shared caches may cache 
193      * many copies of a resource that is varied by User-Agent - one per variation of the 
194      * User-Agent, unless the cache does some normalization of the UA string.
195      * @param vary The value of the Vary header set if a response can be compressed.
196      */
197     public void setVary(String vary)
198     {
199         _vary = vary;
200     }
201 
202     /* ------------------------------------------------------------ */
203     /**
204      * Get the buffer size.
205      *
206      * @return the buffer size
207      */
208     public int getBufferSize()
209     {
210         return _bufferSize;
211     }
212 
213     /* ------------------------------------------------------------ */
214     /**
215      * Set the buffer size.
216      *
217      * @param bufferSize
218      *            buffer size to set
219      */
220     public void setBufferSize(int bufferSize)
221     {
222         _bufferSize = bufferSize;
223     }
224 
225     /* ------------------------------------------------------------ */
226     /**
227      * Get the minimum reponse size.
228      *
229      * @return minimum reponse size
230      */
231     public int getMinGzipSize()
232     {
233         return _minGzipSize;
234     }
235 
236     /* ------------------------------------------------------------ */
237     /**
238      * Set the minimum reponse size.
239      *
240      * @param minGzipSize
241      *            minimum reponse size
242      */
243     public void setMinGzipSize(int minGzipSize)
244     {
245         _minGzipSize = minGzipSize;
246     }
247     
248     /* ------------------------------------------------------------ */
249     @Override
250     protected void doStart() throws Exception
251     {
252         if (_mimeTypes.size()==0)
253         {
254             for (String type:MimeTypes.getKnownMimeTypes())
255             {
256                 if (type.startsWith("image/")||
257                     type.startsWith("audio/")||
258                     type.startsWith("video/"))
259                     _mimeTypes.add(type);
260                 _mimeTypes.add("application/compress");
261                 _mimeTypes.add("application/zip");
262                 _mimeTypes.add("application/gzip");
263             }
264         }
265     }
266 
267     /* ------------------------------------------------------------ */
268     /**
269      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
270      */
271     @Override
272     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
273     {
274         if (_handler!=null && isStarted())
275         {
276             String ae = request.getHeader("accept-encoding");
277             if (ae != null && ae.indexOf("gzip")>=0 && !response.containsHeader("Content-Encoding")
278                     && !HttpMethod.HEAD.is(request.getMethod()))
279             {
280                 if (_excludedUA!=null)
281                 {
282                     String ua = request.getHeader("User-Agent");
283                     if (_excludedUA.contains(ua))
284                     {
285                         _handler.handle(target,baseRequest, request, response);
286                         return;
287                     }
288                 }
289 
290                 final CompressedResponseWrapper wrappedResponse = newGzipResponseWrapper(request,response);
291 
292                 boolean exceptional=true;
293                 try
294                 {
295                     _handler.handle(target, baseRequest, request, wrappedResponse);
296                     exceptional=false;
297                 }
298                 finally
299                 {
300                     if (request.isAsyncStarted())
301                     {
302                         request.getAsyncContext().addListener(new AsyncListener()
303                         {
304                             
305                             @Override
306                             public void onTimeout(AsyncEvent event) throws IOException
307                             {
308                             }
309                             
310                             @Override
311                             public void onStartAsync(AsyncEvent event) throws IOException
312                             {
313                             }
314                             
315                             @Override
316                             public void onError(AsyncEvent event) throws IOException
317                             {
318                             }
319                             
320                             @Override
321                             public void onComplete(AsyncEvent event) throws IOException
322                             {
323                                 try
324                                 {
325                                     wrappedResponse.finish();
326                                 }
327                                 catch(IOException e)
328                                 {
329                                     LOG.warn(e);
330                                 }
331                             }
332                         });
333                     }
334                     else if (exceptional && !response.isCommitted())
335                     {
336                         wrappedResponse.resetBuffer();
337                         wrappedResponse.noCompression();
338                     }
339                     else
340                         wrappedResponse.finish();
341                 }
342             }
343             else
344             {
345                 _handler.handle(target,baseRequest, request, response);
346             }
347         }
348     }
349 
350     /**
351      * Allows derived implementations to replace ResponseWrapper implementation.
352      *
353      * @param request the request
354      * @param response the response
355      * @return the gzip response wrapper
356      */
357     protected CompressedResponseWrapper newGzipResponseWrapper(HttpServletRequest request, HttpServletResponse response)
358     {
359         return new CompressedResponseWrapper(request,response)
360         {
361             {
362                 super.setMimeTypes(GzipHandler.this._mimeTypes,GzipHandler.this._excludeMimeTypes);
363                 super.setBufferSize(GzipHandler.this._bufferSize);
364                 super.setMinCompressSize(GzipHandler.this._minGzipSize);
365             }
366 
367             @Override
368             protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
369             {
370                 return new AbstractCompressedStream("gzip",request,this,_vary)
371                 {
372                     @Override
373                     protected DeflaterOutputStream createStream() throws IOException
374                     {
375                         return new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
376                     }
377                 };
378             }
379 
380             @Override
381             protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
382             {
383                 return GzipHandler.this.newWriter(out,encoding);
384             }
385         };
386     }
387 
388     /**
389      * Allows derived implementations to replace PrintWriter implementation.
390      *
391      * @param out the out
392      * @param encoding the encoding
393      * @return the prints the writer
394      * @throws UnsupportedEncodingException
395      */
396     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
397     {
398         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
399     }
400 }