ClassLoaderFactory.java
/*
* 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.catalina.startup;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
/**
* <p>
* Utility class for building class loaders for Catalina. The factory method requires the following parameters in order
* to build a new class loader (with suitable defaults in all cases):
* </p>
* <ul>
* <li>A set of directories containing unpacked classes (and resources) that should be included in the class loader's
* repositories.</li>
* <li>A set of directories containing classes and resources in JAR files. Each readable JAR file discovered in these
* directories will be added to the class loader's repositories.</li>
* <li><code>ClassLoader</code> instance that should become the parent of the new class loader.</li>
* </ul>
*
* @author Craig R. McClanahan
*/
public final class ClassLoaderFactory {
private static final Log log = LogFactory.getLog(ClassLoaderFactory.class);
// --------------------------------------------------------- Public Methods
/**
* Create and return a new class loader, based on the configuration defaults and the specified directory paths:
*
* @param unpacked Array of pathnames to unpacked directories that should be added to the repositories of the class
* loader, or <code>null</code> for no unpacked directories to be considered
* @param packed Array of pathnames to directories containing JAR files that should be added to the repositories
* of the class loader, or <code>null</code> for no directories of JAR files to be considered
* @param parent Parent class loader for the new class loader, or <code>null</code> for the system class loader.
*
* @return the new class loader
*
* @exception Exception if an error occurs constructing the class loader
*/
public static ClassLoader createClassLoader(File unpacked[], File packed[], final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Creating new class loader");
}
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<>();
// Add unpacked directories
if (unpacked != null) {
for (File file : unpacked) {
if (!file.canRead()) {
continue;
}
file = new File(file.getCanonicalPath());
URL url = file.toURI().toURL();
if (log.isDebugEnabled()) {
log.debug(" Including directory " + url);
}
set.add(url);
}
}
// Add packed directory JAR files
if (packed != null) {
for (File directory : packed) {
if (!directory.isDirectory() || !directory.canRead()) {
continue;
}
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
for (String s : filenames) {
String filename = s.toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar")) {
continue;
}
File file = new File(directory, s);
if (log.isDebugEnabled()) {
log.debug(" Including jar file " + file.getAbsolutePath());
}
URL url = file.toURI().toURL();
set.add(url);
}
}
}
// Construct the class loader itself
final URL[] array = set.toArray(new URL[0]);
return AccessController.doPrivileged((PrivilegedAction<URLClassLoader>) () -> {
if (parent == null) {
return new URLClassLoader(array);
} else {
return new URLClassLoader(array, parent);
}
});
}
/**
* Create and return a new class loader, based on the configuration defaults and the specified directory paths:
*
* @param repositories List of class directories, jar files, jar directories or URLS that should be added to the
* repositories of the class loader.
* @param parent Parent class loader for the new class loader, or <code>null</code> for the system class
* loader.
*
* @return the new class loader
*
* @exception Exception if an error occurs constructing the class loader
*/
public static ClassLoader createClassLoader(List<Repository> repositories, final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Creating new class loader");
}
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<>();
if (repositories != null) {
for (Repository repository : repositories) {
if (repository.getType() == RepositoryType.URL) {
URL url = buildClassLoaderUrl(repository.getLocation());
if (log.isDebugEnabled()) {
log.debug(" Including URL " + url);
}
set.add(url);
} else if (repository.getType() == RepositoryType.DIR) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.DIR)) {
continue;
}
URL url = buildClassLoaderUrl(directory);
if (log.isDebugEnabled()) {
log.debug(" Including directory " + url);
}
set.add(url);
} else if (repository.getType() == RepositoryType.JAR) {
File file = new File(repository.getLocation());
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
URL url = buildClassLoaderUrl(file);
if (log.isDebugEnabled()) {
log.debug(" Including jar file " + url);
}
set.add(url);
} else if (repository.getType() == RepositoryType.GLOB) {
File directory = new File(repository.getLocation());
directory = directory.getCanonicalFile();
if (!validateFile(directory, RepositoryType.GLOB)) {
continue;
}
if (log.isDebugEnabled()) {
log.debug(" Including directory glob " + directory.getAbsolutePath());
}
String filenames[] = directory.list();
if (filenames == null) {
continue;
}
for (String s : filenames) {
String filename = s.toLowerCase(Locale.ENGLISH);
if (!filename.endsWith(".jar")) {
continue;
}
File file = new File(directory, s);
file = file.getCanonicalFile();
if (!validateFile(file, RepositoryType.JAR)) {
continue;
}
if (log.isDebugEnabled()) {
log.debug(" Including glob jar file " + file.getAbsolutePath());
}
URL url = buildClassLoaderUrl(file);
set.add(url);
}
}
}
}
// Construct the class loader itself
final URL[] array = set.toArray(new URL[0]);
if (log.isTraceEnabled()) {
for (int i = 0; i < array.length; i++) {
log.trace(" location " + i + " is " + array[i]);
}
}
return AccessController.doPrivileged((PrivilegedAction<URLClassLoader>) () -> {
if (parent == null) {
return new URLClassLoader(array);
} else {
return new URLClassLoader(array, parent);
}
});
}
private static boolean validateFile(File file, RepositoryType type) throws IOException {
if (RepositoryType.DIR == type || RepositoryType.GLOB == type) {
if (!file.isDirectory() || !file.canRead()) {
String msg = "Problem with directory [" + file + "], exists: [" + file.exists() + "], isDirectory: [" +
file.isDirectory() + "], canRead: [" + file.canRead() + "]";
File home = new File(Bootstrap.getCatalinaHome());
home = home.getCanonicalFile();
File base = new File(Bootstrap.getCatalinaBase());
base = base.getCanonicalFile();
File defaultValue = new File(base, "lib");
// Existence of ${catalina.base}/lib directory is optional.
// Hide the warning if Tomcat runs with separate catalina.home
// and catalina.base and that directory is absent.
if (!home.getPath().equals(base.getPath()) && file.getPath().equals(defaultValue.getPath()) &&
!file.exists()) {
log.debug(msg);
} else {
log.warn(msg);
}
return false;
}
} else if (RepositoryType.JAR == type) {
if (!file.canRead()) {
log.warn("Problem with JAR file [" + file + "], exists: [" + file.exists() + "], canRead: [" +
file.canRead() + "]");
return false;
}
}
return true;
}
/*
* These two methods would ideally be in the utility class org.apache.tomcat.util.buf.UriUtil but that class is not
* visible until after the class loaders have been constructed.
*/
private static URL buildClassLoaderUrl(String urlString) throws MalformedURLException, URISyntaxException {
// URLs passed to class loaders may point to directories that contain
// JARs. If these URLs are used to construct URLs for resources in a JAR
// the URL will be used as is. It is therefore necessary to ensure that
// the sequence "!/" is not present in a class loader URL.
String result = urlString.replace("!/", "%21/");
return new URI(result).toURL();
}
private static URL buildClassLoaderUrl(File file) throws MalformedURLException, URISyntaxException {
// Could be a directory or a file
String fileUrlString = file.toURI().toString();
fileUrlString = fileUrlString.replace("!/", "%21/");
return new URI(fileUrlString).toURL();
}
public enum RepositoryType {
DIR,
GLOB,
JAR,
URL
}
public static class Repository {
private final String location;
private final RepositoryType type;
public Repository(String location, RepositoryType type) {
this.location = location;
this.type = type;
}
public String getLocation() {
return location;
}
public RepositoryType getType() {
return type;
}
}
}