Home > Software design >  JFileChooser on Windows - exception when only space is entered
JFileChooser on Windows - exception when only space is entered

Time:10-19

Description

A JFileChooser is used in a Java Swing application. Users are able to enter any filename permitted by the operating system, but from time to time they will enter erroneous filenames, such as names including invalid characters.

If a user enters a name ending with a space, such as

  • SomeName

an error message is shown. This is done by overriding JFileChooser#approveSelection, matching the filename to an undesired regex and then displaying an error dialog.

However when the user enters only a space, then an exception is thrown:

2022-10-18 12:34:54 SEVERE (CustomExceptionHandler::uncaughtException) Uncaught exception: java.nio.file.InvalidPathException: Trailing char < > at index 22: C:\X\Y\Z\  on [AWT-EventQueue-0]
Exception Stacktrace:
java.nio.file.InvalidPathException: Trailing char < > at index 22: C:\X\Y\Z\ 
    at java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:191)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
    at java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
    at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
    at java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:229)
    at java.base/java.nio.file.Path.of(Path.java:147)
    at java.base/java.nio.file.Paths.get(Paths.java:69)
    at java.desktop/sun.awt.shell.ShellFolder.getShellFolder(ShellFolder.java:247)
    at java.desktop/javax.swing.plaf.basic.BasicFileChooserUI.changeDirectory(BasicFileChooserUI.java:1353)
    at java.desktop/javax.swing.plaf.basic.BasicFileChooserUI$ApproveSelectionAction.actionPerformed(BasicFileChooserUI.java:1142)
    at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1967)
    at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2308)
    at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:405)
    at java.desktop/javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:262)
    at java.desktop/javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:279)
    at java.desktop/java.awt.AWTEventMulticaster.mouseReleased(AWTEventMulticaster.java:297)
    at java.desktop/java.awt.Component.processMouseEvent(Component.java:6635)
    at java.desktop/javax.swing.JComponent.processMouseEvent(JComponent.java:3342)
    at java.desktop/java.awt.Component.processEvent(Component.java:6400)
    at java.desktop/java.awt.Container.processEvent(Container.java:2263)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5011)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4918)
    at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Container.java:4547)
    at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Container.java:4488)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2307)
    at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2772)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:772)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:745)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:743)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:117)
    at java.desktop/java.awt.WaitDispatchSupport$2.run(WaitDispatchSupport.java:190)
    at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:235)
    at java.desktop/java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:233)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.desktop/java.awt.WaitDispatchSupport.enter(WaitDispatchSupport.java:233)
    at java.desktop/java.awt.Dialog.show(Dialog.java:1070)
    at java.desktop/javax.swing.JFileChooser.showDialog(JFileChooser.java:769)
    at java.desktop/javax.swing.JFileChooser.showSaveDialog(JFileChooser.java:691)
    at core.RetainerExportController.exportRetainer(RetainerExportController.java:156)
    at core.RetainerExportController.lambda$1(RetainerExportController.java:64)
    at java.desktop/javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1967)
    at java.desktop/javax.swing.AbstractButton$Handler.actionPerformed(AbstractButton.java:2308)
    at java.desktop/javax.swing.DefaultButtonModel.fireActionPerformed(DefaultButtonModel.java:405)
    at java.desktop/javax.swing.DefaultButtonModel.setPressed(DefaultButtonModel.java:262)
    at java.desktop/javax.swing.plaf.basic.BasicButtonListener.mouseReleased(BasicButtonListener.java:279)
    at java.desktop/java.awt.AWTEventMulticaster.mouseReleased(AWTEventMulticaster.java:297)
    at java.desktop/java.awt.Component.processMouseEvent(Component.java:6635)
    at java.desktop/javax.swing.JComponent.processMouseEvent(JComponent.java:3342)
    at java.desktop/java.awt.Component.processEvent(Component.java:6400)
    at java.desktop/java.awt.Container.processEvent(Container.java:2263)
    at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5011)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2321)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4918)
    at java.desktop/java.awt.LightweightDispatcher.processMouseEvent(Container.java:4547)
    at java.desktop/java.awt.LightweightDispatcher.dispatchEvent(Container.java:4488)
    at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2307)
    at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2772)
    at java.desktop/java.awt.Component.dispatchEvent(Component.java:4843)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:772)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:721)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:715)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:95)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:745)
    at java.desktop/java.awt.EventQueue$5.run(EventQueue.java:743)
    at java.base/java.security.AccessController.doPrivileged(Native Method)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:85)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:742)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:203)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:124)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:109)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:90)

