Skip to content

Commit 6c4a910

Browse files
authored
[Core] Support nested jar file systems (#2830)
Spring Boot 3.2 changed the URL format of their nested jars[1] to be more compliant with JDK expectations. They now represented nested jars as their own `nested` scheme rather than the `file` scheme. This allows these URLs to be used seamlessly with `FileSystems.newFileSystem`. Unfortunately the workarounds for Spring Boot 3.1 did not account for this. Additionally, our jar uri parsing assumed naively that there would only be a single `!/` in a regular jar uri. However, jar uris are recursively defined as[2]: ``` jar:<url>!/[<entry>] ``` And while this should allow Cucumber to discover resources in nested jars as well it does seem that Spring Boot 3.2 still has some issues[3]. Closes: #2828 1. https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes 2. https://www.iana.org/assignments/uri-schemes/prov/jar 3. spring-projects/spring-boot#38595
1 parent 6d08c72 commit 6c4a910

File tree

4 files changed

+64
-14
lines changed

4 files changed

+64
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
### Fixed
1616
- [Guice] Inject static fields prior to before all hooks ([#2803](https://github.com/cucumber/cucumber-jvm/pull/2803) M.P. Korstanje)
1717

18+
### Added
19+
- [Core] Support nested jar file systems (i.e. Spring Boot 3.2) ([#2830](https://github.com/cucumber/cucumber-jvm/pull/2830) M.P. Korstanje)
20+
1821
## [7.14.0] - 2023-09-09
1922
### Changed
2023
- [Core] Update dependency io.cucumber:html-formatter to v20.4.0

cucumber-core/src/main/java/io/cucumber/core/resource/ClasspathSupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ static String nestedJarEntriesExplanation(URI uri) {
167167
"This typically happens when trying to run Cucumber inside a Spring Boot Executable Jar.\n" +
168168
"Cucumber currently doesn't support classpath scanning in nested jars.\n" +
169169
"\n" +
170-
"You can avoid this error by unpacking your application before executing.\n" +
170+
"You can avoid this error by unpacking your application before executing or upgrading to Spring Boot 3.2 or higher.\n"
171+
+
171172
"\n" +
172173
"Alternatively you can restrict which packages cucumber scans configuring the glue path such that " +
173174
"Cucumber only scans un-nested jars.\n" +

cucumber-core/src/main/java/io/cucumber/core/resource/JarUriFileSystemService.java

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class JarUriFileSystemService {
2222
private static final String JAR_URI_SCHEME = "jar";
2323
private static final String JAR_URI_SCHEME_PREFIX = JAR_URI_SCHEME + ":";
2424
private static final String JAR_FILE_SUFFIX = ".jar";
25-
private static final String JAR_URI_SEPARATOR = "!";
25+
private static final String JAR_URI_SEPARATOR = "!/";
2626

2727
private static final Map<URI, FileSystem> openFiles = new HashMap<>();
2828
private static final Map<URI, AtomicInteger> referenceCount = new HashMap<>();
@@ -67,13 +67,14 @@ private static boolean hasFileUriSchemeWithJarExtension(URI uri) {
6767
}
6868

6969
static CloseablePath open(URI uri) throws URISyntaxException, IOException {
70-
if (hasJarUriScheme(uri)) {
71-
return handleJarUriScheme(uri);
72-
}
70+
assert supports(uri);
7371
if (hasFileUriSchemeWithJarExtension(uri)) {
7472
return handleFileUriSchemeWithJarExtension(uri);
7573
}
76-
throw new IllegalArgumentException("Unsupported uri " + uri.toString());
74+
if (isSpringBoot31OrLower(uri)) {
75+
return handleSpringBoot31JarUri(uri);
76+
}
77+
return handleJarUriScheme(uri);
7778
}
7879

7980
private static CloseablePath handleFileUriSchemeWithJarExtension(URI uri) throws IOException, URISyntaxException {
@@ -82,22 +83,44 @@ private static CloseablePath handleFileUriSchemeWithJarExtension(URI uri) throws
8283
}
8384

8485
private static CloseablePath handleJarUriScheme(URI uri) throws IOException, URISyntaxException {
85-
String[] parts = uri.toString().split(JAR_URI_SEPARATOR);
86-
// Regular jar schemes
87-
if (parts.length <= 2) {
88-
String jarUri = parts[0];
89-
String jarPath = parts.length == 2 ? parts[1] : "/";
90-
return open(new URI(jarUri), fileSystem -> fileSystem.getPath(jarPath));
86+
// Regular Jar Uris
87+
// Format: jar:<url>!/[<entry>]
88+
String uriString = uri.toString();
89+
int lastJarUriSeparator = uriString.lastIndexOf(JAR_URI_SEPARATOR);
90+
if (lastJarUriSeparator < 0) {
91+
throw new IllegalArgumentException(String.format("jar uri '%s' must contain '%s'", uri, JAR_URI_SEPARATOR));
9192
}
93+
String url = uriString.substring(0, lastJarUriSeparator);
94+
String entry = uriString.substring(lastJarUriSeparator + 1);
95+
return open(new URI(url), fileSystem -> fileSystem.getPath(entry));
96+
}
9297

93-
// Spring boot jar scheme
98+
private static boolean isSpringBoot31OrLower(URI uri) {
99+
// Starting Spring Boot 3.2 the nested scheme is used. This works with
100+
// regular jar file handling and doesn't need a workaround.
101+
// Example 3.2:
102+
// jar:nested:/dir/myjar.jar/!/BOOT-INF/lib/nested.jar!/com/example/MyClass.class
103+
// Example 3.1:
104+
// jar:file:/dir/myjar.jar/!/BOOT-INF/lib/nested.jar!/com/example/MyClass.class
105+
String schemeSpecificPart = uri.getSchemeSpecificPart();
106+
return schemeSpecificPart.startsWith("file:") && schemeSpecificPart.contains("!/BOOT-INF");
107+
}
108+
109+
private static CloseablePath handleSpringBoot31JarUri(URI uri) throws IOException, URISyntaxException {
110+
// Spring boot 3.1 jar scheme
111+
// Examples:
112+
// jar:file:/home/user/application.jar!/BOOT-INF/lib/dependency.jar!/com/example/dependency/resource.txt
113+
// jar:file:/home/user/application.jar!/BOOT-INF/classes!/com/example/package/resource.txt
114+
String[] parts = uri.toString().split("!");
94115
String jarUri = parts[0];
95116
String jarEntry = parts[1];
96117
String subEntry = parts[2];
97118
if (jarEntry.endsWith(JAR_FILE_SUFFIX)) {
98119
throw new CucumberException(nestedJarEntriesExplanation(uri));
99120
}
121+
// We're looking directly at the files in the jar, so we construct the
122+
// file path by concatenating the jarEntry and subEntry without the jar
123+
// uri separator.
100124
return open(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry + subEntry));
101125
}
102-
103126
}

cucumber-core/src/test/java/io/cucumber/core/resource/ResourceScannerTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,29 @@ void scanForResourcesJarUri() {
159159
assertThat(resources, contains(resourceUri));
160160
}
161161

162+
@Test
163+
void scanForResourcesJarUriMalformed() {
164+
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/jar-resource.jar").toURI();
165+
URI resourceUri = URI
166+
.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "/com/example/package-jar-resource.txt");
167+
IllegalArgumentException exception = assertThrows(
168+
IllegalArgumentException.class,
169+
() -> resourceScanner.scanForResourcesUri(resourceUri));
170+
assertThat(exception.getMessage(),
171+
containsString("jar uri '" + resourceUri + "' must contain '!/'"));
172+
}
173+
174+
@Test
175+
void scanForResourcesJarUriMissingEntry() {
176+
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/jar-resource.jar").toURI();
177+
URI resourceUri = URI.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "");
178+
IllegalArgumentException exception = assertThrows(
179+
IllegalArgumentException.class,
180+
() -> resourceScanner.scanForResourcesUri(resourceUri));
181+
assertThat(exception.getMessage(),
182+
containsString("jar uri '" + resourceUri + "' must contain '!/'"));
183+
}
184+
162185
@Test
163186
void scanForResourcesNestedJarUri() {
164187
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/spring-resource.jar").toURI();

0 commit comments

Comments
 (0)