Skip to content

Commit

Permalink
Add ClassPathUtils, copied from Quarkus. (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez authored Sep 30, 2020
1 parent 7831f5f commit 2145fa1
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 0 deletions.
10 changes: 10 additions & 0 deletions classloader/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
<artifactId>asm</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.shrinkwrap</groupId>
<artifactId>shrinkwrap-impl-base</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<profiles>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package io.smallrye.common.classloader;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.function.Consumer;
import java.util.function.Function;

public class ClassPathUtils {
private static final String FILE = "file";
private static final String JAR = "jar";

/**
* Invokes {@link #consumeAsStreams(ClassLoader, String, Consumer)} passing in
* an instance of the current thread's context classloader as the classloader
* from which to load the resources.
*
* @param resource resource path
* @param consumer resource input stream consumer
* @throws IOException in case of an IO failure
*/
public static void consumeAsStreams(String resource, Consumer<InputStream> consumer) throws IOException {
consumeAsStreams(Thread.currentThread().getContextClassLoader(), resource, consumer);
}

/**
* Locates all the occurrences of a resource on the classpath of the provided classloader
* and invokes the consumer providing the input streams for each located resource.
* The consumer does not have to close the provided input stream.
* This method was introduced to avoid calling {@link java.net.URL#openStream()} which
* in case the resource is found in an archive (such as JAR) locks the containing archive
* even if the caller properly closes the stream.
*
* @param cl classloader to load the resources from
* @param resource resource path
* @param consumer resource input stream consumer
* @throws IOException in case of an IO failure
*/
public static void consumeAsStreams(ClassLoader cl, String resource, Consumer<InputStream> consumer) throws IOException {
final Enumeration<URL> resources = cl.getResources(resource);
while (resources.hasMoreElements()) {
consumeStream(resources.nextElement(), consumer);
}
}

/**
* Invokes {@link #consumeAsPaths(ClassLoader, String, Consumer)} passing in
* an instance of the current thread's context classloader as the classloader
* from which to load the resources.
*
* @param resource resource path
* @param consumer resource path consumer
* @throws IOException in case of an IO failure
*/
public static void consumeAsPaths(String resource, Consumer<Path> consumer) throws IOException {
consumeAsPaths(Thread.currentThread().getContextClassLoader(), resource, consumer);
}

/**
* Locates specified resources on the classpath and attempts to represent them as local file system paths
* to be processed by a consumer. If a resource appears to be an actual file or a directory, it is simply
* passed to the consumer as-is. If a resource is an entry in a JAR, the entry will be resolved as an instance
* of {@link java.nio.file.Path} in a {@link java.nio.file.FileSystem} representing the JAR.
* If the protocol of the URL representing the resource is neither 'file' nor 'jar', the method will fail
* with an exception.
*
* @param cl classloader to load the resources from
* @param resource resource path
* @param consumer resource path consumer
* @throws IOException in case of an IO failure
*/
public static void consumeAsPaths(ClassLoader cl, String resource, Consumer<Path> consumer) throws IOException {
final Enumeration<URL> resources = cl.getResources(resource);
while (resources.hasMoreElements()) {
consumeAsPath(resources.nextElement(), consumer);
}
}

/**
* Attempts to represent a resource as a local file system path to be processed by a consumer.
* If a resource appears to be an actual file or a directory, it is simply passed to the consumer as-is.
* If a resource is an entry in a JAR, the entry will be resolved as an instance
* of {@link java.nio.file.Path} in a {@link java.nio.file.FileSystem} representing the JAR.
* If the protocol of the URL representing the resource is neither 'file' nor 'jar', the method will fail
* with an exception.
*
* @param url resource url
* @param consumer resource path consumer
*/
public static void consumeAsPath(URL url, Consumer<Path> consumer) {
processAsPath(url, p -> {
consumer.accept(p);
return null;
});
}

/**
* Attempts to represent a resource as a local file system path to be processed by a function.
* If a resource appears to be an actual file or a directory, it is simply passed to the function as-is.
* If a resource is an entry in a JAR, the entry will be resolved as an instance
* of {@link java.nio.file.Path} in a {@link java.nio.file.FileSystem} representing the JAR.
* If the protocol of the URL representing the resource is neither 'file' nor 'jar', the method will fail
* with an exception.
*
* @param url resource url
* @param function resource path function
*/
public static <R> R processAsPath(URL url, Function<Path, R> function) {
if (JAR.equals(url.getProtocol())) {
final String file = url.getFile();
final int exclam = file.lastIndexOf('!');
final Path jar;
try {
jar = toLocalPath(exclam >= 0 ? new URL(file.substring(0, exclam)) : url);
} catch (MalformedURLException e) {
throw new RuntimeException("Failed to create a URL for '" + file.substring(0, exclam) + "'", e);
}
try (FileSystem jarFs = FileSystems.newFileSystem(jar, (ClassLoader) null)) {
Path localPath = jarFs.getPath("/");
if (exclam >= 0) {
localPath = localPath.resolve(file.substring(exclam + 1));
}
return function.apply(localPath);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read " + jar, e);
}
}

if (FILE.equals(url.getProtocol())) {
return function.apply(toLocalPath(url));
}

throw new IllegalArgumentException("Unexpected protocol " + url.getProtocol() + " for URL " + url);
}

/**
* Invokes a consumer providing the input streams to read the content of the URL.
* The consumer does not have to close the provided input stream.
* This method was introduced to avoid calling {@link java.net.URL#openStream()} which
* in case the resource is found in an archive (such as JAR) locks the containing archive
* even if the caller properly closes the stream.
*
* @param url URL
* @param consumer input stream consumer
* @throws IOException in case of an IO failure
*/
public static void consumeStream(URL url, Consumer<InputStream> consumer) throws IOException {
readStream(url, is -> {
consumer.accept(is);
return null;
});
}

/**
* Invokes a function providing the input streams to read the content of the URL.
* The function does not have to close the provided input stream.
* This method was introduced to avoid calling {@link java.net.URL#openStream()} which
* in case the resource is found in an archive (such as JAR) locks the containing archive
* even if the caller properly closes the stream.
*
* @param url URL
* @param function input stream processing function
* @throws IOException in case of an IO failure
*/
public static <R> R readStream(URL url, Function<InputStream, R> function) throws IOException {
if (JAR.equals(url.getProtocol())) {
final URI uri = toURI(url);
final String file = uri.getSchemeSpecificPart();
final int fileExclam = file.lastIndexOf('!');
final URL jarURL;
if (fileExclam > 0) {
// we need to use the original url instead of the scheme specific part because it contains the properly encoded path
String urlFile = url.getFile();
int urlExclam = urlFile.lastIndexOf('!');
jarURL = new URL(urlFile.substring(0, urlExclam));
} else {
jarURL = url;
}
final Path jar = toLocalPath(jarURL);
final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
try {
// We are loading "installed" FS providers that are loaded from from the system classloader anyway
// To avoid potential ClassCastExceptions we are setting the context classloader to the system one
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
try (FileSystem jarFs = FileSystems.newFileSystem(jar, (ClassLoader) null)) {
try (InputStream is = Files.newInputStream(jarFs.getPath(file.substring(fileExclam + 1)))) {
return function.apply(is);
}
}
} finally {
Thread.currentThread().setContextClassLoader(ccl);
}
}
if (FILE.equals(url.getProtocol())) {
try (InputStream is = Files.newInputStream(toLocalPath(url))) {
return function.apply(is);
}
}
try (InputStream is = url.openStream()) {
return function.apply(is);
}
}

private static URI toURI(URL url) throws IOException {
final URI uri;
try {
uri = new URI(url.toString());
} catch (URISyntaxException e) {
throw new IOException(e);
}
return uri;
}

/**
* Translates a URL to local file system path.
* In case the the URL couldn't be translated to a file system path,
* an instance of {@link IllegalArgumentException} will be thrown.
*
* @param url URL
* @return local file system path
*/
public static Path toLocalPath(final URL url) {
try {
return Paths.get(url.toURI());
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Failed to translate " + url + " to local path", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.smallrye.common.classloader;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.exporter.ZipExporter;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class ClassPathUtilsTest {
@Test
void consumeAsStreams() throws Exception {
Properties properties = new Properties();
ClassPathUtils.consumeAsStreams("resources.properties", inputStream -> {
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
properties.load(reader);
} catch (IOException e) {
fail();
}
});
assertEquals("1234", properties.getProperty("my.prop"));
}

@Test
void consumeAsPaths() throws Exception {
Properties properties = new Properties();
ClassPathUtils.consumeAsPaths("resources.properties", path -> {
try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(path))) {
properties.load(reader);
} catch (IOException e) {
fail();
}
});
assertEquals("1234", properties.getProperty("my.prop"));
}

@Test
void consumeAsPathsWithClassLoader() throws Exception {
Properties properties = new Properties();
ClassPathUtils.consumeAsPaths(ClassPathUtils.class.getClassLoader(), "resources.properties", path -> {
try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(path))) {
properties.load(reader);
} catch (IOException e) {
fail();
}
});
assertEquals("1234", properties.getProperty("my.prop"));
}

