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