-
Notifications
You must be signed in to change notification settings - Fork 16
Migration service
As you develop your application, you may have to add, rename or modify properties in your configuration. With ConfigMe, you can define a migration service that allows you to migrate old properties to newer representations without losing the user's values from the previous configuration.
- Using a migration service
- Checking that all properties are present
- Renaming a property
- Migrating a property to a different type
- Implementation examples
The MigrationService
is an interface with one method:
boolean checkAndMigrate(PropertyReader reader, ConfigurationData configurationData);
The method is called by the settings manager and receives two arguments:
- a
PropertyReader
—a read-only view of the resource (i.e. the config file) - the modifiable
ConfigurationData
, with which we can modify the value of the properties.
The boolean
returned from the method indicates to the settings manager whether the configuration should be saved to
the property resource. true
tells the migration service it needs to save the configuration, false
signals that
nothing needs to be done.
You can pass your migration service to the settings manager builder:
MigrationService migrationService = new MyAppMigrationService();
SettingsManager settingsManager = SettingsManagerBuilder
.withYamlFile(configFile)
.configurationData(TitleConfig.class)
.migrationService(migrationService) // use my migration service
.create();
Note that application users typically extend from the PlainMigrationService
class, which offers some utilities.
If a property is missing in the configuration file, ConfigMe simply takes the default value. Most likely, you will gradually add new properties to your configuration as you work on your application, and you'd like all existing config files of your users to be extended with those properties as they update. How else will they find out about the new properties?
Fortunately, PlainMigrationService checks exactly that: it ensures that all properties are in the configuration file, and if not, it will trigger a write as to complete it. It is therefore recommended to use PlainMigrationService or to extend this class, instead of directly implementing the MigrationService interface.
The plain migration service can be used by calling useDefaultMigrationService()
on the settings manager builder when
you create your settings manager.
You might also want to rename a property in the course of development (restructuring, fix typo). For example, let's assume we have this property:
public static final Property<Boolean> LOG_ERRORS =
newProperty("system.logErrorOccurance", true);
D'oh!, we misspelled "occurrence"! (Don't worry, we aren't the first to do so.) So we fix it in the code:
public static final Property<Boolean> LOG_ERRORS =
newProperty("system.logErrorOccurrence", true);
Unfortunately, if we use the PlainMigrationService, it means that the old value the user may have configured will be
lost since the property now has a new path. However, we can easily rename properties by extending the
PlainMigrationService and using the method moveProperty
which it conveniently provides:
public class MyMigrationService extends PlainMigrationService {
@Override
protected boolean performMigrations(PropertyReader reader,
ConfigurationData configurationData) {
Property<Boolean> oldProperty = newProperty("system.logErrorOccurance", true);
return moveProperty(oldProperty, LOG_ERRORS, reader, configurationData);
}
}
We just provide the old property definition, the new one, and ConfigMe will decide whether it can move an old value
to a new one, provided there's no value at the new path. Let's see what moveProperty
does:
protected static <T> boolean moveProperty(Property<T> oldProperty, Property<T> newProperty,
PropertyReader reader, ConfigurationData configurationData) {
if (reader.contains(oldProperty.getPath())) {
if (!reader.contains(newProperty.getPath())) {
PropertyValue<T> value = oldProperty.determineValue(reader);
configurationData.setValue(newProperty, value.getValue());
}
return true;
}
return false;
}
We see that it does the following:
- Check with the reader if there is a value at the old path. If not, return
false
– there's nothing to migrate. - Check with the reader whether we do NOT have a value at the new path. If there is one, we don't do anything:
we don't want to overwrite the existing value, even if there is still a value at the old path. (But note that the
method still returns
true
in this case, signaling that the config file should be saved again. By writing to the resource, the old path will be removed automatically, since it's not part of theConfigurationData
.) - If we have a value at the old path and no value at the new one, it's migration time! We query the old property for its value based on the reader, and we set exactly that to the new property on the configuration data. Note that if the value is invalid at the old path, the default value of the property will be taken.
As an example, let's say we have a property that defines whether we have a dark or light mode in the application:
public static final Property<Boolean> USE_LIGHT_MODE =
newProperty("appearance.light", true);
Now, we've improved the style mechanism and want to allow an adjustable brightness. We therefore remove the old property
and replace it with a double
property:
public static final Property<Double> BRIGHTNESS_GRADIENT =
newProperty("appearance.brightness", 1.0);
We want to respect the user's previous setting and transfer it to the new brightness gradient property. In other words,
if the configuration currently has true
for the light mode, we want to set 1.0 to the new property, and otherwise
we want to set 0.0 for a false
value. We can do this the following way:
public class AppMigrationService extends PlainMigrationService {
@Override
protected boolean performMigrations(PropertyReader reader,
ConfigurationData configurationData) {
if (!reader.contains(BRIGHTNESS_GRADIENT.getPath())) {
Boolean oldUseLightModeValue = reader.getBoolean("appearance.light");
if (oldUseLightModeValue != null) {
double newGradientValue = oldUseLightModeValue ? 1.0 : 0.0;
configurationData.setValue(BRIGHTNESS_GRADIENT, newGradientValue);
}
return MIGRATION_REQUIRED;
}
return NO_MIGRATION_NEEDED;
}
}
- We check whether there is already a value for the BRIGHTNESS_GRADIENT property. If so, there is nothing to migrate.
- Otherwise, we try to retrieve the boolean value at the old path from the reader. Note that it may be null.
- If it is not null (i.e. there is a value in the file and it is valid), we convert it to the corresponding double value for the new property and set it onto the ConfigurationData.
- Finally, with the return value (using constants from
MigrationService
for readability) we indicate to ConfigMe whether the configuration should be saved again.
Notice how similar the flow is from the moveProperty
method we looked at previously. Most migrations have logic
similar to this.
- Complex real-life example: AuthMe's migration service
- Simpler Kotlin example: PerWorldInventory-kt migration service
- Is it needed to save the configuration in the migration service if changes have been performed?
- What happens if the migration service changes the configuration data but returns
false
from its main method?
Navigation
« The settings manager | Migration service | Bean properties » |
Guide
- Introduction
- Getting started
- Migration service
- Bean properties
- Custom property types
- Technical documentation
Updating
Development (internal)