Configuration Files in JavaFX

I wanted a way to create a JSON configuration file in JavaFX in a cross-platform manner, and that would map easily to a properties object. I also wanted it to update the configuration file should any property change during the life of the application.

I used Gson to do the object-to-JSON mappings.

I created two classes to do this. In this example the properties class has 5 string properties, hence why the default instantiation has 5 empty strings:

Configuration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.example.config;

import com.example.util.Utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;

/**
* The Configuration singleton class creates a default config file in a user's home directory
* if it does not exist and provides access to configuration data through out the application.
* It also provides a method to modify configuration details and persist to file.
*/
public class Configuration {
private static final Logger log = LogManager.getLogger();

private Properties properties;
public Properties getProperties() { return properties; }

// Singleton
public static Configuration getInstance() { return instance; }
private static final Configuration instance = new Configuration();

private Configuration() {
log.debug("Loading Configuration");
File configFile = new File(getConfigPath());

if (!configFile.exists()) {
log.info("Config file not found");
File configFolder = configFile.getParentFile();
if (!configFolder.exists()) {
log.info("Config folder not found");
if (!configFolder.mkdirs()) {
throw new IllegalStateException("Couldn't create dir: " + configFolder);
} else {
log.info("Config folder created");
}
} else {
log.debug("Config folder exists");
}
log.debug("Writing Default Config");
updateProperties(DefaultConfig());
}

try {
log.info("Reading Config from " + configFile.toString());
String config = new String(Files.readAllBytes(configFile.toPath()));
log.debug(config);
log.debug("Parsing Config into Json");
properties = new Gson().fromJson(config, Properties.class);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}

private static Properties DefaultConfig() {
return new Properties(
"",
"",
"",
"",
""
);
}

public static void updateProperties() {
updateProperties(getInstance().getProperties());
}

private static void updateProperties(Properties properties) {
File configFile = new File(getConfigPath());
log.debug("Attempting to Update Config File: " + configFile.toString());

try {
log.debug("Converting Properties to Json");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String contents = gson.toJson(properties, Properties.class);
Path file = Paths.get(configFile.toURI());
log.info("Writing Config");
Files.write(file, contents.getBytes(Charset.forName("UTF-8")));
log.debug("Config Written");
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}

/**
* Helper method to construct the Config path in an OS-agnostic way
*
* @return Full path of config file
*/
private static String getConfigPath() {
return Utils.combinePath(System.getProperty("user.home"), Arrays.asList(".myApp", "config.json"));
}
}

Properties.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.config;

import com.google.gson.annotations.SerializedName;

public class Properties {

// Holds a method reference from Configuration.class to notify it when properties change.
private transient Runnable onPropertyUpdated;

@SerializedName("acc")
private String acc;

@SerializedName("ref")
private String ref;

@SerializedName("exp")
private String exp;

@SerializedName("key")
private String key;

@SerializedName("url")
private String url;

public String getAcc() { return acc; }
public void setAcc(String value) {
acc = value;
onPropertyUpdated.run();
}

public String getRef() { return ref; }
public void setRef(String value) {
ref = value;
onPropertyUpdated.run();
}

public String getExp() { return exp; }
public void setExp(String value) {
exp = value;
onPropertyUpdated.run();
}

public String getKey() { return key; }
public void setKey(String value) {
key = value;
onPropertyUpdated.run();
}

public String getUrl() { return url; }
public void setUrl(String value) {
url = value;
onPropertyUpdated.run();
}

Properties(){
this.onPropertyUpdated = Configuration::updateProperties;
}

Properties(
String acc,
String ref,
String exp,
String key,
String url
) {
this.onPropertyUpdated = Configuration::updateProperties;
this.acc = acc;
this.ref = ref;
this.exp = exp;
this.key = key;
this.url = url;
}
}

The getConfigPath() function ensures that the configuration is read and written to the user’s home directory, in a subdirectory called .myApp. You can of course define somewhere else, but bear in mind with this setup the user running the application is who’s home the file will be written into.

These classes weren’t only the work of me either - the onPropertyUpdated & updateProperties capabilities are the work of Ben Burrough.