Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

GDPR Forget-Me App (Part 3): Conditional Configuration With Spring Boot 2

DZone's Guide to

GDPR Forget-Me App (Part 3): Conditional Configuration With Spring Boot 2

Need help with technical difficulties in the GDPR Forget-Me app? Here's how to fix those problems with the conditional configuration method in Spring Boot 2.

· Java Zone ·
Free Resource

Automist automates your software deliver experience. It's how modern teams deliver modern software.

In the previous part, I explained message flows in detail by implementing inbound 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.

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.

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 the data handler dynamically. Eventually, I stumbled upon OAuth2ClientRegistrationRepositoryConfiguration and it gave me a few ideas.

The app is going to come with a fixed number of built-in modules (data handlers or adapters as I sometimes refer to them). There are pre-configured with both metadata and runtime configuration data. Here is an example of a configuration:

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, you probably noticed that this piece of configuration looks very similar.

The first part (registration) of this configuration 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.

Here is how this is going to work. When the 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 the DATA_HANDLER_PROVIDER_BINDABLE, which is basically a mapping between a set of properties and a binding definition. The framework returns a BindResultobject. Although it resembles its well-known counterpart BindingResult from Spring MVC, it also embraces lambdas. You can use it in a similar way that you would with java.util.Optional.

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 AbstractDataHandlerConfiguredConditionwhere they can define which constellation of provider properties that particular data handler should enable. In this case, MailerLite only has a single property (the API key), as long as 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 that enabled only when there’s was an API key set. Most of the heavy lifting is done by AbstractDataHandlerFlowConfig in terms of creating and configuring the child application context.

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 an arbitrary set of key-value pairs without knowing 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.

Get the open source Atomist Software Delivery Machine and start automating your delivery right there on your own laptop, today!

Topics:
java ,tutorial ,string ,configuration ,gdpr ,spring boot 2

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}