This exception keeps popping up in our error logs, because while users are instructed not to enter spaces as filenames, they still will from time to time.

Investigation

After looking into this, the problem appears to be coming from the BasicFileChooserUI. There is a filter for missing or empty strings in the class:

if (filename == null || filename.length() == 0) {
                // no file selected, multiple selection off, therefore cancel the approve action
                resetGlobFilter();
                return;
}

However there is no filter for a blank filename. So this name is not filtered by BasicFileChooserUI, but it is recognized to be ending on a space by the WindowsPathParser, which leads to the error.

There is more odd behavior as well. For example entering a name consisting purely of forbidden characters, such as

  • ???

results in just nothing happening, i.e.

  • No exception is thrown
  • JFileChooser#approveSelection is also never reached

How to solve?

It is not clear how to solve it, because all of the erroneous behavior occurs in internal Swing and sun.nio.fs code. E.g. there are regex filters in our JFileChooser implementation, which trigger upon approveSelection and check for names consisting of spaces and invalid characters - however approveSelection is never called in this scenario. It is called if WindowsPathParser#parse and WindowsPathParser#normalize are passed, but otherwise the error handling is useless, because some weird internal error handling is done internally beforehand.

I've looked into extending the WindowsFileChooserUI or the BasicFileChooserUI, but not only would that introduce unnecessary explicit dependencies on the desktop package for the latter case, but it is (as far as I see) not possible to do, at least with anything close to clean software design, because both of those classes use private members that are crucial to the problem at hand. E.g. WindowsFileChooserUI holds the JTextField which handles the user input as a private member and sets it up and uses it in the same method, so accessing it and even modifying it in any class attempting to extend WindowsFileChooserUI is going to be a problem.

Long story short - how do I prevent an InvalidPathException from being thrown, when users open the JFileChooser dialog and enter only a space?

Notes

Because internal APIs are affected the used JDK may be relevant: 11.0.14_9

The dialog creation itself is not spectacular, it is created thusly:

JFileChooser dialog = new CustomFileChooser( file );
dialog.setDialogType( JFileChooser.SAVE_DIALOG );
dialog.setAcceptAllFileFilterUsed( false );

The only methods that are overridden by CustomFileChooser are

  • createDialog
  • approveSelection

Minimal reproducible example

Because an example has been requested, the error can be reproduced as follows:

  1. Set up a swing application (e.g. in Eclipse)
  2. Create a JFileChooser dialog as such:
JFileChooser dialog = new JFileChooser( file );
dialog.setDialogType( JFileChooser.SAVE_DIALOG );
dialog.setAcceptAllFileFilterUsed( false );
  1. Run the application, make sure the dialog creation is triggered
  2. In the dialog enter a space as filename:
  3. Confirm
  4. An InvalidPathException is thrown

Note that this error requires the JVM being run on Windows and my tests happened on Java 11.

CodePudding user response:

While older file chooser versions stripped spaces from entered file names, they never cared about the validity of the names beyond spaces. E.g. entering something like \<\>\" gets accepted. Invalid path names didn’t cause exceptions, because java.io.File doesn’t check the syntax either.

In newer versions, the NIO FileSystem API is used at some places and the space is not always stripped. The specific exception occurs because new File(" ").getAbsoluteFile().isDirectory() evaluates to true for some reason (while new File(" ").isDirectory() doesn’t), so the file chooser tries to change the directory to the invalid path, instead of invoking approveSelection() which the application could override.

Since file chooser’s code can’t cope with exceptions for invalid files, I made this workaround which uses a special File subclass which reports not to be a directory and can be detected at approveSelection():

public class FileChooserTest {
    static final class Invalid extends File {
        final String originalName;
        public Invalid(String pathname) {
            super(pathname);
            originalName = pathname;
        }

