How to navigate to a network host in JFileChooser?
Asked Answered
A

2

13

The Problem

I have a JFileChooser and I need to programmatically set its currentDirectory to a network host containing several SMB shares (e.g. \\blah). Technically this is not a "directory" but rather a shell folder representing a list of available shares.

  • JFileChooser has no problems navigating to a specific share (e.g. \\blah\someShare) but cannot handle the host "directory" itself (e.g. \\blah).

  • Users can navigate to such "directories" inside JFileChooser by going via "Network" shell folder, or by finding a specific share and navigating to its parent directory. Debugging shows that under-the-hood this directory is represented as a Win32ShellFolder2. All my attempts to set currentDirectory programmatically have failed so far.

  • new File("\\\\blah") can be created, but does not actually exist from Java's perspective.

Failed Solution Attempts

  • chooser.setCurrentDirectory(new File("\\\\blah"));

    Fails because JFileChooser checks if the given directory exists, and new File("\\\\blah").exists() returns false.

  • File dir = new File("\\\\blah").getCanonicalFile();

    Fails with an exception:

      java.io.IOException: Invalid argument
      at java.io.WinNTFileSystem.canonicalize0(Native Method)
      at java.io.WinNTFileSystem.canonicalize(WinNTFileSystem.java:428)
      at java.io.File.getCanonicalPath(File.java:618)
      at java.io.File.getCanonicalFile(File.java:643)
    
  • File dir = ShellFolder.getShellFolder(new File("\\\\blah"));

    Fails with an exception:

      java.io.FileNotFoundException
      at sun.awt.shell.ShellFolder.getShellFolder(ShellFolder.java:247)
    
  • File dir = new Win32ShellFolderManager2().createShellFolder(new File("\\\\blah"));

    Fails with an exception:

      java.io.FileNotFoundException: File \\blah not found
      at sun.awt.shell.Win32ShellFolderManager2.createShellFolder(Win32ShellFolderManager2.java:80)
      at sun.awt.shell.Win32ShellFolderManager2.createShellFolder(Win32ShellFolderManager2.java:64)
    
  • Path dir = Paths.get("\\\\blah");

    Fails with an exception:

    java.nio.file.InvalidPathException: UNC path is missing sharename: \\blah
    at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:118)
    at sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at sun.nio.fs.WindowsPath.parse(WindowsPath.java:94)
    at sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:255)
    at java.nio.file.Paths.get(Paths.java:84)
    
Antiphonary answered 10/11, 2015 at 22:31 Comment(1)
The solutions below don't seem to work with Java 8+. I suggest that my users create a shortcut to their server on their desktop. Then all they have to do in JFileChooser is hit the home button and double-click on the server shortcut, and all the shared folders are accessible.Castellanos
A
3

I found a Windows-specific solution that allows navigating to any accessible computer node from its name alone (e.g. \\blah or \\blah\), without requiring enumeration of the Network shell folder, or any advance knowledge of network shares on the given node.

Creating a ShellFolder for a computer path

While debugging this issue I discovered that a ShellFolder has to be created for the given computer path to be able to navigate to it. Win32ShellFolderManager2.createShellFolder() will call File.getCanonicalPath() on the given file, which will in turn call WinNTFileSystem.canonicalize(). This last call always fails on computer paths. After much experimentation, I was able to create a ShellFolder for any accessible computer path by wrapping the File object in something that bypasses WinNTFileSystem.canonicalize():

