Skip to content

Migration service

ljacqu edited this page Aug 12, 2023 · 6 revisions

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

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.

Checking that all properties are present

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.

Renaming a property

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 from MigrationUtils:

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:

  1. Check with the reader if there is a value at the old path. If not, return false – there's nothing to migrate.
  2. 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 the ConfigurationData.)
  3. 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.

Migrating a property to a different type

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.

Implementation examples

Quiz questions

  • 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 »