This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push: new 77308f9eda Optimize conversion of method from bytes to String 77308f9eda is described below commit 77308f9eda1d77a491967ddad0e532d3781aa442 Author: Mark Thomas <ma...@apache.org> AuthorDate: Thu Sep 11 08:48:07 2025 +0100 Optimize conversion of method from bytes to String The savings are small but noticeable in performance tests - particularly with embedded that doesn't use the StringCache by default. Because HttpServlet calls HttpServletRequest.getmethod(), nearly every request will convert the method to a String so it is more efficient to do the conversion early and store it as a String rather than use MessageBytes. Unknown methods do attract a small performance penalty but there should be very few of those. --- .../catalina/authenticator/FormAuthenticator.java | 6 +- .../apache/catalina/connector/CoyoteAdapter.java | 6 +- .../apache/catalina/connector/OutputBuffer.java | 4 +- java/org/apache/catalina/connector/Request.java | 2 +- java/org/apache/coyote/Request.java | 27 ++++ java/org/apache/coyote/RequestInfo.java | 2 +- java/org/apache/coyote/ajp/AjpProcessor.java | 8 +- .../apache/coyote/http11/Http11InputBuffer.java | 3 +- java/org/apache/coyote/http11/Http11Processor.java | 5 +- java/org/apache/coyote/http2/Stream.java | 8 +- java/org/apache/coyote/http2/StreamProcessor.java | 3 +- java/org/apache/tomcat/util/http/Method.java | 149 +++++++++++++++++++++ test/org/apache/tomcat/util/http/TestMethod.java | 43 ++++++ .../tomcat/util/http/TestMethodPerformance.java | 67 +++++++++ webapps/docs/changelog.xml | 4 + 15 files changed, 313 insertions(+), 24 deletions(-) diff --git a/java/org/apache/catalina/authenticator/FormAuthenticator.java b/java/org/apache/catalina/authenticator/FormAuthenticator.java index 1c953bd242..ab3b4a3e62 100644 --- a/java/org/apache/catalina/authenticator/FormAuthenticator.java +++ b/java/org/apache/catalina/authenticator/FormAuthenticator.java @@ -448,7 +448,7 @@ public class FormAuthenticator extends AuthenticatorBase { // Always use GET for the login page, regardless of the method used String oldMethod = request.getMethod(); - request.getCoyoteRequest().method().setString("GET"); + request.getCoyoteRequest().setMethod("GET"); RequestDispatcher disp = context.getServletContext().getRequestDispatcher(loginPage); try { @@ -464,7 +464,7 @@ public class FormAuthenticator extends AuthenticatorBase { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); } finally { // Restore original method so that it is written into access log - request.getCoyoteRequest().method().setString(oldMethod); + request.getCoyoteRequest().setMethod(oldMethod); } } @@ -632,7 +632,7 @@ public class FormAuthenticator extends AuthenticatorBase { request.getCoyoteRequest().setContentType(contentType); } - request.getCoyoteRequest().method().setString(method); + request.getCoyoteRequest().setMethod(method); // The method, URI, queryString and protocol are normally stored as // bytes in the HttpInputBuffer and converted lazily to String. At this // point, the method has already been set as String in the line above diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java index e48238aae3..d2c71b4d97 100644 --- a/java/org/apache/catalina/connector/CoyoteAdapter.java +++ b/java/org/apache/catalina/connector/CoyoteAdapter.java @@ -595,7 +595,7 @@ public class CoyoteAdapter implements Adapter { // Check for ping OPTIONS * request if (undecodedURI.equals("*")) { - if (req.method().equals("OPTIONS")) { + if ("OPTIONS".equals(req.getMethod())) { StringBuilder allow = new StringBuilder(); allow.append("GET, HEAD, POST, PUT, DELETE, OPTIONS"); // Trace if allowed @@ -614,7 +614,7 @@ public class CoyoteAdapter implements Adapter { MessageBytes decodedURI = req.decodedURI(); // Filter CONNECT method - if (req.method().equals("CONNECT")) { + if ("CONNECT".equals(req.getMethod())) { response.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, sm.getString("coyoteAdapter.connect")); } else { // No URI for CONNECT requests @@ -813,7 +813,7 @@ public class CoyoteAdapter implements Adapter { } // Filter TRACE method - if (!connector.getAllowTrace() && req.method().equals("TRACE")) { + if (!connector.getAllowTrace() && "TRACE".equals(req.getMethod())) { Wrapper wrapper = request.getWrapper(); StringBuilder header = null; if (wrapper != null) { diff --git a/java/org/apache/catalina/connector/OutputBuffer.java b/java/org/apache/catalina/connector/OutputBuffer.java index 669486a317..7b21d78048 100644 --- a/java/org/apache/catalina/connector/OutputBuffer.java +++ b/java/org/apache/catalina/connector/OutputBuffer.java @@ -238,8 +238,8 @@ public class OutputBuffer extends Writer { // - the content length has not been explicitly set // AND // - some content has been written OR this is NOT a HEAD request - if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1) && - ((bb.remaining() > 0 || !coyoteResponse.getRequest().method().equals("HEAD")))) { + if (!coyoteResponse.isCommitted() && coyoteResponse.getContentLengthLong() == -1 && + (bb.remaining() > 0 || !"HEAD".equals(coyoteResponse.getRequest().getMethod()))) { coyoteResponse.setContentLength(bb.remaining()); } diff --git a/java/org/apache/catalina/connector/Request.java b/java/org/apache/catalina/connector/Request.java index f3debeb779..118984c5fa 100644 --- a/java/org/apache/catalina/connector/Request.java +++ b/java/org/apache/catalina/connector/Request.java @@ -2163,7 +2163,7 @@ public class Request implements HttpServletRequest { @Override public String getMethod() { - return coyoteRequest.method().toStringType(); + return coyoteRequest.getMethod(); } diff --git a/java/org/apache/coyote/Request.java b/java/org/apache/coyote/Request.java index 80dde650e1..12c31a1bbb 100644 --- a/java/org/apache/coyote/Request.java +++ b/java/org/apache/coyote/Request.java @@ -34,6 +34,7 @@ import jakarta.servlet.ServletConnection; import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.buf.MessageBytes; import org.apache.tomcat.util.buf.UDecoder; +import org.apache.tomcat.util.http.Method; import org.apache.tomcat.util.http.MimeHeaders; import org.apache.tomcat.util.http.Parameters; import org.apache.tomcat.util.http.ServerCookies; @@ -317,10 +318,36 @@ public final class Request { return schemeMB; } + /** + * Get a MessageBytes instance that holds the current request's HTTP method. + * + * @return a MessageBytes instance that holds the current request's HTTP method. + * + * @deprecated Use {@link #getMethod()}, {@link Request#setMethod(String)} and {@link #setMethod(byte[], int, int)} + */ + @Deprecated public MessageBytes method() { return methodMB; } + public void setMethod(String method) { + methodMB.setString(method); + } + + public void setMethod(byte[] buf, int start, int len) { + String method = Method.bytesToString(buf, start, len); + if (method == null) { + methodMB.setBytes(buf, start, len); + method = methodMB.toStringType(); + } else { + methodMB.setString(method); + } + } + + public String getMethod() { + return methodMB.toStringType(); + } + public MessageBytes requestURI() { return uriMB; } diff --git a/java/org/apache/coyote/RequestInfo.java b/java/org/apache/coyote/RequestInfo.java index 0dc534da74..214d4ec0e9 100644 --- a/java/org/apache/coyote/RequestInfo.java +++ b/java/org/apache/coyote/RequestInfo.java @@ -65,7 +65,7 @@ public class RequestInfo { // This is useful for long-running requests only public String getMethod() { - return req.method().toString(); + return req.getMethod(); } public String getCurrentUri() { diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java b/java/org/apache/coyote/ajp/AjpProcessor.java index 50587c6a7e..88a6aacfb6 100644 --- a/java/org/apache/coyote/ajp/AjpProcessor.java +++ b/java/org/apache/coyote/ajp/AjpProcessor.java @@ -639,7 +639,7 @@ public class AjpProcessor extends AbstractProcessor { byte methodCode = requestHeaderMessage.getByte(); if (methodCode != Constants.SC_M_JK_STORED) { String methodName = Constants.getMethodForCode(methodCode - 1); - request.method().setString(methodName); + request.setMethod(methodName); } requestHeaderMessage.getBytes(request.protocol()); @@ -826,7 +826,9 @@ public class AjpProcessor extends AbstractProcessor { break; case Constants.SC_A_STORED_METHOD: - requestHeaderMessage.getBytes(request.method()); + requestHeaderMessage.getBytes(tmpMB); + ByteChunk tmpBC = tmpMB.getByteChunk(); + request.setMethod(tmpBC.getBytes(), tmpBC.getStart(), tmpBC.getLength()); break; case Constants.SC_A_SECRET: @@ -924,7 +926,7 @@ public class AjpProcessor extends AbstractProcessor { // Responses with certain status codes and/or methods are not permitted to include a response body. int statusCode = response.getStatus(); if (statusCode < 200 || statusCode == 204 || statusCode == 205 || statusCode == 304 || - request.method().equals("HEAD")) { + "HEAD".equals(request.getMethod())) { // No entity body swallowResponse = true; } diff --git a/java/org/apache/coyote/http11/Http11InputBuffer.java b/java/org/apache/coyote/http11/Http11InputBuffer.java index 832ddc520b..22b1af5f6b 100644 --- a/java/org/apache/coyote/http11/Http11InputBuffer.java +++ b/java/org/apache/coyote/http11/Http11InputBuffer.java @@ -394,8 +394,7 @@ public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler chr = byteBuffer.get(); if (chr == Constants.SP || chr == Constants.HT) { space = true; - request.method().setBytes(byteBuffer.array(), parsingRequestLineStart, - pos - parsingRequestLineStart); + request.setMethod(byteBuffer.array(), parsingRequestLineStart, pos - parsingRequestLineStart); } else if (!HttpParser.isToken(chr)) { // Avoid unknown protocol triggering an additional error request.protocol().setString(Constants.HTTP_11); diff --git a/java/org/apache/coyote/http11/Http11Processor.java b/java/org/apache/coyote/http11/Http11Processor.java index 4f52d834d6..07ce3cecb4 100644 --- a/java/org/apache/coyote/http11/Http11Processor.java +++ b/java/org/apache/coyote/http11/Http11Processor.java @@ -503,7 +503,7 @@ public class Http11Processor extends AbstractProcessor { // Transfer the minimal information required for the copy of the Request // that is passed to the HTTP upgrade process dest.decodedURI().duplicate(source.decodedURI()); - dest.method().duplicate(source.method()); + dest.setMethod(source.getMethod()); dest.getMimeHeaders().duplicate(source.getMimeHeaders()); dest.requestURI().duplicate(source.requestURI()); dest.queryString().duplicate(source.queryString()); @@ -902,8 +902,7 @@ public class Http11Processor extends AbstractProcessor { } } - MessageBytes methodMB = request.method(); - boolean head = methodMB.equals("HEAD"); + boolean head = "HEAD".equals(request.getMethod()); if (head) { // Any entity body, if present, should not be sent outputBuffer.addActiveFilter(outputFilters[Constants.VOID_FILTER]); diff --git a/java/org/apache/coyote/http2/Stream.java b/java/org/apache/coyote/http2/Stream.java index 0ed39115c8..51afc13d1a 100644 --- a/java/org/apache/coyote/http2/Stream.java +++ b/java/org/apache/coyote/http2/Stream.java @@ -368,8 +368,8 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { switch (name) { case ":method": { - if (coyoteRequest.method().isNull()) { - coyoteRequest.method().setString(value); + if (coyoteRequest.getMethod() == null) { + coyoteRequest.setMethod(value); if ("HEAD".equals(value)) { configureVoidOutputFilter(); } @@ -552,8 +552,8 @@ class Stream extends AbstractNonZeroStream implements HeaderEmitter { final boolean receivedEndOfHeaders() throws ConnectionException { - if (coyoteRequest.method().isNull() || coyoteRequest.scheme().isNull() || - !coyoteRequest.method().equals("CONNECT") && coyoteRequest.requestURI().isNull()) { + if (coyoteRequest.getMethod() == null || coyoteRequest.scheme().isNull() || + !"CONNECT".equals(coyoteRequest.getMethod()) && coyoteRequest.requestURI().isNull()) { throw new ConnectionException(sm.getString("stream.header.required", getConnectionId(), getIdAsString()), Http2Error.PROTOCOL_ERROR); } diff --git a/java/org/apache/coyote/http2/StreamProcessor.java b/java/org/apache/coyote/http2/StreamProcessor.java index d92aaffe51..cad90b64db 100644 --- a/java/org/apache/coyote/http2/StreamProcessor.java +++ b/java/org/apache/coyote/http2/StreamProcessor.java @@ -522,8 +522,7 @@ class StreamProcessor extends AbstractProcessor implements NonPipeliningProcesso HttpParser httpParser = handler.getProtocol().getHttp11Protocol().getHttpParser(); // Method name must be a token - String method = request.method().toString(); - if (!HttpParser.isToken(method)) { + if (!HttpParser.isToken(request.getMethod())) { return false; } diff --git a/java/org/apache/tomcat/util/http/Method.java b/java/org/apache/tomcat/util/http/Method.java new file mode 100644 index 0000000000..31411c6171 --- /dev/null +++ b/java/org/apache/tomcat/util/http/Method.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.util.http; + +public class Method { + + // Standard HTTP methods supported by HttpServlet + public static final String GET = "GET"; + public static final String POST = "POST"; + public static final String PUT = "PUT"; + public static final String PATCH = "PATCH"; + public static final String HEAD = "HEAD"; + public static final String OPTIONS = "OPTIONS"; + public static final String DELETE = "DELETE"; + public static final String TRACE = "TRACE"; + // Additional WebDAV methods + public static final String PROPFIND = "PROPFIND"; + public static final String PROPPATCH = "PROPPATCH"; + public static final String MKCOL = "MKCOL"; + public static final String COPY = "COPY"; + public static final String MOVE = "MOVE"; + public static final String LOCK = "LOCK"; + public static final String UNLOCK = "UNLOCK"; + // Other methods recognised by Tomcat + public static final String CONNECT = "CONNECT"; + + + /** + * Provides optimised conversion from bytes to Strings for known HTTP methods. The bytes are assumed to be an + * ISO-8859-1 encoded representation of an HTTP method. The method is not validated as being a token, but only valid + * HTTP method names will be returned. + * <p> + * Doing in this way is ~10x faster than using MessageBytes.toStringType() saving ~40ns per request which is ~1% of + * the processing time for a minimal "Hello World" type servlet. For non-standard methods there is an additional + * overhead of ~2.5ns per request. + * <p> + * Pretty much every request ends up converting the method to a String so it is more efficient to do this straight + * away and always use Strings. + * + * @param buf The byte buffer containing the HTTP method to convert + * @param start The first byte of the HTTP method + * @param len The number of bytes to convert + * + * @return The HTTP method as a String or {@code null} if the method is not recognised. + */ + public static String bytesToString(byte[] buf, int start, int len) { + switch (buf[start]) { + case 'G': { + if (len == 3 && buf[start + 1] == 'E' && buf[start + 2] == 'T') { + return GET; + } + break; + } + case 'P': { + if (len == 4 && buf[start + 1] == 'O' && buf[start + 2] == 'S' && buf[start + 3] == 'T') { + return POST; + } else if (len == 3 && buf[start + 1] == 'U' && buf[start + 2] == 'T') { + return PUT; + } else if (len == 5 && buf[start + 1] == 'A' && buf[start + 2] == 'T' && buf[start + 3] == 'C' && + buf[start + 4] == 'H') { + return PATCH; + } else if (len == 8 && buf[start + 1] == 'R' && buf[start + 2] == 'O' && buf[start + 3] == 'P' && + buf[start + 4] == 'F' && buf[start + 5] == 'I' && buf[start + 6] == 'N' && + buf[start + 7] == 'D') { + return PROPFIND; + } else if (len == 9 && buf[start + 1] == 'R' && buf[start + 2] == 'O' && buf[start + 3] == 'P' && + buf[start + 4] == 'P' && buf[start + 5] == 'A' && buf[start + 6] == 'T' && + buf[start + 7] == 'C' && buf[start + 8] == 'H') { + return PROPPATCH; + } + break; + } + case 'H': { + if (len == 4 && buf[start + 1] == 'E' && buf[start + 2] == 'A' && buf[start + 3] == 'D') { + return HEAD; + } + break; + } + case 'O': { + if (len == 7 && buf[start + 1] == 'P' && buf[start + 2] == 'T' && buf[start + 3] == 'I' && + buf[start + 4] == 'O' && buf[start + 5] == 'N' && buf[start + 6] == 'S') { + return OPTIONS; + } + break; + } + case 'D': { + if (len == 6 && buf[start + 1] == 'E' && buf[start + 2] == 'L' && buf[start + 3] == 'E' && + buf[start + 4] == 'T' && buf[start + 5] == 'E') { + return DELETE; + } + break; + } + case 'T': { + if (len == 5 && buf[start + 1] == 'R' && buf[start + 2] == 'A' && buf[start + 3] == 'C' && + buf[start + 4] == 'E') { + return TRACE; + } + break; + } + case 'M': { + if (len == 5 && buf[start + 1] == 'K' && buf[start + 2] == 'C' && buf[start + 3] == 'O' && + buf[start + 4] == 'L') { + return MKCOL; + } else if (len == 4 && buf[start + 1] == 'O' && buf[start + 2] == 'V' && buf[start + 3] == 'E') { + return MOVE; + } + break; + } + case 'C': { + if (len == 4 && buf[start + 1] == 'O' && buf[start + 2] == 'P' && buf[start + 3] == 'Y') { + return COPY; + } else if (len == 7 && buf[start + 1] == 'O' && buf[start + 2] == 'N' && buf[start + 3] == 'N' && + buf[start + 4] == 'E' && buf[start + 5] == 'C' && buf[start + 6] == 'T') { + return CONNECT; + } + break; + } + case 'L': { + if (len == 4 && buf[start + 1] == 'O' && buf[start + 2] == 'C' && buf[start + 3] == 'K') { + return LOCK; + } + break; + } + case 'U': { + if (len == 6 && buf[start + 1] == 'N' && buf[start + 2] == 'L' && buf[start + 3] == 'O' && + buf[start + 4] == 'C' && buf[start + 5] == 'K') { + return UNLOCK; + } + break; + } + } + + return null; + } +} diff --git a/test/org/apache/tomcat/util/http/TestMethod.java b/test/org/apache/tomcat/util/http/TestMethod.java new file mode 100644 index 0000000000..a5fc7b7c28 --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestMethod.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.util.http; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class TestMethod { + + /* + * Not testing performance. Just checking that there are no errors in the parsing code. + */ + @Test + public void testHttpMethodParsing() { + List<String> methods = Arrays.asList(Method.GET, Method.POST, Method.PUT, Method.PATCH, Method.HEAD, + Method.OPTIONS, Method.DELETE, Method.TRACE, Method.PROPPATCH, Method.PROPFIND, Method.MKCOL, + Method.COPY, Method.MOVE, Method.LOCK, Method.UNLOCK, Method.CONNECT); + + for (String method : methods) { + byte[] bytes = method.getBytes(StandardCharsets.ISO_8859_1); + String result = Method.bytesToString(bytes, 0, bytes.length); + Assert.assertEquals(method, result); + } + } +} diff --git a/test/org/apache/tomcat/util/http/TestMethodPerformance.java b/test/org/apache/tomcat/util/http/TestMethodPerformance.java new file mode 100644 index 0000000000..e2e3212b4e --- /dev/null +++ b/test/org/apache/tomcat/util/http/TestMethodPerformance.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.tomcat.util.http; + +import java.nio.charset.StandardCharsets; + +import org.junit.Test; + +import org.apache.tomcat.util.buf.MessageBytes; + +public class TestMethodPerformance { + + private static final int LOOPS = 6; + private static final int ITERATIONS = 100000000; + + private static final String INPUT = "GET /context-path/servlet-path/path-info HTTP/1.1"; + private static final byte[] INPUT_BYTES = INPUT.getBytes(StandardCharsets.UTF_8); + + private static MessageBytes mb = MessageBytes.newInstance(); + + @Test + public void testGetMethodPerformance() throws Exception { + + for (int j = 0; j < LOOPS; j++) { + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + mb.setBytes(INPUT_BYTES, 0, 3); + mb.toStringType(); + } + long duration = System.nanoTime() - start; + + if (j > 0) { + System.out.println("MessageBytes conversion took :" + duration + "ns"); + } + } + + for (int j = 0; j < LOOPS; j++) { + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + String method = Method.bytesToString(INPUT_BYTES, 0, 3); + if (method == null) { + mb.setBytes(INPUT_BYTES, 0, 5); + mb.toStringType(); + } + } + long duration = System.nanoTime() - start; + + if (j > 0) { + System.out.println("Optimized conversion took :" + duration + "ns"); + } + } + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 3df1637cfd..a90bfbf55a 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -133,6 +133,10 @@ additional PQC certificates defined with type <code>MLDSA</code> are added to contexts which use classic certificates. (jfclere/remm) </update> + <add> + Optimize the conversion of HTTP method from byte form to String form. + (markt) + </add> </changelog> </subsection> <subsection name="Web applications"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org