View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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.server.handler;
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.ServletException;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  
36  import org.eclipse.jetty.continuation.Continuation;
37  import org.eclipse.jetty.continuation.ContinuationListener;
38  import org.eclipse.jetty.continuation.ContinuationSupport;
39  import org.eclipse.jetty.http.HttpMethods;
40  import org.eclipse.jetty.http.gzip.CompressedResponseWrapper;
41  import org.eclipse.jetty.http.gzip.AbstractCompressedStream;
42  import org.eclipse.jetty.server.Request;
43  import org.eclipse.jetty.util.log.Log;
44  import org.eclipse.jetty.util.log.Logger;
45  
46  /* ------------------------------------------------------------ */
47  /**
48   * GZIP Handler This handler will gzip the content of a response if:
49   * <ul>
50   * <li>The filter is mapped to a matching path</li>
51   * <li>The response status code is >=200 and <300
52   * <li>The content length is unknown or more than the <code>minGzipSize</code> initParameter or the minGzipSize is 0(default)</li>
53   * <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
54   * content-type is not "application/gzip"</li>
55   * <li>No content-encoding is specified by the resource</li>
56   * </ul>
57   * 
58   * <p>
59   * 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,
60   * 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.
61   * </p>
62   */
63  public class GzipHandler extends HandlerWrapper
64  {
65      private static final Logger LOG = Log.getLogger(GzipHandler.class);
66  
67      protected Set<String> _mimeTypes;
68      protected Set<String> _excluded;
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         _mimeTypes = mimeTypes;
102     }
103 
104     /* ------------------------------------------------------------ */
105     /**
106      * Set the mime types.
107      * 
108      * @param mimeTypes
109      *            the mime types to set
110      */
111     public void setMimeTypes(String mimeTypes)
112     {
113         if (mimeTypes != null)
114         {
115             _mimeTypes = new HashSet<String>();
116             StringTokenizer tok = new StringTokenizer(mimeTypes,",",false);
117             while (tok.hasMoreTokens())
118             {
119                 _mimeTypes.add(tok.nextToken());
120             }
121         }
122     }
123 
124     /* ------------------------------------------------------------ */
125     /**
126      * Get the excluded user agents.
127      * 
128      * @return excluded user agents
129      */
130     public Set<String> getExcluded()
131     {
132         return _excluded;
133     }
134 
135     /* ------------------------------------------------------------ */
136     /**
137      * Set the excluded user agents.
138      * 
139      * @param excluded
140      *            excluded user agents to set
141      */
142     public void setExcluded(Set<String> excluded)
143     {
144         _excluded = excluded;
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(String excluded)
155     {
156         if (excluded != null)
157         {
158             _excluded = new HashSet<String>();
159             StringTokenizer tok = new StringTokenizer(excluded,",",false);
160             while (tok.hasMoreTokens())
161                 _excluded.add(tok.nextToken());
162         }
163     }
164 
165     /* ------------------------------------------------------------ */
166     /**
167      * @return The value of the Vary header set if a response can be compressed.
168      */
169     public String getVary()
170     {
171         return _vary;
172     }
173 
174     /* ------------------------------------------------------------ */
175     /**
176      * Set the value of the Vary header sent with responses that could be compressed.  
177      * <p>
178      * By default it is set to 'Accept-Encoding, User-Agent' since IE6 is excluded by 
179      * default from the excludedAgents. If user-agents are not to be excluded, then 
180      * this can be set to 'Accept-Encoding'.  Note also that shared caches may cache 
181      * many copies of a resource that is varied by User-Agent - one per variation of the 
182      * User-Agent, unless the cache does some normalization of the UA string.
183      * @param vary The value of the Vary header set if a response can be compressed.
184      */
185     public void setVary(String vary)
186     {
187         _vary = vary;
188     }
189 
190     /* ------------------------------------------------------------ */
191     /**
192      * Get the buffer size.
193      * 
194      * @return the buffer size
195      */
196     public int getBufferSize()
197     {
198         return _bufferSize;
199     }
200 
201     /* ------------------------------------------------------------ */
202     /**
203      * Set the buffer size.
204      * 
205      * @param bufferSize
206      *            buffer size to set
207      */
208     public void setBufferSize(int bufferSize)
209     {
210         _bufferSize = bufferSize;
211     }
212 
213     /* ------------------------------------------------------------ */
214     /**
215      * Get the minimum reponse size.
216      * 
217      * @return minimum reponse size
218      */
219     public int getMinGzipSize()
220     {
221         return _minGzipSize;
222     }
223 
224     /* ------------------------------------------------------------ */
225     /**
226      * Set the minimum reponse size.
227      * 
228      * @param minGzipSize
229      *            minimum reponse size
230      */
231     public void setMinGzipSize(int minGzipSize)
232     {
233         _minGzipSize = minGzipSize;
234     }
235 
236     /* ------------------------------------------------------------ */
237     /**
238      * @see org.eclipse.jetty.server.handler.HandlerWrapper#handle(java.lang.String, org.eclipse.jetty.server.Request, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
239      */
240     @Override
241     public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
242     {
243         if (_handler!=null && isStarted())
244         {
245             String ae = request.getHeader("accept-encoding");
246             if (ae != null && ae.indexOf("gzip")>=0 && !response.containsHeader("Content-Encoding")
247                     && !HttpMethods.HEAD.equalsIgnoreCase(request.getMethod()))
248             {
249                 if (_excluded!=null)
250                 {
251                     String ua = request.getHeader("User-Agent");
252                     if (_excluded.contains(ua))
253                     {
254                         _handler.handle(target,baseRequest, request, response);
255                         return;
256                     }
257                 }
258 
259                 final CompressedResponseWrapper wrappedResponse = newGzipResponseWrapper(request,response);
260                 
261                 boolean exceptional=true;
262                 try
263                 {
264                     _handler.handle(target, baseRequest, request, wrappedResponse);
265                     exceptional=false;
266                 }
267                 finally
268                 {
269                     Continuation continuation = ContinuationSupport.getContinuation(request);
270                     if (continuation.isSuspended() && continuation.isResponseWrapped())   
271                     {
272                         continuation.addContinuationListener(new ContinuationListener()
273                         {
274                             public void onComplete(Continuation continuation)
275                             {
276                                 try
277                                 {
278                                     wrappedResponse.finish();
279                                 }
280                                 catch(IOException e)
281                                 {
282                                     LOG.warn(e);
283                                 }
284                             }
285 
286                             public void onTimeout(Continuation continuation)
287                             {}
288                         });
289                     }
290                     else if (exceptional && !response.isCommitted())
291                     {
292                         wrappedResponse.resetBuffer();
293                         wrappedResponse.noCompression();
294                     }
295                     else
296                         wrappedResponse.finish();
297                 }
298             }
299             else
300             {
301                 _handler.handle(target,baseRequest, request, response);
302             }
303         }
304     }
305 
306     /**
307      * Allows derived implementations to replace ResponseWrapper implementation.
308      *
309      * @param request the request
310      * @param response the response
311      * @return the gzip response wrapper
312      */
313     protected CompressedResponseWrapper newGzipResponseWrapper(HttpServletRequest request, HttpServletResponse response)
314     {
315         return new CompressedResponseWrapper(request,response)
316         {
317             {
318                 super.setMimeTypes(GzipHandler.this._mimeTypes);
319                 super.setBufferSize(GzipHandler.this._bufferSize);
320                 super.setMinCompressSize(GzipHandler.this._minGzipSize);
321             }
322             
323             @Override
324             protected AbstractCompressedStream newCompressedStream(HttpServletRequest request,HttpServletResponse response) throws IOException
325             {
326                 return new AbstractCompressedStream("gzip",request,this,_vary)
327                 {
328                     @Override
329                     protected DeflaterOutputStream createStream() throws IOException
330                     {
331                         return new GZIPOutputStream(_response.getOutputStream(),_bufferSize);
332                     }
333                 };
334             }
335             
336             @Override
337             protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
338             {
339                 return GzipHandler.this.newWriter(out,encoding);
340             }
341         };
342     }
343     
344     /**
345      * Allows derived implementations to replace PrintWriter implementation.
346      *
347      * @param out the out
348      * @param encoding the encoding
349      * @return the prints the writer
350      * @throws UnsupportedEncodingException
351      */
352     protected PrintWriter newWriter(OutputStream out,String encoding) throws UnsupportedEncodingException
353     {
354         return encoding==null?new PrintWriter(out):new PrintWriter(new OutputStreamWriter(out,encoding));
355     }
356 }