1

I've a JavaFX application that I packaged it using antBuild to build a single installer .exe file, my app have some configuration files that was placed in the root of the project this way i load them from the root of the project in order to they can be place beside the .jar file and could be changable:

        try {
        File base = null;
        try {
            base = new File(MainApp.class.getProtectionDomain().getCodeSource().getLocation().toURI())
                    .getParentFile();
        } catch (URISyntaxException e) {
            System.exit(0);
        }
        try {
            File configFile = new File(base, "config.properties");
        }

so after packaging the app even if I put the files manually in the same place with jar file, again the app can not recognize them and put into error.


So what is the proper way to store and where to store some sort of config files and how to add them to the installer to put it to right place during installation?

Leo
  • 69
  • 1
  • 9

1 Answers1

3

If your application is bundled as a jar file, then MainApp.class.getProtectionDomain().getCodeSource().getLocation().toURI() will return a jar: scheme URI. The constructor for File taking a URI assumes it gets a file: scheme URI, which is why you are getting an error here. (Basically, if your application is bundled as a jar file, the resource config.properties is not a file at all, its an entry in an archive file.) There's basically no (reliable) way to update the contents of the jar file bundling the application.

The way I usually approach this is to bundle the default configuration file into the jar file, and to define a path on the user file system that is used to store the editable config file. Usually this will be relative to the user's home directory:

Path configLocation = Paths.get(System.getProperty("user.home"), ".applicationName", "config.properties");

or something similar.

Then at startup you can do:

if (! Files.exists(configLocation)) {
    // create directory if needed
    if (! Files.exists(configLocation.getParent())) {
        Files.createDirectory(configLocation.getParent());
    }

    // extract default config from jar and copy to config location:

    try (
        BufferedReader in = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/config.properties")));
        BufferedWriter out = Files.newBufferedWriter(configLocation);) {

        in.lines().forEach(line -> {
            out.append(line);
            out.newLine();
        });
    } catch (IOException exc) {
        // handle exception, e.g. log and warn user config could not be created
    }
}

Properties config = new Properties();
try (BufferedReader in = Files.newBufferedReader(configLocation)) {
    config.load(in);
} catch (IOException exc) {
    // handle exception...
}

So this checks to see if the config file already exists. If not, it extracts the default config from the jar file and copies its content to the defined location. Then it loads the config from the defined location. Thus the first time the user runs the application, it uses the default configuration. After that, the user can edit the config file and subsequently it will use the edited version. You can of course create a UI to modify the contents if you like. One bonus of this is that if the user does something to make the config unreadable, they can simply delete it and the default will be used again.

Obviously this can be bullet-proofed against exceptions a little better (e.g. handle case where the directory is unwritable for some reason, make the config file location user-definable, etc) but that's the basic structure I use in these scenarios.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • How would you handle an update of the application? Delete old one or merge configs? – Inge Mar 01 '16 at 06:35
  • 1
    Essentially we merge them. In our applications the "load properties" step is more complex than I showed here. Each individual property is assigned a default value if it's absent from the file. On program exit we explicitly write out the properties, including defaults. (We also have a GUI configuration screen in most of the applications, so the config can change during runtime.) So if a new version introduces a new config property, it transparently gets its default value from the old config file the first time it's run. – James_D Mar 01 '16 at 10:51
  • nice proposal, btw just for information, do not forget to close BufferedWriter (out.close) to commit the record – tiamat Jan 05 '21 at 08:43
  • 1
    @tiamat The code uses "try-with-resources": note that both `in` and `out` are declared in the parentheses following the `try` keyword. Since both of these implement `AutoClosable`, this means `close()` will automatically be invoked on both of them, as though it were being done in a `finally` block. So manually invoking `close()` is not needed here; it will be called regardless. – James_D Jan 05 '21 at 13:34