Reloadable property file

There are still applications out there which require a restart after changing user settings, but more commonly the user settings are being observed by a “reloadable property configuration”. If the user edits the settings file, the application will notice and reload the settings.

Tasked with implementing such a feature for a fairly straight-forward YAML file, I came across Apache Commons Configuration2. It is super-powerful, with stuff like JNDI, JDBC, XML, .properties format etc. Of which which we don’t need a single one. There’s a section on reloading, with configurable strategies, serialization managers and whatnot.

What we’re looking for:

  • Simply de-/serialization between YAML and Java bean
  • allows to serialize/store settings from Java bean to YAML, ie. the user edits settings within the application
  • allows to reload settings when the user edits the settings file directly

Luckily Jackson now provides a YAML extension

<dependency>
  <groupId>.fasterxml.jackson.dataformat</groupId>
  <artifactId>-dataformat-yaml</artifactId>
</dependency>

and Java NIO has the very handy java.nio.file.WatchService, so we come up with

/*
 * Created on 20 Jul 2018
 */
package ch.want.demos;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;

@Component
public class UserPropertiesManager {

    private static final Logger LOG = LoggerFactory.getLogger(UserPropertiesManager.class);
    @Autowired
    private UserProperties userProperties; // this is our configuration Java bean
    private final ObjectMapper mapper;
    private File propertiesFile;

    public UserPropertiesManager() {
        mapper = new ObjectMapper(new YAMLFactory());
    }

    @PostConstruct
    public void init() {
        initPropertyFileReference();
        readPropertiesFromFile();
    }

    private void initPropertyFileReference() {
        propertiesFile = new File("config/settings.yml");
    }

    private void readPropertiesFromFile() {
        LOG.debug("Reading configuration from {}", propertiesFile);
        try {
            final UserProperties newProperties = mapper.readValue(propertiesFile, UserProperties.class);
            this.userProperties.copyFrom(newProperties);
            new Thread(new ConfigurationEditWatcher()).start();
        } catch (final IOException e) {
            LOG.error(e.getMessage(), e);
        }
    }

    public void writePropertiesToFile() {
        LOG.debug("Storing configuration to {}", propertiesFile);
        try {
            mapper.writeValue(propertiesFile, userProperties);
        } catch (final IOException e) {
            LOG.error(e.getMessage(), e);
        }
    }

    private class ConfigurationEditWatcher implements Runnable {

        private final WatchService watcher;
        private final Path configDir;

        ConfigurationEditWatcher() throws IOException {
            watcher = FileSystems.getDefault().newWatchService();
            configDir = Paths.get(propertiesFile.getParentFile().toURI());
            configDir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
        }

        @Override
        public void run() {
            try {
                WatchKey key;
                while ((key = watcher.take()) != null) {
                    processWatchKey(key);
                }
            } catch (final InterruptedException | IOException ex) { // NOSONAR
                LOG.info("Terminating WatchService on configuration file");
            }
        }

        @SuppressWarnings("unchecked")
        private void processWatchKey(final WatchKey key) throws IOException {
            for (final WatchEvent event : key.pollEvents()) {
                if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                    processWatchEvent((WatchEvent) event);
                }
            }
            key.reset();
        }

        private void processWatchEvent(final WatchEvent pathEvent) throws IOException {
            final Path filename = pathEvent.context();
            final Path modifiedFile = configDir.resolve(filename);
            if (Files.isSameFile(modifiedFile, propertiesFile.toPath())) {
                readPropertiesFromFile();
            }
        }
    }
}

All we wanted in one simple class. Hope this helps whoever’s reading this.


In the process of developing funnel.travel, a corporate post-booking travel management tool, I’m sharing some hopefully useful insights into Angular 4, Spring Boot, jOOQ, or any other technology we’ll be using.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s