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

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.

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

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.

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.

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.

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.

Laszlo Csontos
 

I've been coding since the age of 9. I knew from childhood that all I wanted to do was code. Now I've been coding for 25 years, with Java for 18 years and professionally for 13 years. During past projects I worked in various roles as a consultant, developer, mentor, team leader and architect. My focus areas have been database- oriented back-end applications, performance tuning techniques and distributed systems. In the last 3 years, I specialized in building microservices with the Spring Ecosystem and also contributed to some of its sub-projects. The newest venture of mine is the creation of craftingjava.com, which aims at helping young software engineers learn Spring.

Leave a Reply

avatar
  Subscribe  
Notify of