Author: fhanik Date: Fri Apr 16 00:02:53 2010 New Revision: 934651 URL: http://svn.apache.org/viewvc?rev=934651&view=rev Log: Add in statement cache
Added: tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java (with props) Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/PooledConnection.java tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/PooledConnection.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/PooledConnection.java?rev=934651&r1=934650&r2=934651&view=diff ============================================================================== --- tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/PooledConnection.java (original) +++ tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/PooledConnection.java Fri Apr 16 00:02:53 2010 @@ -19,6 +19,7 @@ package org.apache.tomcat.jdbc.pool; import java.sql.SQLException; import java.sql.Statement; +import java.util.HashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.apache.juli.logging.Log; @@ -106,6 +107,8 @@ public class PooledConnection { * The parent */ protected ConnectionPool parent; + + private HashMap<Object, Object> attributes = new HashMap<Object, Object>(); /** * Weak reference to cache the list of interceptors for this connection @@ -598,5 +601,9 @@ public class PooledConnection { public boolean isReleased() { return released.get(); } + + public HashMap<Object,Object> getAttributes() { + return attributes; + } } Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java?rev=934651&r1=934650&r2=934651&view=diff ============================================================================== --- tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java (original) +++ tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java Fri Apr 16 00:02:53 2010 @@ -35,7 +35,7 @@ public abstract class AbstractCreateSta protected static final String PREPARE_STATEMENT = "prepareStatement"; protected static final int PREPARE_STATEMENT_IDX = 1; protected static final String PREPARE_CALL = "prepareCall"; - protected static final int PREPARE_IDX = 2; + protected static final int PREPARE_CALL_IDX = 2; protected static final String[] STATEMENT_TYPES = {CREATE_STATEMENT, PREPARE_STATEMENT, PREPARE_CALL}; protected static final int STATEMENT_TYPE_COUNT = STATEMENT_TYPES.length; Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java?rev=934651&r1=934650&r2=934651&view=diff ============================================================================== --- tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java (original) +++ tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractQueryReport.java Fri Apr 16 00:02:53 2010 @@ -182,7 +182,7 @@ public abstract class AbstractQueryRepor }else if (compare(PREPARE_CALL,name)) { //prepareCall sql = (String)args[0]; - constructor = getConstructor(PREPARE_IDX,CallableStatement.class); + constructor = getConstructor(PREPARE_CALL_IDX,CallableStatement.class); prepareCall(sql,time); }else { //do nothing, might be a future unsupported method Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java?rev=934651&r1=934650&r2=934651&view=diff ============================================================================== --- tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java (original) +++ tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java Fri Apr 16 00:02:53 2010 @@ -18,7 +18,6 @@ package org.apache.tomcat.jdbc.pool.inte import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; @@ -36,16 +35,37 @@ public class StatementCache extends Stat protected static final String[] CALLABLE_TYPE = new String[] {PREPARE_CALL}; protected static final String[] PREPARED_TYPE = new String[] {PREPARE_STATEMENT}; protected static final String[] NO_TYPE = new String[] {}; + + protected static final String STATEMENT_CACHE_ATTR = StatementCache.class.getName() + ".cache"; /*begin properties for the statement cache*/ private boolean cachePrepared = true; private boolean cacheCallable = false; private int maxCacheSize = 50; - private ConnectionPool parent; private PooledConnection pcon; private String[] types; + public boolean isCachePrepared() { + return cachePrepared; + } + + public boolean isCacheCallable() { + return cacheCallable; + } + + public int getMaxCacheSize() { + return maxCacheSize; + } + + public String[] getTypes() { + return types; + } + + public AtomicInteger getCacheSize() { + return cacheSize; + } + public void setProperties(Map<String, InterceptorProperty> properties) { super.setProperties(properties); InterceptorProperty p = properties.get("prepared"); @@ -86,35 +106,36 @@ public class StatementCache extends Stat /*begin the actual statement cache*/ - private static ConcurrentHashMap<PooledConnection, ConcurrentHashMap<String,StatementProxy>> statementCache = - new ConcurrentHashMap<PooledConnection, ConcurrentHashMap<String,StatementProxy>>(); - - public void reset(ConnectionPool parent, PooledConnection con) { super.reset(parent, con); if (parent==null) { cacheSize = null; - this.parent = null; this.pcon = null; } else { cacheSize = cacheSizeMap.get(parent); - this.parent = parent; this.pcon = con; + if (!pcon.getAttributes().containsKey(STATEMENT_CACHE_ATTR)) { + ConcurrentHashMap<String,CachedStatement> cache = new ConcurrentHashMap<String, CachedStatement>(); + pcon.getAttributes().put(STATEMENT_CACHE_ATTR,cache); + } } } public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) { - ConcurrentHashMap<String,StatementProxy> statements = statementCache.get(con); + ConcurrentHashMap<String,CachedStatement> statements = + (ConcurrentHashMap<String,CachedStatement>)con.getAttributes().get(STATEMENT_CACHE_ATTR); + if (statements!=null) { - for (Map.Entry<String, StatementProxy> p : statements.entrySet()) { + for (Map.Entry<String, CachedStatement> p : statements.entrySet()) { closeStatement(p.getValue()); } statements.clear(); } + super.disconnected(parent, con, finalizing); } - public void closeStatement(StatementProxy st) { + public void closeStatement(CachedStatement st) { try { if (st==null) return; if (((PreparedStatement)st).isClosed()) return; @@ -125,39 +146,59 @@ public class StatementCache extends Stat } } + @Override + protected Object createDecorator(Object proxy, Method method, Object[] args, + Object statement, Constructor<?> constructor, String sql) + throws InstantiationException, IllegalAccessException, InvocationTargetException { + boolean process = process(this.types, method, false); + if (process) { + Object result = null; + CachedStatement statementProxy = new CachedStatement((Statement)statement,sql); + result = constructor.newInstance(new Object[] { statementProxy }); + statementProxy.setActualProxy(result); + statementProxy.setConnection(proxy); + statementProxy.setConstructor(constructor); + return result; + } else { + return super.createDecorator(proxy, method, args, statement, constructor, sql); + } + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (compare(CLOSE_VAL,method)) { return super.invoke(proxy, method, args); } else { - boolean process = false; - process = process(this.types, method, process); - - if (process) { - //check the cache - //if (isCached) { - - //} else { + boolean process = process(this.types, method, false); + if (process && args.length>0 && args[0] instanceof String) { + CachedStatement statement = isCached((String)args[0]); + if (statement!=null) { + //remove it from the cache since it is used + removeStatement(statement); + return statement.getActualProxy(); + } else { return super.invoke(proxy, method, args); - //} + } } else { return super.invoke(proxy,method,args); } } } - public boolean isCached(String sql) { - ConcurrentHashMap<String,StatementProxy> cache = statementCache.get(pcon); - return cache.containsKey(sql); + public CachedStatement isCached(String sql) { + ConcurrentHashMap<String,CachedStatement> cache = + (ConcurrentHashMap<String,CachedStatement>)pcon.getAttributes().get(STATEMENT_CACHE_ATTR); + return cache.get(sql); } - public boolean cacheStatement(StatementProxy proxy) { - ConcurrentHashMap<String,StatementProxy> cache = statementCache.get(pcon); + public boolean cacheStatement(CachedStatement proxy) { + ConcurrentHashMap<String,CachedStatement> cache = + (ConcurrentHashMap<String,CachedStatement>)pcon.getAttributes().get(STATEMENT_CACHE_ATTR); if (proxy.getSql()==null) { return false; } else if (cache.containsKey(proxy.getSql())) { - cache.put(proxy.getSql(), proxy); - return true; + return false; } else if (cacheSize.get()>=maxCacheSize) { return false; } else if (cacheSize.incrementAndGet()>maxCacheSize) { @@ -165,26 +206,55 @@ public class StatementCache extends Stat return false; } else { //cache the statement + cache.put(proxy.getSql(), proxy); return true; } } + + public boolean removeStatement(CachedStatement proxy) { + ConcurrentHashMap<String,CachedStatement> cache = + (ConcurrentHashMap<String,CachedStatement>)pcon.getAttributes().get(STATEMENT_CACHE_ATTR); + if (cache.remove(proxy.getSql()) != null) { + cacheSize.decrementAndGet(); + return true; + } else { + return false; + } + } /*end the actual statement cache*/ - protected class StatementProxy extends StatementDecoratorInterceptor.StatementProxy { + protected class CachedStatement extends StatementDecoratorInterceptor.StatementProxy<Statement> { boolean cached = false; - public StatementProxy(Object parent, String sql) { + public CachedStatement(Statement parent, String sql) { super(parent, sql); - cached = cacheStatement(this); } - - public void closeInvoked() { - super.closedInvoked(); - if (cached) { + + @Override + public void closedInvoked() { + //should we cache it + boolean shouldClose = true; + if (cacheSize.get() < maxCacheSize) { //cache a proxy so that we don't reuse the facade - StatementProxy proxy = new StatementProxy(getDelegate(),getSql()); - proxy.setActualProxy(getActualProxy()); - proxy.setConnection(getConnection()); + CachedStatement proxy = new CachedStatement(getDelegate(),getSql()); + try { + //create a new facade + Object actualProxy = getConstructor().newInstance(new Object[] { proxy }); + proxy.setActualProxy(actualProxy); + proxy.setConnection(getConnection()); + proxy.setConstructor(getConstructor()); + if (cacheStatement(proxy)) { + proxy.cached = true; + shouldClose = false; + } + } catch (Exception x) { + removeStatement(proxy); + } + } + closed = true; + delegate = null; + if (shouldClose) { + super.closedInvoked(); } } @@ -194,53 +264,6 @@ public class StatementCache extends Stat ((Statement)getDelegate()).close(); } - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // get the name of the method for comparison - final String name = method.getName(); - // was close invoked? - boolean close = compare(JdbcInterceptor.CLOSE_VAL, name); - // allow close to be called multiple times - if (close && closed) - return null; - // are we calling isClosed? - if (compare(JdbcInterceptor.ISCLOSED_VAL, name)) - return Boolean.valueOf(closed); - // if we are calling anything else, bail out - if (closed) - throw new SQLException("Statement closed."); - if (name.equals("getConnection")){ - return getConnection(); - } - boolean process = isExecuteQuery(method); - // check to see if we are about to execute a query - // if we are executing, get the current time - Object result = null; - try { - if (cached && close) { - //dont invoke actual close - } else { - // execute the query - result = method.invoke(delegate, args); - } - } catch (Throwable t) { - if (t instanceof InvocationTargetException) { - InvocationTargetException it = (InvocationTargetException) t; - throw it.getCause() != null ? it.getCause() : it; - } else { - throw t; - } - } - // perform close cleanup - if (close) { - closeInvoked(); - } - if (process){ - Constructor<?> cons = getResultSetConstructor(); - result = cons.newInstance(new Object[]{new ResultSetProxy(getActualProxy(), result)}); - } - return result; - } - } } Modified: tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java?rev=934651&r1=934650&r2=934651&view=diff ============================================================================== --- tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java (original) +++ tomcat/trunk/modules/jdbc-pool/java/org/apache/tomcat/jdbc/pool/interceptor/StatementDecoratorInterceptor.java Fri Apr 16 00:02:53 2010 @@ -94,7 +94,6 @@ public class StatementDecoratorIntercept @Override public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) { try { - Object result = null; String name = method.getName(); Constructor<?> constructor = null; String sql = null; @@ -107,24 +106,32 @@ public class StatementDecoratorIntercept sql = (String)args[0]; } else if (compare(PREPARE_CALL, name)) { // prepareCall - constructor = getConstructor(PREPARE_IDX, CallableStatement.class); + constructor = getConstructor(PREPARE_CALL_IDX, CallableStatement.class); sql = (String)args[0]; } else { // do nothing, might be a future unsupported method // so we better bail out and let the system continue return statement; } - StatementProxy statementProxy = new StatementProxy(statement,sql); - result = constructor.newInstance(new Object[] { statementProxy }); - statementProxy.setActualProxy(result); - statementProxy.setConnection(proxy); - return result; + return createDecorator(proxy, method, args, statement, constructor, sql); } catch (Exception x) { logger.warn("Unable to create statement proxy for slow query report.", x); } return statement; } + protected Object createDecorator(Object proxy, Method method, Object[] args, + Object statement, Constructor<?> constructor, String sql) + throws InstantiationException, IllegalAccessException, InvocationTargetException { + Object result = null; + StatementProxy statementProxy = new StatementProxy<Statement>((Statement)statement,sql); + result = constructor.newInstance(new Object[] { statementProxy }); + statementProxy.setActualProxy(result); + statementProxy.setConnection(proxy); + statementProxy.setConnection(constructor); + return result; + } + protected boolean isExecuteQuery(String methodName) { return EXECUTE_QUERY_TYPES[0].equals(methodName); } @@ -139,19 +146,20 @@ public class StatementDecoratorIntercept * @author fhanik * */ - protected class StatementProxy implements InvocationHandler { + protected class StatementProxy<T extends java.sql.Statement> implements InvocationHandler { protected boolean closed = false; - protected Object delegate; + protected T delegate; private Object actualProxy; private Object connection; private String sql; + private Constructor constructor; - public StatementProxy(Object parent, String sql) { - this.delegate = parent; + public StatementProxy(T delegate, String sql) { + this.delegate = delegate; this.sql = sql; } - public Object getDelegate() { + public T getDelegate() { return this.delegate; } @@ -173,26 +181,40 @@ public class StatementDecoratorIntercept return this.actualProxy; } + + public Constructor getConstructor() { + return constructor; + } + public void setConstructor(Constructor constructor) { + this.constructor = constructor; + } public void closedInvoked() { + if (getDelegate()!=null) { + try { + getDelegate().close(); + }catch (SQLException ignore) { + } + } closed = true; delegate = null; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // get the name of the method for comparison - final String name = method.getName(); + if (compare(TOSTRING_VAL,method)) { + return toString(); + } // was close invoked? - boolean close = compare(JdbcInterceptor.CLOSE_VAL, name); + boolean close = compare(CLOSE_VAL, method); // allow close to be called multiple times if (close && closed) return null; // are we calling isClosed? - if (compare(JdbcInterceptor.ISCLOSED_VAL, name)) + if (compare(ISCLOSED_VAL, method)) return Boolean.valueOf(closed); // if we are calling anything else, bail out if (closed) throw new SQLException("Statement closed."); - if (name.equals("getConnection")){ + if (compare(GETCONNECTION_VAL,method)){ return connection; } boolean process = isExecuteQuery(method); @@ -200,8 +222,13 @@ public class StatementDecoratorIntercept // if we are executing, get the current time Object result = null; try { - // execute the query - result = method.invoke(delegate, args); + // perform close cleanup + if (close) { + closedInvoked(); + } else { + // execute the query + result = method.invoke(delegate, args); + } } catch (Throwable t) { if (t instanceof InvocationTargetException) { InvocationTargetException it = (InvocationTargetException) t; @@ -210,16 +237,26 @@ public class StatementDecoratorIntercept throw t; } } - // perform close cleanup - if (close) { - closeInvoked(); - } if (process){ Constructor<?> cons = getResultSetConstructor(); result = cons.newInstance(new Object[]{new ResultSetProxy(actualProxy, result)}); } return result; } + + public String toString() { + StringBuffer buf = new StringBuffer(StatementProxy.class.getName()); + buf.append("[Proxy="); + buf.append(System.identityHashCode(this)); + buf.append("; Sql="); + buf.append(getSql()); + buf.append("; Delegate="); + buf.append(getDelegate()); + buf.append("; Connection="); + buf.append(getConnection()); + buf.append("]"); + return buf.toString(); + } } protected class ResultSetProxy implements InvocationHandler { Added: tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java URL: http://svn.apache.org/viewvc/tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java?rev=934651&view=auto ============================================================================== --- tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java (added) +++ tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java Fri Apr 16 00:02:53 2010 @@ -0,0 +1,135 @@ +/* + * 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.jdbc.test; + +import java.sql.Connection; +import java.sql.PreparedStatement; + +import org.apache.tomcat.jdbc.pool.interceptor.StatementCache; + +public class TestStatementCache extends DefaultTestCase { + + + public TestStatementCache(String name) { + super(name); + } + + private static volatile TestStatementCacheInterceptor interceptor = null; + + + @Override + protected void tearDown() throws Exception { + // TODO Auto-generated method stub + this.interceptor = null; + super.tearDown(); + } + + + + + private void config(boolean cachePrepared, boolean cacheCallable, int max) { + datasource.getPoolProperties().setJdbcInterceptors(TestStatementCacheInterceptor.class.getName()+ + "(prepared="+cachePrepared+",callable="+cacheCallable+",max="+max+")"); + } + + public void testIsCacheEnabled() throws Exception { + init(); + config(true,true,50); + datasource.getConnection().close(); + assertNotNull("Interceptor was not created.", interceptor); + } + + public void testCacheProperties() throws Exception { + init(); + config(true,true,50); + datasource.getConnection().close(); + assertEquals(true, interceptor.isCacheCallable()); + assertEquals(true, interceptor.isCachePrepared()); + assertEquals(50,interceptor.getMaxCacheSize()); + } + + public void testCacheProperties2() throws Exception { + init(); + config(false,false,100); + datasource.getConnection().close(); + assertEquals(false, interceptor.isCacheCallable()); + assertEquals(false, interceptor.isCachePrepared()); + assertEquals(100,interceptor.getMaxCacheSize()); + } + + public void testPreparedStatementCache() throws Exception { + init(); + config(true,false,100); + Connection con = datasource.getConnection(); + PreparedStatement ps1 = con.prepareStatement("select 1"); + PreparedStatement ps2 = con.prepareStatement("select 1"); + assertEquals(0,interceptor.getCacheSize().get()); + ps1.close(); + assertTrue(ps1.isClosed()); + assertEquals(1,interceptor.getCacheSize().get()); + PreparedStatement ps3 = con.prepareStatement("select 1"); + assertEquals(0,interceptor.getCacheSize().get()); + ps2.close(); + assertTrue(ps2.isClosed()); + ps3.close(); + assertTrue(ps3.isClosed()); + assertEquals(1,interceptor.getCacheSize().get()); + } + + public void testPreparedStatementCache2() throws Exception { + init(); + config(false,false,100); + Connection con = datasource.getConnection(); + PreparedStatement ps1 = con.prepareStatement("select 1"); + PreparedStatement ps2 = con.prepareStatement("select 1"); + assertEquals(0,interceptor.getCacheSize().get()); + ps1.close(); + assertTrue(ps1.isClosed()); + assertEquals(0,interceptor.getCacheSize().get()); + PreparedStatement ps3 = con.prepareStatement("select 1"); + assertEquals(0,interceptor.getCacheSize().get()); + ps2.close(); + assertTrue(ps2.isClosed()); + ps3.close(); + assertTrue(ps3.isClosed()); + assertEquals(0,interceptor.getCacheSize().get()); + } + + public void testCallableStatementCache() throws Exception { + } + + public void testMaxCacheSize() throws Exception { + init(); + config(true,false,100); + Connection con1 = datasource.getConnection(); + Connection con2 = datasource.getConnection(); + for (int i=0; i<120; i++) { + Connection con = (i%2==0)?con1:con2; + PreparedStatement ps = con.prepareStatement("select "+i); + ps.close(); + } + assertEquals(100,interceptor.getCacheSize().get()); + } + + + public static class TestStatementCacheInterceptor extends StatementCache { + public TestStatementCacheInterceptor() { + TestStatementCache.interceptor = this; + } + } + +} Propchange: tomcat/trunk/modules/jdbc-pool/test/org/apache/tomcat/jdbc/test/TestStatementCache.java ------------------------------------------------------------------------------ svn:eol-style = native --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org