Event support
The HotReload is a cool feature, however it would be nice to know which properties has changed so that you can re-configure only the affected services.
This is why OWNER implements a feature rich event system that allows to know when a reload event occurs, when a property is changed, and lets you to check if the new value is compliant to your application requirements and, if not, you can rollback the property change or the entire change set.
Listening for Reloads
As explained in Reload and Hot Reload section, OWNER supports programmatic reload and automatic reload (when a configuration file is changed). In both cases a reload event is generated.
To get notified when a reload event occurs, your interface must extend Reloadable
that exposes some methods to
register a ReloadListener
.
There is also the possibility to implement a TransactionalReloadListener
which does notify you before the reload
becomes effective and allows you to check what is changed and eventually to rollback the reload operation if the
changes you see are not compliant to your application’s requirements.
The Reloadable
interfaces allows to attach and remove ReloadListener
objects.
The ReloadListener
interface defines a method called reloadPerformed()
that receives a ReloadEvent
which contains
the oldProperties
(before the reload) and the newProperties
(after the reload) plus a list of PropertyChangeEvent
for every property which is changed.
The PropertyChangeEvent
contains the propertyName
, the oldValue
and the newValue
.
When calling the method Reloadable.addReloadListener()
it is possible to pass an instance of
TransactionalReloadListener
which extends the ReloadListener
adding the method beforeReload()
.
This listener receives the notification just after the reload is triggered and before the changes are effective. In this
way it is possible for this listener to check what is changed and, if the changes are not welcome, it can trigger the
exception RollbackBatchException
which means that all the changes in the reload will be discarded, and the current
values will be kept.
If the RollbackBatchException
is thrown, the ReloadListener.reloadPerformed()
will not be invoked, since the reload
has been aborted by the listener.
Example:
@Sources("Example.properties")
interface MyConfig extends Reloadable {
@DefaultValue("5")
Integer someInteger();
@DefaultValue("foobar")
String someString();
@DefaultValue("3.14")
Double someDouble();
String nullsByDefault();
}
MyConfig cfg = ConfigFactory.create(MyConfig.class);
final boolean[] reloadPerformed = new boolean[] {false};
cfg.addReloadListener(new TransactionalReloadListener() {
public void beforeReload(ReloadEvent event)
throws RollbackBatchException {
String notAllowedValue = "42";
String newSomeInteger = event.getNewProperties()
.getProperty("someInteger");
// 42 makes the reload to rollback completely!
if (notAllowedValue.equals(newSomeInteger))
throw new RollbackBatchException(
"42 is not allowed for property 'someInteger'");
}
public void reloadPerformed(ReloadEvent event) {
reloadPerformed[0] = true;
}
});
// we update the properties file in the filesystem
File target = new File("Example.properties");
save(target, new Properties() { {
setProperty("someInteger", "41");
setProperty("someString", "bazbar");
setProperty("someDouble", "2.718");
setProperty("nullsByDefault", "NotNullNow");
}});
cfg.reload();
// reload happened
assertTrue(reloadPerformed[0]);
assertEquals(new Integer(41), cfg.someInteger());
assertEquals("bazbar", cfg.someString());
assertEquals(new Double("2.718"), cfg.someDouble());
assertNotNull(cfg.nullsByDefault());
reloadPerformed[0] = false; // reset the flag
save(target, new Properties() { {
setProperty("someInteger", "42"); // not allowed!
setProperty("someString", "blahblah");
setProperty("someDouble", "1.234");
}});
cfg.reload();
// reload was rolled back since 42 is not allowed
assertFalse(reloadPerformed[0]);
assertEquals(new Integer(41), cfg.someInteger());
assertEquals("bazbar", cfg.someString());
assertEquals(new Double("2.718"), cfg.someDouble());
assertNotNull(cfg.nullsByDefault());
Listening for PropertyChanges
A PropertyChangeEvent
happens every time the user changes a property using a method declared in the Mutable
interface, or when a reload happens.
To get notified when a property change event occurs, your interface must extend Mutable
that exposes some methods to
register a PropertyChangeListener
.
There is also the possibility to implement a TransactionalPropertyChangeListener
which does notify you before the
property change becomes effective and allows you to check what is changed and eventually to rollback the single
property change operation or the whole set of changes triggered by the source event.
The Mutable
interfaces allows to attach and remove PropertyChangeListener
objects. It is possible to attach
a listener to a specific property name, so that the listener will be associated to a single property name:
public interface Mutable extends Config {
void addPropertyChangeListener(String propertyName, PropertyChangeListener listener);
void addPropertyChangeListener(PropertyChangeListener listener);
void removePropertyChangeListener(PropertyChangeListener listener);
// ...the rest of the interface is cut...
}
The Mutable
interface defines methods to change the internal values of the properties as well, all of them will
trigger a PropertyChangeEvent
and notify the registered listeners:
public interface Mutable extends Config {
String setProperty(String key, String value);
String removeProperty(String key);
void clear();
void load(InputStream inStream) throws IOException;
void load(Reader reader) throws IOException;
// ...the rest of the interface is cut...
}
The PropertyChangeListener
interface comes from the java.beans
package and defines a method called
propertyChange()
that receives a PropertyChangeEvent
which contains the propertyName
, the oldValue
and
the newValue
.
When calling the method Mutable.addPropertyChangeListener()
it is possible to pass an instance of
TransactionalPropertyChangeListener
which extends the PropertyChangeListener
adding the method
beforePropertyChange()
.
This listener receives the notification just after the change is triggered but before the changes become effective.
In this way it is possible for this listener to check what is changed and, if the changes are not welcome, the listener
can trigger the exception RollbackOperationException
or RollbackBatchException
.
Throwing RollbackOperationException
means that the listener wants to rollback the single property change, even though
the property change may have been triggered due to an event who involves many property changes, and only the single
property change needs to be rolled back.
Instead the RollbackBatchException
means that all the changes triggered by the source event must be rolled back, and
the previous values must be kept.
If the any of the two RollbackException
explained above is thrown, the PropertyChangeListener.propertyChange()
will not be invoked, since the change has been aborted by the listener.
Obviously also the reload()
operation happenning during a hot reload, or when invoked via the Reloadable
interface
triggers the PropertyChangeEvent.
Example:
@Sources("Example.properties")
interface MyConfig extends Reloadable {
@DefaultValue("5")
Integer someInteger();
@DefaultValue("foobar")
String someString();
@DefaultValue("3.14")
Double someDouble();
String nullsByDefault();
}
MyConfig cfg = ConfigFactory.create(MyConfig.class);
final boolean[] reloadPerformed = new boolean[] {false};
cfg.addPropertyChangeListener("someInteger",
new TransactionalPropertyChangeListener() {
public void beforePropertyChange(PropertyChangeEvent event)
throws RollbackOperationException, RollbackBatchException {
String notAllowedValue = "88";
String makesEverythingToRollback = "42";
String newSomeInteger = (String)event.getNewValue();
if (notAllowedValue.equals(newSomeInteger))
throw new RollbackOperationException(
"88 is not allowed for property 'someInteger', " +
"the single property someInteger is rolled back");
if (makesEverythingToRollback.equals(newSomeInteger))
throw new RollbackBatchException(
"42 is not allowed for property 'someInteger', " +
"the whole event is rolled back");
}
public void propertyChange(PropertyChangeEvent evt) {
reloadPerformed[0] = true;
}
});
save(target, new Properties() { {
setProperty("someInteger", "41");
setProperty("someString", "bazbar");
setProperty("someDouble", "2.718");
setProperty("nullsByDefault", "NotNullNow");
}});
cfg.reload();
assertTrue(reloadPerformed[0]);
assertEquals(new Integer(41), cfg.someInteger());
assertEquals("bazbar", cfg.someString());
assertEquals(new Double("2.718"), cfg.someDouble());
assertNotNull(cfg.nullsByDefault());
reloadPerformed[0] = false;
cfg.setProperty("someInteger", "55");
assertTrue(reloadPerformed[0]);
assertEquals(new Integer(55), cfg.someInteger());
reloadPerformed[0] = false;
cfg.setProperty("someInteger", "88");
// 88 is rolled back.
assertFalse(reloadPerformed[0]);
assertEquals(new Integer(55), cfg.someInteger());
reloadPerformed[0] = false;
save(target, new Properties() { {
setProperty("someInteger", "42");
setProperty("someString", "blahblah");
setProperty("someDouble", "1.234");
}});
cfg.reload();
assertFalse(reloadPerformed[0]);
assertEquals(new Integer(55), cfg.someInteger());
assertEquals("bazbar", cfg.someString());
assertEquals(new Double("2.718"), cfg.someDouble());
assertNotNull(cfg.nullsByDefault());
reloadPerformed[0] = false;
save(target, new Properties() { {
setProperty("someInteger", "88");
setProperty("someString", "this is not rolled back");
setProperty("someDouble", "1.2345");
}});
cfg.reload();
assertFalse(reloadPerformed[0]);
// only someInteger=88 is rolled back
assertEquals(new Integer(55), cfg.someInteger());
assertEquals("this is not rolled back", cfg.someString());
assertEquals(new Double("1.2345"), cfg.someDouble());
assertNull(cfg.nullsByDefault());
Conclusions
With ReloadListener
and PropertyChangeListener
it is possible to get notified when your configuration changes.
This allows your application to take actions subsequently.
With TransactionalReloadListener
and TransactionalPropertyChangeListener
you can also check if the changes being
applied are coherent to your requirements and eventually rollback the single property change or the
complete set of changes, keeping things as they where before a reload.
This is a mechanism which allows for some basic validation. A more simple and powerful validation mechanism is planned for the future releases, and the event mechanism will be probably the backbone of this.