View Javadoc

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