mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-07-05 00:56:58 +08:00
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:
parent
4b495ca2a9
commit
3c62defb9d
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
@ -0,0 +1 @@
|
||||
org.springframework.boot.loader.nio.file.NestedFileSystemProvider
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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"
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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");
|
||||
|
Loading…
Reference in New Issue
Block a user