AuditAssertionRegistrar.java

package io.github.databaseaudits.spring.boot;

import java.util.function.Supplier;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;

import io.github.databaseaudits.spring.boot.assertion.AuditAssertion;
import io.github.databaseaudits.spring.boot.assertion.DatabaseAuditAssertions;

/**
 * Registers a {@link DatabaseAuditSuite}'s audit assertions — every
 * {@code *AuditAssertion} in {@link DatabaseAuditSuite#all()} plus the
 * {@link DatabaseAuditAssertions} facade — as beans, each under its concrete
 * type, so a configuration wires the whole roster without enumerating one
 * {@code @Bean} method per audit. Adding a core audit then needs no change here:
 * once the suite wires its assertion, this registrar exposes it as a bean.
 *
 * <p>
 * The registration happens in {@link #afterSingletonsInstantiated()}, after the
 * suite bean exists, by adding a bean definition backed by an
 * {@linkplain RootBeanDefinition#setInstanceSupplier(Supplier) instance supplier}
 * for each assertion. Bean names are the assertion's decapitalized simple name
 * (matching the hand-written {@code @Bean} methods this replaces); a non-empty
 * {@code beanNamePrefix} prefixes them instead, so a second, by-name datasource's
 * suite registers its assertions under distinct names without colliding with the
 * primary datasource's. The {@code primary} flag marks the definitions
 * {@link RootBeanDefinition#setPrimary(boolean) primary}, so the primary
 * datasource's assertions win an unqualified by-type injection when an
 * application also audits a peer datasource.
 *
 * <p>
 * The stock {@link DatabaseAuditTestConfiguration} registers one of these with no
 * prefix and {@code primary = true}; a generated per-datasource configuration
 * registers one with the datasource's name prefix and {@code primary = false}.
 */
public class AuditAssertionRegistrar
        implements SmartInitializingSingleton, BeanFactoryAware {
    private final DatabaseAuditSuite suite;

    private final String beanNamePrefix;

    private final boolean primary;

    private BeanDefinitionRegistry registry;

    /**
     * Creates a registrar for one suite's assertions.
     *
     * @param suite
     *                           the suite whose assertions to register.
     * @param beanNamePrefix
     *                           the prefix for the registered bean names, or the
     *                           empty string to name each bean by its
     *                           decapitalized simple name; a non-empty prefix
     *                           keeps a peer datasource's assertions from
     *                           colliding with the primary datasource's.
     * @param primary
     *                           whether the registered beans are marked primary,
     *                           so they win an unqualified by-type injection.
     */
    public AuditAssertionRegistrar(final DatabaseAuditSuite suite,
            final String beanNamePrefix, final boolean primary) {
        this.suite = suite;
        this.beanNamePrefix = beanNamePrefix;
        this.primary = primary;
    }

    @Override
    public void setBeanFactory(final BeanFactory beanFactory)
            throws BeansException {
        if (!(beanFactory instanceof final BeanDefinitionRegistry beanDefinitionRegistry)) {
            throw new IllegalStateException(
                    "AuditAssertionRegistrar requires a BeanDefinitionRegistry-backed BeanFactory (such as a DefaultListableBeanFactory), but got "
                            + beanFactory.getClass().getName());
        }
        this.registry = beanDefinitionRegistry;
    }

    /**
     * Registers a bean for each of the suite's assertions and for its facade,
     * once every singleton — including the suite this registrar depends on — has
     * been instantiated.
     */
    @Override
    public void afterSingletonsInstantiated() {
        for (final AuditAssertion assertion : suite.all()) {
            register(assertion);
        }
        register(suite.assertions());
    }

    private void register(final Object bean) {
        final RootBeanDefinition definition = new RootBeanDefinition();
        definition.setBeanClass(bean.getClass());
        definition.setInstanceSupplier(() -> bean);
        definition.setPrimary(primary);
        registry.registerBeanDefinition(beanName(bean.getClass()), definition);
    }

    private String beanName(final Class<?> type) {
        final String simpleName = type.getSimpleName();
        if (beanNamePrefix.isEmpty()) {
            return Character.toLowerCase(simpleName.charAt(0))
                    + simpleName.substring(1);
        }
        return beanNamePrefix + simpleName;
    }
}