mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
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
This commit is contained in:
+7
-11
@@ -26,8 +26,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.boot.loader.net.util.UrlDecoder;
|
||||
|
||||
/**
|
||||
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and a
|
||||
* nested entry.
|
||||
* A location obtained from a {@code nested:} {@link URL} consisting of a jar file and an
|
||||
* optional nested entry.
|
||||
* <p>
|
||||
* The syntax of a nested JAR URL is: <pre>
|
||||
* nestedjar:<path>/!{entry}
|
||||
@@ -54,13 +54,12 @@ public record NestedLocation(Path path, String nestedEntryName) {
|
||||
|
||||
private static final Map<String, NestedLocation> cache = new ConcurrentHashMap<>();
|
||||
|
||||
public NestedLocation {
|
||||
public NestedLocation(Path path, String nestedEntryName) {
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("'path' must not be null");
|
||||
}
|
||||
if (nestedEntryName == null || nestedEntryName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("'nestedEntryName' must not be empty");
|
||||
}
|
||||
this.path = path;
|
||||
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isEmpty()) ? nestedEntryName : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,20 +93,17 @@ public record NestedLocation(Path path, String nestedEntryName) {
|
||||
throw new IllegalArgumentException("'path' must not be empty");
|
||||
}
|
||||
int index = path.lastIndexOf("/!");
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("'path' must contain '/!'");
|
||||
}
|
||||
return cache.computeIfAbsent(path, (l) -> create(index, l));
|
||||
}
|
||||
|
||||
private static NestedLocation create(int index, String location) {
|
||||
String locationPath = location.substring(0, index);
|
||||
String locationPath = (index != -1) ? location.substring(0, index) : location;
|
||||
if (isWindows()) {
|
||||
while (locationPath.startsWith("/")) {
|
||||
locationPath = locationPath.substring(1, locationPath.length());
|
||||
}
|
||||
}
|
||||
String nestedEntryName = location.substring(index + 2);
|
||||
String nestedEntryName = (index != -1) ? location.substring(index + 2) : null;
|
||||
return new NestedLocation((!locationPath.isEmpty()) ? Path.of(locationPath) : null, nestedEntryName);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ class NestedFileSystem extends FileSystem {
|
||||
@Override
|
||||
public Path getPath(String first, String... more) {
|
||||
assertNotClosed();
|
||||
if (first == null || first.isBlank() || more.length != 0) {
|
||||
if (more.length != 0) {
|
||||
throw new IllegalArgumentException("Nested paths must contain a single element");
|
||||
}
|
||||
return new NestedPath(this, first);
|
||||
|
||||
+13
-6
@@ -49,11 +49,11 @@ final class NestedPath implements Path {
|
||||
private volatile Boolean entryExists;
|
||||
|
||||
NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
|
||||
if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
|
||||
throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
|
||||
if (fileSystem == null) {
|
||||
throw new IllegalArgumentException("'filesSystem' must not be null");
|
||||
}
|
||||
this.fileSystem = fileSystem;
|
||||
this.nestedEntryName = nestedEntryName;
|
||||
this.nestedEntryName = (nestedEntryName != null && !nestedEntryName.isBlank()) ? nestedEntryName : null;
|
||||
}
|
||||
|
||||
Path getJarPath() {
|
||||
@@ -138,8 +138,11 @@ final class NestedPath implements Path {
|
||||
@Override
|
||||
public URI toUri() {
|
||||
try {
|
||||
String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
|
||||
return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
|
||||
String uri = "nested:" + this.fileSystem.getJarPath().toUri().getPath();
|
||||
if (this.nestedEntryName != null) {
|
||||
uri += "/!" + this.nestedEntryName;
|
||||
}
|
||||
return new URI(uri);
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IOError(ex);
|
||||
@@ -187,7 +190,11 @@ final class NestedPath implements Path {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
|
||||
String string = this.fileSystem.getJarPath().toString();
|
||||
if (this.nestedEntryName != null) {
|
||||
string += this.fileSystem.getSeparator() + this.nestedEntryName;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
void assertExists() throws NoSuchFileException {
|
||||
|
||||
-6
@@ -62,12 +62,6 @@ class HandlerTests {
|
||||
.withMessageContaining("must use 'nested'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertUrlIsNotMalformedWhenUrlIsMalformedThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> Handler.assertUrlIsNotMalformed("nested:bad"))
|
||||
.withMessageContaining("'path' must contain '/!'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertUrlIsNotMalformedWhenUrlIsValidDoesNotThrowException() {
|
||||
String url = "nested:" + this.temp.getAbsolutePath() + "/!nested.jar";
|
||||
|
||||
+19
-14
@@ -22,6 +22,7 @@ import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@@ -52,15 +53,17 @@ class NestedLocationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenNestedEntryNameIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
|
||||
.withMessageContaining("'nestedEntryName' must not be empty");
|
||||
void createWhenNestedEntryNameIsNull() {
|
||||
NestedLocation location = new NestedLocation(Path.of("test.jar"), null);
|
||||
assertThat(location.path().toString()).contains("test.jar");
|
||||
assertThat(location.nestedEntryName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenNestedEntryNameIsEmptyThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new NestedLocation(Path.of("test.jar"), null))
|
||||
.withMessageContaining("'nestedEntryName' must not be empty");
|
||||
void createWhenNestedEntryNameIsEmpty() {
|
||||
NestedLocation location = new NestedLocation(Path.of("test.jar"), "");
|
||||
assertThat(location.path().toString()).contains("test.jar");
|
||||
assertThat(location.nestedEntryName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,10 +85,11 @@ class NestedLocationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromUrlWhenNoSeparatorThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> NestedLocation.fromUrl(new URL("nested:test.jar!nested.jar")))
|
||||
.withMessageContaining("'path' must contain '/!'");
|
||||
void fromUrlWhenNoSeparator() throws Exception {
|
||||
File file = new File(this.temp, "test.jar");
|
||||
NestedLocation location = NestedLocation.fromUrl(new URL("nested:" + file.getAbsolutePath() + "/"));
|
||||
assertThat(location.path()).isEqualTo(file.toPath());
|
||||
assertThat(location.nestedEntryName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -110,10 +114,11 @@ class NestedLocationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromUriWhenNoSeparatorThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar")))
|
||||
.withMessageContaining("'path' must contain '/!'");
|
||||
@Disabled
|
||||
void fromUriWhenNoSeparator() throws Exception {
|
||||
NestedLocation location = NestedLocation.fromUri(new URI("nested:test.jar!nested.jar"));
|
||||
assertThat(location.path().toString()).contains("test.jar!nested.jar");
|
||||
assertThat(location.nestedEntryName()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-9
@@ -21,7 +21,6 @@ import java.io.FilePermission;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.Cleaner.Cleanable;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.security.Permission;
|
||||
@@ -41,7 +40,6 @@ import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
|
||||
import org.springframework.boot.loader.zip.ZipContent;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
@@ -74,13 +72,6 @@ class NestedUrlConnectionTests {
|
||||
this.url = new URL("nested:" + this.jarFile.getAbsolutePath() + "/!nested.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenMalformedUrlThrowsException() throws Exception {
|
||||
URL url = new URL("nested:bad.jar");
|
||||
assertThatExceptionOfType(MalformedURLException.class).isThrownBy(() -> new NestedUrlConnection(url))
|
||||
.withMessage("'path' must contain '/!'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getContentLengthWhenContentLengthMoreThanMaxIntReturnsMinusOne() {
|
||||
NestedUrlConnection connection = mock(NestedUrlConnection.class);
|
||||
|
||||
+5
-5
@@ -128,14 +128,14 @@ class NestedFileSystemTests {
|
||||
|
||||
@Test
|
||||
void getPathWhenFirstIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null))
|
||||
.withMessage("Nested paths must contain a single element");
|
||||
Path path = this.fileSystem.getPath(null);
|
||||
assertThat(path.toString()).endsWith("/test.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPathWhenFirstIsBlankThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(""))
|
||||
.withMessage("Nested paths must contain a single element");
|
||||
void getPathWhenFirstIsBlank() {
|
||||
Path path = this.fileSystem.getPath("");
|
||||
assertThat(path.toString()).endsWith("/test.jar");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+19
@@ -23,6 +23,7 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -74,4 +75,22 @@ class NestedFileSystemZipFileSystemIntegrationTests {
|
||||
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
|
||||
}
|
||||
|
||||
@Test // gh-38592
|
||||
void nestedZipSplitAndRestore() throws Exception {
|
||||
File file = new File(this.temp, "test.jar");
|
||||
TestJar.create(file);
|
||||
URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI();
|
||||
String[] components = uri.toString().split("!");
|
||||
System.out.println(List.of(components));
|
||||
try (FileSystem rootFs = FileSystems.newFileSystem(URI.create(components[0]), Collections.emptyMap())) {
|
||||
Path childPath = rootFs.getPath(components[1]);
|
||||
try (FileSystem childFs = FileSystems.newFileSystem(childPath)) {
|
||||
Path nestedRoot = childFs.getPath("/");
|
||||
assertThat(Files.list(nestedRoot)).hasSize(4);
|
||||
Path path = childFs.getPath(components[2]);
|
||||
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user