FileWatcherReloadingTrigger for Apache Commons configuration2

Apache Commons configuration2 lets you deal with configurations from many sources and formats.

If you want to be able to edit your config file and have your application pick up the changed config without restart, configuration2 provides a flexible approach, but only a somewhat unsatisfying trigger implementation (PeriodicReloadingTrigger), that uses polling to detect config file changes.

Below is my FileWatcherReloadingTrigger class that uses a WatchService to listen for change events from the underlying filesystem and avoids polling. This way your application can almost immediately use the changed configuration.

It can be used in basically the same way as the PeriodicReloadingTrigger, as described by the configuration2 user guide:

Parameters params = new Parameters();
// Read data from this file
File propertiesFile = new File("config.properties");

ReloadingFileBasedConfigurationBuilder<FileBasedConfiguration> builder =
    new ReloadingFileBasedConfigurationBuilder<FileBasedConfiguration>(PropertiesConfiguration.class)
    .configure(params.fileBased()
        .setFile(propertiesFile));
FileWatcherReloadingTrigger trigger = new FileWatcherReloadingTrigger(builder.getReloadingController(),
    null, propertiesFile.toPath());
trigger.start();

And this is the FileWatcherReloadingTrigger class. It uses slf4j-api for logging:

package org.guppy4j.config;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.configuration2.reloading.ReloadingController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public final class FileWatcherReloadingTrigger {

	private final Logger logger = LoggerFactory.getLogger(getClass());

	private final ExecutorService executorService;
	private final WatchService watchService;

	private final ReloadingController controller;
	private final Object controllerParameter;

	private final Path configFilePath;

	private Future<?> execution;

	FileWatcherReloadingTrigger(ReloadingController controller, Object controllerParameter, Path configFilePath) {
		Objects.requireNonNull(controller, "Reloading controller must not be null");
		Objects.requireNonNull(configFilePath, "The path for the configuration file must not be null");
		this.configFilePath = configFilePath.toAbsolutePath();
		this.controller = controller;
		this.controllerParameter = controllerParameter;
		try {
			watchService = FileSystems.getDefault().newWatchService();
			this.configFilePath.getParent().register(watchService, ENTRY_MODIFY);
		} catch (IOException e) {
			throw new IllegalStateException("Could not set up WatcherService for config file reloads", e);
		}
		executorService = Executors.newSingleThreadExecutor();
	}

	public synchronized void start() {
		if (executorService.isShutdown()) {
			throw new IllegalStateException("Already shut down");
		}
		if (execution == null || execution.isCancelled()) {
			execution = executorService.submit(this::watchForFileChanges);
			logger.info("Execution started. Watching for file changes of {}", configFilePath);
		}
	}

	public synchronized void stop() {
		if (execution != null && !execution.isCancelled()) {
			execution.cancel(true);
			execution = null;
			logger.info("Execution stopped. No longer watching for file changes of {}", configFilePath);
		}
	}

	private void watchForFileChanges() {
		try {
			for (WatchKey watchKey = watchService.take(); watchKey != null; watchKey = watchService.take()) {
				for (WatchEvent<?> event : watchKey.pollEvents()) {
					if (ENTRY_MODIFY.equals(event.kind())) {
						final Path filename = Path.class.cast(event.context());
						if (filename.equals(configFilePath.getFileName())) {
							controller.checkForReloading(controllerParameter);
						}
					}
				}
				watchKey.reset();
			}
		} catch (InterruptedException e) {
			// nothing to do
		}
	}

	@Override
	public synchronized void shutdown() {
		try {
			stop();
		} catch (RuntimeException e) {
			logger.warn("Exception while shutting down", e);
		}
		try {
			executorService.shutdown();
		} catch (RuntimeException e) {
			logger.warn("Exception while shutting down executorService", e);
		}
		try {
			watchService.close();
		} catch (IOException e) {
			logger.warn("Exception while shutting down watchService", e);
		}
	}
}

Cleanbrowsing DNS + dnsmasq

I have children and I want to keep porn out of our home network.

Cleanbrowsing DNS provides a free “Family” filter. You can set your router to use their DNS servers. It seems quite good in comparison to other DNS filters:

However, their filter is sometimes a little strict and even blocks reddit.com and, curiously, the Haligonian event magazine website thecoast.ca.

On the other hand, it does not block user-uploaded images on Twitter, and its many porn peddling accounts.

So basically what I wanted was a configurable whitelist and blacklist on top of the Cleanbrowsing Family filter.

To do that, I installed dnsmasq on a Linux server in our network and configured the DHCP server on our router to give out the IP address of that Linux box as DNS server, effectively directing all machines on our home network to get their DNS from the dnsmasq installation.

I configured dnsmasq as a proxy that by default passes on all DNS request to the Cleanbrowsing Family filter:

This is the content of /etc/dnsmasq.d/cleanbrowsing.conf :

# ignore /etc/resolv.conf
no-resolv
no-poll

# use cleanbrowsing family nameservers as default
server=185.228.168.168
server=185.228.169.168

This is currently the content of /etc/dnsmasq.d/whitelist.conf :

server=/reddit.com/1.1.1.1
server=/thecoast.ca/1.1.1.1

This is currently the content of /etc/dnsmasq.d/blacklist.conf :

# block twitter user media servers (porn and tracker pixels)
server=/pbs.twimg.com/
server=/video.twimg.com/

Whenever I edit any of the above I have to restart the dnsmasq service.

The files in /etc/dnsmasq.d are read by default on my Debian GNU/Linux. If you use a different distro you might have to adjust /etc/dnsmasq.conf accordingly, look for the “conf-dir” directive.

If your router allows you to configure outgoing firewall rules, block all DNS requests from anywhere but the dnsmasq server, to prevent a savvy teenager from bypassing your DNS filter.