/* * Copyright 2017 OmniFaces * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. */ package org.omnifaces.filter; import static java.lang.String.format; import static org.omnifaces.util.Utils.unmodifiableSet; import java.io.IOException; import java.util.Arrays; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; import javax.faces.webapp.FacesServlet; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.omnifaces.io.ResettableBuffer; import org.omnifaces.io.ResettableBufferedOutputStream; import org.omnifaces.io.ResettableBufferedWriter; import org.omnifaces.servlet.GzipHttpServletResponse; import org.omnifaces.servlet.HttpServletResponseOutputWrapper; /** * <p> * The {@link GzipResponseFilter} will apply GZIP compression on responses whenever applicable. GZIP will greatly reduce * the response size when applied on character based responses like HTML, CSS and JS, on average it can save up to ~70% * of bandwidth. * <p> * While GZIP is normally to be configured in the servlet container (e.g. <code><Context compression="on"></code> * in Tomcat, or <code><property name="compression" value="on"></code> in Glassfish), this filter allows a * servlet container independent way of configuring GZIP compression and also allows enabling GZIP compression anyway * on 3rd party hosts where you have no control over servlet container configuration. * * <h3>Installation</h3> * <p> * To get it to run, map this filter on the desired <code><url-pattern></code> or maybe even on the * <code><servlet-name></code> of the <code>FacesServlet</code>. A <code>Filter</code> is by default dispatched * on <code>REQUEST</code> only, you might want to explicitly add the <code>ERROR</code> dispatcher to get it to run * on error pages as well. * <pre> * <filter> * <filter-name>gzipResponseFilter</filter-name> * <filter-class>org.omnifaces.filter.GzipResponseFilter</filter-class> * </filter> * <filter-mapping> * <filter-name>gzipResponseFilter</filter-name> * <url-pattern>/*</url-pattern> * <dispatcher>REQUEST</dispatcher> * <dispatcher>ERROR</dispatcher> * </filter-mapping> * </pre> * <p> * Mapping on <code>/*</code> may be too global as some types of requests (comet, long polling, etc) cannot be gzipped. * In that case, consider mapping it to the exact <code><servlet-name></code> of the {@link FacesServlet} in the * same <code>web.xml</code>. * <pre> * <filter> * <filter-name>gzipResponseFilter</filter-name> * <filter-class>org.omnifaces.filter.GzipResponseFilter</filter-class> * </filter> * <filter-mapping> * <filter-name>gzipResponseFilter</filter-name> * <servlet-name>facesServlet</servlet-name> * <dispatcher>REQUEST</dispatcher> * <dispatcher>ERROR</dispatcher> * </filter-mapping> * </pre> * * <h3>Configuration (optional)</h3> * <p> * This filter supports two initialization parameters which needs to be placed in <code><filter></code> element * as follows: * <pre> * <init-param> * <description>The threshold size in bytes. Must be a number between 0 and 9999. Defaults to 150.</description> * <param-name>threshold</param-name> * <param-value>150</param-value> * </init-param> * <init-param> * <description>The mimetypes which needs to be compressed. Must be a commaseparated string. Defaults to the below values.</description> * <param-name>mimetypes</param-name> * <param-value> * text/plain, text/html, text/xml, text/css, text/javascript, text/csv, text/rtf, * application/xml, application/xhtml+xml, application/javascript, application/json, * image/svg+xml * </param-value> * </init-param> * </pre> * <p> * The default <code>threshold</code> is thus 150 bytes. This means that when the response is not larger than 150 bytes, * then it will not be compressed with GZIP. Only when it's larger than 150 bytes, then it will be compressed. A * threshold of between 150 and 1000 bytes is recommended due to overhead and latency of compression/decompression. * The value must be a number between 0 and 9999. A value larger than 2000 is not recommended. * <p> * The <code>mimetypes</code> represents a comma separated string of mime types which needs to be compressed. It's * exactly that value which appears in the <code>Content-Type</code> header of the response. The in the above example * mentioned mime types are already the default values. Note that GZIP does not have any benefit when applied on * binary mimetypes like images, office documents, PDF files, etcetera. So setting it for them is not recommended. * * @author Bauke Scholtz * @since 1.1 * @see GzipHttpServletResponse * @see HttpServletResponseOutputWrapper * @see ResettableBuffer * @see ResettableBufferedOutputStream * @see ResettableBufferedWriter * @see HttpFilter */ public class GzipResponseFilter extends HttpFilter { // Constants ------------------------------------------------------------------------------------------------------ private static final String INIT_PARAM_THRESHOLD = "threshold"; private static final String INIT_PARAM_MIMETYPES = "mimetypes"; private static final int DEFAULT_THRESHOLD = 150; private static final Set<String> DEFAULT_MIMETYPES = unmodifiableSet( "text/plain", "text/html", "text/xml", "text/css", "text/javascript", "text/csv", "text/rtf", "application/xml", "application/xhtml+xml", "application/javascript", "application/json", "image/svg+xml" ); private static final String ERROR_THRESHOLD = "The 'threshold' init param must be a number between 0 and 9999." + " Encountered an invalid value of '%s'."; // Vars ----------------------------------------------------------------------------------------------------------- private Set<String> mimetypes = DEFAULT_MIMETYPES; private int threshold = DEFAULT_THRESHOLD; // Actions -------------------------------------------------------------------------------------------------------- /** * Initializes the filter parameters. */ @Override public void init() throws ServletException { String thresholdParam = getInitParameter(INIT_PARAM_THRESHOLD); if (thresholdParam != null) { if (!thresholdParam.matches("[0-9]{1,4}")) { throw new ServletException(format(ERROR_THRESHOLD, thresholdParam)); } else { threshold = Integer.valueOf(thresholdParam); } } String mimetypesParam = getInitParameter(INIT_PARAM_MIMETYPES); if (mimetypesParam != null) { mimetypes = new HashSet<>(Arrays.asList(mimetypesParam.split("\\s*,\\s*"))); } } /** * Perform the filtering job. Only if the client accepts GZIP based on the request headers, then wrap the response * in a {@link GzipHttpServletResponse} and pass it through the filter chain. */ @Override public void doFilter (HttpServletRequest request, HttpServletResponse response, HttpSession session, FilterChain chain) throws ServletException, IOException { if (acceptsGzip(request)) { GzipHttpServletResponse gzipResponse = new GzipHttpServletResponse(response, threshold, mimetypes); chain.doFilter(request, gzipResponse); gzipResponse.close(); // Mandatory for the case the threshold limit hasn't been reached. } else { chain.doFilter(request, response); } } // Helpers -------------------------------------------------------------------------------------------------------- /** * Returns whether the given request indicates that the client accepts GZIP encoding. * @param request The request to be checked. * @return <code>true</code> if the client accepts GZIP encoding, otherwise <code>false</code>. */ private static boolean acceptsGzip(HttpServletRequest request) { for (Enumeration<String> e = request.getHeaders("Accept-Encoding"); e.hasMoreElements();) { if (e.nextElement().contains("gzip")) { return true; } } return false; } }