de.unkrig.commons.lang.ClassLoaders Maven / Gradle / Ivy
/*
* de.unkrig.commons - A general-purpose Java class library
*
* Copyright (c) 2016, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.commons.lang;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import de.unkrig.commons.nullanalysis.Nullable;
/**
* Helper methods related to class loaders.
*/
public final
class ClassLoaders {
private
ClassLoaders() {}
/**
* Returns the locations of all resources "under" a given directory name.
*
* Iff the name does not end with a slash, then calling this method is equivalent with calling
* {@link ClassLoader#getResources(String)}.
*
*
* Otherwise, if the name does end with a slash, then this method returns the locations of
* all resources who's names begin with the given name. Iff includeDirectories is
* {@code true}, then name, and all the subdirectories underneath, are also included in the result
* set.
*
*
* Notice that it is not (reliably) possible to determine the names of the retrieved resources; to
* get these, use {@link #getSubresources(ClassLoader, String, boolean)}.
*
*
* @param classLoader The class loader to use; {@code null} means use the system class loader
* @param name No leading slash
*/
public static URL[]
getAllSubresources(@Nullable ClassLoader classLoader, String name, boolean includeDirectories) throws IOException {
if (classLoader == null) classLoader = ClassLoader.getSystemClassLoader();
assert classLoader != null;
List result = new ArrayList();
for (URL r : Collections.list(classLoader.getResources(name))) {
result.addAll(ClassLoaders.getSubresourcesOf(r, name, includeDirectories).values());
}
return result.toArray(new URL[result.size()]);
}
/**
* Equivalent with {@link #getSubresources(ClassLoader, String, boolean, boolean)} with the recurse
* parameter set to {@code true}.
*/
public static Map
getSubresources(@Nullable ClassLoader classLoader, String name, boolean includeDirectories) throws IOException {
return ClassLoaders.getSubresources(classLoader, name, includeDirectories, true);
}
/**
* Returns a name-to-URL mapping of all resources "under" a given directory name.
*
* Iff the name does not end with a slash, then calling this method is equivalent with calling
* {@link ClassLoader#getResource(String)}.
*
*
* Otherwise, if the name does end with a slash, then this method returns a name-to-URL
* mapping of all content resources who's names begin with the given name.
* Iff recurse is {@code false}, then only immediate subresources are included.
* Iff includeDirectories is {@code true}, then also directory resources are included in the result
* set; their names all ending with a slash.
*
*
* If multiple resources have the name, then the resources are retrieved from the first
* occurrence.
*
*
* Directories on the BOOTCLASSPATH do not work, e.g. "java/lang/"; directories on the CLASSPATH
* do work (e.g. "de/unkrig/commons/lang/");.
*
*
* @param classLoader The class loader to use; {@code null} means use the system class loader
* @param name No leading slash
* @return Keys ending with a slash map to "directory resources", the other keys map to "content
* resources"
*/
public static Map
getSubresources(@Nullable ClassLoader classLoader, String name, boolean includeDirectories, boolean recurse)
throws IOException {
if (classLoader == null) classLoader = ClassLoader.getSystemClassLoader();
assert classLoader != null;
URL r = classLoader.getResource(name);
if (r == null) {
if (name.startsWith("java/") && name.endsWith("/")) {
// The "rt.jar" (the basic source of the BOOTCLASS) lacks directory entries; to work around this we
// have to get well-known resource. After that, we can list the JAR.
r = classLoader.getResource("java/lang/Object.class");
if (r != null) {
String protocol = r.getProtocol();
if ("jar".equalsIgnoreCase(protocol)) {
JarURLConnection juc = (JarURLConnection) r.openConnection();
juc.setUseCaches(false);
URL jarFileUrl = juc.getJarFileURL();
JarFile jarFile = juc.getJarFile();
Map result = ClassLoaders.getSubresources(
jarFileUrl,
jarFile,
name,
includeDirectories,
recurse
);
return result;
}
}
}
return Collections.emptyMap();
}
return ClassLoaders.getSubresourcesOf(r, name, includeDirectories, recurse);
}
/**
* Equivalent with {@link #getSubresourcesOf(URL, String, boolean, boolean)} with the recurse
* parameter set to {@code true}.
*/
public static Map
getSubresourcesOf(URL root, String rootName, boolean includeDirectories) throws IOException {
return ClassLoaders.getSubresourcesOf(root, rootName, includeDirectories, true);
}
/**
* Returns a name-to-URL mapping of all resources "under" a given root resource.
*
* If the root designates a "content resource" (as opposed to a "directory resource"), then the
* method returns {@code Collections.singletonMap(name, rootName)}.
*
*
* Otherwise, if the root designates a "directory resource", then this method returns a name-to-URL
* mapping of all content resources that are located "under" the root resource.
* Iff recurse is {@code false}, then only immediate subresources are included.
* Iff includeDirectories is {@code true}, then directory resources are also included in the result
* set; their names all ending with a slash.
*
*
* @return Keys ending with a slash map to "directory resources", the other keys map to "content resources"
*/
public static Map
getSubresourcesOf(URL root, String rootName, boolean includeDirectories, boolean recurse) throws IOException {
String protocol = root.getProtocol();
if ("jar".equalsIgnoreCase(protocol)) {
JarURLConnection juc = (JarURLConnection) root.openConnection();
juc.setUseCaches(false);
if (!juc.getJarEntry().isDirectory()) return Collections.singletonMap(rootName, root);
URL jarFileUrl = juc.getJarFileURL();
JarFile jarFile = juc.getJarFile();
Map result = ClassLoaders.getSubresources(
jarFileUrl,
jarFile,
rootName,
includeDirectories,
recurse
);
if (includeDirectories) result.put(rootName, root);
return result;
}
if ("file".equalsIgnoreCase(protocol)) {
return ClassLoaders.getFileResources(root, rootName, includeDirectories, recurse);
}
return Collections.singletonMap(rootName, root);
}
private static Map
getSubresources(URL jarFileUrl, JarFile jarFile, String namePrefix, boolean includeDirectories, boolean recurse) {
Map result = new HashMap();
for (JarEntry je : Collections.list(jarFile.entries())) {
if (
(!je.isDirectory() || includeDirectories)
&& je.getName().startsWith(namePrefix)
&& (recurse || je.getName().indexOf('/', namePrefix.length()) == -1)
) {
URL url;
try {
url = new URL("jar", null, jarFileUrl.toString() + "!/" + je.getName());
} catch (MalformedURLException mue) {
throw new AssertionError(mue);
}
result.put(je.getName(), url);
}
}
return result;
}
private static Map
getFileResources(URL fileUrl, String namePrefix, boolean includeDirectories, boolean recurse) throws IOException {
File file = new File(fileUrl.getFile());
if (file.isFile()) return Collections.singletonMap(namePrefix, fileUrl);
if (file.isDirectory()) {
if (!namePrefix.isEmpty() && !namePrefix.endsWith("/")) namePrefix += '/';
Map result = new HashMap();
if (includeDirectories) result.put(namePrefix, fileUrl);
File[] members = file.listFiles();
if (members == null) {
// MS WINDOWS 7: Read-protected directory produces:
// isDirectory() => true
// canRead() => true
// list() => null
// listFiles() => null
throw new IOException(file + ": Permission denied");
}
for (File member : members) {
String memberName = namePrefix + member.getName();
URL memberUrl = ClassLoaders.fileUrl(member);
if (recurse) {
result.putAll(ClassLoaders.getFileResources(memberUrl, memberName, includeDirectories, recurse));
} else {
if (member.isFile()) result.put(memberName, memberUrl);
}
}
return result;
}
return Collections.emptyMap();
}
private static URL
fileUrl(File file) {
try {
return file.toURI().toURL();
} catch (MalformedURLException mue) {
throw ExceptionUtil.wrap(file.toString(), mue, IllegalStateException.class);
}
}
}