diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java index 8a7851f07c0..64b3ea1685e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LoaderZipEntries.java @@ -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); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java index 7e807b1f9d8..429f63c2b36 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/AbstractJarWriter.java @@ -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); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java index ff84b3bf847..c37555dec87 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/net/protocol/nested/NestedLocation.java @@ -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"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java new file mode 100644 index 00000000000..e41b27fd655 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedByteChannel.java @@ -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); + } + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java new file mode 100644 index 00000000000..c5a7edb559e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileStore.java @@ -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 type) { + return getJarPathFileStore().supportsFileAttributeView(type); + } + + @Override + public boolean supportsFileAttributeView(String name) { + return getJarPathFileStore().supportsFileAttributeView(name); + } + + @Override + public V getFileStoreAttributeView(Class 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); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java new file mode 100644 index 00000000000..be38b10cd02 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystem.java @@ -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 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 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 getRootDirectories() { + assertNotClosed(); + return Collections.emptySet(); + } + + @Override + public Iterable getFileStores() { + assertNotClosed(); + return Collections.emptySet(); + } + + @Override + public Set 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(); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java new file mode 100644 index 00000000000..ca136748df8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedFileSystemProvider.java @@ -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 fileSystems = new HashMap<>(); + + @Override + public String getScheme() { + return "nested"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map 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 options, FileAttribute... attrs) + throws IOException { + NestedPath nestedPath = NestedPath.cast(path); + return new NestedByteChannel(nestedPath.getJarPath(), nestedPath.getNestedEntryName()); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, Filter 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 getFileAttributeView(Path path, Class type, LinkOption... options) { + Path jarPath = getJarPath(path); + return jarPath.getFileSystem().provider().getFileAttributeView(jarPath, type, options); + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) + throws IOException { + Path jarPath = getJarPath(path); + return jarPath.getFileSystem().provider().readAttributes(jarPath, type, options); + } + + @Override + public Map 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(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java new file mode 100644 index 00000000000..163af41784a --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/NestedPath.java @@ -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(); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java new file mode 100644 index 00000000000..6431f845345 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/nio/file/package-info.java @@ -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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 00000000000..425737d36fd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +org.springframework.boot.loader.nio.file.NestedFileSystemProvider diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java index f992624c4f5..5f4314de44d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/net/protocol/nested/NestedLocationTests.java @@ -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"); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java new file mode 100644 index 00000000000..e198d43d6f8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedByteChannelTests.java @@ -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 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 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()); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java new file mode 100644 index 00000000000..9baf2b4f9b1 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileStoreTests.java @@ -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 { + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java new file mode 100644 index 00000000000..dd357cb28d7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemProviderTests.java @@ -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; + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java new file mode 100644 index 00000000000..d8b8825dc8b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java new file mode 100644 index 00000000000..28e26b7f8d4 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedFileSystemZipFileSystemIntegrationTests.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java new file mode 100644 index 00000000000..02558cf3052 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/nio/file/NestedPathTests.java @@ -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)); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle index 37596c62063..8f8cf37e3aa 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/build.gradle @@ -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" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java index 0c9d429350d..245b471b790 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -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(); } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/resources/gh-7161/example.txt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/resources/gh-7161/example.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index 3151e334d9e..84b46b8f9a5 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -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");