Skip to content

Commit e2b5041

Browse files
committed
[Core] Classpath scanning inside nested Jars is not supported.
Spring Boot provides an executable jar format that packages the application and all dependencies into a self contained jar[1]. This format doesn't follow regular jar layouts making it harder to scan. Cucumber currently doesn't support this use case and will throw an error if nested jars are detected. As a work around users can unpack their application or glue code before executing[2]. - [1] https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-executable-jar-format.html - [2] https://docs.spring.io/autorepo/docs/spring-boot/1.5.10.BUILD-SNAPSHOT/maven-plugin/repackage-mojo.html#requiresUnpack
1 parent b668bc3 commit e2b5041

File tree

7 files changed

+57
-6
lines changed

7 files changed

+57
-6
lines changed

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,13 @@ static String determinePackageName(Path baseDir, String basePackageName, Path cl
8484
.collect(joining(PACKAGE_SEPARATOR_STRING));
8585
}
8686

87+
private static String determineSubpackageResourceName(Path baseDir, Path resource) {
88+
Path relativePath = baseDir.relativize(resource.getParent());
89+
return relativePath.toString();
90+
}
91+
8792
static String determineFullyQualifiedResourceName(Path baseDir, String packagePath, Path resource) {
88-
String subPackageName = determineSubpackageName(baseDir, resource);
93+
String subPackageName = determineSubpackageResourceName(baseDir, resource);
8994
String resourceName = resource.getFileName().toString();
9095
return of(packagePath, subPackageName, resourceName)
9196
.filter(value -> !value.isEmpty()) // default package .

core/src/main/java/io/cucumber/core/resource/PathScanner.java

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.cucumber.core.resource;
22

3+
import io.cucumber.core.exception.CucumberException;
34
import io.cucumber.core.logging.Logger;
45
import io.cucumber.core.logging.LoggerFactory;
56

@@ -118,7 +119,7 @@ static class JarUriFileSystemService {
118119
private static final String FILE_URI_SCHEME = "file";
119120
private static final String JAR_URI_SCHEME = "jar";
120121
private static final String JAR_URI_SCHEME_PREFIX = JAR_URI_SCHEME + ":";
121-
private static final String JAR_FILE_EXTENSION = ".jar";
122+
private static final String JAR_FILE_SUFFIX = ".jar";
122123
private static final String JAR_URI_SEPARATOR = "!";
123124

124125
private static CloseablePath createForJarFileSystem(URI jarUri, Function<FileSystem, Path> pathProvider)
@@ -135,6 +136,9 @@ static boolean supports(URI uri) {
135136
static CloseablePath create(URI uri) throws URISyntaxException, IOException {
136137
if (hasJarUriScheme(uri)) {
137138
String[] parts = uri.toString().split(JAR_URI_SEPARATOR);
139+
if (parts.length > 2) {
140+
throw nestedJarEntriesAreUnsupported(uri);
141+
}
138142
String jarUri = parts[0];
139143
String jarEntry = parts[1];
140144
return createForJarFileSystem(new URI(jarUri), fileSystem -> fileSystem.getPath(jarEntry));
@@ -147,8 +151,20 @@ static CloseablePath create(URI uri) throws URISyntaxException, IOException {
147151
return null;
148152
}
149153

154+
private static CucumberException nestedJarEntriesAreUnsupported(URI uri) {
155+
return new CucumberException("" +
156+
"The resource " + uri + " is located in a nested jar.\n" +
157+
"\n" +
158+
"This typically happens when trying to run Cucumber inside a Spring Boot Executable Jar.\n" +
159+
"Cucumber currently doesn't support classpath scanning in nested jars.\n" +
160+
"Feel free to send a pull request to make this possible!\n" +
161+
"\n" +
162+
"You can avoid this error by unpacking your application or glue code before executing."
163+
);
164+
}
165+
150166
private static boolean hasFileUriSchemeWithJarExtension(URI uri) {
151-
return FILE_URI_SCHEME.equals(uri.getScheme()) && uri.getPath().endsWith(JAR_FILE_EXTENSION);
167+
return FILE_URI_SCHEME.equals(uri.getScheme()) && uri.getPath().endsWith(JAR_FILE_SUFFIX);
152168
}
153169

154170
private static boolean hasJarUriScheme(URI uri) {

core/src/test/java/io/cucumber/core/resource/ClasspathScannerTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ClasspathScannerTest {
2121
@Test
2222
void scanForSubClassesInPackage() {
2323
List<Class<? extends ExampleInterface>> classes = scanner.
24-
scanForSubClassesInPackage("io.cucumber.core.resource", ExampleInterface.class);
24+
scanForSubClassesInPackage("io.cucumber", ExampleInterface.class);
2525

2626
assertThat(classes, contains(ExampleClass.class));
2727
}

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

+30
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package io.cucumber.core.resource;
22

3+
import io.cucumber.core.exception.CucumberException;
34
import org.junit.jupiter.api.Test;
45

56
import java.io.File;
67
import java.net.URI;
78
import java.util.List;
89

910
import static java.util.Optional.of;
11+
import static org.hamcrest.CoreMatchers.containsString;
1012
import static org.hamcrest.MatcherAssert.assertThat;
1113
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
1214
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
15+
import static org.junit.jupiter.api.Assertions.assertThrows;
1316

1417
class ResourceScannerTest {
1518

@@ -118,6 +121,33 @@ void scanForResourcesJarUri() {
118121
assertThat(resources, contains(resourceUri));
119122
}
120123

124+
125+
@Test
126+
void scanForResourcesNestedJarUri() {
127+
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/spring-resource.jar").toURI();
128+
URI resourceUri = URI.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "!/BOOT-INF/lib/jar-resource.jar!/com/example/package-jar-resource.txt");
129+
130+
CucumberException exception = assertThrows(
131+
CucumberException.class,
132+
() -> resourceScanner.scanForResourcesUri(resourceUri)
133+
);
134+
assertThat(exception.getMessage(), containsString("Cucumber currently doesn't support classpath scanning in nested jars."));
135+
136+
}
137+
138+
@Test
139+
void scanForResourcesNestedJarUriUnPackaged() {
140+
URI jarFileUri = new File("src/test/resources/io/cucumber/core/resource/test/spring-resource.jar").toURI();
141+
URI resourceUri = URI.create("jar:file://" + jarFileUri.getSchemeSpecificPart() + "!/BOOT-INF/classes!/com/example/");
142+
143+
CucumberException exception = assertThrows(
144+
CucumberException.class,
145+
() -> resourceScanner.scanForResourcesUri(resourceUri)
146+
);
147+
assertThat(exception.getMessage(), containsString("Cucumber currently doesn't support classpath scanning in nested jars."));
148+
}
149+
150+
121151
@Test
122152
void scanForResourcesDirectoryUri() {
123153
File file = new File("src/test/resources/io/cucumber/core/resource");
Binary file not shown.

junit/src/main/java/io/cucumber/junit/Cucumber.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public Cucumber(Class clazz) throws InitializationError {
132132
.build(junitEnvironmentOptions);
133133

134134
// Parse the features early. Don't proceed when there are lexer errors
135-
Supplier<ClassLoader> classLoader = clazz::getClassLoader;
135+
Supplier<ClassLoader> classLoader = Thread.currentThread()::getContextClassLoader;
136136
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions);
137137
this.features = featureSupplier.get();
138138

testng/src/main/java/io/cucumber/testng/TestNGCucumberRunner.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public TestNGCucumberRunner(Class clazz) {
8282
.addDefaultSummaryPrinterIfAbsent()
8383
.build(environmentOptions);
8484

85-
Supplier<ClassLoader> classLoader = clazz::getClassLoader;
85+
Supplier<ClassLoader> classLoader = Thread.currentThread()::getContextClassLoader;
8686
featureSupplier = new FeaturePathFeatureSupplier(classLoader, runtimeOptions);
8787

8888
this.bus = new TimeServiceEventBus(Clock.systemUTC());

0 commit comments

Comments
 (0)