From 01fd7f0703ef6c1617401ad3f8d626e47090d34f Mon Sep 17 00:00:00 2001 From: "A.Fink" Date: Mon, 25 Mar 2024 18:04:36 +0300 Subject: [PATCH] test https://github.com/smallrye/smallrye-common/pull/293 --- build.gradle | 2 +- .../common/classloader/ClassDefiner.java | 26 ++ .../common/classloader/ClassPathUtils.java | 239 ++++++++++++++++++ .../classloader/DefineClassPermission.java | 40 +++ 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/smallrye/common/classloader/ClassDefiner.java create mode 100644 src/main/java/io/smallrye/common/classloader/ClassPathUtils.java create mode 100644 src/main/java/io/smallrye/common/classloader/DefineClassPermission.java diff --git a/build.gradle b/build.gradle index 96afeaf..3db6817 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ configurations.configureEach { exclude group: 'io.vertx', module: 'vertx-ignite' //!!! quick fix STEP 2: remove original smallrye-common-classloader and use fix instead - // exclude group: "io.smallrye.common", module: "smallrye-common-classloader" + exclude group: "io.smallrye.common", module: "smallrye-common-classloader" } dependencyManagement { diff --git a/src/main/java/io/smallrye/common/classloader/ClassDefiner.java b/src/main/java/io/smallrye/common/classloader/ClassDefiner.java new file mode 100644 index 0000000..0e5fe27 --- /dev/null +++ b/src/main/java/io/smallrye/common/classloader/ClassDefiner.java @@ -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>() { + @Override + public Class run() { + try { + MethodHandles.Lookup privateLookupIn = MethodHandles.privateLookupIn(parent, lookup); + return privateLookupIn.defineClass(classBytes); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } + } + }); + } +} diff --git a/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java b/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java new file mode 100644 index 0000000..934aa6e --- /dev/null +++ b/src/main/java/io/smallrye/common/classloader/ClassPathUtils.java @@ -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 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 consumer) throws IOException { + final Enumeration 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 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 consumer) throws IOException { + final Enumeration 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 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 processAsPath(URL url, Function 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 processAsJarPath(Path jarPath, String path, Function 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 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 readStream(URL url, Function 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); + } + } +} diff --git a/src/main/java/io/smallrye/common/classloader/DefineClassPermission.java b/src/main/java/io/smallrye/common/classloader/DefineClassPermission.java new file mode 100644 index 0000000..5a44fd4 --- /dev/null +++ b/src/main/java/io/smallrye/common/classloader/DefineClassPermission.java @@ -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 ""; + } +}