diff --git a/test/jdk/jdk/internal/loader/URLClassPath/JarClassPathDfsOrder.java b/test/jdk/jdk/internal/loader/URLClassPath/JarClassPathDfsOrder.java new file mode 100644 index 00000000000..4a6b2b03aaa --- /dev/null +++ b/test/jdk/jdk/internal/loader/URLClassPath/JarClassPathDfsOrder.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * @test + * @bug 8375580 + * @summary Verify that URLClassPath discovers JAR Class-Path URLs in DFS order + * @run junit JarClassPathDfsOrder + */ +public class JarClassPathDfsOrder { + + // Name of the JAR resource use for lookups + private static final String ENTRY_NAME = "JarClassPathOrdering.txt"; + + /** + * Verify that URLClassPath discovers JAR files in DFS order when only + * root JARs are on the 'original' class path and the other JARs are + * found via multiple levels of Class-Path Manifest attributes. + * + * @throws IOException if an unexpected error occurs + */ + @Test + public void shouldLoadJARsInDFSOrder() throws IOException { + + // Set up a 'Class-Path' JAR tree of depth 3 + JarTree specs = new JarTree(); + specs.addJars(3); + + // List of "root" JAR file URLs to use for URLClassLoader + List searchPath = new ArrayList<>(); + + // Order of JAR files we expect URLClassPath to find + List dfsJarNames = new ArrayList<>(); + + for (JarSpec spec : specs.dfs) { + Path jar = createJar(spec); + dfsJarNames.add(jar.getFileName().toString()); + // Only root JARs in the search path, others are discovered transitively via "Class-Path" + if (spec.isRoot()) { + searchPath.add(jar.toUri().toURL()); + } + } + + // Load ENTRY_NAME to identify all JARs found by URLClassPath + try (URLClassLoader loader = new URLClassLoader(searchPath.toArray(new URL[0]))) { + Enumeration resources = loader.getResources(ENTRY_NAME); + // Collect all JAR file names in the order discovered by URLClassPath + List actualJarNames = Collections.list(resources) + .stream() + .map(this::extractJarName) + .collect(Collectors.toList()); + // JARs should be found in DFS order + assertEquals(dfsJarNames, actualJarNames, "JAR files not found in expected DFS order"); + } + } + + // Extract file name of JAR file from a JAR URL + private String extractJarName(URL url) { + String jarPath = url.getPath().substring(0, url.getPath().indexOf("!/")); + String jarName = jarPath.substring(jarPath.lastIndexOf('/') + 1); + return jarName; + } + + // Create a JAR file according to the spec, possibly including a Class-Path attribute + private Path createJar(JarSpec spec) throws IOException { + Path file = Path.of(spec.name +".jar"); + Manifest man = new Manifest(); + Attributes attrs = man.getMainAttributes(); + attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + + // Set Class-Path attribute + if (!spec.children.isEmpty()) { + String path = String.join(" ", spec.children + .stream() + .map(js -> js.name +".jar") + .collect(Collectors.toList())); + attrs.put(Attributes.Name.CLASS_PATH, path); + } + + try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(file), man)) { + // All JARs include the same entry + out.putNextEntry(new JarEntry(ENTRY_NAME)); + } + + return file; + } + + + // Helper class to represent a tree of JARs related by "Class-Path" + static class JarTree { + static final List NAMES = List.of("a", "b", "c"); + List dfs = new ArrayList<>(); + + private void addJar(String prefix, String name, JarSpec parent, int depth, int maxDepth) { + if (depth > maxDepth) { + return; + } + JarSpec spec = new JarSpec(prefix + name, parent); + dfs.add(spec); + + if (parent != null) { + parent.children.add(spec); + } + + for (String childName : NAMES) { + addJar(prefix + name, childName, spec, depth + 1, maxDepth); + } + } + + /* Make a tree of JARs related by the 'Class-Path' Manifest attribute: + * a.jar + * aa.jar + * aaa.jar + * aab.jar + * [...] + * ccc.jar + */ + public void addJars(int maxDepth) { + for (String name : NAMES) { + addJar("", name, null, 1, maxDepth); + } + } + } + + // Helper class to represent a JAR file to be found by URLClassPath + static class JarSpec { + final String name; + final JarSpec parent; + final List children = new ArrayList<>(); + + JarSpec(String name, JarSpec parent) { + this.name = name; + this.parent = parent; + } + boolean isRoot() { + return parent == null; + } + } +}