From 715ca5a60d71e4b49414b3d26a60eb28f3103c02 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sat, 24 Apr 2021 18:13:00 +0300 Subject: [PATCH] Ensure that RunnerClassLoader returns unique entries Fixes: #16770 --- .../quarkus/bootstrap/runner/JarResource.java | 16 ++++ .../runner/SerializedApplication.java | 79 +++++++++++++++---- .../projects/rr-with-json-logging/pom.xml | 5 ++ .../java/org/acme/ClasspathResources.java | 20 ++++- 4 files changed, 102 insertions(+), 18 deletions(-) diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java index f0af84755c227..273d3c0593378 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/JarResource.java @@ -12,6 +12,7 @@ import java.security.CodeSource; import java.security.ProtectionDomain; import java.security.cert.Certificate; +import java.util.Objects; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -202,4 +203,19 @@ public String toString() { jarPath.getFileName() + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + JarResource that = (JarResource) o; + return Objects.equals(manifestInfo, that.manifestInfo) && jarPath.equals(that.jarPath); + } + + @Override + public int hashCode() { + return Objects.hash(manifestInfo, jarPath); + } } diff --git a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java index 23d25656fe96e..b3e8b88bdc60a 100644 --- a/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java +++ b/independent-projects/bootstrap/runner/src/main/java/io/quarkus/bootstrap/runner/SerializedApplication.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.Manifest; @@ -40,6 +41,9 @@ public class SerializedApplication { private static final int MAGIC = 0XF0315432; private static final int VERSION = 2; + private static final ClassLoadingResource[] EMPTY_ARRAY = new ClassLoadingResource[0]; + private static final JarResource SENTINEL = new JarResource(null, Path.of("wqxehxivam")); + private final RunnerClassLoader runnerClassLoader; private final String mainClass; @@ -108,7 +112,7 @@ public static SerializedApplication read(InputStream inputStream, Path appRoot) throw new RuntimeException("Wrong class path version"); } String mainClass = in.readUTF(); - Map resourceDirectoryMap = new HashMap<>(); + ResourceDirectoryTracker resourceDirectoryTracker = new ResourceDirectoryTracker(); Set parentFirstPackages = new HashSet<>(); int numPaths = in.readUnsignedShort(); ClassLoadingResource[] allClassLoadingResources = new ClassLoadingResource[numPaths]; @@ -127,10 +131,10 @@ public static SerializedApplication read(InputStream inputStream, Path appRoot) String dir = in.readUTF(); int j = dir.indexOf('/'); while (j >= 0) { - addResourceDir(dir.substring(0, j), resource, resourceDirectoryMap); + resourceDirectoryTracker.addResourceDir(dir.substring(0, j), resource); j = dir.indexOf('/', j + 1); } - addResourceDir(dir, resource, resourceDirectoryMap); + resourceDirectoryTracker.addResourceDir(dir, resource); } } int packages = in.readUnsignedShort(); @@ -156,7 +160,7 @@ public static SerializedApplication read(InputStream inputStream, Path appRoot) directlyIndexedResourcesIndexMap.put(resource, matchingResources); } RunnerClassLoader runnerClassLoader = new RunnerClassLoader(ClassLoader.getSystemClassLoader(), - resourceDirectoryMap, parentFirstPackages, + resourceDirectoryTracker.getResult(), parentFirstPackages, nonExistentResources, FULLY_INDEXED_PATHS, directlyIndexedResourcesIndexMap); for (ClassLoadingResource classLoadingResource : allClassLoadingResources) { classLoadingResource.init(runnerClassLoader); @@ -165,19 +169,6 @@ public static SerializedApplication read(InputStream inputStream, Path appRoot) } } - private static void addResourceDir(String dir, JarResource resource, - Map resourceDirectoryMap) { - ClassLoadingResource[] existing = resourceDirectoryMap.get(dir); - if (existing == null) { - resourceDirectoryMap.put(dir, new ClassLoadingResource[] { resource }); - } else { - ClassLoadingResource[] newResources = new ClassLoadingResource[existing.length + 1]; - System.arraycopy(existing, 0, newResources, 0, existing.length); - newResources[existing.length] = resource; - resourceDirectoryMap.put(dir, newResources); - } - } - private static String readNullableString(DataInputStream in) throws IOException { if (in.readBoolean()) { return in.readUTF(); @@ -321,4 +312,58 @@ private static void writeNullableString(DataOutputStream out, String string) thr } } + /** + * This class is to build up the final directory to Resource map + * The idea here is that directories will only be contained by a single resource, so we optimistically + * create a single item array when we encounter a directory for the first time. + * However, when a resource is added for the same directory, we switch to using a Set to track the resources + * of that directory and then at the end use those sets to override to create the final array of resources. + * The reason for doing it this way to only create Sets when needed (which is only a fraction of the cases) + */ + private static class ResourceDirectoryTracker { + private final Map result = new HashMap<>(); + private final Map> overrides = new HashMap<>(); + + void addResourceDir(String dir, JarResource resource) { + ClassLoadingResource[] existing = result.get(dir); + if (existing == null) { + // this is the first the dir was ever tracked + result.put(dir, new JarResource[] { resource }); + } else { + ClassLoadingResource existingResource = existing[0]; + if (existingResource.equals(resource)) { + // we don't need to do anything as the resource has already been tracked and an attempt + // to add it again was made + } else { + Set dirOverrides = overrides.get(dir); + if (dirOverrides == null) { + // we need to create the override set as this is the first time we find a resource for the dir + // that is not the same as the one in the array + dirOverrides = new LinkedHashSet<>(2); + dirOverrides.add(existingResource); + dirOverrides.add(resource); + overrides.put(dir, dirOverrides); + + //replace the value in the original array with a sentinel in order to allow for quick comparisons + existing[0] = SENTINEL; + } else { + // in this case, overrides has already been created in a previous invocation so all we need to + // do is add the new resource + dirOverrides.add(resource); + } + } + } + } + + Map getResult() { + overrides.forEach(new BiConsumer>() { + @Override + public void accept(String dir, Set jarResources) { + result.put(dir, jarResources.toArray(EMPTY_ARRAY)); + } + }); + return result; + } + } + } diff --git a/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/pom.xml b/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/pom.xml index 47e68eef895c2..4bad2ac3ebb3c 100644 --- a/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/pom.xml +++ b/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/pom.xml @@ -45,6 +45,11 @@ commons-io 2.6 + + org.drools + drools-engine + 7.51.0.Final + io.quarkus quarkus-junit5 diff --git a/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/src/main/java/org/acme/ClasspathResources.java b/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/src/main/java/org/acme/ClasspathResources.java index 07e1106b5dc71..031f2d10eb40e 100644 --- a/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/src/main/java/org/acme/ClasspathResources.java +++ b/integration-tests/maven/src/test/resources/projects/rr-with-json-logging/src/main/java/org/acme/ClasspathResources.java @@ -12,6 +12,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.ArrayList; @@ -32,7 +33,8 @@ public String readClassPathResources() { () -> assertCorrectExactFileLocation(), () -> assertInvalidDirectory(), () -> assertCorrectDirectory(), - () -> assertMultiRelease() + () -> assertMultiRelease(), + () -> assertUniqueDirectories() ); } @@ -160,6 +162,22 @@ private String assertCorrectDirectory() { } } + private String assertUniqueDirectories() { + final String testType = "unique-directories"; + try { + Enumeration resources = this.getClass().getClassLoader().getResources("META-INF/kie.conf"); + List resourcesList = Collections.list(resources); + // 'META-INF/kie.conf' should be present in 'kie-internal', 'drools-core', 'drools-compiler' and 'drools-model-compiler' + if (resourcesList.size() != 4) { + return errorResult(testType, "wrong number of directory urls"); + } + return SUCCESS; + } catch (Exception e) { + e.printStackTrace(); + return errorResult(testType, "exception during resolution of resource"); + } + } + private List urlList(Enumeration enumeration) { if (enumeration == null) { return Collections.emptyList();