From 9daa467417b84c812ee04da5d909c79d5a729580 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 1 May 2024 09:24:59 +1000 Subject: [PATCH] Allow ClassLoader to return multiple resources Previously if the resources were in the same PathTree only one would be returned. Fixes #40371 --- .../io/quarkus/paths/CachingPathTree.java | 5 ++++ .../io/quarkus/paths/FilteredPathTree.java | 9 ++++++ .../io/quarkus/paths/MultiRootPathTree.java | 20 +++++++++++++ .../main/java/io/quarkus/paths/PathTree.java | 14 +++++++++ .../quarkus/paths/SharedArchivePathTree.java | 5 ++++ .../classloading/ClassPathElement.java | 5 ++++ .../PathTreeClassPathElement.java | 22 ++++++++++++++ .../classloading/QuarkusClassLoader.java | 29 +++++++++++-------- .../io/quarkus/it/extension/my_resource.txt | 0 .../quarkus/it/extension/ClassLoaderTest.java | 29 +++++++++++++++++++ 10 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 integration-tests/test-extension/tests/src/main/resources/io/quarkus/it/extension/my_resource.txt create mode 100644 integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java index a2349c3024766..47bde335702f4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/CachingPathTree.java @@ -87,6 +87,11 @@ public void accept(String relativePath, Consumer func) { delegate.accept(relativePath, func); } + @Override + public void acceptAll(String relativePath, Consumer func) { + delegate.acceptAll(relativePath, func); + } + @Override public boolean contains(String relativePath) { final LinkedHashMap snapshot = walkSnapshot; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java index 11e125a04fbf1..8265ef59771f1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java @@ -54,6 +54,15 @@ public void accept(String relativePath, Consumer consumer) { } } + @Override + public void acceptAll(String relativePath, Consumer consumer) { + if (!PathFilter.isVisible(filter, relativePath)) { + consumer.accept(null); + } else { + original.acceptAll(relativePath, consumer); + } + } + @Override public boolean contains(String relativePath) { return PathFilter.isVisible(filter, relativePath) && original.contains(relativePath); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java index 4681cec199deb..e09b70ce12168 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java @@ -86,6 +86,26 @@ public void accept(PathVisit t) { } } + @Override + public void acceptAll(String relativePath, Consumer func) { + final AtomicBoolean consumed = new AtomicBoolean(); + final Consumer wrapper = new Consumer<>() { + @Override + public void accept(PathVisit t) { + if (t != null) { + func.accept(t); + consumed.set(true); + } + } + }; + for (PathTree tree : trees) { + tree.accept(relativePath, wrapper); + } + if (!consumed.get()) { + func.accept(null); + } + } + @Override public boolean contains(String relativePath) { for (PathTree tree : trees) { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index b126db5924d26..5b68db900bc93 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -134,6 +134,20 @@ default boolean isEmpty() { */ void accept(String relativePath, Consumer consumer); + /** + * Consumes a given path relative to the root of the tree. + * If the path isn't found in the tree, the {@link PathVisit} argument + * passed to the consumer will be {@code null}. + * + * If multiple items match then the consumer will be called multiple times. + * + * @param relativePath relative path to consume + * @param consumer path consumer + */ + default void acceptAll(String relativePath, Consumer consumer) { + accept(relativePath, consumer); + } + /** * Checks whether the tree contains a relative path. * diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index b5cd7e9cdd3a8..1206da6cc618c 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -158,6 +158,11 @@ public void accept(String relativePath, Consumer consumer) { delegate.accept(relativePath, consumer); } + @Override + public void acceptAll(String relativePath, Consumer consumer) { + delegate.acceptAll(relativePath, consumer); + } + @Override public boolean contains(String relativePath) { return delegate.contains(relativePath); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java index 53723350b5476..a76d68963b328 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java @@ -4,6 +4,7 @@ import java.nio.file.Path; import java.security.ProtectionDomain; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.jar.Manifest; @@ -141,4 +142,8 @@ public void close() { } }; + + default List getResources(String name) { + return List.of(getResource(name)); + } } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index 17dab87f95563..5d6ca6c91855e 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -12,8 +12,10 @@ import java.security.CodeSource; import java.security.ProtectionDomain; import java.security.cert.Certificate; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReadWriteLock; @@ -106,6 +108,26 @@ public ClassPathResource getResource(String name) { return apply(tree -> tree.apply(sanitized, visit -> visit == null ? null : new Resource(visit))); } + @Override + public List getResources(String name) { + final String sanitized = sanitize(name); + final Set resources = this.resources; + if (resources != null && !resources.contains(sanitized)) { + return null; + } + List ret = new ArrayList<>(); + apply(tree -> { + tree.acceptAll(sanitized, visit -> { + if (visit != null) { + ret.add(new Resource(visit)); + + } + }); + return null; + }); + return ret; + } + @Override public T apply(Function func) { lock.readLock().lock(); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index e1b20fe85d657..1f1c3bf2c1cdf 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -11,6 +11,7 @@ import java.sql.Driver; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -246,27 +247,31 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea if (providers != null) { boolean endsWithTrailingSlash = unsanitisedName.endsWith("/"); for (ClassPathElement element : providers) { - ClassPathResource res = element.getResource(name); + Collection resList = element.getResources(name); //if the requested name ends with a trailing / we make sure //that the resource is a directory, and return a URL that ends with a / //this matches the behaviour of URLClassLoader - if (endsWithTrailingSlash) { - if (res.isDirectory()) { - try { - resources.add(new URL(res.getUrl().toString() + "/")); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + for (var res : resList) { + if (endsWithTrailingSlash) { + if (res.isDirectory()) { + try { + resources.add(new URL(res.getUrl().toString() + "/")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } } + } else { + resources.add(res.getUrl()); } - } else { - resources.add(res.getUrl()); } } } else if (name.isEmpty()) { for (ClassPathElement i : elements) { - ClassPathResource res = i.getResource(""); - if (res != null) { - resources.add(res.getUrl()); + List resList = i.getResources(""); + for (var res : resList) { + if (res != null) { + resources.add(res.getUrl()); + } } } } diff --git a/integration-tests/test-extension/tests/src/main/resources/io/quarkus/it/extension/my_resource.txt b/integration-tests/test-extension/tests/src/main/resources/io/quarkus/it/extension/my_resource.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java new file mode 100644 index 0000000000000..b863c448676ee --- /dev/null +++ b/integration-tests/test-extension/tests/src/test/java/io/quarkus/it/extension/ClassLoaderTest.java @@ -0,0 +1,29 @@ +package io.quarkus.it.extension; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ClassLoaderTest { + + @Test + void testClassLoaderResources() throws IOException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + ArrayList resources = Collections.list(contextClassLoader.getResources("io/quarkus/it/extension")); + Assertions.assertEquals(2, resources.size()); + } + + @Test + void testClassLoaderSingleResource() throws IOException { + ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + URL resource = contextClassLoader.getResource("io/quarkus/it/extension/my_resource.txt"); + Assertions.assertNotNull(resource); + } +}