如何导航到 JFileChooser 中的网络主机?

How to navigate to a network host in JFileChooser?

问题

我有一个 JFileChooser,我需要以编程方式将其 currentDirectory 设置为包含多个 SMB 共享的网络主机(例如 \blah)。从技术上讲,这不是 "directory",而是代表可用共享列表的 shell 文件夹。

失败的解决方案尝试

曾几何时,我遇到过这样的任务,我可以说这真的很烦人。一开始听起来很容易,但是当你开始挖掘和尝试时,越来越多的问题出现了。我想谈谈我的旅程。
据我了解,这里的问题是 \ComputerName\ 不是文件系统中的真实位置。它是一个抽象层,其内容取决于您的身份验证凭据。而且它仅适用于 windows 机器,因此去那里会破坏 Java 的系统独立性法则。总结一下,File 对象不能指向任何东西。 您可以使用 Samba 库 jcifs but in their implementation the class SmbFile needs user authentication and is not compatible with java File class. So you can't use it with jFileChooser. And sadly they are not interessted in changing it as you can read here.
我尝试自己开发一个文件包装器,作为 FileSmbFile Class 的混合体。但我放弃了,因为它给我带来了噩梦。
然后我有了写一个简单的 Dialog 的想法,列出以前用 jcifs 扫描的网络共享,让用户选择其中一个。然后应该会显示带有所选共享的 jFileChooser

当我实现这个想法时,整个问题的超级简单解决方案让我大吃一惊。

由于指向 \ComputerName\ShareName 并单击 One level higher 按钮绝对没有问题,因此必须可以重现此步骤。它是。实际上,在查看 jFileChooser 的底层时,我了解到像 MyComputerNetwork 这样的地方是 ShellFolders,它们是 File 对象的特例。但是这些 Shell 文件夹是受保护的,而不是 Java API.
的一部分 所以我不能直接实例化这些。但是我可以访问 FileSystemView 来处理文件系统上的系统相关视图,例如为特殊位置创建这些 Shell 文件夹。
这么长的文字简短的回答。如果您知道一个共享名,请为该共享名创建一个文件。然后使用 FileSystemView 获取其父文件。瞧,你可以使用生成的 File 对象,它扩展了 ShellFolder jFileChooser.

File f = new File("\\ComputerName\ShareFolder");
FileSystemView fsv = FileSystemView.getFileSystemView();
f = fsv.getParentDirectory(f);
JFileChooser fileChooser = new JFileChooser();
fileChooser.setCurrentDirectory(f);

最后一点注意:此解决方案不会要求您提供登录信息。因此,在此处使用共享之前必须先在 Windows 中访问这些共享。

编辑:抱歉,文字太长了。除夕夜,我喝醉了。现在我想补充一点,我发现了相反的情况。

FileSystemView fsv = FileSystemView.getFileSystemView();
File Desktop = fsv.getRoots()[0];

在 Windows 系统上,这应该会为您提供桌面文件夹。如果您在此处列出所有文件:

for(File file : Desktop.listFiles())
    System.out.println(file.getName());

您会注意到一些名称奇怪的条目:

::{20D04FE0-3AEA-1069-A2D8-08002B30309D}   // My Computer
::{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}   // Network
::{031E4825-7B94-4DC3-B131-E946B44C8DD5}   // User Directory

我不知道这些代码是否适用于所有 Windows 版本,但它们似乎适用于 Windows7。因此,您可以使用它来获取网络 Shell 文件夹,然后是具有共享的计算机。

File Network = fsv.getChild(Desktop, "::{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}");
File Host = fsv.getChild(Network, "COMPUTERNAME");  // Must be in Capital Letters

这里的问题是这将花费大约 10 秒,因为会扫描网络文件夹中的内容。

我找到了一个 Windows-specific 解决方案,它允许仅从其名称(例如 \blah\blah\)导航到任何可访问的计算机节点,而无需枚举网络 shell 文件夹,或任何有关给定节点上网络共享的高级知识。

正在为计算机路径创建 ShellFolder

在调试这个问题时我发现必须为给定的计算机路径创建 ShellFolder 才能导航到它。 Win32ShellFolderManager2.createShellFolder() 将在给定文件上调用 File.getCanonicalPath(),后者又会调用 WinNTFileSystem.canonicalize()。最后一次调用在计算机路径上总是失败。经过多次试验,我能够通过将 File 对象包装在绕过 WinNTFileSystem.canonicalize():

的内容中,为任何可访问的计算机路径创建一个 ShellFolder
/**
 * Create a shell folder for a given network path.
 *
 * @param path File to test for existence.
 * @return ShellFolder representing the given computer node.
 * @throws IllegalArgumentException given path is not a computer node.
 * @throws FileNotFoundException given path could not be found.
 */
public static ShellFolder getComputerNodeFolder(String path)
        throws FileNotFoundException {
    File file = new NonCanonicalizingFile(path);
    if (ShellFolder.isComputerNode(file)) {
        return new Win32ShellFolderManager2().createShellFolder(file);
    } else {
        throw new IllegalArgumentException("Given path is not a computer node.");
    }
}

private static final class NonCanonicalizingFile extends File {
    public NonCanonicalizingFile(String path) {
        super(path);
    }

