/* * 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.servlet; import static org.omnifaces.util.Utils.isOneOf; import java.io.IOException; import java.io.OutputStream; import java.util.Set; import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import javax.servlet.http.HttpServletResponse; import org.omnifaces.io.ResettableBufferedOutputStream; /** * This HTTP servlet response wrapper will GZIP the response when the given threshold has exceeded and the response * content type matches one of the given mimetypes. * * @author Bauke Scholtz * @since 1.1 */ public class GzipHttpServletResponse extends HttpServletResponseOutputWrapper { // Constants ------------------------------------------------------------------------------------------------------ private static final Pattern NO_TRANSFORM = Pattern.compile("((.*)[\\s,])?no-transform([\\s,](.*))?", Pattern.CASE_INSENSITIVE); // Properties ----------------------------------------------------------------------------------------------------- private int threshold; private Set<String> mimetypes; private long contentLength; private String vary; private boolean noGzip; private boolean closing; private GzipThresholdOutputStream output; // Constructors --------------------------------------------------------------------------------------------------- /** * Construct a new GZIP HTTP servlet response based on the given wrapped response, threshold and mimetypes. * @param wrapped The wrapped response. * @param threshold The GZIP buffer threshold. * @param mimetypes The mimetypes which needs to be compressed with GZIP. */ public GzipHttpServletResponse(HttpServletResponse wrapped, int threshold, Set<String> mimetypes) { super(wrapped); this.threshold = threshold; this.mimetypes = mimetypes; } // Actions -------------------------------------------------------------------------------------------------------- @Override public void setContentLength(int contentLength) { // Get hold of content length locally to avoid it from being set on responses which will actually be gzipped. this.contentLength = contentLength; } // @Override Servlet 3.1. public void setContentLengthLong(long contentLength) { // Get hold of content length locally to avoid it from being set on responses which will actually be gzipped. this.contentLength = contentLength; } @Override public void setHeader(String name, String value) { super.setHeader(name, value); if (name != null) { String lowerCasedName = name.toLowerCase(); if ("vary".equals(lowerCasedName)) { vary = value; } else if ("content-range".equals(lowerCasedName)) { noGzip = (value != null); } else if ("cache-control".equals(lowerCasedName)) { noGzip = (value != null && NO_TRANSFORM.matcher(value).matches()); } } } @Override public void addHeader(String name, String value) { super.addHeader(name, value); if (name != null && value != null) { String lowerCasedName = name.toLowerCase(); if ("vary".equals(lowerCasedName)) { vary = ((vary != null) ? (vary + ",") : "") + value; } else if ("content-range".equals(lowerCasedName)) { noGzip = true; } else if ("cache-control".equals(lowerCasedName)) { noGzip = (noGzip || NO_TRANSFORM.matcher(value).matches()); } } } @Override public void flushBuffer() throws IOException { if (isCommitted()) { super.flushBuffer(); } } @Override public void reset() { super.reset(); if (!isCommitted()) { contentLength = 0; vary = null; noGzip = false; if (output != null) { output.reset(); } } } @Override public void close() throws IOException { closing = true; super.close(); closing = false; } @Override protected OutputStream createOutputStream() { output = new GzipThresholdOutputStream(threshold); return output; } // Inner classes -------------------------------------------------------------------------------------------------- /** * This output stream will switch to GZIP compression when the given threshold is exceeded. * <p> * This is an inner class because it needs to be able to manipulate the response headers once the decision whether * to GZIP or not has been made. * * @author Bauke Scholtz */ private class GzipThresholdOutputStream extends ResettableBufferedOutputStream { // Constructors ----------------------------------------------------------------------------------------------- public GzipThresholdOutputStream(int threshold) { super(threshold); } // Actions ---------------------------------------------------------------------------------------------------- /** * Create GZIP output stream if necessary. That is, when the given <code>doGzip</code> argument is * <code>true</code>, the current response does not have the <code>Cache-Control: no-transform</code> or * <code>Content-Range</code> headers, the current response is not committed, the content type is not * <code>null</code> and the content type matches one of the mimetypes. */ @Override public OutputStream createOutputStream(boolean doGzip) throws IOException { HttpServletResponse originalResponse = (HttpServletResponse) getResponse(); if (doGzip && !noGzip && (closing || !isCommitted())) { String contentType = getContentType(); if (contentType != null && mimetypes.contains(contentType.split(";", 2)[0])) { addHeader("Content-Encoding", "gzip"); setHeader("Vary", (!isOneOf(vary, null, "*") ? (vary + ",") : "") + "Accept-Encoding"); return new GZIPOutputStream(originalResponse.getOutputStream()); } } if (!doGzip) { setContentLength(getWrittenBytes()); } if (contentLength > 0) { originalResponse.setHeader("Content-Length", String.valueOf(contentLength)); } return originalResponse.getOutputStream(); } } }