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

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

DZone's Guide to

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

In this third part, we'll explore one of those technical challenges in detail that the application's modular design raises and how it can be tackled by using Spring Boot 2's new property Binder API.

· Integration Zone ·
Free Resource

SnapLogic is the leading self-service enterprise-grade integration platform. Download the 2018 GartnerMagic Quadrant for Enterprise iPaaS or play around on the platform, risk free, for 30 days.

In my previous article, 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.

What Will You Learn After Reading This Article?

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 deciding 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 data handler dynamically. Eventually, I stumbled upon OAuth2ClientRegistrationRepositoryConfiguration, and it gave me 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) and they 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, you probably 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.

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 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.

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 only a single property (API key), it suffices 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 and configuring the child application context. I'll get to that in the next article.

Conclusion

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

  • It comes in very handy when you want to bind an 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.

Next In This Series

In the next article, 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.

With SnapLogic’s integration platform you can save millions of dollars, increase integrator productivity by 5X, and reduce integration time to value by 90%. Sign up for our risk-free 30-day trial!

Topics:
spring boot 2 ,spring configuration ,integration ,gdpr ,gdpr compliance

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}