diff --git a/core/citrus-api/src/main/java/org/citrusframework/spi/ClasspathResourceResolver.java b/core/citrus-api/src/main/java/org/citrusframework/spi/ClasspathResourceResolver.java index 289dcc3a92..33e19f70e6 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/spi/ClasspathResourceResolver.java +++ b/core/citrus-api/src/main/java/org/citrusframework/spi/ClasspathResourceResolver.java @@ -19,12 +19,12 @@ package org.citrusframework.spi; +import static org.citrusframework.spi.Resources.CLASSPATH_RESOURCE_PREFIX; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; @@ -71,8 +71,8 @@ public Set getResources(String path) throws IOException { path = path.substring(0, path.length() - 2); } - if (path.startsWith(Resources.CLASSPATH_RESOURCE_PREFIX)) { - path = path.substring(Resources.CLASSPATH_RESOURCE_PREFIX.length()); + if (path.startsWith(CLASSPATH_RESOURCE_PREFIX)) { + path = path.substring(CLASSPATH_RESOURCE_PREFIX.length()); } if (path.startsWith("/")) { @@ -157,15 +157,12 @@ private static void loadFromNestedJar(ClassLoader classLoader, String path, Stri private static void readFromJarStream(ClassLoader classLoader, String path, String urlPath, Set resources, Predicate filter, InputStream jarInputStream) { List entries = new ArrayList<>(); - try (JarInputStream jarStream = new JarInputStream(jarInputStream);) { + try (JarInputStream jarStream = new JarInputStream(jarInputStream)) { JarEntry entry; while ((entry = jarStream.getNextJarEntry()) != null) { final String name = entry.getName().trim(); - if (!entry.isDirectory() && filter.test(name)) { - // name is FQN so it must start with package name - if (name.startsWith(path)) { + if (!entry.isDirectory() && filter.test(name) && name.startsWith(path)) { entries.add(name); - } } } @@ -211,17 +208,7 @@ private void loadResourcesInDirectory(String path, File location, Set resu private String parseUrlPath(URL url) { String urlPath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8); - if (urlPath.startsWith("file:")) { - try { - urlPath = new URI(url.getFile()).getPath(); - } catch (URISyntaxException e) { - // do nothing - } - - if (urlPath.startsWith("file:")) { - urlPath = urlPath.substring(5); - } - } + urlPath = removeNestedProtocol(urlPath); // osgi bundles should be skipped if (url.toString().startsWith("bundle:") || urlPath.startsWith("bundle:")) { @@ -239,6 +226,22 @@ private String parseUrlPath(URL url) { return urlPath.contains("!") ? urlPath.substring(0, urlPath.lastIndexOf("!")) : urlPath; } + /** + * Removes any nested protocol from the URL path, particularly addressing cases when dealing with + * Spring Boot fat JARs. + *

+ * Two common cases are: + * 1. 'jar:file:/path' - for nested URLs in Spring Boot versions up to 3.1.x. + * 2. 'jar:nested:/path' - for nested URLs in Spring Boot versions starting from 3.2.x. + */ + private static String removeNestedProtocol(String urlPath) { + int protocolSeparatorIndex = urlPath.indexOf(':'); + if (protocolSeparatorIndex > -1 && protocolSeparatorIndex < urlPath.indexOf('/')) { + urlPath = urlPath.substring(protocolSeparatorIndex+1); + } + return urlPath; + } + private Set getClassLoaders() { Set classLoaders = new LinkedHashSet<>(); try { diff --git a/core/citrus-api/src/test/java/org/citrusframework/spi/ClassPathResourceResolverTest.java b/core/citrus-api/src/test/java/org/citrusframework/spi/ClassPathResourceResolverTest.java index 8a0676470a..d64bd408c4 100644 --- a/core/citrus-api/src/test/java/org/citrusframework/spi/ClassPathResourceResolverTest.java +++ b/core/citrus-api/src/test/java/org/citrusframework/spi/ClassPathResourceResolverTest.java @@ -1,5 +1,11 @@ package org.citrusframework.spi; +import static java.lang.Thread.currentThread; +import static java.util.Objects.requireNonNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.testng.Assert.assertTrue; + import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; @@ -9,46 +15,69 @@ import java.util.Enumeration; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; -class ClassPathResourceResolverTest { +public class ClassPathResourceResolverTest { - @Test - void loadFromFatJar() throws IOException { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + public static final String LOAD_FROM_FAT_JAR_SPRING_BOOT = "loadFromFatJarSpringBoot"; + private final ClasspathResourceResolver fixture = new ClasspathResourceResolver(); + + @DataProvider(name = LOAD_FROM_FAT_JAR_SPRING_BOOT) + public Object[][] loadFromFatJarSpringBoot() { + return new Object[][]{ + {"META-INF/citrus/test/parser/core", "file"}, + {"META-INF/citrus/test/parser/core/*", "file"}, + {"META-INF/citrus/test/parser/core.*", "file"}, + {"/META-INF/citrus/test/parser/core.*", "file"}, + {"classpath:META-INF/citrus/test/parser/core.*", "file"}, + {"META-INF/citrus/test/parser/core", "nested"}, + {"META-INF/citrus/test/parser/core/*", "nested"}, + {"META-INF/citrus/test/parser/core.*", "nested"}, + {"/META-INF/citrus/test/parser/core.*", "nested"}, + {"classpath:META-INF/citrus/test/parser/core.*", "nested"} + }; + } + + @Test(dataProvider = LOAD_FROM_FAT_JAR_SPRING_BOOT) + public void loadFromFatJarSpringBoot(String resourcePath, String nestedProtocol) + throws IOException { + ClassLoader contextClassLoader = currentThread().getContextClassLoader(); try { - Thread.currentThread() - .setContextClassLoader(new SimulatedNestedJarClassLoader("fatjar.jar", "!/BOOT-INF/lib/test-nested-jar.jar", contextClassLoader)); - ClasspathResourceResolver resolver = new ClasspathResourceResolver(); - Set resources = resolver.getResources("META-INF/citrus/test/parser/core"); - Assertions.assertTrue( + currentThread() + .setContextClassLoader( + new SimulatedNestedJarClassLoader(nestedProtocol, "fatjar.jar", + "!/BOOT-INF/lib/test-nested-jar.jar", contextClassLoader)); + Set resources = fixture.getResources(resourcePath); + + assertTrue( resources.contains(Path.of("META-INF/citrus/test/parser/core/schema-collection"))); - Assertions.assertTrue(resources.contains( + assertTrue(resources.contains( Path.of("META-INF/citrus/test/parser/core/xml-data-dictionary"))); - Assertions.assertTrue(resources.contains( + assertTrue(resources.contains( Path.of("META-INF/citrus/test/parser/core/xpath-data-dictionary"))); } finally { - Thread.currentThread().setContextClassLoader(contextClassLoader); + currentThread().setContextClassLoader(contextClassLoader); } } @Test void loadFromSimpleJar() throws IOException { - ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + ClassLoader contextClassLoader = currentThread().getContextClassLoader(); try { - Thread.currentThread() - .setContextClassLoader(new SimulatedNestedJarClassLoader("simplejar.jar", "", contextClassLoader)); - ClasspathResourceResolver resolver = new ClasspathResourceResolver(); - Set resources = resolver.getResources("META-INF/citrus/test/parser/core"); - Assertions.assertTrue( + currentThread() + .setContextClassLoader( + new SimulatedNestedJarClassLoader("file", "simplejar.jar", "", + contextClassLoader)); + Set resources = fixture.getResources("META-INF/citrus/test/parser/core"); + assertTrue( resources.contains(Path.of("META-INF/citrus/test/parser/core/schema-collection"))); - Assertions.assertTrue(resources.contains( + assertTrue(resources.contains( Path.of("META-INF/citrus/test/parser/core/xml-data-dictionary"))); - Assertions.assertTrue(resources.contains( + assertTrue(resources.contains( Path.of("META-INF/citrus/test/parser/core/xpath-data-dictionary"))); } finally { - Thread.currentThread().setContextClassLoader(contextClassLoader); + currentThread().setContextClassLoader(contextClassLoader); } } @@ -56,13 +85,16 @@ void loadFromSimpleJar() throws IOException { * A classloader that simulates resolving from a nested jar. This kind of jar, also known as fat * jar or uber jar is used in spring boot applications. */ - private class SimulatedNestedJarClassLoader extends ClassLoader { + private static class SimulatedNestedJarClassLoader extends ClassLoader { + private final String nestedProtocol; private final String baseJar; private final String nestedJar; private final ClassLoader delegate; - private SimulatedNestedJarClassLoader(String baseJar, String nestedJar, ClassLoader delegate) { + private SimulatedNestedJarClassLoader(String nestedProtocol, String baseJar, + String nestedJar, ClassLoader delegate) { + this.nestedProtocol = nestedProtocol; this.baseJar = baseJar; this.nestedJar = nestedJar; this.delegate = delegate; @@ -73,9 +105,18 @@ public Enumeration getResources(String name) throws IOException { if (name.equals("META-INF/citrus/test/parser/core/")) { URL url = delegate.getResource(baseJar); - URL jarResourceUrl = new URL("jar:" + url.toString().replace("\\", "/") - + nestedJar+ "!/META-INF/citrus/test/parser/core"); - return Collections.enumeration(List.of(jarResourceUrl)); + requireNonNull(url); + + URL jarResourceUrl = new URL("jar:" + normalizeUrl(url) + + nestedJar + "!/META-INF/citrus/test/parser/core"); + + // "nested" is not recognized protocol and can thus not be used for creating URLS. + // Therefore, use a spy to fake in the "nested" protocol if needed. + URL urlSpy = spy(jarResourceUrl); + doReturn(jarResourceUrl.getFile().replace("file:", nestedProtocol + ":")).when( + urlSpy).getFile(); + + return Collections.enumeration(List.of(urlSpy)); } return delegate.getResources(name); } @@ -97,8 +138,10 @@ public URL getResource(String name) { private URL getNestedJarUrl() { URL url = delegate.getResource(baseJar); + requireNonNull(url); + try { - return new URL("jar:" + url.toString().replace("\\", "/") + return new URL("jar:" + normalizeUrl(url) + "!/BOOT-INF/lib/test-nested-jar.jar"); } catch (MalformedURLException e) { throw new RuntimeException(e); @@ -110,5 +153,8 @@ public InputStream getResourceAsStream(String name) { return delegate.getResourceAsStream(name); } + private static String normalizeUrl(URL url) { + return url.toString().replace("\\", "/"); + } } } \ No newline at end of file