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:
- Set up a swing application (e.g. in Eclipse)
- Create a
JFileChooser
dialog as such:
JFileChooser dialog = new JFileChooser( file );
dialog.setDialogType( JFileChooser.SAVE_DIALOG );
dialog.setAcceptAllFileFilterUsed( false );
- Run the application, make sure the dialog creation is triggered
- In the dialog enter a space as filename:
- Confirm
- 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;
}
}