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
19package org.eclipse.jetty.server.handler;
20
21import java.io.IOException;
22import java.io.OutputStream;
23import java.io.OutputStreamWriter;
24import java.io.PrintWriter;
25import java.io.UnsupportedEncodingException;
26import java.util.HashSet;
27import java.util.Set;
28import java.util.StringTokenizer;
29import java.util.zip.DeflaterOutputStream;
30import java.util.zip.GZIPOutputStream;
31
32import javax.servlet.ServletException;
33import javax.servlet.http.HttpServletRequest;
34import javax.servlet.http.HttpServletResponse;
35
36import org.eclipse.jetty.continuation.Continuation;
37import org.eclipse.jetty.continuation.ContinuationListener;
38import org.eclipse.jetty.continuation.ContinuationSupport;
39import org.eclipse.jetty.http.HttpMethods;
40import org.eclipse.jetty.http.gzip.CompressedResponseWrapper;
41import org.eclipse.jetty.http.gzip.AbstractCompressedStream;
42import org.eclipse.jetty.server.Request;
43import org.eclipse.jetty.util.log.Log;
44import 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 */
63public 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}
357