-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f248dea
commit 01fd7f0
Showing
4 changed files
with
306 additions
and
1 deletion.
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
26 changes: 26 additions & 0 deletions
26
src/main/java/io/smallrye/common/classloader/ClassDefiner.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,26 @@ | ||
package io.smallrye.common.classloader; | ||
|
||
import java.lang.invoke.MethodHandles; | ||
import java.security.AccessController; | ||
import java.security.PrivilegedAction; | ||
|
||
public class ClassDefiner { | ||
public static Class<?> defineClass(MethodHandles.Lookup lookup, Class<?> parent, String className, byte[] classBytes) { | ||
SecurityManager sm = System.getSecurityManager(); | ||
if (sm != null) { | ||
sm.checkPermission(DefineClassPermission.getInstance()); | ||
} | ||
|
||
return AccessController.doPrivileged(new PrivilegedAction<Class<?>>() { | ||
@Override | ||
public Class<?> run() { | ||
try { | ||
MethodHandles.Lookup privateLookupIn = MethodHandles.privateLookupIn(parent, lookup); | ||
return privateLookupIn.defineClass(classBytes); | ||
} catch (IllegalAccessException e) { | ||
throw new IllegalAccessError(e.getMessage()); | ||
} | ||
} | ||
}); | ||
} | ||
} |
239 changes: 239 additions & 0 deletions
239
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.URISyntaxException; | ||
import java.net.URL; | ||
import java.net.URLConnection; | ||
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.nio.file.spi.FileSystemProvider; | ||
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 ClassLoader ccl = Thread.currentThread().getContextClassLoader(); | ||
try { | ||
// We are loading "installed" FS providers that are loaded from the system classloader anyway | ||
// To avoid potential ClassCastExceptions we are setting the context classloader to the system one | ||
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); | ||
FileSystemProvider.installedProviders(); | ||
} finally { | ||
Thread.currentThread().setContextClassLoader(ccl); | ||
} | ||
|
||
final String file = url.getFile(); | ||
final int exclam = file.indexOf('!'); | ||
try { | ||
URL fileUrl; | ||
Path jarPath; | ||
String subPath; | ||
if (exclam == -1) { | ||
// assume the first element is a JAR file, not a plain file, since it was a `jar:` URL | ||
fileUrl = new URL(file); | ||
subPath = "/"; | ||
} else { | ||
fileUrl = new URL(file.substring(0, exclam)); | ||
subPath = file.substring(exclam + 1); | ||
} | ||
if (!fileUrl.getProtocol().equals("file")) { | ||
throw new IllegalArgumentException("Sub-URL of JAR URL is expected to have a scheme of `file`"); | ||
} | ||
jarPath = toLocalPath(fileUrl); | ||
return processAsJarPath(jarPath, subPath, function); | ||
} catch (MalformedURLException e) { | ||
throw new RuntimeException("Failed to create a URL for '" + file.substring(0, exclam) + "'", e); | ||
} | ||
} | ||
|
||
if (FILE.equals(url.getProtocol())) { | ||
return function.apply(toLocalPath(url)); | ||
} | ||
|
||
throw new IllegalArgumentException("Unexpected protocol " + url.getProtocol() + " for URL " + url); | ||
} | ||
|
||
private static <R> R processAsJarPath(Path jarPath, String path, Function<Path, R> function) { | ||
try (FileSystem jarFs = FileSystems.newFileSystem(jarPath, (ClassLoader) null)) { | ||
Path localPath = jarFs.getPath("/"); | ||
int idx = path.indexOf('!'); | ||
if (idx == -1) { | ||
return function.apply(localPath.resolve(path)); | ||
} else { | ||
return processAsJarPath(localPath.resolve(path.substring(0, idx)), path.substring(idx + 1), function); | ||
} | ||
} catch (IOException e) { | ||
throw new UncheckedIOException("Failed to read " + jarPath, e); | ||
} | ||
} | ||
|
||
/** | ||
* 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()} directly, | ||
* 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())) { | ||
URLConnection urlConnection = url.openConnection(); | ||
// prevent locking the jar after the inputstream is closed | ||
urlConnection.setUseCaches(false); | ||
try (InputStream is = urlConnection.getInputStream()) { | ||
return function.apply(is); | ||
} | ||
} | ||
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); | ||
} | ||
} | ||
|
||
/** | ||
* 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); | ||
} | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
src/main/java/io/smallrye/common/classloader/DefineClassPermission.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,40 @@ | ||
package io.smallrye.common.classloader; | ||
|
||
import java.security.Permission; | ||
|
||
public class DefineClassPermission extends Permission { | ||
private static final long serialVersionUID = 142067672163413424L; | ||
private static final DefineClassPermission INSTANCE = new DefineClassPermission(); | ||
|
||
public DefineClassPermission() { | ||
super(""); | ||
} | ||
|
||
public DefineClassPermission(final String name, final String actions) { | ||
this(); | ||
} | ||
|
||
public static DefineClassPermission getInstance() { | ||
return INSTANCE; | ||
} | ||
|
||
@Override | ||
public boolean implies(final Permission permission) { | ||
return permission != null && permission.getClass() == this.getClass(); | ||
} | ||
|
||
@Override | ||
public boolean equals(final Object obj) { | ||
return obj instanceof DefineClassPermission; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return getClass().hashCode(); | ||
} | ||
|
||
@Override | ||
public String getActions() { | ||
return ""; | ||
} | ||
} |