Stop tabs from resetting your config files

Aug 9, 2016
Stop tabs from resetting your config files
  • The Objective
    As a plugin developer, if you have a config.yml file in your plugin, you know the pain when an optimistic user tells you their entire config.yml has been reset. With a facepalm, you know that this could have been prevented if the user just used spaces instead of tabs in their configuration file.

    So what do you do? With this tutorial, you can listen for tabs in your yml files to prevent file resets. No more online yaml parsers! Your plugin will be able to effectively handle the issue with efficiency and ease.


    Before you Begin

    It is highly recommended you have the following skills under your belt to adequately follow this tutorial.
    Without further ado, let's begin!

    Setting up your Class
    We are going to be using the main class for our tab detector setup. We are going to skip plugin.yml setup for the sake of this tutorial.
    Code (Java):
    /**
    * Our class name is going to be YamlTabParser. Replace with your main class
    * name!
    *
    */

    public class YamlTabParser extends JavaPlugin {}

    Basic Operations
    Next, we must define two member variables:
    Code (Java):

    public class YamlTabParser extends JavaPlugin {

        /*
         * These two member variables are pointers to the config.yml file we will be
         * creating. They are fields because they have a very broad scope and must
         * be handled appropriately during save and load operations
         */

        private File file;
        private FileConfiguration config;
    }
    Then we need to effectively override the onEnable().
    Code (Java):
        @Override
        public void onEnable() {
            // define member variables
            // NOTE: You cannot use a constructor in the main class! This would
            // crash your plugin!
            file = new File(getDataFolder(), "config.yml");
            config = new YamlConfiguration();

            // reload and save files, we will be overriding the JavaPlugin
            // implementations in a later step.
            reloadConfig();
            saveConfig();
        }

    Overriding saveConfig() and getConfig()
    After we have setup our onEnable(), we must override the saveConfig() and getConfig() operations to match our member variables we defined earlier.
    Code (Java):
        /**
         * Overrides by returning our defined FileConfiguration variable
         */

        @Override
        public FileConfiguration getConfig() {
            return config;
        }

        /**
         * Overrides the default save operation by saving using our predefined File
         * and FileConfiguration variables.
         */

        @Override
        public void saveConfig() {
            try {
                config.save(file);
            }
            catch (IOException e) {
                // print stacktrace if you prefer
                getLogger().severe(
                        "Could not save config.yml due to: " + e.getMessage());
            }
        }
    Note there is not much to actually discuss here, we are just doing basic operations to maintain consistency and ensure our member variables persist.
    Tip: Use @Override annotations, so that if you make a spelling mistake in your method header your compiler will let you know!

    Creating your Scanner
    Now, if you have followed the previous steps, we can get to the meat of this tutorial: Scanning your file for tabs.
    Note: If you have not worked with Scanners, I would highly recommend you to spend some more time reading about them. It will make the next several steps a breeze!

    Our method header is void with no passed parameters. Because our FileConfiguration and File variables are globalized, this is the most efficient approach.
    Code (Java):
        /**
         * Opens a java.util.Scanner to scan the config file for any tabs.
         */

        private void scanConfig() {}

    Declare the Scanner:
    Code (Java):
        private void scanConfig() {
            // declare our scanner variable
            Scanner scan = null;
        }
    Define the Scanner:
    Code (Java):
        private void scanConfig() {
            Scanner scan = null;
            try {
                scan = new Scanner(file);
            }
            catch (FileNotFoundException e) {
                // this error should never happen if the file exists
                e.printStackTrace();
            }
        }
    Now, we have to lay down some more groundwork. The scanner effectively functions by reading a file one line at a time, character by character. While we read the file, we want to keep track of two things: the row we are on in the file and the contents of the line we are on.
    Note that we do not actually need the row number; it's purely of convenience to the user. If we keep track of the row number and we find a tab, we can tell the user what line of the config file the tab was on!

    Code (Java):
        private void scanConfig() {
            Scanner scan = null;
            try {
                scan = new Scanner(file);

                int row = 0;
                String line = "";
            }
            catch (FileNotFoundException e) {
                // this error should never happen if the file exists
                e.printStackTrace();
            }
        }

    Now we must actually read across the file, line by line. A while loop is the most effective way of doing this. While the file has another line, we will redefine our local variable line to be the next line of the file.

    Code (Java):
        private void scanConfig() {

            Scanner scan = null;
            try {
                scan = new Scanner(file);
               
                int row = 0;
                String line = "";
               
                // iterate through the file line by line
                while (scan.hasNextLine()) {
                    line = scan.nextLine();
                    // add to the row
                    row++;
                }
            }
            catch (FileNotFoundException e) {
                // this error should never happen if the file exists
                e.printStackTrace();
            }
        }
    Now that we are iterating through each line of the file, we can check to see if it contains a tab. In Java, tabs are parsed using regular expressions. For the sake of this tutorial, you do not need to know everything about regular expressions. A basic understanding helps, and is very useful to know, but is not necessary! The regular expression for tabs is '\t'. Every time there is a tab in your file, it will be parsed as '\t'. We don't care where the tab is on the line, but that it exists. To do this, we use String#indexOf() to determine if there is a tab.
    Code (Java):
        private void scanConfig() {

            Scanner scan = null;
            try {
                scan = new Scanner(file);

                int row = 0;
                String line = "";

                // iterate through the file line by line
                while (scan.hasNextLine()) {
                    line = scan.nextLine();
                    // add to the row
                    row++;

                    // If a tab is found ... \t = tab in regex
                    if (line.indexOf("\t") != -1) {
                        /*
                         * Tell the user where the tab is! We throw an
                         * IllegalArgumentException here. The reason for this will
                         * be explained further down the article.
                         */

                        String error = ("Tab found in config-file on line # " + row + "!");
                        throw new IllegalArgumentException(error);
                    }
                }
            }
            catch (FileNotFoundException e) {
                // this error should never happen if the file exists
                e.printStackTrace();
            }
        }
    }
    Because this method handles the file reading, we must load the file if no tabs were found. We must also close the scanner to prevent memory leaks.
    Note: the load operation throws InvalidConfigurationException and IOException; you must handle these appropriately.

    Code (Java):
        /**
         * Opens a java.util.Scanner to scan the config file for any tabs.
         */

        private void scanConfig() {
            // declare our scanner variable
            Scanner scan = null;
            try {
                scan = new Scanner(file);

                int row = 0;
                String line = "";

                // iterate through the file line by line
                while (scan.hasNextLine()) {
                    line = scan.nextLine();
                    // add to the row
                    row++;

                    // If a tab is found ... \t = tab in regex
                    if (line.indexOf("\t") != -1) {
                        /*
                         * Tell the user where the tab is! We throw an
                         * IllegalArgumentException here.
                         */

                        String error = ("Tab found in config-file on line # " + row + "!");
                        throw new IllegalArgumentException(error);
                    }
                }
                /*
                 * load the file, if tabs were found then this will never execute
                 * because of IllegalArgumentException
                 */

                config.load(file);
            }
            catch (FileNotFoundException e) {
                // this error should never happen if the file exists
                e.printStackTrace();
            }
            catch (IOException e) {
                // failed loading error
                e.printStackTrace();
            }
            catch (InvalidConfigurationException e) {
                // snakeyaml error if the config setup is incorrect.
                e.printStackTrace();
            }
            finally {
                // Close the scanner to avoid memory leaks.
                if (scan != null) {
                    scan.close();
                }
            }
        }
    Overriding the Reload Operation
    We aren't done yet! We want to scan the config on two occasions: when the plugin is loaded or reloaded. We call reloadConfig() when the plugin is enabled, and reloadConfig() is called anytime changes are written to memory. As such, we will override the reload operation to properly scan the file for tabs every time.

    Code (Text):
        /**
         * Override default implementation for reloading
         */
        @Override
        public void reloadConfig() {
            if (!file.exists()) {
                // Create file if it doesn't exist. Change appropriately based on
                // your setup
                saveDefaultConfig();
            }
            scanConfig();
        }
    Summary
    If you followed these steps correctly, you now have a yaml tab detector! The full class file can be viewed as a Github Gist here.

    Tip: If you want a thread-safe operation, do not use a Scanner. A BufferedReader is synchronized and accomplishes the same task, but I will not delve into the setup here. The Bukkit-API is largely not thread safe so it makes no sense to use this operation asynchronously.
  • Loading...
  • Loading...