/**
 * 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();
    }
}

Admittedly this solution has a couple edge-cases (e.g. \\blah\ works but \\blah\someShare\..\ does not), and ideally OpenJDK should fix these quirks on their end. This is also an OS-specific and implementation-specific solution, and will not work outside OpenJDK-on-Windows setup.

Integrating with JFileChooser: Option 1

The simplest way to integrate this with JFileChooser is to override its approveSelection() method. This allows user to type in a computer path (\\blah or \\blah\) in the dialog and press Enter to navigate there. An alert message is shown when a non-existent or non-accessible path was given.

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

Integrating with JFileChooser: Option 2

Alternatively, FileSystemView can be augmented by overriding its createFileObject(String) method to check for computer paths. This allows passing a computer path to JFileChooser(String,FileSystemView) constructor and still allows user to navigate to accessible computer paths. However, there is still no easy way to message the user about non-accessible computer paths without overriding JFileChooser.approveSelection():

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

Usage:

ComputerNodeFriendlyFileSystemView fsv
    = new ComputerNodeFriendlyFileSystemView(FileSystemView.getFileSystemView());
JFileChooser chooser = new JFileChooser("\\\\blah", fsv);
chooser.showOpenDialog(null);
Antiphonary answered 5/1, 2016 at 20:10 Comment(1)
Win32ShellFolderManager2 appears to be no longer part of Java in Java 8.Castellanos
M
6

Once upon a time I have faced such a task and I can say it was really annoying. First it sounds so easy, but when you start digging and trying, more and more problems show up. I want to talk about my journey.
From what I have understood, the thing here is that \\ComputerName\ is not a real place to be in the filesystem. It is an abstaction layer which content depends on your authentication credentials. And it is for windows machines only so going there would break Java's law of system independency. Summerizing it up it is nothing a File object could point to. You can use the Samba library 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.
I tried myself to develop a File wrapper that acts as a hybrid for File and SmbFile Class. But I gave it up since it brought me nightmares.
Then I had the idea of writing a simple Dialog that lists the network shares previously scaned with jcifs and let the user choose one of them. Then a jFileChooser with the selected share should show up.

While I implemented this idea the super simple solution to the whole problem kicked me in the butt.

Since it is absolutely no problem to point to \\ComputerName\ShareName and click the One level higher Button it must be possible to reproduce this step. And it is. Actually while looking under the hood of jFileChooser I learned that places like MyComputer or Network are ShellFolders which are special cases of File Objects. But these Shell Folders are protected and not Part of the Java API.
So I could not instantiate these directly. But I could access the FileSystemView that handles the system dependent view on the file system like creating these Shell Folders for the special locations.
So long text for a short answer. If you know one Sharename, create a File to this share name. Then use FileSystemView to get its Parent File. And voila you can use the resulting File object which extends a ShellFolder with jFileChooser.

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

One last note: This solution will not ask you for login information. So getting access to the shares must have been in Windows before using them here.

EDIT: Sorry for the long text. New Year's Eve and I was drunk. Now I want to add that I discovered the other way round.

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

On Windows Systems this should give you the Desktop Folder. And if you list all the Files here:

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

You will notice some Entries with strange names:

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

I don't know if these codes are the same for all Windows Versions but it seems that they are for Windows7. So you can use this to get the Network Shell Folder and after that the Computer with the shares.

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

The problem here is that this will take about 10 seconds because the Network Folder is scanned for content.

Maynord answered 31/12, 2015 at 16:5 Comment(1)
Thank you for investigating and sharing these ideas. Unfortunately neither proposal works for my use-case, because our code does not have advance knowledge of any share on the given computer node, and the given node may not be listed under the Network shell folder. Luckily, a somewhat more general solution was found that does work for me: https://mcmap.net/q/879843/-how-to-navigate-to-a-network-host-in-jfilechooserAntiphonary
A
3

I found a Windows-specific solution that allows navigating to any accessible computer node from its name alone (e.g. \\blah or \\blah\), without requiring enumeration of the Network shell folder, or any advance knowledge of network shares on the given node.

Creating a ShellFolder for a computer path

While debugging this issue I discovered that a ShellFolder has to be created for the given computer path to be able to navigate to it. Win32ShellFolderManager2.createShellFolder() will call File.getCanonicalPath() on the given file, which will in turn call WinNTFileSystem.canonicalize(). This last call always fails on computer paths. After much experimentation, I was able to create a ShellFolder for any accessible computer path by wrapping the File object in something that bypasses WinNTFileSystem.canonicalize():

/**
 * 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();
    }
}

Admittedly this solution has a couple edge-cases (e.g. \\blah\ works but \\blah\someShare\..\ does not), and ideally OpenJDK should fix these quirks on their end. This is also an OS-specific and implementation-specific solution, and will not work outside OpenJDK-on-Windows setup.

Integrating with JFileChooser: Option 1

The simplest way to integrate this with JFileChooser is to override its approveSelection() method. This allows user to type in a computer path (\\blah or \\blah\) in the dialog and press Enter to navigate there. An alert message is shown when a non-existent or non-accessible path was given.

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

Integrating with JFileChooser: Option 2

Alternatively, FileSystemView can be augmented by overriding its createFileObject(String) method to check for computer paths. This allows passing a computer path to JFileChooser(String,FileSystemView) constructor and still allows user to navigate to accessible computer paths. However, there is still no easy way to message the user about non-accessible computer paths without overriding JFileChooser.approveSelection():

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

Usage:

ComputerNodeFriendlyFileSystemView fsv
    = new ComputerNodeFriendlyFileSystemView(FileSystemView.getFileSystemView());
JFileChooser chooser = new JFileChooser("\\\\blah", fsv);
chooser.showOpenDialog(null);
Antiphonary answered 5/1, 2016 at 20:10 Comment(1)
Win32ShellFolderManager2 appears to be no longer part of Java in Java 8.Castellanos

© 2022 - 2024 — McMap. All rights reserved.