Config Stores
Configuration files are stored in entities called Stores.
The methods that need to be implemented on a store are:
Read is self-explanatory:
public String read(String name) {
// Return the contents of the specified configuration.
}
Write is slightly trickier:
public String write(String name, String oldContents, String newContents) {
// If the old contents match the current stored contents, the new contents will get stored,
// and the method returns null indicating success.
// If the old contents DO NOT match the current stored contents (i.e. it was modified in some way),
// the new contents are NOT stored, and the method returns the current stored contents.
// If the old contents are null, then just always write the new contents.
}
The update method is called whenever the stored file gets modified externally:
public String update(String name, String newContents) {
// Do something with the updated contents.
}
Two configuration stores are provided by default:
The store is defined on the Config object via the following setting:
// Create a config with in-memory storage.
Config config = Config.create("MyConfig.cfg").store(MemoryStore.DEFAULT).build();
The default store used is FileStore.DEFAULT which defines the execution directory as the file system directory to store and retrieve files.
MemoryStore
The MemoryStore class is simply an in-memory storage location for configuration files.
There is no hard persistence and is used primarily for testing purposes. However, the implementation provides a good idea on how stores work (especially the write method):
public class MemoryStore extends ConfigStore {
// Some methods ommitted.
private final ConcurrentHashMap cache = new ConcurrentHashMap();
@Override /* ConfigStore */
public synchronized String read(String name) {
return emptyIfNull(cache.get(name));
}
@Override /* ConfigStore */
public synchronized String write(String name, String expectedContents, String newContents) {
// This is a no-op.
if (isEquals(expectedContents, newContents))
return null;
String currentContents = read(name);
if (expectedContents != null && ! isEquals(currentContents, expectedContents))
return currentContents;
update(name, newContents);
// Success!
return null;
}
@Override /* ConfigStore */
public synchronized MemoryStore update(String name, String newContents) {
cache.put(name, newContents);
super.update(name, newContents); // Trigger any listeners.
return this;
}
}
FileStore
The FileStore is the typical store used for configuration files.
It provides the following configurable settings:
// Create a config store with a watcher thread and high sensitivity.
FileStore fileStore = FileStore
.create()
.directory("configs")
.enableWatcher()
.watcherSensitivity(HIGH)
.build();
// Create a config using the store defined above.
Config config = Config
.create("MyConfig.cfg")
.store(fileStore)
.build();
Custom config stores
The ConfigStore API has been written to allow easy development of custom configuration storage classes.
The example below shows a starting point for an implementation based on polling a relational database.
The source can be found here: SqlStore.
Completing it is left as an exercise:
public class SqlStore extends ConfigStore {
private final String jdbcUrl;
private final String tableName, nameColumn, valueColumn;
private final Timer watcher;
private final ConcurrentHashMap cache = new ConcurrentHashMap();
protected SqlStore(ConfigStore.Builder builder) {
super(builder);
this.jdbcUrl = builder.jdbcUrl;
this.tableName = builder.tableName;
this.nameColumn = builder.nameColumn;
this.valueColumn = builder.valueColumn;
int pollInterval = builder.pollInterval;
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
SqlStore.this.poll();
}
};
this.watcher = new Timer("MyTimer");
watcher.scheduleAtFixedRate(timerTask, 0, pollInterval * 1000);
}
synchronized void poll() {
// Loop through all our entries and find the latest values.
for (Map.Entry e : cache.entrySet()) {
String name = e.getKey();
String cacheContents = e.getValue();
String newContents = getDatabaseValue(name);
// Change detected!
if (! cacheContents.equals(newContents))
update(name, newContents);
}
}
// Reads the value from the database.
protected String getDatabaseValue(String name) {
// Implement me!
return null;
}
@Override /* ConfigStore */
public boolean exists(String name) {
// Implement me!
return false;
}
@Override /* ConfigStore */
public synchronized String read(String name) {
String contents = cache.get(name);
if (contents == null) {
contents = getDatabaseValue(name);
update(name, contents);
}
return contents;
}
@Override /* ConfigStore */
public synchronized String write(String name, String expectedContents, String newContents) {
// This is a no-op.
if (StringUtils.eq(expectedContents, newContents))
return null;
String currentContents = read(name);
if (expectedContents != null && StringUtils.ne(currentContents, expectedContents))
return currentContents;
update(name, newContents);
// Success!
return null;
}
@Override /* ConfigStore */
public synchronized SqlStore update(String name, String newContents) {
cache.put(name, newContents);
super.update(name, newContents); // Trigger any listeners.
return this;
}
@Override /* Closeable */
public synchronized void close() {
if (watcher != null)
watcher.cancel();
}
}
Config store listeners
The ConfigStore class has the following listener methods:
Note that this is a different listener than ConfigEventListener. In this case, we're just listening for changed files:
This listener is used by the Config class to listen for changes on the file system so that it can be updated in
real-time.
In This Section
📄️ 2.5.9.1. ClasspathStore
The ClasspathStore reads configuration files