Support java.nio.file Paths and FileSystems with nested jars

Add a `NestedFileSystemProvider` implementation so that the JDK's
`ZipFileSystem` can load content from nested jars and nested
directory entries.

Creating a `ZipFileSystem` may be a relatively expensive operation as
zip structures need to be parsed and in the case of directory entries
a virtual datablock nees to be generated on the fly. As such, we
install the `ZipFileSystem` as late as possible since in a typical
application it may never be needed.

This commit also tweaks Gradle and Maven plugins to ensure that the
service loader file is written to repackaged jars.

Closes gh-7161
This commit is contained in:
Phillip Webb
2023-10-18 14:57:05 -07:00
parent 4b495ca2a9
commit 3c62defb9d
21 changed files with 2115 additions and 5 deletions

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.loader.net.protocol.nested;
import java.io.File;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
@@ -96,4 +97,32 @@ class NestedLocationTests {
assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar");
}
@Test
void fromUriWhenUrlIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUri(null))
.withMessageContaining("'uri' must not be null");
}
@Test
void fromUriWhenNotNestedProtocolThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> NestedLocation.fromUri(new URI("file://test.jar")))
.withMessageContaining("must use 'nested' scheme");
}
@Test
void fromUriWhenNoSeparatorThrowsExceptiuon() {
assertThatIllegalArgumentException()
.isThrownBy(() -> NestedLocation.fromUri(new URI("nested:test.jar!nested.jar")))
.withMessageContaining("'path' must contain '/!'");
}
@Test
void fromUriReturnsNestedLocation() throws Exception {
File file = new File(this.temp, "test.jar");
NestedLocation location = NestedLocation
.fromUri(new URI("nested:" + file.getAbsolutePath() + "/!lib/nested.jar"));
assertThat(location.path()).isEqualTo(file.toPath());
assertThat(location.nestedEntryName()).isEqualTo("lib/nested.jar");
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.testsupport.TestJar;
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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedByteChannel}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class NestedByteChannelTests {
@TempDir
File temp;
private File file;
private NestedByteChannel channel;
@BeforeEach
void setup() throws Exception {
this.file = new File(this.temp, "test.jar");
TestJar.create(this.file);
this.channel = new NestedByteChannel(this.file.toPath(), "nested.jar");
}
@AfterEach
void cleanuo() throws Exception {
this.channel.close();
}
@Test
void isOpenWhenOpenReturnsTrue() {
assertThat(this.channel.isOpen()).isTrue();
}
@Test
void isOpenWhenClosedReturnsFalse() throws Exception {
this.channel.close();
assertThat(this.channel.isOpen()).isFalse();
}
@Test
void closeCleansResources() throws Exception {
Cleaner cleaner = mock(Cleaner.class);
Cleanable cleanable = mock(Cleanable.class);
given(cleaner.register(any(), any())).willReturn(cleanable);
NestedByteChannel channel = new NestedByteChannel(this.file.toPath(), "nested.jar", cleaner);
channel.close();
then(cleanable).should().clean();
ArgumentCaptor<Runnable> actionCaptor = ArgumentCaptor.forClass(Runnable.class);
then(cleaner).should().register(any(), actionCaptor.capture());
actionCaptor.getValue().run();
}
@Test
void closeWhenAlreadyClosedDoesNothing() throws IOException {
Cleaner cleaner = mock(Cleaner.class);
Cleanable cleanable = mock(Cleanable.class);
given(cleaner.register(any(), any())).willReturn(cleanable);
NestedByteChannel channel = new NestedByteChannel(this.file.toPath(), "nested.jar", cleaner);
channel.close();
then(cleanable).should().clean();
ArgumentCaptor<Runnable> actionCaptor = ArgumentCaptor.forClass(Runnable.class);
then(cleaner).should().register(any(), actionCaptor.capture());
actionCaptor.getValue().run();
channel.close();
then(cleaner).shouldHaveNoMoreInteractions();
}
@Test
void readReadsBytesAndIncrementsPosition() throws IOException {
ByteBuffer dst = ByteBuffer.allocate(10);
assertThat(this.channel.position()).isZero();
this.channel.read(dst);
assertThat(this.channel.position()).isEqualTo(10L);
assertThat(dst.array()).isNotEqualTo(ByteBuffer.allocate(10).array());
}
@Test
void writeThrowsException() {
assertThatExceptionOfType(NonWritableChannelException.class)
.isThrownBy(() -> this.channel.write(ByteBuffer.allocate(10)));
}
@Test
void positionWhenClosedThrowsException() throws Exception {
this.channel.close();
assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.position());
}
@Test
void positionWhenOpenReturnsPosition() throws Exception {
assertThat(this.channel.position()).isEqualTo(0L);
}
@Test
void positionWithLongWhenClosedThrowsException() throws Exception {
this.channel.close();
assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.position(0L));
}
@Test
void positionWithLongWhenLessThanZeroThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.channel.position(-1));
}
@Test
void positionWithLongWhenEqualToSizeThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.channel.position(this.channel.size()));
}
@Test
void positionWithLongWhenOpenUpdatesPosition() throws Exception {
ByteBuffer dst1 = ByteBuffer.allocate(10);
ByteBuffer dst2 = ByteBuffer.allocate(10);
dst2.position(1);
this.channel.read(dst1);
this.channel.position(1);
this.channel.read(dst2);
dst2.array()[0] = dst1.array()[0];
assertThat(dst1.array()).isEqualTo(dst2.array());
}
@Test
void sizeWhenClosedThrowsException() throws Exception {
this.channel.close();
assertThatExceptionOfType(ClosedChannelException.class).isThrownBy(() -> this.channel.size());
}
@Test
void sizeWhenOpenReturnsSize() throws IOException {
try (ZipContent content = ZipContent.open(this.file.toPath())) {
assertThat(this.channel.size()).isEqualTo(content.getEntry("nested.jar").getUncompressedSize());
}
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.nio.file.FileStore;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileStoreAttributeView;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedFileStore}.
*
* @author Phillip Webb
*/
class NestedFileStoreTests {
@TempDir
File temp;
private NestedFileSystemProvider provider;
private Path jarPath;
private NestedFileSystem fileSystem;
private TestNestedFileStore fileStore;
@BeforeEach
void setup() {
this.provider = new NestedFileSystemProvider();
this.jarPath = new File(this.temp, "test.jar").toPath();
this.fileSystem = new NestedFileSystem(this.provider, this.jarPath);
this.fileStore = new TestNestedFileStore(this.fileSystem);
}
@Test
void nameReturnsName() {
assertThat(this.fileStore.name()).isEqualTo(this.jarPath.toAbsolutePath().toString());
}
@Test
void typeReturnsNestedFs() {
assertThat(this.fileStore.type()).isEqualTo("nestedfs");
}
@Test
void isReadOnlyReturnsTrue() {
assertThat(this.fileStore.isReadOnly()).isTrue();
}
@Test
void getTotalSpaceReturnsZero() throws Exception {
assertThat(this.fileStore.getTotalSpace()).isZero();
}
@Test
void getUsableSpaceReturnsZero() throws Exception {
assertThat(this.fileStore.getUsableSpace()).isZero();
}
@Test
void getUnallocatedSpaceReturnsZero() throws Exception {
assertThat(this.fileStore.getUnallocatedSpace()).isZero();
}
@Test
void supportsFileAttributeViewWithClassDelegatesToJarPathFileStore() {
FileStore jarFileStore = mock(FileStore.class);
given(jarFileStore.supportsFileAttributeView(BasicFileAttributeView.class)).willReturn(true);
this.fileStore.setJarPathFileStore(jarFileStore);
assertThat(this.fileStore.supportsFileAttributeView(BasicFileAttributeView.class)).isTrue();
then(jarFileStore).should().supportsFileAttributeView(BasicFileAttributeView.class);
}
@Test
void supportsFileAttributeViewWithStringDelegatesToJarPathFileStore() {
FileStore jarFileStore = mock(FileStore.class);
given(jarFileStore.supportsFileAttributeView("basic")).willReturn(true);
this.fileStore.setJarPathFileStore(jarFileStore);
assertThat(this.fileStore.supportsFileAttributeView("basic")).isTrue();
then(jarFileStore).should().supportsFileAttributeView("basic");
}
@Test
void getFileStoreAttributeViewDelegatesToJarPathFileStore() {
FileStore jarFileStore = mock(FileStore.class);
TestFileStoreAttributeView attributeView = mock(TestFileStoreAttributeView.class);
given(jarFileStore.getFileStoreAttributeView(TestFileStoreAttributeView.class)).willReturn(attributeView);
this.fileStore.setJarPathFileStore(jarFileStore);
assertThat(this.fileStore.getFileStoreAttributeView(TestFileStoreAttributeView.class)).isEqualTo(attributeView);
then(jarFileStore).should().getFileStoreAttributeView(TestFileStoreAttributeView.class);
}
@Test
void getAttributeDelegatesToJarPathFileStore() throws Exception {
FileStore jarFileStore = mock(FileStore.class);
given(jarFileStore.getAttribute("test")).willReturn("spring");
this.fileStore.setJarPathFileStore(jarFileStore);
assertThat(this.fileStore.getAttribute("test")).isEqualTo("spring");
then(jarFileStore).should().getAttribute("test");
}
static class TestNestedFileStore extends NestedFileStore {
TestNestedFileStore(NestedFileSystem fileSystem) {
super(fileSystem);
}
private FileStore jarPathFileStore;
void setJarPathFileStore(FileStore jarPathFileStore) {
this.jarPathFileStore = jarPathFileStore;
}
@Override
protected FileStore getJarPathFileStore() {
return (this.jarPathFileStore != null) ? this.jarPathFileStore : super.getJarPathFileStore();
}
}
abstract static class TestFileStoreAttributeView implements FileStoreAttributeView {
}
}