@Test
void consumeAsPath() {
URL resource = ClassPathUtils.class.getClassLoader().getResource("resources.properties");
Properties properties = new Properties();
ClassPathUtils.consumeAsPath(resource, path -> {
try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(path))) {
properties.load(reader);
} catch (IOException e) {
fail();
}
});
assertEquals("1234", properties.getProperty("my.prop"));
}

@Test
void processAsPath(@TempDir Path tempDir) throws Exception {
JavaArchive jar = ShrinkWrap
.create(JavaArchive.class, "resources.jar")
.addAsResource("resources.properties");

Path filePath = tempDir.resolve("resources.jar");
jar.as(ZipExporter.class).exportTo(filePath.toFile());

URL resource = new URL("jar:file:" + filePath.toString() + "!/resources.properties");
Properties properties = ClassPathUtils.processAsPath(resource, path -> {
Properties properties1 = new Properties();
try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(path))) {
properties1.load(reader);
} catch (IOException e) {
fail();
}
return properties1;
});

assertEquals("1234", properties.getProperty("my.prop"));
}

@Test
void readStream(@TempDir Path tempDir) throws Exception {
JavaArchive jar = ShrinkWrap
.create(JavaArchive.class, "resources.jar")
.addAsResource("resources.properties");

Path filePath = tempDir.resolve("resources.jar");
jar.as(ZipExporter.class).exportTo(filePath.toFile());

URL resource = new URL("jar:file:" + filePath.toString() + "!/resources.properties");
Properties properties = ClassPathUtils.readStream(resource, inputStream -> {
Properties properties1 = new Properties();
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
properties1.load(reader);
} catch (IOException e) {
fail();
}
return properties1;
});

assertEquals("1234", properties.getProperty("my.prop"));
}
}
1 change: 1 addition & 0 deletions classloader/src/test/resources/resources.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
my.prop=1234

0 comments on commit 2145fa1

Please sign in to comment.