You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2017/10/18 20:53:55 UTC
svn commit: r1812581 - in /tomcat/tc7.0.x/trunk:
java/org/apache/catalina/loader/ java/org/apache/tomcat/util/compat/
java/org/apache/tomcat/util/scan/ webapps/docs/
Author: markt
Date: Wed Oct 18 20:53:55 2017
New Revision: 1812581
URL: http://svn.apache.org/viewvc?rev=1812581&view=rev
Log:
Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=61601
Handle multi-release JARs for packed and unpacked web applications
Modified:
tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java
tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java
tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java
tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml
Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappClassLoaderBase.java Wed Oct 18 20:53:55 2017
@@ -3006,7 +3006,7 @@ public abstract class WebappClassLoaderB
if (jarFiles[0] == null) {
for (int i = 0; i < jarFiles.length; i++) {
try {
- jarFiles[i] = new JarFile(jarRealFiles[i]);
+ jarFiles[i] = JreCompat.getInstance().jarFileNewInstance(jarRealFiles[i]);
} catch (IOException e) {
log.warn(sm.getString("webappClassLoader.jarOpenFail", jarFiles[i]), e);
closeJARs(true);
@@ -3695,7 +3695,7 @@ public abstract class WebappClassLoaderB
JarFile jarFile = null;
try {
- jarFile = new JarFile(file);
+ jarFile = JreCompat.getInstance().jarFileNewInstance(file);
for (int i = 0; i < triggers.length; i++) {
Class<?> clazz = null;
try {
Modified: tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/catalina/loader/WebappLoader.java Wed Oct 18 20:53:55 2017
@@ -59,6 +59,7 @@ import org.apache.naming.resources.DirCo
import org.apache.naming.resources.DirContextURLStreamHandlerFactory;
import org.apache.naming.resources.Resource;
import org.apache.tomcat.util.ExceptionUtils;
+import org.apache.tomcat.util.compat.JreCompat;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.res.StringManager;
@@ -999,7 +1000,7 @@ public class WebappLoader extends Lifecy
}
try {
- JarFile jarFile = new JarFile(destFile);
+ JarFile jarFile = JreCompat.getInstance().jarFileNewInstance(destFile);
classLoader.addJar(filename, jarFile, destFile);
} catch (Exception ex) {
// Catch the exception if there is an empty jar file
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre7Compat.java Wed Oct 18 20:53:55 2017
@@ -22,6 +22,8 @@ import java.util.Locale;
class Jre7Compat extends JreCompat {
+ private static final int RUNTIME_MAJOR_VERSION = 7;
+
private static final Method forLanguageTagMethod;
@@ -55,4 +57,10 @@ class Jre7Compat extends JreCompat {
return null;
}
}
+
+
+ @Override
+ public int jarFileRuntimeMajorVersion() {
+ return RUNTIME_MAJOR_VERSION;
+ }
}
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre8Compat.java Wed Oct 18 20:53:55 2017
@@ -25,6 +25,8 @@ import javax.net.ssl.SSLServerSocket;
class Jre8Compat extends Jre7Compat {
+ private static final int RUNTIME_MAJOR_VERSION = 8;
+
private static final Method getSSLParametersMethod;
private static final Method setUseCipherSuitesOrderMethod;
private static final Method setSSLParametersMethod;
@@ -93,4 +95,10 @@ class Jre8Compat extends Jre7Compat {
throw new UnsupportedOperationException(e);
}
}
+
+
+ @Override
+ public int jarFileRuntimeMajorVersion() {
+ return RUNTIME_MAJOR_VERSION;
+ }
}
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/Jre9Compat.java Wed Oct 18 20:53:55 2017
@@ -16,7 +16,9 @@
*/
package org.apache.tomcat.util.compat;
+import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
@@ -25,6 +27,8 @@ import java.net.URL;
import java.net.URLConnection;
import java.util.Deque;
import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.zip.ZipFile;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
@@ -44,14 +48,13 @@ class Jre9Compat extends Jre8Compat {
private static final Method locationMethod;
private static final Method isPresentMethod;
private static final Method getMethod;
+ private static final Constructor<JarFile> jarFileConstructor;
+ private static final Method isMultiReleaseMethod;
- static {
- Class<?> moduleLayerClazz = null;
- Class<?> configurationClazz = null;
- Class<?> resolvedModuleClazz = null;
- Class<?> moduleReferenceClazz = null;
- Class<?> optionalClazz = null;
+ private static final Object RUNTIME_VERSION;
+ private static final int RUNTIME_MAJOR_VERSION;
+ static {
Class<?> c1 = null;
Method m4 = null;
Method m5 = null;
@@ -61,13 +64,20 @@ class Jre9Compat extends Jre8Compat {
Method m9 = null;
Method m10 = null;
Method m11 = null;
+ Constructor<JarFile> c12 = null;
+ Method m13 = null;
+ Object o14 = null;
+ Object o15 = null;
try {
- moduleLayerClazz = Class.forName("java.lang.ModuleLayer");
- configurationClazz = Class.forName("java.lang.module.Configuration");
- resolvedModuleClazz = Class.forName("java.lang.module.ResolvedModule");
- moduleReferenceClazz = Class.forName("java.lang.module.ModuleReference");
- optionalClazz = Class.forName("java.util.Optional");
+ Class<?> moduleLayerClazz = Class.forName("java.lang.ModuleLayer");
+ Class<?> configurationClazz = Class.forName("java.lang.module.Configuration");
+ Class<?> resolvedModuleClazz = Class.forName("java.lang.module.ResolvedModule");
+ Class<?> moduleReferenceClazz = Class.forName("java.lang.module.ModuleReference");
+ Class<?> optionalClazz = Class.forName("java.util.Optional");
+ Class<?> versionClazz = Class.forName("java.lang.Runtime$Version");
+ Method runtimeVersionMethod = JarFile.class.getMethod("runtimeVersion");
+ Method majorMethod = versionClazz.getMethod("major");
c1 = Class.forName("java.lang.reflect.InaccessibleObjectException");
m4 = URLConnection.class.getMethod("setDefaultUseCaches", String.class, boolean.class);
@@ -78,13 +88,25 @@ class Jre9Compat extends Jre8Compat {
m9 = moduleReferenceClazz.getMethod("location");
m10 = optionalClazz.getMethod("isPresent");
m11 = optionalClazz.getMethod("get");
+ c12 = JarFile.class.getConstructor(File.class, boolean.class, int.class, versionClazz);
+ m13 = JarFile.class.getMethod("isMultiRelease");
+ o14 = runtimeVersionMethod.invoke(null);
+ o15 = majorMethod.invoke(o14);
+
} catch (SecurityException e) {
// Should never happen
} catch (NoSuchMethodException e) {
// Should never happen
} catch (ClassNotFoundException e) {
// Must be Java 8
+ } catch (IllegalArgumentException e) {
+ // Should never happen
+ } catch (IllegalAccessException e) {
+ // Should never happen
+ } catch (InvocationTargetException e) {
+ // Should never happen
}
+
inaccessibleObjectExceptionClazz = c1;
setDefaultUseCachesMethod = m4;
bootMethod = m5;
@@ -94,6 +116,16 @@ class Jre9Compat extends Jre8Compat {
locationMethod = m9;
isPresentMethod = m10;
getMethod = m11;
+ jarFileConstructor = c12;
+ isMultiReleaseMethod = m13;
+
+ RUNTIME_VERSION = o14;
+ if (o15 != null) {
+ RUNTIME_MAJOR_VERSION = ((Integer) o15).intValue();
+ } else {
+ // Must be Java 8
+ RUNTIME_MAJOR_VERSION = 8;
+ }
}
@@ -154,4 +186,41 @@ class Jre9Compat extends Jre8Compat {
throw new UnsupportedOperationException(e);
}
}
+
+
+ @Override
+ public JarFile jarFileNewInstance(File f) throws IOException {
+ try {
+ return jarFileConstructor.newInstance(
+ f, Boolean.TRUE, Integer.valueOf(ZipFile.OPEN_READ), RUNTIME_VERSION);
+ } catch (IllegalArgumentException e) {
+ throw new IOException(e);
+ } catch (InstantiationException e) {
+ throw new IOException(e);
+ } catch (IllegalAccessException e) {
+ throw new IOException(e);
+ } catch (InvocationTargetException e) {
+ throw new IOException(e);
+ }
+ }
+
+
+ @Override
+ public boolean jarFileIsMultiRelease(JarFile jarFile) {
+ try {
+ return ((Boolean) isMultiReleaseMethod.invoke(jarFile)).booleanValue();
+ } catch (IllegalArgumentException e) {
+ return false;
+ } catch (IllegalAccessException e) {
+ return false;
+ } catch (InvocationTargetException e) {
+ return false;
+ }
+ }
+
+
+ @Override
+ public int jarFileRuntimeMajorVersion() {
+ return RUNTIME_MAJOR_VERSION;
+ }
}
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/compat/JreCompat.java Wed Oct 18 20:53:55 2017
@@ -16,11 +16,13 @@
*/
package org.apache.tomcat.util.compat;
+import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Deque;
import java.util.Locale;
+import java.util.jar.JarFile;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLServerSocket;
@@ -34,6 +36,8 @@ import org.apache.tomcat.util.res.String
*/
public class JreCompat {
+ private static final int RUNTIME_MAJOR_VERSION = 6;
+
private static final JreCompat instance;
private static StringManager sm =
StringManager.getManager(JreCompat.class.getPackage().getName());
@@ -190,7 +194,40 @@ public class JreCompat {
* added
*/
public void addBootModulePath(Deque<URL> classPathUrlsToProcess) {
- // NO-OP for Java 8. There is no module path.
+ // NO-OP. There is no module path prior to Java 9.
+ }
+
+
+ /**
+ * Creates a new JarFile instance. When running on Java 9 and later, the
+ * JarFile will be multi-release JAR aware.
+ *
+ * @param f The JAR file to open
+ *
+ * @return A JarFile instance based on the provided file
+ *
+ * @throws IOException If an I/O error occurs creating the JarFile instance
+ */
+ public JarFile jarFileNewInstance(File f) throws IOException {
+ return new JarFile(f);
}
+
+ /**
+ * Is this JarFile a multi-release JAR file.
+ *
+ * @param jarFile The JarFile to test
+ *
+ * @return {@code true} If it is a multi-release JAR file and is configured
+ * to behave as such.
+ */
+ public boolean jarFileIsMultiRelease(JarFile jarFile) {
+ // There is no multi-release JAR support prior to Java 9
+ return false;
+ }
+
+
+ public int jarFileRuntimeMajorVersion() {
+ return RUNTIME_MAJOR_VERSION;
+ }
}
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/FileUrlJar.java Wed Oct 18 20:53:55 2017
@@ -21,10 +21,14 @@ import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
+import org.apache.tomcat.util.compat.JreCompat;
+
/**
* Implementation of {@link Jar} that is optimised for file based JAR URLs (e.g
* URLs of the form jar:file:...).
@@ -32,13 +36,17 @@ import java.util.zip.ZipEntry;
public class FileUrlJar implements Jar {
private JarFile jarFile;
+ private final boolean multiRelease;
private Enumeration<JarEntry> entries;
+ private Set<String> entryNamesSeen;
private JarEntry entry = null;
public FileUrlJar(URL url) throws IOException {
JarURLConnection jarConn = (JarURLConnection) url.openConnection();
jarConn.setUseCaches(false);
+ // JarFile returned will be multi-release aware if the OS supports it.
jarFile = jarConn.getJarFile();
+ multiRelease = JreCompat.getInstance().jarFileIsMultiRelease(jarFile);
}
@Override
@@ -49,6 +57,7 @@ public class FileUrlJar implements Jar {
@Override
public InputStream getInputStream(String name) throws IOException {
+ // JarFile#getEntry() is multi-release aware
ZipEntry entry = jarFile.getEntry(name);
if (entry == null) {
return null;
@@ -70,13 +79,58 @@ public class FileUrlJar implements Jar {
@Override
public void nextEntry() {
+ // JarFile#entries() is NOT multi-release aware
if (entries == null) {
entries = jarFile.entries();
+ if (multiRelease) {
+ entryNamesSeen = new HashSet<String>();
+ }
}
- if (entries.hasMoreElements()) {
- entry = entries.nextElement();
+ if (multiRelease) {
+ // Need to ensure that:
+ // - the one, correct entry is returned where multiple versions
+ // are available
+ // - that the order of entries in the JAR doesn't prevent the
+ // correct entries being returned
+ // - the case where an entry appears in the versions location
+ // but not in the the base location is handled correctly
+
+ // Enumerate the entries until one is reached that represents an
+ // entry that has not been seen before.
+ String name = null;
+ while (true) {
+ if (entries.hasMoreElements()) {
+ entry = entries.nextElement();
+ name = entry.getName();
+ // Get 'base' name
+ if (name.startsWith("META-INF/versions/")) {
+ int i = name.indexOf('/', 18);
+ if (i == -1) {
+ continue;
+ }
+ name = name.substring(i + 1);
+ }
+ if (name.length() == 0 || entryNamesSeen.contains(name)) {
+ continue;
+ }
+
+ entryNamesSeen.add(name);
+
+ // JarFile.getJarEntry is version aware so use it
+ entry = jarFile.getJarEntry(entry.getName());
+ break;
+ } else {
+ entry = null;
+ break;
+ }
+ }
+
} else {
- entry = null;
+ if (entries.hasMoreElements()) {
+ entry = entries.nextElement();
+ } else {
+ entry = null;
+ }
}
}
@@ -101,6 +155,7 @@ public class FileUrlJar implements Jar {
@Override
public void reset() throws IOException {
entries = null;
+ entryNamesSeen = null;
entry = null;
}
}
Modified: tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java (original)
+++ tomcat/tc7.0.x/trunk/java/org/apache/tomcat/util/scan/UrlJar.java Wed Oct 18 20:53:55 2017
@@ -21,7 +21,13 @@ import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.jar.JarEntry;
+import java.util.jar.Manifest;
+
+import org.apache.tomcat.util.compat.JreCompat;
/**
* Implementation of {@link Jar} that is optimised for non-file based JAR URLs
@@ -32,10 +38,23 @@ public class UrlJar implements Jar {
private NonClosingJarInputStream jarInputStream = null;
private URL url = null;
private JarEntry entry = null;
+ private Map<String,String> mrMap = null;
public UrlJar(URL url) throws IOException {
this.url = url;
this.jarInputStream = createJarInputStream();
+
+ boolean multiRelease = false;
+ if (JreCompat.isJre9Available()) {
+ Manifest manifest = jarInputStream.getManifest();
+ String mrValue = manifest.getMainAttributes().getValue("Multi-Release");
+ if (mrValue != null) {
+ multiRelease = Boolean.valueOf(mrValue).booleanValue();
+ }
+ }
+ if (multiRelease) {
+ populateMrMap();
+ }
}
@Override
@@ -84,7 +103,22 @@ public class UrlJar implements Jar {
public void nextEntry() {
try {
entry = jarInputStream.getNextJarEntry();
- } catch (IOException ioe) {
+ if (mrMap != null) {
+ // Skip base entries where there is a multi-release entry
+ // Skip multi-release entries that are not being used
+ while (entry != null &&
+ (mrMap.keySet().contains(entry.getName()) ||
+ entry.getName().startsWith("META-INF/versions/") &&
+ !mrMap.values().contains(entry.getName()))) {
+ entry = jarInputStream.getNextJarEntry();
+ }
+ } else {
+ // Skip multi-release entries
+ while (entry != null && entry.getName().startsWith("META-INF/versions/")) {
+ entry = jarInputStream.getNextJarEntry();
+ }
+ }
+ } catch (IOException ioe) {
entry = null;
}
}
@@ -106,6 +140,60 @@ public class UrlJar implements Jar {
@Override
public void reset() throws IOException {
close();
+ entry = null;
+ jarInputStream = createJarInputStream();
+ }
+
+
+ private void populateMrMap() throws IOException {
+ int targetVersion = JreCompat.getInstance().jarFileRuntimeMajorVersion();
+
+ Map<String,Integer> mrVersions = new HashMap<String,Integer>();
+
+ JarEntry jarEntry = jarInputStream.getNextJarEntry();
+
+ // Tracking the base name and the latest valid version found is
+ // sufficient to be able to create the renaming map required
+ while (jarEntry != null) {
+ String name = jarEntry.getName();
+ if (name.startsWith("META-INF/versions/") && name.endsWith(".class")) {
+
+ // Get the base name and version for this versioned entry
+ int i = name.indexOf('/', 18);
+ if (i > 0) {
+ String baseName = name.substring(i + 1);
+ int version = Integer.parseInt(name.substring(18, i));
+
+ // Ignore any entries targeting for a later version than
+ // the target for this runtime
+ if (version <= targetVersion) {
+ Integer mappedVersion = mrVersions.get(baseName);
+ if (mappedVersion == null) {
+ // No version found for this name. Create one.
+ mrVersions.put(baseName, Integer.valueOf(version));
+ } else {
+ // Ignore any entry for which we have already found
+ // a later version
+ if (version > mappedVersion.intValue()) {
+ // Replace the earlier version
+ mrVersions.put(baseName, Integer.valueOf(version));
+ }
+ }
+ }
+ }
+ }
+ jarEntry = jarInputStream.getNextJarEntry();
+ }
+
+ mrMap = new HashMap<String,String>();
+
+ for (Entry<String,Integer> mrVersion : mrVersions.entrySet()) {
+ mrMap.put(mrVersion.getKey() , "META-INF/versions/" + mrVersion.getValue().toString() +
+ "/" + mrVersion.getKey());
+ }
+
+ // Reset stream back to the beginning of the JAR
+ close();
jarInputStream = createJarInputStream();
}
}
Modified: tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml?rev=1812581&r1=1812580&r2=1812581&view=diff
==============================================================================
--- tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/tc7.0.x/trunk/webapps/docs/changelog.xml Wed Oct 18 20:53:55 2017
@@ -80,6 +80,10 @@
Fix the JMX descriptor for <code>Wrapper.findInitParameter()</code>.
(rjung)
</fix>
+ <fix>
+ <bug>61601</bug>: Add support for multi-release JARs in JAR scanning and
+ web application class loading. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Jasper">
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org