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:
@@ -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
|
||||
Reference in New Issue
Block a user