Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll discuss different ways to validate if a given String has a valid filename for the OS, using Java. We want to check the value against restricted characters or length limits.

Through examples, we’ll just focus on core solutions, without using any external dependencies. We’ll check the SDK’s java.io and NIO2 packages, and finally implement our own solutions.

2. Using java.io.File

Let’s start with the very first example, using the java.io.File class. In this solution, we need to create a File instance with a given string and then create a file on the local disk:

public static boolean validateStringFilenameUsingIO(String filename) throws IOException {
    File file = new File(filename);
    boolean created = false;
    try {
        created = file.createNewFile();
        return created;
    } finally {
        if (created) {
            file.delete();
        }
    }
}

When the given filename is incorrect, it throws an IOException. Let’s note, due to the file creation inside, this method requires that the given filename String doesn’t correspond to the already existing file.

We know that different file systems have their own filename limitations. Thus, by using java.io.File methods, we don’t need to specify the rules per OS, because Java automatically takes care of it for us.

However, we need to create a dummy file. When we succeed, we must remember to delete it at the end. Moreover, we must ensure that we have proper permissions to perform those actions. Any failures might also cause an IOException, so it’s also better to check the error message:

assertThatThrownBy(() -> validateStringFilenameUsingIO("baeldung?.txt"))
  .isInstanceOf(IOException.class)
  .hasMessageContaining("Invalid file path");

3. Using NIO2 API

As we know the java.io package has many drawbacks, because it was created in the first versions of Java. The NIO2 API, the successor of the java.io package, brings many improvements, which also greatly simplifies our previous solution:

public static boolean validateStringFilenameUsingNIO2(String filename) {
    Paths.get(filename);
    return true;
}

Our function is now streamlined, so it’s the fastest way to perform such a test. We don’t create any files, so we don’t need to have any disk permissions and perform cleaning after the test.

The invalid filename throws the InvalidPathException, which extends the RuntimeException. The error message also contains more details than the previous one:

assertThatThrownBy(() -> validateStringFilenameUsingNIO2(filename))
  .isInstanceOf(InvalidPathException.class)
  .hasMessageContaining("character not allowed");

This solution has one serious drawback connected with the file system limitations. The Path class might represent the file path with subdirectories. Unlike the first example, this method doesn’t check the filename characters’ overflow limit. Let’s check it against a five-hundred-character random String generated using the randomAlphabetic() method from the Apache Commons:

String filename = RandomStringUtils.randomAlphabetic(500);
assertThatThrownBy(() -> validateStringFilenameUsingIO(filename))
  .isInstanceOf(IOException.class)
  .hasMessageContaining("File name too long");

assertThat(validateStringFilenameUsingNIO2(filename)).isTrue();

To fix that, we should, as previously, create a file and check the result.

4. Custom Implementations

Finally, let’s try to implement our own custom function to test filenames. We’ll also try to avoid any I/O functionalities and use only core Java methods.

These kinds of solutions give more control and allow us to implement our own rules. However, we must consider many additional limitations for different systems.

4.1. Using String.contains

We can use the String.contains() method to check if the given String holds any of the forbidden characters. First of all, we need to manually specify some example values:

public static final Character[] INVALID_WINDOWS_SPECIFIC_CHARS = {'"', '*', '<', '>', '?', '|'};
public static final Character[] INVALID_UNIX_SPECIFIC_CHARS = {'\000'};

In our example, let’s focus only on those two OS. As we know Windows filenames are more restricted than UNIX. Also, some whitespace characters might be problematic.

After defining the restricted character sets, let’s determine the current OS:

public static Character[] getInvalidCharsByOS() {
    String os = System.getProperty("os.name").toLowerCase();
    if (os.contains("win")) {
        return INVALID_WINDOWS_SPECIFIC_CHARS;
    } else if (os.contains("nix") || os.contains("nux") || os.contains("mac")) {
        return INVALID_UNIX_SPECIFIC_CHARS;
    } else {
        return new Character[]{};
    }
}

And now we can use it to test the given value:

public static boolean validateStringFilenameUsingContains(String filename) {
    if (filename == null || filename.isEmpty() || filename.length() > 255) {
        return false;
    }
    return Arrays.stream(getInvalidCharsByOS())
      .noneMatch(ch -> filename.contains(ch.toString()));
}

This Stream predicate returns true if any of our defined characters is not in a given filename. Additionally, we implemented support for null values and incorrect length.

4.2. Regex Pattern Matching

We can also use regular expressions directly on the given String. Let’s implement a pattern accepting only alphanumeric and dot characters, with the length not larger than 255:

public static final String REGEX_PATTERN = "^[A-Za-z0-9.]{1,255}$";

public static boolean validateStringFilenameUsingRegex(String filename) {
    if (filename == null) {
        return false;
    }
    return filename.matches(REGEX_PATTERN);
}

Now, we can test the given value against the previously prepared pattern. We can also easily modify the pattern. We skipped the OS check feature in this example.

5. Conclusion

In this article, we focused on filenames and their limitations. We introduced different algorithms to detect an invalid filename using Java.

We started from the java.io package, which takes care of most of the system limitations for us, but performs additional I/O actions and might require some permissions. Then we checked the NIO2 API, which is the fastest solution, with the filename length check limitation.

Finally, we implemented our own methods, without using any I/O API, but requiring the custom implementation of file system rules.

You can find all the examples with additional tests over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.