-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ClassPathUtils, copied from Quarkus. (#63)
- Loading branch information
Showing
4 changed files
with
368 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
239 changes: 239 additions & 0 deletions
239
classloader/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
classloader/src/test/java/io/smallrye/common/classloader/ClassPathUtilsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
my.prop=1234 |