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

@ -66,8 +66,8 @@ class LoaderZipEntries {
writeDirectory(new ZipArchiveEntry(entry), out);
written.addDirectory(entry);
}
else if (entry.getName().endsWith(".class")) {
writeClass(new ZipArchiveEntry(entry), loaderJar, out);
else if (entry.getName().endsWith(".class") || entry.getName().startsWith("META-INF/services/")) {
writeFile(new ZipArchiveEntry(entry), loaderJar, out);
written.addFile(entry);
}
entry = loaderJar.getNextEntry();
@ -82,7 +82,7 @@ class LoaderZipEntries {
out.closeArchiveEntry();
}
private void writeClass(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException {
private void writeFile(ZipArchiveEntry entry, ZipInputStream in, ZipArchiveOutputStream out) throws IOException {
prepareEntry(entry, this.fileMode);
out.putArchiveEntry(entry);
copy(in, out);

View File

@ -220,7 +220,7 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {
JarEntry entry;
while ((entry = inputStream.getNextJarEntry()) != null) {
if (isDirectoryEntry(entry) || isClassEntry(entry)) {
if (isDirectoryEntry(entry) || isClassEntry(entry) || isServicesEntry(entry)) {
writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));
}
}
@ -235,6 +235,10 @@ public abstract class AbstractJarWriter implements LoaderClassesWriter {
return entry.getName().endsWith(".class");
}
private boolean isServicesEntry(JarEntry entry) {
return !entry.isDirectory() && entry.getName().startsWith("META-INF/services/");
}
private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter) throws IOException {
writeEntry(entry, null, entryWriter, UnpackHandler.NEVER);
}

View File

@ -75,6 +75,19 @@ public record NestedLocation(Path path, String nestedEntryName) {
return parse(UrlDecoder.decode(url.getPath()));
}
/**
* Create a new {@link NestedLocation} from the given URI.
* @param uri the nested URI
* @return a new {@link NestedLocation} instance
* @throws IllegalArgumentException if the URI is not valid
*/
public static NestedLocation fromUri(URI uri) {
if (uri == null || !"nested".equalsIgnoreCase(uri.getScheme())) {
throw new IllegalArgumentException("'uri' must not be null and must use 'nested' scheme");
}
return parse(uri.getSchemeSpecificPart());
}
static NestedLocation parse(String path) {
if (path == null || path.isEmpty()) {
throw new IllegalArgumentException("'path' must not be empty");

View File

@ -0,0 +1,176 @@
/*
* 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.IOException;
import java.io.UncheckedIOException;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Path;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
import org.springframework.boot.loader.ref.Cleaner;
import org.springframework.boot.loader.zip.CloseableDataBlock;
import org.springframework.boot.loader.zip.DataBlock;
import org.springframework.boot.loader.zip.ZipContent;
/**
* {@link SeekableByteChannel} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedByteChannel implements SeekableByteChannel {
private long position;
private final Resources resources;
private final Cleanable cleanup;
private final long size;
private volatile boolean closed;
NestedByteChannel(Path path, String nestedEntryName) throws IOException {
this(path, nestedEntryName, Cleaner.instance);
}
NestedByteChannel(Path path, String nestedEntryName, Cleaner cleaner) throws IOException {
this.resources = new Resources(path, nestedEntryName);
this.cleanup = cleaner.register(this, this.resources);
this.size = this.resources.getData().size();
}
@Override
public boolean isOpen() {
return !this.closed;
}
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
this.closed = true;
try {
this.cleanup.clean();
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
@Override
public int read(ByteBuffer dst) throws IOException {
assertNotClosed();
int count = this.resources.getData().read(dst, this.position);
if (count > 0) {
this.position += count;
}
return count;
}
@Override
public int write(ByteBuffer src) throws IOException {
throw new NonWritableChannelException();
}
@Override
public long position() throws IOException {
assertNotClosed();
return this.position;
}
@Override
public SeekableByteChannel position(long position) throws IOException {
assertNotClosed();
if (position < 0 || position >= this.size) {
throw new IllegalArgumentException("Position must be in bounds");
}
this.position = position;
return this;
}
@Override
public long size() throws IOException {
assertNotClosed();
return this.size;
}
@Override
public SeekableByteChannel truncate(long size) throws IOException {
throw new NonWritableChannelException();
}
private void assertNotClosed() throws ClosedChannelException {
if (this.closed) {
throw new ClosedChannelException();
}
}
/**
* Resources used by the channel and suitable for registration with a {@link Cleaner}.
*/
static class Resources implements Runnable {
private final ZipContent zipContent;
private final CloseableDataBlock data;
Resources(Path path, String nestedEntryName) throws IOException {
this.zipContent = ZipContent.open(path, nestedEntryName);
this.data = this.zipContent.openRawZipData();
}
DataBlock getData() {
return this.data;
}
@Override
public void run() {
releaseAll();
}
private void releaseAll() {
IOException exception = null;
try {
this.data.close();
}
catch (IOException ex) {
exception = ex;
}
try {
this.zipContent.close();
}
catch (IOException ex) {
if (exception != null) {
ex.addSuppressed(exception);
}
exception = ex;
}
if (exception != null) {
throw new UncheckedIOException(exception);
}
}
}
}

View File

@ -0,0 +1,106 @@
/*
* 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.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.FileStoreAttributeView;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileStore} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedFileStore extends FileStore {
private final NestedFileSystem fileSystem;
NestedFileStore(NestedFileSystem fileSystem) {
this.fileSystem = fileSystem;
}
@Override
public String name() {
return this.fileSystem.toString();
}
@Override
public String type() {
return "nestedfs";
}
@Override
public boolean isReadOnly() {
return this.fileSystem.isReadOnly();
}
@Override
public long getTotalSpace() throws IOException {
return 0;
}
@Override
public long getUsableSpace() throws IOException {
return 0;
}
@Override
public long getUnallocatedSpace() throws IOException {
return 0;
}
@Override
public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
return getJarPathFileStore().supportsFileAttributeView(type);
}
@Override
public boolean supportsFileAttributeView(String name) {
return getJarPathFileStore().supportsFileAttributeView(name);
}
@Override
public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
return getJarPathFileStore().getFileStoreAttributeView(type);
}
@Override
public Object getAttribute(String attribute) throws IOException {
try {
return getJarPathFileStore().getAttribute(attribute);
}
catch (UncheckedIOException ex) {
throw ex.getCause();
}
}
protected FileStore getJarPathFileStore() {
try {
return Files.getFileStore(this.fileSystem.getJarPath());
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}

View File

@ -0,0 +1,229 @@
/*
* 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.IOException;
import java.net.URI;
import java.nio.file.ClosedFileSystemException;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.WatchService;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileSystem} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
class NestedFileSystem extends FileSystem {
private static final Set<String> SUPPORTED_FILE_ATTRIBUTE_VIEWS = Set.of("basic");
private static final String FILE_SYSTEMS_CLASS_NAME = FileSystems.class.getName();
private static final Object EXISTING_FILE_SYSTEM = new Object();
private final NestedFileSystemProvider provider;
private final Path jarPath;
private volatile boolean closed;
private final Map<String, Object> zipFileSystems = new HashMap<>();
NestedFileSystem(NestedFileSystemProvider provider, Path jarPath) {
if (provider == null || jarPath == null) {
throw new IllegalArgumentException("Provider and JarPath must not be null");
}
this.provider = provider;
this.jarPath = jarPath;
}
void installZipFileSystemIfNecessary(String nestedEntryName) {
try {
boolean seen;
synchronized (this.zipFileSystems) {
seen = this.zipFileSystems.putIfAbsent(nestedEntryName, EXISTING_FILE_SYSTEM) != null;
}
if (!seen) {
URI uri = new URI("jar:nested:" + this.jarPath.toUri().getPath() + "/!" + nestedEntryName);
if (!hasFileSystem(uri)) {
FileSystem zipFileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap());
synchronized (this.zipFileSystems) {
this.zipFileSystems.put(nestedEntryName, zipFileSystem);
}
}
}
}
catch (Exception ex) {
// Ignore
}
}
private boolean hasFileSystem(URI uri) {
try {
FileSystems.getFileSystem(uri);
return true;
}
catch (FileSystemNotFoundException ex) {
return isCreatingNewFileSystem();
}
}
private boolean isCreatingNewFileSystem() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
if (stack != null) {
for (StackTraceElement element : stack) {
if (FILE_SYSTEMS_CLASS_NAME.equals(element.getClassName())) {
return "newFileSystem".equals(element.getMethodName());
}
}
}
return false;
}
@Override
public FileSystemProvider provider() {
return this.provider;
}
Path getJarPath() {
return this.jarPath;
}
@Override
public void close() throws IOException {
if (this.closed) {
return;
}
this.closed = true;
synchronized (this.zipFileSystems) {
this.zipFileSystems.values()
.stream()
.filter(FileSystem.class::isInstance)
.map(FileSystem.class::cast)
.forEach(this::closeZipFileSystem);
}
this.provider.removeFileSystem(this);
}
private void closeZipFileSystem(FileSystem zipFileSystem) {
try {
zipFileSystem.close();
}
catch (Exception ex) {
}
}
@Override
public boolean isOpen() {
return !this.closed;
}
@Override
public boolean isReadOnly() {
return true;
}
@Override
public String getSeparator() {
return "/!";
}
@Override
public Iterable<Path> getRootDirectories() {
assertNotClosed();
return Collections.emptySet();
}
@Override
public Iterable<FileStore> getFileStores() {
assertNotClosed();
return Collections.emptySet();
}
@Override
public Set<String> supportedFileAttributeViews() {
assertNotClosed();
return SUPPORTED_FILE_ATTRIBUTE_VIEWS;
}
@Override
public Path getPath(String first, String... more) {
assertNotClosed();
if (first == null || first.isBlank() || more.length != 0) {
throw new IllegalArgumentException("Nested paths must contain a single element");
}
return new NestedPath(this, first);
}
@Override
public PathMatcher getPathMatcher(String syntaxAndPattern) {
throw new UnsupportedOperationException("Nested paths do not support path matchers");
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
throw new UnsupportedOperationException("Nested paths do not have a user principal lookup service");
}
@Override
public WatchService newWatchService() throws IOException {
throw new UnsupportedOperationException("Nested paths do not support the WacherService");
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
NestedFileSystem other = (NestedFileSystem) obj;
return this.jarPath.equals(other.jarPath);
}
@Override
public int hashCode() {
return this.jarPath.hashCode();
}
@Override
public String toString() {
return this.jarPath.toAbsolutePath().toString();
}
private void assertNotClosed() {
if (this.closed) {
throw new ClosedFileSystemException();
}
}
}

View File

@ -0,0 +1,186 @@
/*
* 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.IOException;
import java.net.URI;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemAlreadyExistsException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.LinkOption;
import java.nio.file.NotDirectoryException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.ReadOnlyFileSystemException;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.spi.FileSystemProvider;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
/**
* {@link FileSystemProvider} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @since 3.2.0
*/
public class NestedFileSystemProvider extends FileSystemProvider {
private Map<Path, NestedFileSystem> fileSystems = new HashMap<>();
@Override
public String getScheme() {
return "nested";
}
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
NestedLocation location = NestedLocation.fromUri(uri);
Path jarPath = location.path();
synchronized (this.fileSystems) {
if (this.fileSystems.containsKey(jarPath)) {
throw new FileSystemAlreadyExistsException();
}
NestedFileSystem fileSystem = new NestedFileSystem(this, location.path());
this.fileSystems.put(location.path(), fileSystem);
return fileSystem;
}
}
@Override
public FileSystem getFileSystem(URI uri) {
NestedLocation location = NestedLocation.fromUri(uri);
synchronized (this.fileSystems) {
NestedFileSystem fileSystem = this.fileSystems.get(location.path());
if (fileSystem == null) {
throw new FileSystemNotFoundException();
}
return fileSystem;
}
}
@Override
public Path getPath(URI uri) {
NestedLocation location = NestedLocation.fromUri(uri);
synchronized (this.fileSystems) {
NestedFileSystem fileSystem = this.fileSystems.computeIfAbsent(location.path(),
(path) -> new NestedFileSystem(this, path));
fileSystem.installZipFileSystemIfNecessary(location.nestedEntryName());
return fileSystem.getPath(location.nestedEntryName());
}
}
void removeFileSystem(NestedFileSystem fileSystem) {
synchronized (this.fileSystems) {
this.fileSystems.remove(fileSystem.getJarPath());
}
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
throws IOException {
NestedPath nestedPath = NestedPath.cast(path);
return new NestedByteChannel(nestedPath.getJarPath(), nestedPath.getNestedEntryName());
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
throw new NotDirectoryException(NestedPath.cast(dir).toString());
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void delete(Path path) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public void move(Path source, Path target, CopyOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
@Override
public boolean isSameFile(Path path, Path path2) throws IOException {
return path.equals(path2);
}
@Override
public boolean isHidden(Path path) throws IOException {
return false;
}
@Override
public FileStore getFileStore(Path path) throws IOException {
NestedPath nestedPath = NestedPath.cast(path);
nestedPath.assertExists();
return new NestedFileStore(nestedPath.getFileSystem());
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
Path jarPath = getJarPath(path);
jarPath.getFileSystem().provider().checkAccess(jarPath, modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().getFileAttributeView(jarPath, type, options);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
throws IOException {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().readAttributes(jarPath, type, options);
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
Path jarPath = getJarPath(path);
return jarPath.getFileSystem().provider().readAttributes(jarPath, attributes, options);
}
protected Path getJarPath(Path path) {
return NestedPath.cast(path).getJarPath();
}
@Override
public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
throw new ReadOnlyFileSystemException();
}
}

View File

@ -0,0 +1,221 @@
/*
* 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.IOError;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.ProviderMismatchException;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Objects;
import org.springframework.boot.loader.net.protocol.nested.NestedLocation;
import org.springframework.boot.loader.zip.ZipContent;
/**
* {@link Path} implementation for {@link NestedLocation nested} jar files.
*
* @author Phillip Webb
* @see NestedFileSystemProvider
*/
final class NestedPath implements Path {
private final NestedFileSystem fileSystem;
private final String nestedEntryName;
private volatile Boolean entryExists;
NestedPath(NestedFileSystem fileSystem, String nestedEntryName) {
if (fileSystem == null || nestedEntryName == null || nestedEntryName.isBlank()) {
throw new IllegalArgumentException("'filesSystem' and 'nestedEntryName' are required");
}
this.fileSystem = fileSystem;
this.nestedEntryName = nestedEntryName;
}
Path getJarPath() {
return this.fileSystem.getJarPath();
}
String getNestedEntryName() {
return this.nestedEntryName;
}
@Override
public NestedFileSystem getFileSystem() {
return this.fileSystem;
}
@Override
public boolean isAbsolute() {
return true;
}
@Override
public Path getRoot() {
return null;
}
@Override
public Path getFileName() {
return this;
}
@Override
public Path getParent() {
return null;
}
@Override
public int getNameCount() {
return 1;
}
@Override
public Path getName(int index) {
if (index != 0) {
throw new IllegalArgumentException("Nested paths only have a single element");
}
return this;
}
@Override
public Path subpath(int beginIndex, int endIndex) {
if (beginIndex != 0 || endIndex != 1) {
throw new IllegalArgumentException("Nested paths only have a single element");
}
return this;
}
@Override
public boolean startsWith(Path other) {
return equals(other);
}
@Override
public boolean endsWith(Path other) {
return equals(other);
}
@Override
public Path normalize() {
return this;
}
@Override
public Path resolve(Path other) {
throw new UnsupportedOperationException("Unable to resolve nested path");
}
@Override
public Path relativize(Path other) {
throw new UnsupportedOperationException("Unable to relativize nested path");
}
@Override
public URI toUri() {
try {
String jarFilePath = this.fileSystem.getJarPath().toUri().getPath();
return new URI("nested:" + jarFilePath + "/!" + this.nestedEntryName);
}
catch (URISyntaxException ex) {
throw new IOError(ex);
}
}
@Override
public Path toAbsolutePath() {
return this;
}
@Override
public Path toRealPath(LinkOption... options) throws IOException {
return this;
}
@Override
public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
throw new UnsupportedOperationException("Nested paths cannot be watched");
}
@Override
public int compareTo(Path other) {
NestedPath otherNestedPath = cast(other);
return this.nestedEntryName.compareTo(otherNestedPath.nestedEntryName);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
NestedPath other = (NestedPath) obj;
return Objects.equals(this.fileSystem, other.fileSystem)
&& Objects.equals(this.nestedEntryName, other.nestedEntryName);
}
@Override
public int hashCode() {
return Objects.hash(this.fileSystem, this.nestedEntryName);
}
@Override
public String toString() {
return this.fileSystem.getJarPath() + this.fileSystem.getSeparator() + this.nestedEntryName;
}
void assertExists() throws NoSuchFileException {
if (!Files.isRegularFile(getJarPath())) {
throw new NoSuchFileException(toString());
}
Boolean entryExists = this.entryExists;
if (entryExists == null) {
try {
try (ZipContent content = ZipContent.open(getJarPath(), this.nestedEntryName)) {
entryExists = true;
}
}
catch (IOException ex) {
entryExists = false;
}
this.entryExists = entryExists;
}
if (!entryExists) {
throw new NoSuchFileException(toString());
}
}
static NestedPath cast(Path path) {
if (path instanceof NestedPath nestedPath) {
return nestedPath;
}
throw new ProviderMismatchException();
}
}

View File

@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Non-blocking IO {@link java.nio.file.FileSystem} implementation for nested suppoprt.
*/
package org.springframework.boot.loader.nio.file;

View File

@ -0,0 +1 @@
org.springframework.boot.loader.nio.file.NestedFileSystemProvider

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));
}
}

View File

@ -1,6 +1,8 @@
plugins {
id "java"
id "org.springframework.boot"
// id 'org.springframework.boot' version '3.1.4'
// id 'io.spring.dependency-management' version '1.1.3'
}
apply plugin: "io.spring.dependency-management"

View File

@ -17,8 +17,13 @@
package org.springframework.boot.loaderapp;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import jakarta.servlet.ServletContext;
@ -27,6 +32,8 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
@SpringBootApplication
@ -49,9 +56,20 @@ public class LoaderTestApplication {
String message = (!Arrays.equals(resourceContent, directContent)) ? "NO MATCH"
: directContent.length + " BYTES";
System.out.println(">>>>> " + message + " from " + resourceUrl);
testGh7161();
};
}
private void testGh7161() {
try {
Resource resource = new ClassPathResource("gh-7161");
Path path = Paths.get(resource.getURI());
System.out.println(">>>>> gh-7161 " + Files.list(path).toList());
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
public static void main(String[] args) {
SpringApplication.run(LoaderTestApplication.class, args).close();
}

View File

@ -51,11 +51,12 @@ class LoaderIntegrationTests {
@ParameterizedTest
@MethodSource("javaRuntimes")
void readUrlsWithoutWarning(JavaRuntime javaRuntime) {
void runJar(JavaRuntime javaRuntime) {
try (GenericContainer<?> container = createContainer(javaRuntime, "spring-boot-loader-tests-app", null)) {
container.start();
System.out.println(this.output.toUtf8String());
assertThat(this.output.toUtf8String()).contains(">>>>> 287649 BYTES from")
.contains(">>>>> gh-7161 [/gh-7161/example.txt]")
.doesNotContain("WARNING:")
.doesNotContain("illegal")
.doesNotContain("jar written to temp");