GDPR forget-me app (Part 3): Conditional configuration with Spring Boot 2

June 26, 2018 · 5 min read – Last updated on October 26, 2019

In the previous part I explained one of the messages flows in detail from the point of view of implementing in- and outbound messaging with Spring Integration’s AMQP support. I briefly mentioned that data handler adapters are loaded dynamically and they’re plugged into the message flow. In this third part, we’ll explore one of those technical challenges in detail that the application’s modular design raise and how it can be tackled by using Spring Boot 2’s new property Binder API.

1. What will you learn after reading this part?

For most of the use cases having a predefined, static message flow is sufficient. However, that’s not the case for the forget-me app, as multiple data handlers can be configured which will carry data erasure out. One major challenge to address is to decide whether or not a particular data handler needs to be initialized and plugged into the main message flow. I can tell you beforehand that Spring’s conditional configuration will be used to do that.

You can find the entire source code of the app on GitHub. Be aware however, that the app hasn’t been released yet and I do code reorganizations from time to time.

2. Conditional configuration with Spring Boot 2

I had been searching for a proper solution with regards to configuring and initializing the child application contexts of data handler dynamically. Eventually, I stumbled upon OAuth2ClientRegistrationRepositoryConfiguration and it gave me ideas.

2.1. Yaml configuration

The app is going to come with a fixed number of built-in modules (data handlers or adapters as I sometimes refer to them) and they’re are pre-configured with both meta-data and runtime configuration data. Such a configuration looks like this.

forgetme:
  data-handler:
    registration:
      mailerlite:
        name: mailerlite
        display-name: MailerLite
        description: Email Marketing
        url: https://www.mailerlite.com/
        data-scopes:
            - notification
            - profile
      provider:
        mailerlite:
          api-key: ${MAILERLITE_API_KEY:#{null}}

If you used the new OAuth2 support in Spring Security, probably you noticed that this piece of configuration looks very much alike.

The first part (registration) of this configuration just holds metadata about the data handler, but the second part (provider) may contain arbitrary key value pairs for configuring it. In this case an API key, MailerLite needs only that.

2.2. Binder API

How this is going to work is the following. When MAILERLITE_API_KEY is defined, the corresponding child application context gets loaded, otherwise it remains inactive. As the configuration key/value pairs for individual data handlers cannot be known in advance, Spring Boot 2’s property Binder API is a good fit for loading them.

@Getter
@Setter
public class DataHandlerRegistration {

  static final Bindable<Map<String, String>> DATA_HANDLER_PROVIDER_BINDABLE =
    Bindable.mapOf(String.class, String.class);
  
  static final String DATA_HANDLER_PROVIDER_PREFIX = "forgetme.data-handler.provider";
  
  static final Bindable<Map<String, DataHandlerRegistration>> DATA_HANDLER_REGISTRATION_BINDABLE =
    Bindable.mapOf(String.class, DataHandlerRegistration.class);
  
  static final String DATA_HANDLER_REGISTRATION_PREFIX = "forgetme.data-handler.registration";
  
  private String name;
  private String displayName;
  private String description;
  private URI url;
  private Set<DataScope> dataScopes;
  
  public Optional<URI> getUrl() {
    return Optional.ofNullable(url);
  }
  
  public void validate() {
    Assert.hasText(getName(), "Data handler name must not be empty.");
    Assert.hasText(getDisplayName(), "Data handler display-name must not be empty.");
    Assert.hasText(getDescription(), "Data handler description must not be empty.");
    Assert.notEmpty(getDataScopes(), "Data handler data-scopes must not be empty.");
  }
  
  public enum DataScope {
    ACCOUNT,
    CORRESPONDENCE,
    ENQUIRY,
    NOTIFICATION,
    PROFILE,
    PUBLICATION,
    USAGE;
  }

}

What’s relevant here is DATA_HANDLER_PROVIDER_BINDABLE, which is basically a mapping between a set of properties and a binding definition. The framework returns a BindResult object. Although it resembles to its well-known counterpart BindingResult from Spring MVC, it also embraces lambdas and you can use it in a similar way you would do with java.util.Optional.

2.3. Conditional configuration

public abstract class AbstractDataHandlerConfiguredCondition extends SpringBootCondition {

  private final String dataHandlerName;
  
  public AbstractDataHandlerConfiguredCondition(String dataHandlerName) {
    this.dataHandlerName = dataHandlerName;
  }
  
  @Override
  public ConditionOutcome getMatchOutcome(
    ConditionContext context, AnnotatedTypeMetadata metadata) {
  
    ConditionMessage.Builder message = ConditionMessage
      .forCondition("Data handler configured:", dataHandlerName);
    
    Map<String, String> dataHandlerProperties = getDataHandlerProperties(context.getEnvironment());
    if (isDataHandlerConfigured(dataHandlerProperties)) {
      return ConditionOutcome.match(message.available(dataHandlerName));
    }
    
    return ConditionOutcome.noMatch(message.notAvailable(dataHandlerName));
  }
  
  protected abstract boolean isDataHandlerConfigured(Map<String, String> dataHandlerProperties);
  
  private Map<String, String> getDataHandlerProperties(Environment environment) {
    String propertyName = DATA_HANDLER_PROVIDER_PREFIX + "." + dataHandlerName;
    return Binder.get(environment)
      .bind(propertyName, DATA_HANDLER_PROVIDER_BINDABLE)
      .orElse(Collections.emptyMap());
  }

}

Actual data handler adapters implement AbstractDataHandlerConfiguredCondition where they can define upon which constellation of provider properties should that particular data handler be enabled. In this case, as MailerLite has got only a single property (API key), it’s suffice to determine, if that one contains a non-empty piece of text.

@Configuration
@Conditional(MailerLiteConfiguredCondition.class)
public class MailerLiteFlowConfig extends AbstractDataHandlerFlowConfig {

  static final String DATA_HANDLER_NAME = "mailerlite";
  
  @Override
  protected String getDataHandlerName() {
    return DATA_HANDLER_NAME;
  }
  
  static class MailerLiteConfiguredCondition extends AbstractDataHandlerConfiguredCondition {
  
  public MailerLiteConfiguredCondition() {
    super(DATA_HANDLER_NAME);
  }
  
  @Override
  protected boolean isDataHandlerConfigured(Map<String, String> dataHandlerProperties) {
    return Optional.ofNullable(dataHandlerProperties.get("api-key"))
      .filter(StringUtils::hasText)
      .isPresent();
  }

}

Here you can see MailerLite’s own configuration which is enabled only when there’s an API key set. Most of the heavy lifting is done by AbstractDataHandlerFlowConfig in terms of creating an configuring the child application context. I’ll get to that in the next part.

3. Conclusion

Using conditional configuration with Spring Boot 2’s new property Binder API is a powerful combination.

  • It comes very handy when you want to bind arbitrary set of key value pairs without knowing for sure if they’re present or not.
  • Compared to Environment, it’s much more convenient to use and it also provides an API similar to java.util.Optional.
  • You can even delay the initialization of @ConfigurationProperties annotated configuration, because configuration data through the Binder API is available even before that happens.

4. Next in this series

In the next part we’ll look into how message sub-flows are created within their own child application context and how these child contexts interact with the main message flow.