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