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         super.doStart();
266     }
267 
268     /* ------------------------------------------------------------ */
269     /**
270      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
271      */
272     @Override
273     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
274     {
275         if (_handler!=null && isStarted())
276         {
277             String ae = request.getHeader("accept-encoding");
278             if (ae != null && ae.indexOf("gzip")>=0 && !response.containsHeader("Content-Encoding")
279                     && !HttpMethod.HEAD.is(request.getMethod()))
280             {
281                 if (_excludedUA!=null)
282                 {
283                     String ua = request.getHeader("User-Agent");
284                     if (_excludedUA.contains(ua))
285                     {
286                         _handler.handle(target,baseRequest, request, response);
287                         return;
288                     }
289                 }
290 
291                 final CompressedResponseWrapper wrappedResponse = newGzipResponseWrapper(request,response);
292 
293                 boolean exceptional=true;
294                 try
295                 {
296                     _handler.handle(target, baseRequest, request, wrappedResponse);
297                     exceptional=false;
298                 }
299                 finally
300                 {
301                     if (request.isAsyncStarted())
302                     {
303                         request.getAsyncContext().addListener(new AsyncListener()
304                         {
305                             
306                             @Override
307                             public void onTimeout(AsyncEvent event) throws IOException
308                             {
309                             }
310                             
311                             @Override
312                             public void onStartAsync(AsyncEvent event) throws IOException
313                             {
314                             }
315                             
316                             @Override
317                             public void onError(AsyncEvent event) throws IOException
318                             {
319                             }
320                             
321                             @Override
322                             public void onComplete(AsyncEvent event) throws IOException
323                             {
324                                 try
325                                 {
326                                     wrappedResponse.finish();
327                                 }
328                                 catch(IOException e)
329                                 {
330                                     LOG.warn(e);
331                                 }
332                             }
333                         });
334                     }
335                     else if (exceptional && !response.isCommitted())
336                     {
337                         wrappedResponse.resetBuffer();
338                         wrappedResponse.noCompression();
339                     }
340                     else
341                         wrappedResponse.finish();
342                 }
343             }
344             else
345             {
346                 _handler.handle(target,baseRequest, request, response);
347             }
348         }
349     }
350 
351     /**
352      * Allows derived implementations to replace ResponseWrapper implementation.
353      *
354      * @param request the request
355      * @param response the response
356      * @return the gzip response wrapper
357      */
358     protected CompressedResponseWrapper newGzipResponseWrapper(HttpServletRequest request, HttpServletResponse response)
359     {
360         return new CompressedResponseWrapper(request,response)
361         {
362             {
363                 super.setMimeTypes(GzipHandler.this._mimeTypes,GzipHandler.this._excludeMimeTypes);
364                 super.setBufferSize(GzipHandler.this._bufferSize);
365                 super.setMinCompressSize(GzipHandler.this._minGzipSize);
366             }
367 
368             @Override
369             protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
370             {
371                 return new AbstractCompressedStream("gzip",request,this,_vary)
372                 {
373                     @Override
374                     protected DeflaterOutputStream createStream() throws IOException
375                     {
376                         return new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
377                     }
378                 };
379             }
380 
381             @Override
382             protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
383             {
384                 return GzipHandler.this.newWriter(out,encoding);
385             }
386         };
387     }
388 
389     /**
390      * Allows derived implementations to replace PrintWriter implementation.
391      *
392      * @param out the out
393      * @param encoding the encoding
394      * @return the prints the writer
395      * @throws UnsupportedEncodingException
396      */
397     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
398     {
399         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
400     }
401 }