Skip to content

Commit

Permalink
fix(citrusframework#1094): fix nested jar url issue
Browse files Browse the repository at this point in the history
  • Loading branch information
Thorsten Schlathoelter authored and bbortt committed Jan 16, 2024
1 parent 52eeec5 commit 902df4f
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -71,8 +71,8 @@ public Set<Path> 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("/")) {
Expand Down Expand Up @@ -157,15 +157,12 @@ private static void loadFromNestedJar(ClassLoader classLoader, String path, Stri
private static void readFromJarStream(ClassLoader classLoader, String path, String urlPath,
Set<Path> resources, Predicate<String> filter, InputStream jarInputStream) {
List<String> 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);
}
}
}

Expand Down Expand Up @@ -211,17 +208,7 @@ private void loadResourcesInDirectory(String path, File location, Set<Path> 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:")) {
Expand All @@ -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.
* <p>
* 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<ClassLoader> getClassLoaders() {
Set<ClassLoader> classLoaders = new LinkedHashSet<>();
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,60 +15,86 @@
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<Path> 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<Path> 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<Path> resources = resolver.getResources("META-INF/citrus/test/parser/core");
Assertions.assertTrue(
currentThread()
.setContextClassLoader(
new SimulatedNestedJarClassLoader("file", "simplejar.jar", "",
contextClassLoader));
Set<Path> 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);
}
}

/**
* 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;
Expand All @@ -73,9 +105,18 @@ public Enumeration<URL> 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);
}
Expand All @@ -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);
Expand All @@ -110,5 +153,8 @@ public InputStream getResourceAsStream(String name) {
return delegate.getResourceAsStream(name);
}

private static String normalizeUrl(URL url) {
return url.toString().replace("\\", "/");
}
}
}

0 comments on commit 902df4f

Please sign in to comment.