Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Support nested jar file systems #2830

Merged
merged 1 commit into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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