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 }