Hi, 2016-03-08 22:45 GMT+02:00 <ma...@apache.org>: > > 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: dev-unsubscr...@tomcat.apache.org > For additional commands, e-mail: dev-h...@tomcat.apache.org >