Hi,
2016-03-08 22:45 GMT+02:00 <[email protected]>:
>
> 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;
I think that CompressionFormat should implement also Serializable.
What do you think?
Regards,
Violeta
>
> /**
> * 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]
>