Skip to content

Commit

Permalink
[Core] Support nested jar file systems (#2830)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mpkorstanje committed Dec 11, 2023
1 parent 6d08c72 commit 6c4a910
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 14 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- [Guice] Inject static fields prior to before all hooks ([#2803](https://github.com/cucumber/cucumber-jvm/pull/2803) M.P. Korstanje)

### Added
- [Core] Support nested jar file systems (i.e. Spring Boot 3.2) ([#2830](https://github.com/cucumber/cucumber-jvm/pull/2830) M.P. Korstanje)

## [7.14.0] - 2023-09-09
### Changed
- [Core] Update dependency io.cucumber:html-formatter to v20.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ static String nestedJarEntriesExplanation(URI uri) {
"This typically happens when trying to run Cucumber inside a Spring Boot Executable Jar.\n" +
"Cucumber currently doesn't support classpath scanning in nested jars.\n" +
"\n" +
"You can avoid this error by unpacking your application before executing.\n" +
"You can avoid this error by unpacking your application before executing or upgrading to Spring Boot 3.2 or higher.\n"
+
"\n" +
"Alternatively you can restrict which packages cucumber scans configuring the glue path such that " +
"Cucumber only scans un-nested jars.\n" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class JarUriFileSystemService {
private static final String JAR_URI_SCHEME = "jar";
private static final String JAR_URI_SCHEME_PREFIX = JAR_URI_SCHEME + ":";
private static final String JAR_FILE_SUFFIX = ".jar";
private static final String JAR_URI_SEPARATOR = "!";
private static final String JAR_URI_SEPARATOR = "!/";

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

static CloseablePath open(URI uri) throws URISyntaxException, IOException {
if (hasJarUriScheme(uri)) {
return handleJarUriScheme(uri);
}
assert supports(uri);
if (hasFileUriSchemeWithJarExtension(uri)) {
return handleFileUriSchemeWithJarExtension(uri);
}
throw new IllegalArgumentException("Unsupported uri " + uri.toString());
if (isSpringBoot31OrLower(uri)) {
return handleSpringBoot31JarUri(uri);
}
return handleJarUriScheme(uri);
}

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

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

// Spring boot jar scheme
private static boolean isSpringBoot31OrLower(URI uri) {
// Starting Spring Boot 3.2 the nested scheme is used. This works with
// regular jar file handling and doesn't need a workaround.
// Example 3.2:
// jar:nested:/dir/myjar.jar/!/BOOT-INF/lib/nested.jar!/com/example/MyClass.class
// Example 3.1:
// jar:file:/dir/myjar.jar/!/BOOT-INF/lib/nested.jar!/com/example/MyClass.class
String schemeSpecificPart = uri.getSchemeSpecificPart();
return schemeSpecificPart.startsWith("file:") && schemeSpecificPart.contains("!/BOOT-INF");
}

private static CloseablePath handleSpringBoot31JarUri(URI uri) throws IOException, URISyntaxException {
// Spring boot 3.1 jar scheme
// Examples:
// jar:file:/home/user/application.jar!/BOOT-INF/lib/dependency.jar!/com/example/dependency/resource.txt
// jar:file:/home/user/application.jar!/BOOT-INF/classes!/com/example/package/resource.txt
String[] parts = uri.toString().split("!");
String jarUri = parts[0];
String jarEntry = parts[1];
String subEntry = parts[2];
if (jarEntry.endsWith(JAR_FILE_SUFFIX)) {
throw new CucumberException(nestedJarEntriesExplanation(uri));
}
// We're looking directly at the files in the jar, so we construct the
// file path by concatenating the jarEntry and subEntry without the jar
// uri separator.
return open(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry + subEntry));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,29 @@ void scanForResourcesJarUri() {
assertThat(resources, contains(resourceUri));
}

@Test
void scanForResourcesJarUriMalformed() {
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/jar-resource.jar").toURI();
URI resourceUri = URI
.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "/com/example/package-jar-resource.txt");
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> resourceScanner.scanForResourcesUri(resourceUri));
assertThat(exception.getMessage(),
containsString("jar uri '" + resourceUri + "' must contain '!/'"));
}

@Test
void scanForResourcesJarUriMissingEntry() {
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/jar-resource.jar").toURI();
URI resourceUri = URI.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "");
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> resourceScanner.scanForResourcesUri(resourceUri));
assertThat(exception.getMessage(),
containsString("jar uri '" + resourceUri + "' must contain '!/'"));
}

@Test
void scanForResourcesNestedJarUri() {
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/spring-resource.jar").toURI();
Expand Down

0 comments on commit 6c4a910

Please sign in to comment.