Process Spring Boot externalized property values

If finally got this to work. (Mainly thanks to stephane-deraco on github)

Key to the solution is a class that implements ApplicationContextInitializer<ConfigurableApplicationContext>. I called it PropertyPasswordDecodingContextInitializer.

The main problem was to get spring to use this ApplicationContextInitializer. Important information can be found in the reference. I chose the approach using a META-INF/spring.factories with following content:

org.springframework.context.ApplicationContextInitializer=ch.mycompany.myproject.PropertyPasswordDecodingContextInitializer

The PropertyPasswordDecodingContextInitializer uses a PropertyPasswordDecoder and an implementing class, currently for simplicity a Base64PropertyPasswordDecoder.

PropertyPasswordDecodingContextInitializer.java

package ch.mycompany.myproject;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;

@Component
public class PropertyPasswordDecodingContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    private static final Pattern decodePasswordPattern = Pattern.compile("password\\((.*?)\\)");

    private PropertyPasswordDecoder passwordDecoder = new Base64PropertyPasswordDecoder();

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        for (PropertySource<?> propertySource : environment.getPropertySources()) {
            Map<String, Object> propertyOverrides = new LinkedHashMap<>();
            decodePasswords(propertySource, propertyOverrides);
            if (!propertyOverrides.isEmpty()) {
                PropertySource<?> decodedProperties = new MapPropertySource("decoded "+ propertySource.getName(), propertyOverrides);
                environment.getPropertySources().addBefore(propertySource.getName(), decodedProperties);
            }
        }
    }

    private void decodePasswords(PropertySource<?> source, Map<String, Object> propertyOverrides) {
        if (source instanceof EnumerablePropertySource) {
            EnumerablePropertySource<?> enumerablePropertySource = (EnumerablePropertySource<?>) source;
            for (String key : enumerablePropertySource.getPropertyNames()) {
                Object rawValue = source.getProperty(key);
                if (rawValue instanceof String) {
                    String decodedValue = decodePasswordsInString((String) rawValue);
                    propertyOverrides.put(key, decodedValue);
                }
            }
        }
    }

    private String decodePasswordsInString(String input) {
        if (input == null) return null;
        StringBuffer output = new StringBuffer();
        Matcher matcher = decodePasswordPattern.matcher(input);
        while (matcher.find()) {
            String replacement = passwordDecoder.decodePassword(matcher.group(1));
            matcher.appendReplacement(output, replacement);
        }
        matcher.appendTail(output);
        return output.toString();
    }

}

PropertyPasswordDecoder.java

package ch.mycompany.myproject;

public interface PropertyPasswordDecoder {

    public String decodePassword(String encodedPassword);

}

Base64PropertyPasswordDecoder.java

package ch.mycompany.myproject;

import java.io.UnsupportedEncodingException;

import org.apache.commons.codec.binary.Base64;

public class Base64PropertyPasswordDecoder implements PropertyPasswordDecoder {

    @Override
    public String decodePassword(String encodedPassword) {
        try {
            byte[] decodedData = Base64.decodeBase64(encodedPassword);
            String decodedString = new String(decodedData, "UTF-8");
            return decodedString;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }


}

Mind you, the ApplicationContext has not finished initialized at this stage, so autowiring or any other bean related mechanisms won't work.


Update: Included @jny's suggestions.


I used @Daniele Torino's answer and made several minor changes.

First, thanks to his link to the options on how to make spring recognize Initializer, I chose to do it in the Application:

public static void main(String[] args) throws Exception {
    SpringApplication application=new SpringApplication(Application.class);
    application.addInitializers(new PropertyPasswordDecodingContextInitializer());
    application.run(args);
}

Second, IDEA told me that that else if (source instanceof CompositePropertySource) { is redundant and it is because CompositePropertySource inherits from EnumerablePropertySource.

Third, I beleive there is a minor bug: it messes up the order of property resolution. If you have one encoded property in environment, and another one in application.properties file the environment value will be overwritten with the application.properties value. I changed the logic to insert the decodedProperties right before encoded:

        for (PropertySource<?> propertySource : environment.getPropertySources()) {
                Map<String, Object> propertyOverrides = new LinkedHashMap<>();
                decodePasswords(propertySource, propertyOverrides);
                if (!propertyOverrides.isEmpty()) {
                       environment.getPropertySources().addBefore(propertySource.getName(), new MapPropertySource("decoded"+propertySource.getName(), propertyOverrides));
                }
        }

Just use https://github.com/ulisesbocchio/jasypt-spring-boot, works out of the box