View File

@@ -0,0 +1,273 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.ReadOnlyFileSystemException;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedFileSystemProvider}.
*
* @author Phillip Webb
*/
class NestedFileSystemProviderTests {
@TempDir
File temp;
private File file;
private TestNestedFileSystemProvider provider = new TestNestedFileSystemProvider();
private String uriPrefix;
@BeforeEach
void setup() throws Exception {
this.file = new File(this.temp, "test.jar");
TestJar.create(this.file);
this.uriPrefix = "nested:" + this.file.toURI().getPath() + "/!";
}
@Test
void getSchemeReturnsScheme() {
assertThat(this.provider.getScheme()).isEqualTo("nested");
}
@Test
void newFilesSystemWhenBadUrlThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.provider.newFileSystem(new URI("bad:notreal"), Collections.emptyMap()))
.withMessageContaining("must use 'nested' scheme");
}
@Test
void newFileSystemWhenAlreadyExistsThrowsException() throws Exception {
this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null);
assertThatExceptionOfType(FileSystemAlreadyExistsException.class)
.isThrownBy(() -> this.provider.newFileSystem(new URI(this.uriPrefix + "other.jar"), null));
}
@Test
void newFileSystemReturnsFileSystem() throws Exception {
FileSystem fileSystem = this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null);
assertThat(fileSystem).isInstanceOf(NestedFileSystem.class);
}
@Test
void getFileSystemWhenBadUrlThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.provider.getFileSystem(new URI("bad:notreal")))
.withMessageContaining("must use 'nested' scheme");
}
@Test
void getFileSystemWhenNotCreatedThrowsException() {
assertThatExceptionOfType(FileSystemNotFoundException.class)
.isThrownBy(() -> this.provider.getFileSystem(new URI(this.uriPrefix + "nested.jar")));
}
@Test
void getFileSystemReturnsFileSystem() throws Exception {
FileSystem expected = this.provider.newFileSystem(new URI(this.uriPrefix + "nested.jar"), null);
assertThat(this.provider.getFileSystem(new URI(this.uriPrefix + "nested.jar"))).isSameAs(expected);
}
@Test
void getPathWhenFileSystemExistsReturnsPath() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
this.provider.newFileSystem(uri, null);
assertThat(this.provider.getPath(uri)).isInstanceOf(NestedPath.class);
}
@Test
void getPathWhenFileSystemDoesNtExistReturnsPath() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
assertThat(this.provider.getPath(uri)).isInstanceOf(NestedPath.class);
}
@Test
void newByteChannelReturnsByteChannel() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
SeekableByteChannel byteChannel = this.provider.newByteChannel(path, Set.of(StandardOpenOption.READ));
assertThat(byteChannel).isInstanceOf(NestedByteChannel.class);
}
@Test
void newDirectoryStreamThrowsException() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
assertThatExceptionOfType(NotDirectoryException.class)
.isThrownBy(() -> this.provider.newDirectoryStream(path, null));
}
@Test
void createDirectoryThrowsException() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
assertThatExceptionOfType(ReadOnlyFileSystemException.class)
.isThrownBy(() -> this.provider.createDirectory(path));
}
@Test
void deleteThrowsException() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.delete(path));
}
@Test
void copyThrowsException() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.copy(path, path));
}
@Test
void moveThrowsException() throws Exception {
URI uri = new URI(this.uriPrefix + "nested.jar");
Path path = this.provider.getPath(uri);
assertThatExceptionOfType(ReadOnlyFileSystemException.class).isThrownBy(() -> this.provider.move(path, path));
}
@Test
void isSameFileWhenSameReturnsTrue() throws Exception {
Path p1 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path p2 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
assertThat(this.provider.isSameFile(p1, p1)).isTrue();
assertThat(this.provider.isSameFile(p1, p2)).isTrue();
}
@Test
void isSameFileWhenDifferentReturnsFalse() throws Exception {
Path p1 = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path p2 = this.provider.getPath(new URI(this.uriPrefix + "other.jar"));
assertThat(this.provider.isSameFile(p1, p2)).isFalse();
}
@Test
void isHiddenReturnsFalse() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
assertThat(this.provider.isHidden(path)).isFalse();
}
@Test
void getFileStoreWhenFileDoesNotExistThrowsException() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "missing.jar"));
assertThatExceptionOfType(NoSuchFileException.class).isThrownBy(() -> this.provider.getFileStore(path));
}
@Test
void getFileStoreReturnsFileStore() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
assertThat(this.provider.getFileStore(path)).isInstanceOf(NestedFileStore.class);
}
@Test
void checkAccessDelegatesToJarPath() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path jarPath = mockJarPath();
this.provider.setMockJarPath(jarPath);
this.provider.checkAccess(path);
then(jarPath.getFileSystem().provider()).should().checkAccess(jarPath);
}
@Test
void getFileAttributeViewDelegatesToJarPath() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path jarPath = mockJarPath();
this.provider.setMockJarPath(jarPath);
this.provider.getFileAttributeView(path, BasicFileAttributeView.class);
then(jarPath.getFileSystem().provider()).should().getFileAttributeView(jarPath, BasicFileAttributeView.class);
}
@Test
void readAttributesWithTypeDelegatesToJarPath() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path jarPath = mockJarPath();
this.provider.setMockJarPath(jarPath);
this.provider.readAttributes(path, BasicFileAttributes.class);
then(jarPath.getFileSystem().provider()).should().readAttributes(jarPath, BasicFileAttributes.class);
}
@Test
void readAttributesWithNameDelegatesToJarPath() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
Path jarPath = mockJarPath();
this.provider.setMockJarPath(jarPath);
this.provider.readAttributes(path, "basic");
then(jarPath.getFileSystem().provider()).should().readAttributes(jarPath, "basic");
}
@Test
void setAttributeThrowsException() throws Exception {
Path path = this.provider.getPath(new URI(this.uriPrefix + "nested.jar"));
assertThatExceptionOfType(ReadOnlyFileSystemException.class)
.isThrownBy(() -> this.provider.setAttribute(path, "test", "test"));
}
private Path mockJarPath() {
Path path = mock(Path.class);
FileSystem fileSystem = mock(FileSystem.class);
given(path.getFileSystem()).willReturn(fileSystem);
FileSystemProvider provider = mock(FileSystemProvider.class);
given(fileSystem.provider()).willReturn(provider);
return path;
}
static class TestNestedFileSystemProvider extends NestedFileSystemProvider {
private Path mockJarPath;
@Override
protected Path getJarPath(Path path) {
return (this.mockJarPath != null) ? this.mockJarPath : super.getJarPath(path);
}
void setMockJarPath(Path mockJarPath) {
this.mockJarPath = mockJarPath;
}
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.nio.file.ClosedFileSystemException;
import java.nio.file.Path;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link NestedFileSystem}.
*
* @author Phillip Webb
*/
class NestedFileSystemTests {
@TempDir
File temp;
private NestedFileSystemProvider provider;
private Path jarPath;
private NestedFileSystem fileSystem;
@BeforeEach
void setup() {
this.provider = new NestedFileSystemProvider();
this.jarPath = new File(this.temp, "test.jar").toPath();
this.fileSystem = new NestedFileSystem(this.provider, this.jarPath);
}
@Test
void providerReturnsProvider() {
assertThat(this.fileSystem.provider()).isSameAs(this.provider);
}
@Test
void getJarPathReturnsJarPath() {
assertThat(this.fileSystem.getJarPath()).isSameAs(this.jarPath);
}
@Test
void closeClosesFileSystem() throws Exception {
this.fileSystem.close();
assertThat(this.fileSystem.isOpen()).isFalse();
}
@Test
void closeWhenAlreadyClosedDoesNothing() throws Exception {
this.fileSystem.close();
this.fileSystem.close();
assertThat(this.fileSystem.isOpen()).isFalse();
}
@Test
void isOpenWhenOpenReturnsTrue() {
assertThat(this.fileSystem.isOpen()).isTrue();
}
@Test
void isOpenWhenClosedReturnsFalse() throws Exception {
this.fileSystem.close();
assertThat(this.fileSystem.isOpen()).isFalse();
}
@Test
void isReadOnlyReturnsTrue() {
assertThat(this.fileSystem.isReadOnly()).isTrue();
}
@Test
void getSeparatorReturnsSeparator() {
assertThat(this.fileSystem.getSeparator()).isEqualTo("/!");
}
@Test
void getRootDirectoryWhenOpenReturnsEmptyIterable() {
assertThat(this.fileSystem.getRootDirectories()).isEmpty();
}
@Test
void getRootDirectoryWhenClosedThrowsException() throws Exception {
this.fileSystem.close();
assertThatExceptionOfType(ClosedFileSystemException.class)
.isThrownBy(() -> this.fileSystem.getRootDirectories());
}
@Test
void supportedFileAttributeViewsWhenOpenReturnsBasic() {
assertThat(this.fileSystem.supportedFileAttributeViews()).containsExactly("basic");
}
@Test
void supportedFileAttributeViewsWhenClosedThrowsException() throws Exception {
this.fileSystem.close();
assertThatExceptionOfType(ClosedFileSystemException.class)
.isThrownBy(() -> this.fileSystem.supportedFileAttributeViews());
}
@Test
void getPathWhenClosedThrowsException() throws Exception {
this.fileSystem.close();
assertThatExceptionOfType(ClosedFileSystemException.class)
.isThrownBy(() -> this.fileSystem.getPath("nested.jar"));
}
@Test
void getPathWhenFirstIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(null))
.withMessage("Nested paths must contain a single element");
}
@Test
void getPathWhenFirstIsBlankThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath(""))
.withMessage("Nested paths must contain a single element");
}
@Test
void getPathWhenMoreIsNotEmptyThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.fileSystem.getPath("nested.jar", "another.jar"))
.withMessage("Nested paths must contain a single element");
}
@Test
void getPathReturnsPath() {
assertThat(this.fileSystem.getPath("nested.jar")).isInstanceOf(NestedPath.class);
}
@Test
void getPathMatchThrowsException() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.fileSystem.getPathMatcher("*"))
.withMessage("Nested paths do not support path matchers");
}
@Test
void getUserPrincipalLookupServiceThrowsException() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.fileSystem.getUserPrincipalLookupService())
.withMessage("Nested paths do not have a user principal lookup service");
}
@Test
void newWatchServiceThrowsException() {
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> this.fileSystem.newWatchService())
.withMessage("Nested paths do not support the WacherService");
}
@Test
void toStringReturnsString() {
assertThat(this.fileSystem).hasToString(this.jarPath.toAbsolutePath().toString());
}
@Test
void equalsAndHashCode() {
Path jp1 = new File(this.temp, "test1.jar").toPath();
Path jp2 = new File(this.temp, "test1.jar").toPath();
Path jp3 = new File(this.temp, "test2.jar").toPath();
NestedFileSystem f1 = new NestedFileSystem(this.provider, jp1);
NestedFileSystem f2 = new NestedFileSystem(this.provider, jp1);
NestedFileSystem f3 = new NestedFileSystem(this.provider, jp2);
NestedFileSystem f4 = new NestedFileSystem(this.provider, jp3);
assertThat(f1.hashCode()).isEqualTo(f2.hashCode());
assertThat(f1).isEqualTo(f1).isEqualTo(f2).isEqualTo(f3).isNotEqualTo(f4);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.net.protocol.jar.JarUrl;
import org.springframework.boot.loader.testsupport.TestJar;
import org.springframework.boot.loader.zip.AssertFileChannelDataBlocksClosed;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link NestedFileSystem} in combination with
* {@code ZipFileSystem}.
*
* @author Phillip Webb
*/
@AssertFileChannelDataBlocksClosed
class NestedFileSystemZipFileSystemIntegrationTests {
@TempDir
File temp;
@Test
void zip() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URI uri = JarUrl.create(file).toURI();
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
assertThat(Files.readAllBytes(fs.getPath("1.dat"))).containsExactly(0x1);
}
}
@Test
void nestedZip() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URI uri = JarUrl.create(file, "nested.jar").toURI();
try (FileSystem fs = FileSystems.newFileSystem(uri, Collections.emptyMap())) {
assertThat(Files.readAllBytes(fs.getPath("3.dat"))).containsExactly(0x3);
}
}
@Test
void nestedZipWithoutNewFileSystem() throws Exception {
File file = new File(this.temp, "test.jar");
TestJar.create(file);
URI uri = JarUrl.create(file, "nested.jar", "3.dat").toURI();
Path path = Path.of(uri);
assertThat(Files.readAllBytes(path)).containsExactly(0x3);
}
}

