InjectedTestCaseConfigurer.java

/*
 *
 * The DbUnit Database Testing Framework
 * Copyright (C)2002-2026, DbUnit.org
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */
package org.dbunit.annotation.runtime;

import java.util.Properties;
import java.util.function.Supplier;

import org.dbunit.DefaultPrepAndExpectedTestCase;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.assertion.FailureHandler;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.util.fileloader.DataFileLoader;
import org.dbunit.util.fileloader.FileExtensionDataFileLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Pushes the {@code @DbUnitConfig}-driven values that reach an injected {@code @DbUnitTestCase}
 * instance only through its own public API - the data file loader, the failure handler, the
 * {@code @DbUnitProperty} values, {@code closeConnectionAfterTest}, and the
 * {@code @DbUnitRowCountCheck} override - onto that instance, in a fixed order.
 *
 * <p>Each setter first checks - via
 * {@link DefaultMethodOverrideCheck#overridesDefaultMethod} - that the instance actually
 * overrides it, since a non-overriding instance inherits {@link PrepAndExpectedTestCase}'s
 * no-op default body and the configured value would otherwise silently never take effect. When
 * a non-default value cannot land that way, most setters fail fast;
 * {@code setCloseConnectionAfterTest} only warns, because {@link AnnotatedTestExecutor} still
 * honors that flag for its own connection regardless. Re-applying a value to a
 * freshly-constructed instance is harmless - its constructor already received the same one -
 * so every setter also runs on a non-configured test, to reset a value an earlier test left on
 * a reused instance.
 *
 * <p>Not intended for direct use by test code; machinery consumed by {@link ExpectedLifecycle}.
 *
 * @author Jeff Jensen
 * @since 3.6.0
 */
final class InjectedTestCaseConfigurer
{
    private static final Logger log = LoggerFactory.getLogger(InjectedTestCaseConfigurer.class);

    private final AnnotatedTestConfiguration configuration;
    private final PrepAndExpectedTestCase testCase;

    /**
     * Creates a configurer for one injected test case.
     *
     * @param configuration The resolved configuration whose {@code @DbUnitConfig} values to
     *            apply.
     * @param testCase The already-constructed {@code @DbUnitTestCase} instance to apply them to.
     */
    InjectedTestCaseConfigurer(final AnnotatedTestConfiguration configuration,
            final PrepAndExpectedTestCase testCase)
    {
        this.configuration = configuration;
        this.testCase = testCase;
    }

    /**
     * Applies every {@code @DbUnitConfig}-driven setter to {@link #testCase}, in a fixed order:
     * data file loader, failure handler, {@code DatabaseConfig} properties, close-connection
     * flag, then row count check override.
     */
    void applyAll()
    {
        applyDataFileLoader();
        applyFailureHandler();
        applyDatabaseConfigProperties();
        applyCloseConnectionAfterTest();
        applyRowCountCheckOverride();
    }

    private void applyDataFileLoader()
    {
        final DataFileLoader dataFileLoader = configuration.getDataFileLoader();
        final boolean configured =
                dataFileLoader.getClass() != FileExtensionDataFileLoader.class;
        requireApplicable(configured, false, "setDataFileLoader",
                new Class<?>[] {DataFileLoader.class},
                () -> "DbUnitConfig.dataFileLoader() names "
                        + dataFileLoader.getClass().getName(),
                "it would silently keep loading with whatever loader it was already constructed"
                        + " with. Override setDataFileLoader(), or drop dataFileLoader() from"
                        + " @DbUnitConfig for this test.");
        testCase.setDataFileLoader(dataFileLoader);
    }

    private void applyFailureHandler()
    {
        final FailureHandler failureHandler = configuration.getFailureHandler();
        requireApplicable(failureHandler != null, false, "setFailureHandler",
                new Class<?>[] {FailureHandler.class},
                () -> "DbUnitConfig.failureHandler() names "
                        + failureHandler.getClass().getName(),
                "it would silently keep dbUnit's own default handler instead. Override"
                        + " setFailureHandler(), or drop failureHandler() from @DbUnitConfig for"
                        + " this test.");
        testCase.setFailureHandler(failureHandler);
    }

    private void applyDatabaseConfigProperties()
    {
        final Properties properties = configuration.getDatabaseConfigProperties();
        requireApplicable(!properties.isEmpty(), false, "setDatabaseConfigProperties",
                new Class<?>[] {Properties.class},
                () -> "DbUnitConfig declares " + properties.size()
                        + " @DbUnitProperty value(s)",
                "this path never reaches the connection any other way - the setup/teardown"
                        + " path's IOperationListener-based application never triggers here."
                        + " Override setDatabaseConfigProperties(), or drop"
                        + " properties()/propertiesProvider() from @DbUnitConfig for this"
                        + " test.");
        warnIfOverridesSetUpDatabaseConfig(properties);
        testCase.setDatabaseConfigProperties(properties);
    }

    private void applyCloseConnectionAfterTest()
    {
        requireApplicable(!configuration.isCloseConnectionAfterTest(), true,
                "setCloseConnectionAfterTest", new Class<?>[] {boolean.class},
                () -> "DbUnitConfig.closeConnectionAfterTest() is false",
                "it may still close a connection this test's tester shares with other tests."
                        + " This executor's own connection - for the row count check or"
                        + " parameter injection - still honors the false value regardless.");
        testCase.setCloseConnectionAfterTest(configuration.isCloseConnectionAfterTest());
    }

    private void applyRowCountCheckOverride()
    {
        requireApplicable(configuration.isRowCountCheckDeclared(), false,
                "setRowCountCheckOverride", new Class<?>[] {boolean.class, String[].class},
                () -> "@DbUnitRowCountCheck is declared",
                "the check would silently never run for this test. Override"
                        + " setRowCountCheckOverride()/clearRowCountCheckOverride(), or drop"
                        + " @DbUnitRowCountCheck for this test.");
        if (configuration.isRowCountCheckDeclared())
        {
            testCase.setRowCountCheckOverride(configuration.isRowCountCheckEnabled(),
                    configuration.getRowCountCheckExclude());
        } else
        {
            testCase.clearRowCountCheckOverride();
        }
    }

    /**
     * Fails fast - or, when {@code warnOnly}, logs a warning - when {@code configured} is true
     * but {@link #testCase}'s runtime type does not override {@code setterName}, inheriting
     * {@link PrepAndExpectedTestCase}'s no-op default body so the configured value would
     * silently never take effect. A no-op when nothing was configured for this setter, or it is
     * genuinely overridden. See {@link DefaultMethodOverrideCheck#overridesDefaultMethod} for
     * this check's own known limitation.
     *
     * @param configured Whether {@code @DbUnitConfig} configured a non-default value for this
     *            setter.
     * @param warnOnly True to log a warning rather than throw - only
     *            {@code setCloseConnectionAfterTest}, which {@link AnnotatedTestExecutor} still
     *            honors for its own connection regardless.
     * @param setterName The setter's name.
     * @param setterParameterTypes The setter's parameter types.
     * @param configuredClause What was configured, e.g.
     *            {@code "DbUnitConfig.failureHandler() names X"}, evaluated only when
     *            {@code configured} - it may read a value that is null otherwise.
     * @param consequence What silently goes wrong when the value cannot land, and how to fix it.
     */
    private void requireApplicable(final boolean configured, final boolean warnOnly,
            final String setterName, final Class<?>[] setterParameterTypes,
            final Supplier<String> configuredClause, final String consequence)
    {
        if (!configured
                || DefaultMethodOverrideCheck.overridesDefaultMethod(testCase.getClass(),
                        PrepAndExpectedTestCase.class, setterName, setterParameterTypes))
        {
            return;
        }
        final String message = configuredClause.get() + ", but the injected DbUnitTestCase "
                + testCase.getClass().getName() + " does not override " + setterName + "(); "
                + consequence;
        if (warnOnly)
        {
            log.warn(message);
        } else
        {
            throw new IllegalStateException(message);
        }
    }

    /**
     * The extra {@code @DbUnitProperty} check {@link #applyDatabaseConfigProperties()} needs on
     * top of the shared not-overridden check: a {@link DefaultPrepAndExpectedTestCase} subclass
     * overriding {@code setUpDatabaseConfig(DatabaseConfig)} - the pre-3.6.0 way to configure a
     * {@link DatabaseConfig} - drops the values just applied unless that override calls
     * {@code super.setUpDatabaseConfig(config)}, since that same hook is where they would
     * otherwise be applied. Reflection cannot tell whether the override calls {@code super},
     * only that one exists, so this warns rather than fails. A no-op for a
     * non-{@link DefaultPrepAndExpectedTestCase} implementation, whose own
     * {@code setDatabaseConfigProperties()} override (already required by
     * {@link #applyDatabaseConfigProperties()}) is its own business.
     *
     * @param properties The resolved {@code @DbUnitProperty} values; a no-op when empty.
     */
    private void warnIfOverridesSetUpDatabaseConfig(final Properties properties)
    {
        if (properties.isEmpty() || !overridesSetUpDatabaseConfig())
        {
            return;
        }
        log.warn("DbUnitConfig declares {} @DbUnitProperty value(s) and the injected"
                + " DbUnitTestCase {} overrides setUpDatabaseConfig(DatabaseConfig); those"
                + " values reach the connection only if that override calls"
                + " super.setUpDatabaseConfig(config). Add the super call, or drop"
                + " properties()/propertiesProvider() and configure the DatabaseConfig"
                + " entirely in the override.", properties.size(),
                testCase.getClass().getName());
    }

    /**
     * Returns whether {@link #testCase} is a {@link DefaultPrepAndExpectedTestCase} subclass
     * that redeclares {@code setUpDatabaseConfig(DatabaseConfig)} somewhere below
     * {@link DefaultPrepAndExpectedTestCase} itself.
     *
     * @return True when a {@link DefaultPrepAndExpectedTestCase} subclass overrides
     *         {@code setUpDatabaseConfig(DatabaseConfig)}.
     */
    private boolean overridesSetUpDatabaseConfig()
    {
        return DefaultMethodOverrideCheck.declaresBelow(testCase.getClass(),
                DefaultPrepAndExpectedTestCase.class, "setUpDatabaseConfig",
                DatabaseConfig.class);
    }
}