/*
* 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;
}
}