View File

@@ -0,0 +1,235 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.loader.nio.file;
import java.io.File;
import java.net.URI;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.nio.file.WatchService;
import java.util.Set;
import java.util.TreeSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.loader.testsupport.TestJar;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link NestedPath}.
*
* @author Phillip Webb
*/
class NestedPathTests {
@TempDir
File temp;
private NestedFileSystem fileSystem;
private NestedFileSystemProvider provider;
private Path jarPath;
private NestedPath path;
@BeforeEach
void setup() {
this.jarPath = new File(this.temp, "test.jar").toPath();
this.provider = new NestedFileSystemProvider();
this.fileSystem = new NestedFileSystem(this.provider, this.jarPath);
this.path = new NestedPath(this.fileSystem, "nested.jar");
}
@Test
void getJarPathReturnsJarPath() {
assertThat(this.path.getJarPath()).isEqualTo(this.jarPath);
}
@Test
void getNestedEntryNameReturnsNestedEntryName() {
assertThat(this.path.getNestedEntryName()).isEqualTo("nested.jar");
}
@Test
void getFileSystemReturnsFileSystem() {
assertThat(this.path.getFileSystem()).isSameAs(this.fileSystem);
}
@Test
void isAbsoluteRerturnsTrue() {
assertThat(this.path.isAbsolute()).isTrue();
}
@Test
void getRootReturnsNull() {
assertThat(this.path.getRoot()).isNull();
}
@Test
void getFileNameReturnsPath() {
assertThat(this.path.getFileName()).isSameAs(this.path);
}
@Test
void getParentReturnsNull() {
assertThat(this.path.getParent()).isNull();
}
@Test
void getNameCountReturnsOne() {
assertThat(this.path.getNameCount()).isEqualTo(1);
}
@Test
void subPathWhenBeginZeroEndOneReturnsPath() {
assertThat(this.path.subpath(0, 1)).isSameAs(this.path);
}
@Test
void subPathWhenBeginIndexNotZeroThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.path.subpath(1, 1))
.withMessage("Nested paths only have a single element");
}
@Test
void subPathThenEndIndexNotOneThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.path.subpath(0, 2))
.withMessage("Nested paths only have a single element");
}
@Test
void startsWithWhenStartsWithReturnsTrue() {
NestedPath otherPath = new NestedPath(this.fileSystem, "nested.jar");
assertThat(this.path.startsWith(otherPath)).isTrue();
}
@Test
void startsWithWhenNotStartsWithReturnsFalse() {
NestedPath otherPath = new NestedPath(this.fileSystem, "other.jar");
assertThat(this.path.startsWith(otherPath)).isFalse();
}
@Test
void endsWithWhenEndsWithReturnsTrue() {
NestedPath otherPath = new NestedPath(this.fileSystem, "nested.jar");
assertThat(this.path.endsWith(otherPath)).isTrue();
}
@Test
void endsWithWhenNotEndsWithReturnsFalse() {
NestedPath otherPath = new NestedPath(this.fileSystem, "other.jar");
assertThat(this.path.endsWith(otherPath)).isFalse();
}
@Test
void normalizeReturnsPath() {
assertThat(this.path.normalize()).isSameAs(this.path);
}
@Test
void resolveThrowsException() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.resolve(this.path))
.withMessage("Unable to resolve nested path");
}
@Test
void relativizeThrowsException() {
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.relativize(this.path))
.withMessage("Unable to relativize nested path");
}
@Test
void toUriReturnsUri() throws Exception {
assertThat(this.path.toUri()).isEqualTo(new URI("nested:" + this.jarPath.toUri().getPath() + "/!nested.jar"));
}
@Test
void toAbsolutePathReturnsPath() {
assertThat(this.path.toAbsolutePath()).isSameAs(this.path);
}
@Test
void toRealPathReturnsPath() throws Exception {
assertThat(this.path.toRealPath()).isSameAs(this.path);
}
@Test
void registerThrowsException() {
WatchService watcher = mock(WatchService.class);
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> this.path.register(watcher))
.withMessage("Nested paths cannot be watched");
}
@Test
void compareToComparesOnNestedEntryName() {
NestedPath a = new NestedPath(this.fileSystem, "a.jar");
NestedPath b = new NestedPath(this.fileSystem, "b.jar");
NestedPath c = new NestedPath(this.fileSystem, "c.jar");
assertThat(new TreeSet<>(Set.of(c, a, b))).containsExactly(a, b, c);
}
@Test
void hashCodeAndEquals() {
NestedFileSystem fs2 = new NestedFileSystem(this.provider, new File(this.temp, "test2.jar").toPath());
NestedPath p1 = new NestedPath(this.fileSystem, "a.jar");
NestedPath p2 = new NestedPath(this.fileSystem, "a.jar");
NestedPath p3 = new NestedPath(this.fileSystem, "c.jar");
NestedPath p4 = new NestedPath(fs2, "c.jar");
assertThat(p1.hashCode()).isEqualTo(p2.hashCode());
assertThat(p1).isEqualTo(p1).isEqualTo(p2).isNotEqualTo(p3).isNotEqualTo(p4);
}
@Test
void toStringReturnsString() {
assertThat(this.path).hasToString(this.jarPath.toString() + "/!nested.jar");
}
@Test
void assertExistsWhenExists() throws Exception {
TestJar.create(this.jarPath.toFile());
this.path.assertExists();
}
@Test
void assertExistsWhenDoesNotExistThrowsException() {
assertThatExceptionOfType(NoSuchFileException.class).isThrownBy(this.path::assertExists);
}
@Test
void castWhenNestedPathReturnsNestedPath() {
assertThat(NestedPath.cast(this.path)).isSameAs(this.path);
}
@Test
void castWhenNullThrowsException() {
assertThatExceptionOfType(ProviderMismatchException.class).isThrownBy(() -> NestedPath.cast(null));
}
@Test
void castWhenNotNestedPathThrowsException() {
assertThatExceptionOfType(ProviderMismatchException.class).isThrownBy(() -> NestedPath.cast(this.jarPath));
}
}