/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * @author Scott Ferguson */ package com.caucho.filters; import com.caucho.util.FreeList; import com.caucho.util.RuntimeExceptionWrapper; import com.caucho.vfs.GzipStream; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.CharArrayWriter; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.util.HashMap; /** * Compresses the response output if the browser accepts compression. * * <p/>Browsers which support gzip compression will set the Accept-Encoding * header. If GzipFilter detects the gzip compression, it will compress * the output. * * <p/>GzipFilter will always set the "Vary: Accept-Encoding" header because * the output depends on the request </p> * * @since Resin 2.0.6 */ public class GzipFilter implements Filter { private final FreeList<GzipResponse> _freeList = new FreeList<GzipResponse>(16); private final FreeList<GzipPlainResponse> _plainFreeList = new FreeList<GzipPlainResponse>(16); private static final int NONE = 0; private static final int GZIP = 1; private static final int DEFLATE = 2; private static final AllowEntry ALLOW = new AllowEntry(); private static final AllowEntry DENY = new AllowEntry(); private ServletContext _app; private boolean _embedError; private boolean _useVary = true; private boolean _noCache = false; private HashMap<String,AllowEntry> _contentTypeMap; private boolean _hasDeny; /** * Set true if the vary support should be enabled. */ public void setUseVary(boolean useVary) { _useVary = useVary; } /** * Set true if the output should not be cached. */ public void setNoCache(boolean noCache) { _noCache = noCache; } /** * Set true if errors should be embedded in the output. */ public void setEmbedErrorInOutput(boolean embedError) { _embedError = embedError; } /** * Adds an allowed content type. */ public void addAllowContentType(String type) { if (_contentTypeMap == null) _contentTypeMap = new HashMap<String,AllowEntry>(); _contentTypeMap.put(type, ALLOW); } /** * Adds a deny content type. */ public void addDenyContentType(String type) { if (_contentTypeMap == null) _contentTypeMap = new HashMap<String,AllowEntry>(); _hasDeny = true; _contentTypeMap.put(type, DENY); } public void init(FilterConfig config) throws ServletException { _app = config.getServletContext(); _embedError = "true".equals(config.getInitParameter("embed-error-in-output")); String value = config.getInitParameter("use-vary"); if (value == null) { } else if ("false".equals(value)) _useVary = false; else if ("false".equals(value)) _useVary = true; value = config.getInitParameter("no-cache"); if (value == null) { } else if ("true".equals(value)) _noCache = true; else if ("false".equals(value)) _noCache = true; } /** * Creates a wrapper to compress the output. */ public void doFilter(ServletRequest request, ServletResponse response, FilterChain nextFilter) throws ServletException, IOException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; int encoding = allowGzip(req, res); if (encoding != NONE) { GzipResponse gzipResponse = _freeList.allocate(); if (gzipResponse == null) gzipResponse = new GzipResponse(); gzipResponse.setUseDeflate(encoding == DEFLATE); gzipResponse.init(res); try { nextFilter.doFilter(req, gzipResponse); } catch (Exception e) { handleError(e, gzipResponse); } gzipResponse.close(); _freeList.free(gzipResponse); } else { GzipPlainResponse plainRes = _plainFreeList.allocate(); if (plainRes == null) plainRes = new GzipPlainResponse(); plainRes.init(res); // addVaryHeader(res); nextFilter.doFilter(req, plainRes); plainRes.close(); _plainFreeList.free(plainRes); } } protected void addVaryHeader(HttpServletResponse response) { if (_noCache) response.setHeader("Cache-Control", "no-cache"); else if (_useVary) { // #3043, server/183q if (! response.containsHeader("Vary")) response.addHeader("Vary", "Accept-Encoding"); } else response.setHeader("Cache-Control", "private"); } /** * Returns true if the GZip is allowed. */ protected int allowGzip(HttpServletRequest req, HttpServletResponse res) { String acceptEncoding = req.getHeader("Accept-Encoding"); if (acceptEncoding == null) return NONE; else if (req.getHeader("Range") != null) return NONE; else if (acceptEncoding.indexOf("gzip") >= 0) return GZIP; else if (acceptEncoding.indexOf("deflate") >= 0) return DEFLATE; else return NONE; } /** * Any cleanup for the filter. */ public void destroy() { } private void handleError(Exception e, CauchoResponseWrapper res) throws ServletException, IOException { if (_embedError && res.isCommitted()) { _app.log(e.getMessage(), e); CharArrayWriter writer = new CharArrayWriter(); PrintWriter pw = new PrintWriter(writer); e.printStackTrace(pw); pw.flush(); res.getWriter().print(writer.toCharArray()); } else if (e instanceof ServletException) throw (ServletException) e; else if (e instanceof IOException) throw (IOException) e; else throw RuntimeExceptionWrapper.create(e); } class GzipResponse extends CauchoResponseWrapper { private boolean _useVary = true; private boolean _allowGzip = true; private boolean _useDeflate = false; private final GzipStream _savedGzipStream = new GzipStream(); private GzipStream _gzipStream; /** * Set true if the response should use deflate. */ public void setUseDeflate(boolean useDeflate) { _useDeflate = useDeflate; } /** * Check for valid content type. */ @Override public void setContentType(String value) { super.setContentType(value); if (_contentTypeMap == null) { return; } int p = value.indexOf(';'); if (p > 0) value = value.substring(0, p); AllowEntry entry = _contentTypeMap.get(value); if (entry == ALLOW) _allowGzip = true; else if (entry == DENY) { _useVary = false; _allowGzip = false; } else if (! _hasDeny) { _useVary = false; _allowGzip = false; } else { _allowGzip = true; } } /** * Check for valid content type. */ public void setHeader(String header, String value) { if (header.equalsIgnoreCase("Content-Type")) setContentType(value); else if (header.equalsIgnoreCase("Content-Encoding")) { _allowGzip = false; super.setHeader(header, value); } else super.setHeader(header, value); } /** * Check for valid content type. */ public void addHeader(String header, String value) { if (header.equalsIgnoreCase("Content-Type")) setContentType(value); else if (header.equalsIgnoreCase("Content-Encoding")) { _allowGzip = false; super.addHeader(header, value); } else super.addHeader(header, value); } /** * This needs to be bypassed because the file's content * length has nothing to do with the returned length. */ public void setContentLength(int length) { } /** * If the status changes, need to disable the response. */ public void setStatus(int status, String message) { super.setStatus(status, message); if (_gzipStream != null) { _gzipStream.setEnable(false); _response.setHeader("Content-Encoding", "plain"); } _allowGzip = false; } /** * If the status changes, need to disable the response. */ public void setStatus(int status) { super.setStatus(status); /* if (status == 206 || status == 200) return; */ if (status == 200) return; _allowGzip = false; } /** * Clears the output stream */ public void reset() { super.reset(); if (_gzipStream != null) _gzipStream.reset(); } /** * Returns the underlying stream */ @Override public OutputStream getStream() throws IOException { if (_useVary) addVaryHeader(_response); if (! _allowGzip) return _response.getOutputStream(); OutputStream os = _response.getOutputStream(); if (_useDeflate) _response.setHeader("Content-Encoding", "deflate"); else _response.setHeader("Content-Encoding", "gzip"); _gzipStream = _savedGzipStream; _gzipStream.setGzip(! _useDeflate); _gzipStream.init(os); return _gzipStream; } public void close() throws IOException { try { super.close(); } finally { _useVary = true; _allowGzip = true; _useDeflate = false; GzipStream gzipStream = _gzipStream; _gzipStream = null; if (gzipStream != null) { if (gzipStream.isData()) gzipStream.close(); else gzipStream.free(); } } } } // handles a non-gzipped response because the client can't support class GzipPlainResponse extends CauchoResponseWrapper { private boolean _useVary = true; /** * Check for valid content type. */ @Override public void setContentType(String value) { super.setContentType(value); if (_contentTypeMap == null) { return; } int p = value.indexOf(';'); if (p > 0) value = value.substring(0, p); AllowEntry entry = _contentTypeMap.get(value); if (entry == ALLOW) _useVary = true; else if (entry == DENY) _useVary = false; else if (! _hasDeny) _useVary = false; else _useVary = true; } /** * Returns the underlying stream */ public OutputStream getStream() throws IOException { if (_useVary) addVaryHeader(_response); return _response.getOutputStream(); } public void close() throws IOException { try { super.close(); } finally { _useVary = true; } } } static class AllowEntry { } }