        @Override
        public boolean isDirectory() {
            return false;
        }

        @Override
        public String getName() {
            return originalName;
        }
    }
    public static void main(String... args) {
        if(!EventQueue.isDispatchThread()) {
            EventQueue.invokeLater(FileChooserTest::main);
            return;
        }
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
            throw new RuntimeException(e);
        }
        JFileChooser fc = new JFileChooser() {
            @Override
            public void approveSelection() {
                if(getSelectedFile() instanceof Invalid) {
                    setSelectedFile(null);
                    return;
                }
                super.approveSelection();
            }
        };
        fc.setFileSystemView(new FileSystemView() {
            @Override
            public File createFileObject(String path) {
                try {
                    Paths.get(path);
                } catch(InvalidPathException ex) {
                    return new Invalid(path);
                }
                return super.createFileObject(path);
            }

            @Override
            public File createFileObject(File dir, String filename) {
                try {
                    Paths.get(filename);
                } catch(InvalidPathException ex) {
                    return new Invalid(filename);
                }
                return super.createFileObject(dir, filename);
            }

            @Override
            public File createNewFolder(File containingDir) throws IOException {
                return FileSystemView.getFileSystemView().createNewFolder(containingDir);
            }
        });
        if(fc.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
            System.out.println(fc.getSelectedFile());
        }
        else System.out.println("Not approved");
    }
}

This does not only eliminate the exception but also prevents invalid files from getting approved. Of course, it could be improved, e.g. by providing feedback to the user. But it would be better if bugs like JDK-8196673 get fixed anyway.


Note: the reason why names containing * or ? are not rejected, is that they are converted to a file name filter. So when you enter, e.g. *.txt, it should show up in the filter combobox.

CodePudding user response:

I extended JFileChooser to add a confirmation dialog if the file I wanted to save has the same name as an existing file. Perhaps you can use this to start a more robust version of JFileChooser.

import java.io.File;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;

public class OSFileChooser extends JFileChooser {

    private static final long serialVersionUID = 1L;

    @Override
    public void approveSelection() {
        File f = getSelectedFile();
        
        if (f.exists() && getDialogType() == SAVE_DIALOG) {
            int result = JOptionPane.showConfirmDialog(this, f.getName()   
                    " exists, overwrite?",
                    "Existing file", JOptionPane.YES_NO_CANCEL_OPTION);
            switch (result) {
            case JOptionPane.YES_OPTION:
                super.approveSelection();
                return;
            case JOptionPane.NO_OPTION:
                return;
            case JOptionPane.CLOSED_OPTION:
                return;
            case JOptionPane.CANCEL_OPTION:
                cancelSelection();
                return;
            }
        }
        
        super.approveSelection();
    }
    
    @Override
    public File getSelectedFile() {
        File file = super.getSelectedFile();
        
        if (file != null && getDialogType() == SAVE_DIALOG) {
            String extension = getExtension(file);
            if (extension.isEmpty()) {
                FileTypeFilter filter = (FileTypeFilter) getFileFilter();
                if (filter != null) {
                    extension = filter.getExtension();
                    String fileName = file.getPath();
                    fileName  = "."   extension;
                    file = new File(fileName);
                }
            }
        }
        
        return file;
    }
    
    public String getExtension(File file) {
        String extension = "";
        String s = file.getName();
        int i = s.lastIndexOf('.');

        if (i > 0 && i < (s.length() - 1)) {
            extension = s.substring(i   1).toLowerCase();
        }

        return extension;
    }
    
}

And the associated FileTypeFilter class.

import java.io.File;

import javax.swing.filechooser.FileFilter;

public class FileTypeFilter extends FileFilter {
    private String extension;
    private String description;
 
    public FileTypeFilter(String description, String extension) {
        this.extension = extension;
        this.description = description;
    }
 
    @Override
    public boolean accept(File file) {
        if (file.isDirectory()) {
            return true;
        }
        return file.getName().endsWith("."   extension);
    }
 
    @Override
    public String getDescription() {
        return description   String.format(" (*.%s)", extension);
    }
    
    public String getExtension() {
        return extension;
    }
    
}
  • Related