Author: markt
Date: Tue Mar 8 20:45:57 2016
New Revision: 1734150
URL: http://svn.apache.org/viewvc?rev=1734150&view=rev
Log:
Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=59017
Make the pre-compressed file support in the Default Servlet generic so any
compression may be used rather than just gzip.
Patch provided by Mikko Tiihonen.
This closes #28
Modified:
tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
tomcat/trunk/test/org/apache/catalina/servlets/TestDefaultServlet.java
tomcat/trunk/webapps/docs/changelog.xml
tomcat/trunk/webapps/docs/default-servlet.xml
Modified: tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java?rev=1734150&r1=1734149&r2=1734150&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
(original)
+++ tomcat/trunk/java/org/apache/catalina/servlets/DefaultServlet.java Tue Mar
8 20:45:57 2016
@@ -36,6 +36,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
+import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
@@ -193,9 +194,9 @@ public class DefaultServlet extends Http
protected boolean readOnly = true;
/**
- * Should be serve gzip versions of files. By default, it's set to false.
+ * List of compression formats to serve and their preference order.
*/
- protected boolean gzip = false;
+ protected CompressionFormat[] compressionFormats;
/**
* The output buffer size to use when serving resources.
@@ -280,8 +281,8 @@ public class DefaultServlet extends Http
if (getServletConfig().getInitParameter("readonly") != null)
readOnly =
Boolean.parseBoolean(getServletConfig().getInitParameter("readonly"));
- if (getServletConfig().getInitParameter("gzip") != null)
- gzip =
Boolean.parseBoolean(getServletConfig().getInitParameter("gzip"));
+ compressionFormats =
parseCompressionFormats(getServletConfig().getInitParameter("precompressed"),
+ getServletConfig().getInitParameter("gzip"));
if (getServletConfig().getInitParameter("sendfileSize") != null)
sendfileSize =
@@ -321,6 +322,27 @@ public class DefaultServlet extends Http
}
}
+ private CompressionFormat[] parseCompressionFormats(String precompressed,
String gzip) {
+ List<CompressionFormat> ret = new ArrayList<>();
+ if (precompressed != null && precompressed.indexOf('=') > 0) {
+ for (String pair : precompressed.split(",")) {
+ String[] setting = pair.split("=");
+ String encoding = setting[0];
+ String extension = setting[1];
+ ret.add(new CompressionFormat(extension, encoding));
+ }
+ } else if (precompressed != null) {
+ if (Boolean.parseBoolean(precompressed)) {
+ ret.add(new CompressionFormat(".br", "br"));
+ ret.add(new CompressionFormat(".gz", "gzip"));
+ }
+ } else if (Boolean.parseBoolean(gzip)) {
+ // gzip handling is for backwards compatibility with Tomcat 8.x
+ ret.add(new CompressionFormat(".gz", "gzip"));
+ }
+ return ret.toArray(new CompressionFormat[ret.size()]);
+ }
+
// ------------------------------------------------------ Protected Methods
@@ -790,7 +812,7 @@ public class DefaultServlet extends Http
}
// These need to reflect the original resource, not the potentially
- // gzip'd version of the resource so get them now if they are going to
+ // precompressed version of the resource so get them now if they are
going to
// be needed later
String eTag = null;
String lastModifiedHttp = null;
@@ -800,11 +822,11 @@ public class DefaultServlet extends Http
}
- // Serve a gzipped version of the file if present
- boolean usingGzippedVersion = false;
- if (gzip && !included && resource.isFile() && !path.endsWith(".gz")) {
- WebResource gzipResource = resources.getResource(path + ".gz");
- if (gzipResource.exists() && gzipResource.isFile()) {
+ // Serve a precompressed version of the file if present
+ boolean usingPrecompressedVersion = false;
+ if (compressionFormats.length > 0 && !included && resource.isFile() &&
!pathEndsWithCompressedExtension(path)) {
+ List<PrecompressedResource> precompressedResources =
getAvailablePrecompressedResources(path);
+ if (!precompressedResources.isEmpty()) {
Collection<String> varyHeaders = response.getHeaders("Vary");
boolean addRequired = true;
for (String varyHeader : varyHeaders) {
@@ -817,10 +839,11 @@ public class DefaultServlet extends Http
if (addRequired) {
response.addHeader("Vary", "accept-encoding");
}
- if (checkIfGzip(request)) {
- response.addHeader("Content-Encoding", "gzip");
- resource = gzipResource;
- usingGzippedVersion = true;
+ PrecompressedResource bestResource =
getBestPrecompressedResource(request, precompressedResources);
+ if (bestResource != null) {
+ response.addHeader("Content-Encoding",
bestResource.format.encoding);
+ resource = bestResource.resource;
+ usingPrecompressedVersion = true;
}
}
}
@@ -878,7 +901,7 @@ public class DefaultServlet extends Http
} catch (IllegalStateException e) {
// If it fails, we try to get a Writer instead if we're
// trying to serve a text file
- if (!usingGzippedVersion &&
+ if (!usingPrecompressedVersion &&
((contentType == null) ||
(contentType.startsWith("text")) ||
(contentType.endsWith("xml")) ||
@@ -1039,6 +1062,81 @@ public class DefaultServlet extends Http
}
}
+ private boolean pathEndsWithCompressedExtension(String path) {
+ for (CompressionFormat format : compressionFormats) {
+ if (path.endsWith(format.extension)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<PrecompressedResource>
getAvailablePrecompressedResources(String path) {
+ List<PrecompressedResource> ret = new
ArrayList<>(compressionFormats.length);
+ for (CompressionFormat format : compressionFormats) {
+ WebResource precompressedResource = resources.getResource(path +
format.extension);
+ if (precompressedResource.exists() &&
precompressedResource.isFile()) {
+ ret.add(new PrecompressedResource(precompressedResource,
format));
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Match the client preferred encoding formts to the available
precompressed resources.
+ *
+ * @param request The servlet request we are processing
+ * @param precompressedResources List of available precompressed
resources.
+ * @return The best matching precompressed resource or null if no match
was found.
+ */
+ private PrecompressedResource
getBestPrecompressedResource(HttpServletRequest request,
List<PrecompressedResource> precompressedResources) {
+ Enumeration<String> headers = request.getHeaders("Accept-Encoding");
+ PrecompressedResource bestResource = null;
+ double bestResourceQuality = 0;
+ while (headers.hasMoreElements()) {
+ String header = headers.nextElement();
+ for (String preference : header.split(",")) {
+ if (bestResourceQuality >= 1) {
+ return bestResource;
+ }
+ double quality = 1;
+ int qualityIdx = preference.indexOf(';');
+ if (qualityIdx > 0) {
+ int equalsIdx = preference.indexOf('=', qualityIdx + 1);
+ if (equalsIdx == -1) {
+ continue;
+ }
+ quality =
Double.parseDouble(preference.substring(equalsIdx + 1).trim());
+ }
+ if (quality > bestResourceQuality) {
+ String encoding = preference;
+ if (qualityIdx > 0) {
+ encoding = encoding.substring(0, qualityIdx);
+ }
+ encoding = encoding.trim();
+ if ("identity".equals(encoding)) {
+ bestResource = null;
+ bestResourceQuality = quality;
+ continue;
+ }
+ if ("*".equals(encoding)) {
+ bestResource = precompressedResources.get(0);
+ bestResourceQuality = quality;
+ continue;
+ }
+ for (PrecompressedResource resource :
precompressedResources) {
+ if (encoding.equals(resource.format.encoding)) {
+ bestResource = resource;
+ bestResourceQuality = quality;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return bestResource;
+ }
+
private void doDirectoryRedirect(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
StringBuilder location = new StringBuilder(request.getRequestURI());
@@ -1945,25 +2043,6 @@ public class DefaultServlet extends Http
}
/**
- * Check if the user agent supports gzip encoding.
- *
- * @param request The servlet request we are processing
- * @return <code>true</code> if the user agent supports gzip encoding,
- * and <code>false</code> if the user agent does not support gzip encoding.
- */
- protected boolean checkIfGzip(HttpServletRequest request) {
- Enumeration<String> headers = request.getHeaders("Accept-Encoding");
- while (headers.hasMoreElements()) {
- String header = headers.nextElement();
- if (header.indexOf("gzip") != -1) {
- return true;
- }
- }
- return false;
- }
-
-
- /**
* Check if the if-unmodified-since condition is satisfied.
*
* @param request The servlet request we are processing
@@ -2290,6 +2369,25 @@ public class DefaultServlet extends Http
}
}
+ protected static class CompressionFormat {
+ public final String extension;
+ public final String encoding;
+
+ public CompressionFormat(String extension, String encoding) {
+ this.extension = extension;
+ this.encoding = encoding;
+ }
+ }
+
+ private static class PrecompressedResource {
+ public final WebResource resource;
+ public final CompressionFormat format;
+
+ private PrecompressedResource(WebResource resource, CompressionFormat
format) {
+ this.resource = resource;
+ this.format = format;
+ }
+ }
/**
* This is secure in the sense that any attempt to use an external entity
Modified: tomcat/trunk/test/org/apache/catalina/servlets/TestDefaultServlet.java
URL:
http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/servlets/TestDefaultServlet.java?rev=1734150&r1=1734149&r2=1734150&view=diff
==============================================================================
--- tomcat/trunk/test/org/apache/catalina/servlets/TestDefaultServlet.java
(original)
+++ tomcat/trunk/test/org/apache/catalina/servlets/TestDefaultServlet.java Tue
Mar 8 20:45:57 2016
@@ -33,6 +33,7 @@ import javax.servlet.http.HttpServletRes
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -40,6 +41,9 @@ import org.junit.Assert;
import org.junit.Test;
import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.not;
import org.apache.catalina.Context;
import org.apache.catalina.Wrapper;
@@ -118,19 +122,21 @@ public class TestDefaultServlet extends
tomcat.start();
- TestGzipClient gzipClient = new TestGzipClient(getPort());
+ TestCompressedClient gzipClient = new TestCompressedClient(getPort());
gzipClient.reset();
gzipClient.setRequest(new String[] {
"GET /index.html HTTP/1.1" + CRLF +
"Host: localhost" + CRLF +
"Connection: Close" + CRLF +
- "Accept-Encoding: gzip" + CRLF + CRLF });
+ "Accept-Encoding: gzip, br" + CRLF + CRLF });
gzipClient.connect();
gzipClient.processRequest();
assertTrue(gzipClient.isResponse200());
List<String> responseHeaders = gzipClient.getResponseHeaders();
+ assertTrue(responseHeaders.contains("Content-Encoding: gzip"));
assertTrue(responseHeaders.contains("Content-Length: " + gzipSize));
+ assertTrue(responseHeaders.contains("Vary: accept-encoding"));
gzipClient.reset();
gzipClient.setRequest(new String[] {
@@ -144,6 +150,172 @@ public class TestDefaultServlet extends
assertTrue(responseHeaders.contains("Content-Type: text/html"));
assertFalse(responseHeaders.contains("Content-Encoding: gzip"));
assertTrue(responseHeaders.contains("Content-Length: " + indexSize));
+ assertTrue(responseHeaders.contains("Vary: accept-encoding"));
+ }
+
+ /*
+ * Verify serving of brotli compressed resources from context root.
+ */
+ @Test
+ public void testBrotliCompressedFile() throws Exception {
+
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File("test/webapp");
+
+ long brSize = new File(appDir, "index.html.br").length();
+ long indexSize = new File(appDir, "index.html").length();
+
+ // app dir is relative to server home
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+ Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default",
+ "org.apache.catalina.servlets.DefaultServlet");
+ defaultServlet.addInitParameter("precompressed", "true");
+
+ ctxt.addServletMapping("/", "default");
+ ctxt.addMimeMapping("html", "text/html");
+
+ tomcat.start();
+
+ TestCompressedClient client = new TestCompressedClient(getPort());
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ "Accept-Encoding: br, gzip" + CRLF + CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ List<String> responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders, hasItem("Content-Encoding: br"));
+ assertThat(responseHeaders, hasItem("Content-Length: " + brSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF+ CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders, hasItem("Content-Type: text/html"));
+ assertThat(responseHeaders,
not(hasItem(containsString("Content-Encoding"))));
+ assertThat(responseHeaders, hasItem("Content-Length: " + indexSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
+ }
+
+ /*
+ * Verify serving of custom compressed resources from context root.
+ */
+ @Test
+ public void testCustomCompressedFile() throws Exception {
+
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File("test/webapp");
+
+ long brSize = new File(appDir, "index.html.br").length();
+ long gzSize = new File(appDir, "index.html.gz").length();
+
+ // app dir is relative to server home
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+ Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default",
+ DefaultServlet.class.getName());
+ defaultServlet.addInitParameter("precompressed",
"gzip=.gz,custom=.br");
+
+ ctxt.addServletMapping("/", "default");
+ ctxt.addMimeMapping("html", "text/html");
+
+ tomcat.start();
+
+ TestCompressedClient client = new TestCompressedClient(getPort());
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ "Accept-Encoding: br, gzip ; q = 0.5 , custom" + CRLF
+ CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ List<String> responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders, hasItem("Content-Encoding: custom"));
+ assertThat(responseHeaders, hasItem("Content-Length: " + brSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ "Accept-Encoding: br;q=1,gzip,custom" + CRLF + CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders, hasItem("Content-Encoding: gzip"));
+ assertThat(responseHeaders, hasItem("Content-Length: " + gzSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
+ }
+
+ /*
+ * Verify that "*" and "identity" values are handled correctly in
accept-encoding header.
+ */
+ @Test
+ public void testIdentityAndStarAcceptEncodings() throws Exception {
+
+ Tomcat tomcat = getTomcatInstance();
+
+ File appDir = new File("test/webapp");
+
+ long brSize = new File(appDir, "index.html.br").length();
+ long indexSize = new File(appDir, "index.html").length();
+
+ // app dir is relative to server home
+ Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
+ Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default",
+ DefaultServlet.class.getName());
+ defaultServlet.addInitParameter("precompressed", "br=.br,gzip=.gz");
+
+ ctxt.addServletMapping("/", "default");
+ ctxt.addMimeMapping("html", "text/html");
+
+ tomcat.start();
+
+ TestCompressedClient client = new TestCompressedClient(getPort());
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ "Accept-Encoding: gzip;q=0.9,*" + CRLF + CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ List<String> responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders, hasItem("Content-Encoding: br"));
+ assertThat(responseHeaders, hasItem("Content-Length: " + brSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
+
+ client.reset();
+ client.setRequest(new String[] {
+ "GET /index.html HTTP/1.1" + CRLF +
+ "Host: localhost" + CRLF +
+ "Connection: Close" + CRLF +
+ "Accept-Encoding: gzip;q=0.9,br;q=0,identity," + CRLF
+ CRLF });
+ client.connect();
+ client.processRequest();
+ assertTrue(client.isResponse200());
+ responseHeaders = client.getResponseHeaders();
+ assertThat(responseHeaders,
not(hasItem(containsString("Content-Encoding"))));
+ assertThat(responseHeaders, hasItem("Content-Length: " + indexSize));
+ assertThat(responseHeaders, hasItem("Vary: accept-encoding"));
}
/*
@@ -387,9 +559,9 @@ public class TestDefaultServlet extends
}
}
- private static class TestGzipClient extends SimpleHttpClient {
+ private static class TestCompressedClient extends SimpleHttpClient {
- public TestGzipClient(int port) {
+ public TestCompressedClient(int port) {
setPort(port);
}
Modified: tomcat/trunk/webapps/docs/changelog.xml
URL:
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1734150&r1=1734149&r2=1734150&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Tue Mar 8 20:45:57 2016
@@ -170,6 +170,11 @@
related memory leaks when the key class but not the value class has
been
loaded by the web application class loader. (markt)
</fix>
+ <add>
+ <bug>59017</bug>: Make the pre-compressed file support in the Default
+ Servlet generic so any compression may be used rather than just gzip.
+ Patch provided by Mikko Tiihonen. (markt)
+ </add>
</changelog>
</subsection>
<subsection name="Coyote">
Modified: tomcat/trunk/webapps/docs/default-servlet.xml
URL:
http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/default-servlet.xml?rev=1734150&r1=1734149&r2=1734150&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/default-servlet.xml (original)
+++ tomcat/trunk/webapps/docs/default-servlet.xml Tue Mar 8 20:45:57 2016
@@ -94,15 +94,22 @@ directory listings are disabled and debu
expensive. Multiple requests for large directory listings can consume
significant proportions of server resources.
</property>
- <property name="gzip">
- If a gzipped version of a file exists (a file with <code>.gz</code>
- appended to the file name located alongside the original file), Tomcat
- will serve the gzipped file if the user agent supports gzip and this
+ <property name="precompressed">
+ If a precompressed version of a file exists (a file with
<code>.br</code>
+ or <code>.gz</code> appended to the file name located alongside the
+ original file), Tomcat will serve the precompressed file if the user
+ agent supports the matching content encoding (br or gzip) and this
option is enabled. [false]
<br />
- The file with the <code>.gz</code> extension will be accessible if
- requested directly so if the original resource is protected with a
- security constraint, the gzipped version must be similarly protected.
+ The precompressed file with the with <code>.br</code> or
<code>.gz</code>
+ extension will be accessible if requested directly so if the original
+ resource is protected with a security constraint, the precompressed
+ versions must be similarly protected.
+ <br />
+ It is also possible to configure the list of precompressed formats.
+ The syntax is comma separated list of
+ <code>[content-encoding]=[file-extension]</code> pairs. For example:
+ <code>br=.br,gzip=.gz,bzip2=.bz2</code>.
</property>
<property name="readmeFile">
If a directory listing is presented, a readme file may also
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]