ConfigProviderConfigurer.java

package dev.orne.config.spring;

/*-
 * #%L
 * Orne Config
 * %%
 * Copyright (C) 2019 - 2025 Orne Developments
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import java.util.Map;

import javax.validation.constraints.NotNull;

import org.apiguardian.api.API;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;

import dev.orne.config.Config;
import dev.orne.config.ConfigProvider;
import dev.orne.config.impl.ConfigProviderImpl;

/**
 * Bean factory post processor that provides a {@code ConfigProvider}
 * customized per Spring context configuration.
 * 
 * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
 * @version 1.0, 2025-11
 * @since 1.0
 */
@API(status = API.Status.INTERNAL, since = "1.0")
public class ConfigProviderConfigurer
implements EnvironmentAware, BeanFactoryPostProcessor, BeanPostProcessor {

    /** The class logger. */
    private static final Logger LOG = LoggerFactory.getLogger(ConfigProviderConfigurer.class);

    /** The name of the automatic {@code Config} bean. */
    public static final String AUTO_CONFIG = "orneConfigAutomaticConfig";
    /** The name of the automatic {@code ConfigProvider} bean. */
    public static final String AUTO_CONFIG_PROVIDER = "orneConfigAutomaticConfigProvider";

    /** The Spring environment. */
    protected Environment environment;
    /** The bean factory. */
    protected ListableBeanFactory beanFactory;
    /** The application provided configuration provider bean name. */
    protected String appConfigProviderBeanName;
    /** The application provided configuration provider customizer bean name. */
    protected String customizerBeanName;
    /** The configuration provider. */
    protected ConfigProvider configProvider;
    /** The created configuration provider. */
    protected ConfigProviderImpl ownConfigProvider;

    /**
     * Creates a new instance.
     */
    public ConfigProviderConfigurer() {
        super();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setEnvironment(
            final Environment environment) {
        this.environment = environment;
    }

    /**
     * Detects application provided {@code ConfigProvider} and
     * {@code ConfigProviderCustomizer} beans.
     * 
     * {@inheritDoc}
     */
    @Override
    public void postProcessBeanFactory(
            final @NotNull ConfigurableListableBeanFactory beanFactory)
    throws BeansException {
        this.beanFactory = beanFactory;
        final String[] providerNames = beanFactory.getBeanNamesForType(ConfigProvider.class);
        final String[] customizerNames = beanFactory.getBeanNamesForType(ConfigProviderCustomizer.class);
        if (providerNames.length > 1) {
            throw new BeanInitializationException(providerNames.length + " implementations of " +
                    "ConfigProvider were found when only 1 is supported. " +
                    "Refactor the configuration such that ConfigProvider is " +
                    "implemented only once or not at all.");
        }
        if (customizerNames.length > 1) {
            throw new BeanInitializationException(customizerNames.length + " implementations of " +
                    "ConfigProviderCustomizer were found when only 1 is supported. " +
                    "Refactor the configuration such that ConfigProvider is " +
                    "implemented only once or not at all.");
        }
        if (providerNames.length == 1) {
            this.appConfigProviderBeanName = providerNames[0];
            if (customizerNames.length == 1) {
                LOG.warn("A ConfigProviderCustomizer was found ({}) but will be ignored " +
                        "because a ConfigProvider implementation is also provided ({}).",
                        customizerNames[0],
                        this.appConfigProviderBeanName);
            }
        } else if (customizerNames.length == 1) {
            this.customizerBeanName = customizerNames[0];
        }
    }

    /**
     * Registers 
     */
    @Override
    public @NotNull Object postProcessAfterInitialization(
            final @NotNull Object bean,
            final @NotNull String beanName)
    throws BeansException {
        if (bean instanceof Config && this.ownConfigProvider != null) {
            LOG.debug("Registering Config bean '{}' initialized after ConfigProvider creation...", beanName);
            this.ownConfigProvider.registerConfig((Config) bean);
        }
        return bean;
    }

    /**
     * Creates and exposes the automatically created {@code Config} bean,
     * if any.
     * 
     * @return The automatically created {@code ConfigProvider} bean.
     */
    public synchronized @NotNull ConfigProvider getConfigProvider() {
        if (this.configProvider == null) {
            if (this.appConfigProviderBeanName == null) {
                LOG.debug("Creating default ConfigProvider bean...");
                this.ownConfigProvider = createConfigProvider();
                this.configProvider = this.ownConfigProvider;
            } else {
                LOG.debug("Using application provided ConfigProvider: {}",
                        this.appConfigProviderBeanName);
                this.configProvider = this.beanFactory.getBean(
                        this.appConfigProviderBeanName,
                        ConfigProvider.class);
            }
        }
        return this.configProvider;
    }

    /**
     * Creates a new configuration provider.
     * 
     * @return The new configuration provider.
     */
    protected @NotNull ConfigProviderImpl createConfigProvider() {
        final Map<String, Config> configs = BeanFactoryUtils.beansOfTypeIncludingAncestors(
                beanFactory,
                Config.class);
        final ConfigProviderImpl provider;
        if (this.customizerBeanName == null) {
            provider = new ConfigProviderImpl(createDefaultConfig());
            configs.values().stream().forEach(provider::registerConfig);
        } else {
            LOG.debug("Using application provided ConfigProvider customizer: {}",
                    this.customizerBeanName);
            final ConfigProviderCustomizer customizer = beanFactory.getBean(
                    this.customizerBeanName,
                    ConfigProviderCustomizer.class);
            provider = new ConfigProviderImpl(
                    customizer.configureDefaultConfig(configs));
            customizer.registerAdditionalConfigs(provider::registerConfig, configs);
        }
        return provider;
    }

    /**
     * Creates a Spring based default configuration.
     * 
     * @return The default configuration.
     */
    protected @NotNull Config createDefaultConfig() {
        LOG.info("Creating default Spring Environment based Config...");
        return Config.fromSpringEnvironment()
                .ofEnvironment(this.environment)
                .withIterableKeys()
                .build();
    }
}