Skip to content

Commit 6fd691a

Browse files
committed
Allow FileSystems to be create by splitting URLs
Relax the constraint that a `NestedLocation` must have a nested entry name specified so that URLs can be split and rebuilt. Prior to this commit, given a URL of the following form: jar:nested:/myjar.jar!/nested.jar!/my/file It was possible to create a FileSystem from "jar:nested:/myjar.jar!/nested.jar" and from that create a path to "my/file". However, it wasn't possible to create a FileSystem from "jar:nested:/myjar.jar", then create another file system from the path "nested.jar" and then finally create a path to "/nested.jar". This was because `nested:/myjar.jar` was not considered a value URL because it didn't include a nested entry name. Projects such as `JobRunr` were relying on the ability to compose file systems, so it makes sense to remove our somewhat artificial restriction. Fixes gh-38592
1 parent 9a0f954 commit 6fd691a

File tree

8 files changed

+64
-52
lines changed

8 files changed

+64
-52
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
import org.springframework.boot.loader.net.util.UrlDecoder;
2727

2828
/**
29-
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a
30-
* nested entry.
29+
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and an
30+
* optional nested entry.
3131
* <p>
3232
* The syntax of a nested JAR URL is: <pre>
3333
* nestedjar:&lt;path&gt;/!{entry}
@@ -54,13 +54,12 @@ public record NestedLocation(Path path, String nestedEntryName) {
5454

5555
private static final Map<String, NestedLocation> cache = new ConcurrentHashMap<>();
5656

57-
public NestedLocation {
57+
public NestedLocation(Path path, String nestedEntryName) {
5858
if (path == null) {
5959
throw new IllegalArgumentException("'path' must not be null");
6060
}
61-
if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) {
62-
throw new IllegalArgumentException("'nestedEntryName' must not be empty");
63-
}
61+
this.path = path;
62+
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isEmpty()) ? nestedEntryName : null;
6463
}
6564

6665
/**
@@ -94,20 +93,17 @@ static NestedLocation parse(String path) {
9493
throw new IllegalArgumentException("'path' must not be empty");
9594
}
9695
int index = path.lastIndexOf("/!");
97-
if (index == -1) {
98-
throw new IllegalArgumentException("'path' must contain '/!'");
99-
}
10096
return cache.computeIfAbsent(path, (l) -> create(index, l));
10197
}
10298

10399
private static NestedLocation create(int index, String location) {
104-
String locationPath = location.substring(0, index);
100+
String locationPath = (index != -1) ? location.substring(0, index) : location;
105101
if (isWindows()) {
106102
while (locationPath.startsWith("/")) {
107103
locationPath = locationPath.substring(1, locationPath.length());
108104
}
109105
}
110-
String nestedEntryName = location.substring(index + 2);
106+
String nestedEntryName = (index != -1) ? location.substring(index + 2) : null;
111107
return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName);
112108
}
113109

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public Set<String> supportedFileAttributeViews() {
177177
@Override
178178
public Path getPath(String first, String... more) {
179179
assertNotClosed();
180-
if (first == null || first.isBlank() || more.length != 0) {
180+
if (more.length != 0) {
181181
throw new IllegalArgumentException("Nested paths must contain a single element");
182182
}
183183
return new NestedPath(this, first);

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ final class NestedPath implements Path {
4949
private volatile Boolean entryExists;
5050

5151
NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
52-
if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
53-
throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
52+
if (fileSystem == null) {
53+
throw new IllegalArgumentException("'filesSystem' must not be null");
5454
}
5555
this.fileSystem = fileSystem;
56-
this.nestedEntryName = nestedEntryName;
56+
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isBlank()) ? nestedEntryName : null;
5757
}
5858

5959
Path getJarPath() {
@@ -138,8 +138,11 @@ public Path relativize(Path other) {
138138
@Override
139139
public URI toUri() {
140140
try {
141-
String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
142-
return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
141+
String uri = "nested:" + this.fileSystem.getJarPath().toUri().getPath();
142+
if (this.nestedEntryName != null) {
143+
uri += "/!" + this.nestedEntryName;
144+
}
145+
return new URI(uri);
143146
}
144147
catch (URISyntaxException ex) {
145148
throw new IOError(ex);
@@ -187,7 +190,11 @@ public int hashCode() {
187190

188191
@Override
189192
public String toString() {
190-
return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
193+
String string = this.fileSystem.getJarPath().toString();
194+
if (this.nestedEntryName != null) {
195+
string += this.fileSystem.getSeparator() + this.nestedEntryName;
196+
}
197+
return string;
191198
}
192199

193200
void assertExists() throws NoSuchFileException {

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/HandlerTests.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,6 @@ void assertUrlIsNotMalformedWhenUrlIsNotNestedThrowsException() {
6262
.withMessageContaining("must use 'nested'");
6363
}
6464

65-
@Test
66-
void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() {
67-
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad"))
68-
.withMessageContaining("'path' must contain '/!'");
69-
}
70-
7165
@Test
7266
void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() {
7367
String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar";

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.nio.file.Path;
2323

2424
import org.junit.jupiter.api.BeforeAll;
25+
import org.junit.jupiter.api.Disabled;
2526
import org.junit.jupiter.api.Test;
2627
import org.junit.jupiter.api.io.TempDir;
2728

@@ -52,15 +53,17 @@ void createWhenPathIsNullThrowsException() {
5253
}
5354

5455
@Test
55-
void createWhenNestedEntryNameIsNullThrowsException() {
56-
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
57-
.withMessageContaining("'nestedEntryName' must not be empty");
56+
void createWhenNestedEntryNameIsNull() {
57+
NestedLocation location = new NestedLocation(Path.of("test.jar"), null);
58+
assertThat(location.path().toString()).contains("test.jar");
59+
assertThat(location.nestedEntryName()).isNull();
5860
}
5961

6062
@Test
61-
void createWhenNestedEntryNameIsEmptyThrowsException() {
62-
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
63-
.withMessageContaining("'nestedEntryName' must not be empty");
63+
void createWhenNestedEntryNameIsEmpty() {
64+
NestedLocation location = new NestedLocation(Path.of("test.jar"), "");
65+
assertThat(location.path().toString()).contains("test.jar");
66+
assertThat(location.nestedEntryName()).isNull();
6467
}
6568

6669
@Test
@@ -82,10 +85,11 @@ void fromUrlWhenNoPathThrowsException() {
8285
}
8386

8487
@Test
85-
void fromUrlWhenNoSeparatorThrowsException() {
86-
assertThatIllegalArgumentException()
87-
.isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar")))
88-
.withMessageContaining("'path' must contain '/!'");
88+
void fromUrlWhenNoSeparator() throws Exception {
89+
File file = new File(this.temp, "test.jar");
90+
NestedLocation location = NestedLocation.fromUrl(new URL("nested:" + file.getAbsolutePath() + "/"));
91+
assertThat(location.path()).isEqualTo(file.toPath());
92+
assertThat(location.nestedEntryName()).isNull();
8993
}
9094

9195
@Test
@@ -110,10 +114,11 @@ void fromUriWhenNotNestedProtocolThrowsException() {
110114
}
111115

112116
@Test
113-
void fromUriWhenNoSeparatorThrowsException() {
114-
assertThatIllegalArgumentException()
115-
.isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar")))
116-
.withMessageContaining("'path' must contain '/!'");
117+
@Disabled
118+
void fromUriWhenNoSeparator() throws Exception {
119+
NestedLocation location = NestedLocation.fromUri(new URI("nested:test.jar!nested.jar"));
120+
assertThat(location.path().toString()).contains("test.jar!nested.jar");
121+
assertThat(location.nestedEntryName()).isNull();
117122
}
118123

119124
@Test

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedUrlConnectionTests.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.lang.ref.Cleaner.Cleanable;
24-
import java.net.MalformedURLException;
2524
import java.net.URL;
2625
import java.net.URLConnection;
2726
import java.security.Permission;
@@ -41,7 +40,6 @@
4140
import org.springframework.boot.loader.zip.ZipContent;
4241

4342
import static org.assertj.core.api.Assertions.assertThat;
44-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4543
import static org.mockito.ArgumentMatchers.any;
4644
import static org.mockito.BDDMockito.given;
4745
import static org.mockito.BDDMockito.then;
@@ -74,13 +72,6 @@ void setup() throws Exception {
7472
this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar");
7573
}
7674

77-
@Test
78-
void createWhenMalformedUrlThrowsException() throws Exception {
79-
URL url = new URL("nested:bad.jar");
80-
assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url))
81-
.withMessage("'path' must contain '/!'");
82-
}
83-
8475
@Test
8576
void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() {
8677
NestedUrlConnection connection = mock(NestedUrlConnection.class);

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,14 @@ void getPathWhenClosedThrowsException() throws Exception {
128128

129129
@Test
130130
void getPathWhenFirstIsNullThrowsException() {
131-
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null))
132-
.withMessage("Nested paths must contain a single element");
131+
Path path = this.fileSystem.getPath(null);
132+
assertThat(path.toString()).endsWith("/test.jar");
133133
}
134134

135135
@Test
136-
void getPathWhenFirstIsBlankThrowsException() {
137-
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(""))
138-
.withMessage("Nested paths must contain a single element");
136+
void getPathWhenFirstIsBlank() {
137+
Path path = this.fileSystem.getPath("");
138+
assertThat(path.toString()).endsWith("/test.jar");
139139
}
140140

141141
@Test

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.nio.file.Files;
2424
import java.nio.file.Path;
2525
import java.util.Collections;
26+
import java.util.List;
2627

2728
import org.junit.jupiter.api.Test;
2829
import org.junit.jupiter.api.io.TempDir;
@@ -74,4 +75,22 @@ void nestedZipWithoutNewFileSystem() throws Exception {
7475
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
7576
}
7677

78+
@Test // gh-38592
79+
void nestedZipSplitAndRestore() throws Exception {
80+
File file = new File(this.temp, "test.jar");
81+
TestJar.create(file);
82+
URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI();
83+
String[] components = uri.toString().split("!");
84+
System.out.println(List.of(components));
85+
try (FileSystem rootFs = FileSystems.newFileSystem(URI.create(components[0]), Collections.emptyMap())) {
86+
Path childPath = rootFs.getPath(components[1]);
87+
try (FileSystem childFs = FileSystems.newFileSystem(childPath)) {
88+
Path nestedRoot = childFs.getPath("/");
89+
assertThat(Files.list(nestedRoot)).hasSize(4);
90+
Path path = childFs.getPath(components[2]);
91+
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
92+
}
93+
}
94+
}
95+
7796
}

0 commit comments

Comments
 (0)