User-Friendly Configuration with GSON
I recently converted my SeleniumTelluride configuration system to use JSON, specifically the Gson library. Gson is excellent at converting JSON to Java variables, but the outputted JSON is not easy to work with. I decided to tweak the output, as well as add bells and whistles like auto-regeneration.
Step 1 - Basic Configuration
The first step is to set up a system to read the config file as JSON. With Gson, this is pretty simple -
gson.fromJson(Reader, Class<?>) takes a FileInputStream wrapped in an InputStreamReader, and the class you want it to output (
Config.class in our case), and returns an instance of the class with its fields set from the JSON in the file.
For a start, this is good. It reads the file, converts it to JSON, converts it to a Config object, and saves it to the instance variable. If the file doesn't exist...wait, how does the file get created in the first place? That's the next step.
If the file doesn't exist, this is probably the first time the user has run the program. The config file should be created and initialized with some default values to get them started. But in order for that to happen, we need to actually create some values to set.
You may be wondering while the fields are set to a value if we're just going to overwrite them with the values that are read. The answer is that we can very easily use this system to write the default values to a generated file, rather than having to set each of the values manually in the method.
This creates a new Config object (setting the each field to its default values), puts it in the instance variable, converts it to JSON, and writes it to the output - all in one shot.
This is the contents of the file after running the program:
The last step here is to allow access to the config fields from elsewhere in your code. We'll do this with some static getter methods that just retrieve the value from the instance.
Finally, let's test the system. If you change any of the values, the value returned by that getter should change to match. And voila - you have a basic configuration file reader!
Step 2 - Auto-Regeneration
The next system I implemented was auto-regeneration. If the program has a config option that doesn't exist in the file, it will automatically regenerate the file with all the same options as before, but also including the new option(s).
The pseudocode is simple - once you read the instance, convert it back to JSON and compare against the file contents. If the two are different, regenerate the file with the reconstructed JSON. The fields that existed in the file will have been overridden by Gson, and the fields that didn't exist will be their default values - just what we want in the file.
Unfortunately, because we need access to the file's contents, we have to make our file reading code a little uglier. We need to get a String from the file, and pass that to Gson, rather than just passing the FileInputStream.
Now, let's add a new field.
To test that existing values aren't changed, I'm going to modify one of the values in the config.
Now, after running the program, the config file has changed to include our new field, without changing the values of the others.
Step 3 - User-Friendliness
As you can see from the previous displays of the config file, Gson's output is not very user-friendly: there are no whitespaces or linebreaks, which makes reading a long config file difficult. This is what I want my config file to look like:
inputDir: "input" outputDir: "output" doStuff: false loopDeDoos: 5
Notice the lack of curly brackets, commas, and quotes around the keys, as well as the space after each colon.
However, Gson won't be able to read this, as it is not well-formed JSON. We'll have to edit the file's contents as they is read in to make sure it matches Gson's output by the time the output is checked for the auto-regeneration.
Fortunately, every line in our config file will match the following regular expression:
^([^:]+): (.*)$. This means match everything until the first colon and capture it in group 1, then match a colon and a space, and finally capture everything until the end of the line into group 2. We can then replace this with
"\1":\2, to created well-formed JSON. Similarly, we can write a regex to convert each JSON item to a line in our config file. I've written both of these regexes into methods:
We can now use these in the init() method to convert between the config-file format and JSON.
Voila! Our config reader now looks much better to the user, and is still fully accessible from the code.