    @Override
    public String getCanonicalPath() throws IOException {
        // Win32ShellFolderManager2.createShellFolder() will call getCanonicalPath() on this file.
        // Base implementation of getCanonicalPath() calls WinNTFileSystem.canonicalize() which fails on
        // computer nodes (e.g. "\blah"). We skip the canonicalize call, which is safe at this point because we've
        // confirmed (in approveSelection()) that this file represents a computer node.
        return getAbsolutePath();
    }
}

诚然,此解决方案有几个 edge-cases(例如 \blah\ 有效,但 \blah\someShare\..\ 无效),理想情况下,OpenJDK 应该最终解决这些问题。这也是一个 OS-specific 和 implementation-specific 解决方案,在 OpenJDK-on-Windows 设置之外将不起作用。

与 JFileChooser 集成:选项 1

将其与 JFileChooser 集成的最简单方法是覆盖其 approveSelection() 方法。这允许用户在对话框中输入计算机路径(\blah\blah\),然后按 Enter 键导航到那里。当给出 non-existent 或 non-accessible 路径时,会显示一条警告消息。

JFileChooser chooser = new JFileChooser() {
    @Override
    public void approveSelection() {
        File selectedFile = getSelectedFile();
        if (selectedFile != null && ShellFolder.isComputerNode(selectedFile)) {
            try {
                // Resolve path and try to navigate to it
                setCurrentDirectory(getComputerNodeFolder(selectedFile.getPath()));
            } catch (FileNotFoundException ex) {
                // Alert user if given computer node cannot be accessed
                JOptionPane.showMessageDialog(this, "Cannot access " + selectedFile.getPath());
            }
        } else {
            super.approveSelection();
        }
    }
};
chooser.showOpenDialog(null);

与 JFileChooser 集成:选项 2

或者,FileSystemView 可以通过重写其 createFileObject(String) 方法来检查计算机路径。这允许将计算机路径传递给 JFileChooser(String,FileSystemView) 构造函数,并且仍然允许用户导航到可访问的计算机路径。但是,仍然没有简单的方法可以在不覆盖 JFileChooser.approveSelection():

的情况下向用户发送有关 non-accessible 计算机路径的消息
public static class ComputerNodeFriendlyFileSystemView extends FileSystemView {

    private final FileSystemView delegate;

    public ComputerNodeFriendlyFileSystemView(FileSystemView delegate) {
        this.delegate = delegate;
    }

    @Override
    public File createFileObject(String path) {
        File placeholderFile = new File(path);
        if (ShellFolder.isComputerNode(placeholderFile)) {
            try {
                return getComputerNodeFolder(path);
            } catch (FileNotFoundException ex) {
                return placeholderFile;
            }
        } else {
            return delegate.createFileObject(path);
        }
    }

    // All code below simply delegates everything to the "delegate"

    @Override
    public File createNewFolder(File containingDir) throws IOException {
        return delegate.createNewFolder(containingDir);
    }

    @Override
    public boolean isRoot(File f) {
        return delegate.isRoot(f);
    }

    @Override
    public Boolean isTraversable(File f) {
        return delegate.isTraversable(f);
    }

    @Override
    public String getSystemDisplayName(File f) {
        return delegate.getSystemDisplayName(f);
    }

    @Override
    public String getSystemTypeDescription(File f) {
        return delegate.getSystemTypeDescription(f);
    }

    @Override
    public Icon getSystemIcon(File f) {
        return delegate.getSystemIcon(f);
    }

    @Override
    public boolean isParent(File folder, File file) {
        return delegate.isParent(folder, file);
    }

    @Override
    public File getChild(File parent, String fileName) {
        return delegate.getChild(parent, fileName);
    }

    @Override
    public boolean isFileSystem(File f) {
        return delegate.isFileSystem(f);
    }

    @Override
    public boolean isHiddenFile(File f) {
        return delegate.isHiddenFile(f);
    }

    @Override
    public boolean isFileSystemRoot(File dir) {
        return delegate.isFileSystemRoot(dir);
    }

    @Override
    public boolean isDrive(File dir) {
        return delegate.isDrive(dir);
    }

    @Override
    public boolean isFloppyDrive(File dir) {
        return delegate.isFloppyDrive(dir);
    }

    @Override
    public boolean isComputerNode(File dir) {
        return delegate.isComputerNode(dir);
    }

    @Override
    public File[] getRoots() {
        return delegate.getRoots();
    }

    @Override
    public File getHomeDirectory() {
        return delegate.getHomeDirectory();
    }

    @Override
    public File getDefaultDirectory() {
        return delegate.getDefaultDirectory();
    }

    @Override
    public File createFileObject(File dir, String filename) {
        return delegate.createFileObject(dir, filename);
    }

    @Override
    public File[] getFiles(File dir, boolean useFileHiding) {
        return delegate.getFiles(dir, useFileHiding);
    }

    @Override
    public File getParentDirectory(File dir) {
        return delegate.getParentDirectory(dir);
    }
}

用法:

ComputerNodeFriendlyFileSystemView fsv
    = new ComputerNodeFriendlyFileSystemView(FileSystemView.getFileSystemView());
JFileChooser chooser = new JFileChooser("\\blah", fsv);
chooser.showOpenDialog(null);