TestInstanceTesterResolver.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.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.concurrent.Callable;

import org.dbunit.DatabaseTesterFactory;
import org.dbunit.IDatabaseTester;
import org.dbunit.PrepAndExpectedTestCase;
import org.dbunit.annotation.DbUnitConfig;
import org.dbunit.annotation.DbUnitTestCase;
import org.dbunit.annotation.DbUnitTester;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The binding-neutral part of resolving, for one test, the {@link IDatabaseTester} and (when
 * present) the {@link PrepAndExpectedTestCase} an {@link AnnotatedTestExecutor} drives: what the
 * test instances' fields and {@link DbUnitConfig#databaseTesterFactory()} say. Written against
 * plain {@code java.lang.reflect}, so {@code DbUnitExtension} (JUnit 5/6) and
 * {@code DbUnitTestExecutionListener} (Spring) share one set of rules and one set of messages
 * for the same mistakes, whichever test framework or annotation-scanning utility each binding
 * itself uses.
 *
 * <p>A binding passes the test instances in scope, innermost first - for a {@code @Nested} test
 * class, its own instance, then its enclosing class's instance, and so on outward - and supplies
 * only what differs between frameworks: where to look when no field names a tester. Resolution
 * is first match wins:
 * <ol>
 *   <li>A field annotated {@link DbUnitTestCase}, whose type implements
 *   {@link PrepAndExpectedTestCase} - that instance is driven directly.</li>
 *   <li>A field annotated {@link DbUnitTester}, whose type implements
 *   {@link IDatabaseTester}.</li>
 *   <li>Whatever the binding's {@code unmarkedTester} supplies, normally built from
 *   {@link #fromFactory(AnnotatedTestConfiguration)} and
 *   {@link #fromUnmarkedField(List)} plus the binding's own tiers.</li>
 * </ol>
 *
 * <p>The first instance, innermost first, that declares either marker supplies the marked field
 * and shadows every outer instance entirely, whichever marker the outer one uses. Within one
 * instance, the whole class hierarchy is searched, and a marked field is ambiguous - rejected
 * with an {@link IllegalStateException} - when it is declared more than once there, on one class
 * or across a class and its superclass, or when both markers are declared.
 *
 * <p>Public so a binding outside {@code org.dbunit.annotation.runtime} - such as
 * {@code DbUnitExtension} or a Spring {@code TestExecutionListener} - can reach it too.
 *
 * @author Jeff Jensen
 * @since 3.6.0
 */
public class TestInstanceTesterResolver
{
    private static final Logger log = LoggerFactory.getLogger(TestInstanceTesterResolver.class);

    private TestInstanceTesterResolver()
    {
    }

    /**
     * Resolves the tester and test case from the marked fields of {@code instances}, falling
     * back to {@code unmarkedTester} when no instance declares a marked field - and for the
     * tester a {@code @DbUnitTestCase} field's test case does not carry itself.
     *
     * @param instances The test instances in scope, innermost first.
     * @param unmarkedTester Supplies the tester when no {@code @DbUnitTester} field does; must
     *            throw rather than return {@code null} when it finds none.
     * @return The resolved tester, and the test case when a {@code @DbUnitTestCase} field
     *         supplied one.
     * @throws IllegalStateException If a marked field is ambiguous, null, or of the wrong type.
     * @throws Exception If {@code unmarkedTester} fails.
     */
    public static Resolution resolve(final List<Object> instances,
            final Callable<IDatabaseTester> unmarkedTester) throws Exception
    {
        final MarkedFields marked = findMarkedFields(instances);
        if (marked.testCaseField != null)
        {
            return resolveFromTestCaseField(marked.testCaseField, unmarkedTester);
        }
        if (marked.testerField != null)
        {
            final IDatabaseTester markedTester = readTesterField(marked.testerField);
            return new Resolution(markedTester, null);
        }
        final IDatabaseTester tester = unmarkedTester.call();
        return new Resolution(tester, null);
    }

    /**
     * Creates the tester {@link DbUnitConfig#databaseTesterFactory()} names.
     *
     * @param configuration The resolved configuration.
     * @return The factory-created tester, or {@code null} when no factory is configured.
     * @throws IllegalStateException If the factory cannot be instantiated or returns
     *             {@code null}.
     * @throws Exception If the factory fails creating the tester.
     */
    public static IDatabaseTester fromFactory(final AnnotatedTestConfiguration configuration)
            throws Exception
    {
        final Class<? extends DatabaseTesterFactory> factoryClass =
                configuration.getDatabaseTesterFactory();
        if (factoryClass == null)
        {
            return null;
        }
        final DatabaseTesterFactory factory =
                ReflectiveInstantiation.instantiate(factoryClass,
                        "DbUnitConfig.databaseTesterFactory");
        final IDatabaseTester created = factory.getDatabaseTester();
        return ProvidedAttribute.requireProvided(created,
                DatabaseTesterFactory.class.getSimpleName(), factoryClass,
                "DbUnitConfig.databaseTesterFactory", "getDatabaseTester");
    }

    /**
     * The original (3.5.0) field auto-scan: within each instance, innermost first, and within its
     * class hierarchy (most-derived class first), the first class declaring exactly one
     * non-static field assignable to {@link IDatabaseTester}; two or more at the same declaring
     * class are ambiguous.
     *
     * @param instances The test instances in scope, innermost first.
     * @return The tester that field holds, or {@code null} when no instance declares such a
     *         field.
     * @throws IllegalStateException If a class declares more than one such field, or the one
     *             found holds {@code null}.
     * @throws IllegalAccessException If the field cannot be read.
     */
    public static IDatabaseTester fromUnmarkedField(final List<Object> instances)
            throws IllegalAccessException
    {
        for (final Object instance : instances)
        {
            Class<?> clazz = instance.getClass();
            while (clazz != null && clazz != Object.class)
            {
                final Field field = findUnmarkedTesterField(clazz, instance);
                if (field != null)
                {
                    return readUnmarkedTesterField(field, instance);
                }
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }

    private static Resolution resolveFromTestCaseField(final FieldMatch testCaseField,
            final Callable<IDatabaseTester> unmarkedTester) throws Exception
    {
        final Object value = testCaseField.value();
        if (value == null)
        {
            throw new IllegalStateException("PrepAndExpectedTestCase field '"
                    + testCaseField.field.getName() + "' in " + testCaseField.instanceClassName()
                    + " is null.");
        }
        final String fieldDescription = testCaseField.describe();
        if (!(value instanceof PrepAndExpectedTestCase))
        {
            throw new IllegalStateException(fieldDescription
                    + " is annotated @DbUnitTestCase, but its value's type ("
                    + value.getClass().getName() + ") does not implement PrepAndExpectedTestCase.");
        }
        final PrepAndExpectedTestCase testCase = (PrepAndExpectedTestCase) value;
        // testCase does not override getDatabaseTester()/setDatabaseTester(), or does and was
        // simply built without a tester yet (e.g. the no-arg-tester constructor form) - the
        // fallback resolves the same tester a bare @DbUnitTestCase-less test would use, rather
        // than leaving the resolution's tester null.
        final IDatabaseTester tester = InjectedTestCaseTesterBinding.resolveTester(testCase,
                fieldDescription, unmarkedTester);
        return new Resolution(tester, testCase);
    }

    private static IDatabaseTester readTesterField(final FieldMatch testerField)
            throws IllegalAccessException
    {
        final Object value = testerField.value();
        if (value == null)
        {
            throw new IllegalStateException("IDatabaseTester field '"
                    + testerField.field.getName() + "' in " + testerField.instanceClassName()
                    + " is null.");
        }
        if (!(value instanceof IDatabaseTester))
        {
            throw new IllegalStateException(testerField.describe()
                    + " is annotated @DbUnitTester, but its value's type ("
                    + value.getClass().getName() + ") does not implement IDatabaseTester.");
        }
        return (IDatabaseTester) value;
    }

    /**
     * Finds the innermost test instance declaring a {@link DbUnitTestCase} or
     * {@link DbUnitTester} field, checking both markers together on each instance in turn - the
     * same innermost-instance-wins precedent {@link #fromUnmarkedField(List)} establishes for the
     * plain, unannotated field - so a match on one instance shadows an outer instance's marked
     * field entirely, regardless of whether the outer field uses the same marker or the other
     * one, rather than the two being resolved independently and compared across instances.
     *
     * @throws IllegalStateException If one instance declares both markers, or more than one
     *             field for the same marker within one instance's class hierarchy.
     */
    private static MarkedFields findMarkedFields(final List<Object> instances)
    {
        for (final Object instance : instances)
        {
            final FieldMatch testCaseField = findMarkedField(instance, DbUnitTestCase.class);
            final FieldMatch testerField = findMarkedField(instance, DbUnitTester.class);
            if (testCaseField != null && testerField != null)
            {
                throw new IllegalStateException(
                        "Both @DbUnitTestCase and @DbUnitTester fields are declared in "
                                + instance.getClass().getName() + "; declare at most one.");
            }
            if (testCaseField != null || testerField != null)
            {
                return new MarkedFields(testCaseField, testerField);
            }
        }
        return new MarkedFields(null, null);
    }

    /**
     * Finds a field annotated {@code marker} within one test instance's whole class hierarchy.
     *
     * @throws IllegalStateException If more than one such field is found.
     */
    private static FieldMatch findMarkedField(final Object instance,
            final Class<? extends Annotation> marker)
    {
        Field match = null;
        Class<?> clazz = instance.getClass();
        while (clazz != null && clazz != Object.class)
        {
            for (final Field field : clazz.getDeclaredFields())
            {
                if (field.isAnnotationPresent(marker))
                {
                    if (match != null)
                    {
                        throw new IllegalStateException("Multiple @" + marker.getSimpleName()
                                + " fields found in " + instance.getClass().getName() + ".");
                    }
                    match = field;
                }
            }
            clazz = clazz.getSuperclass();
        }
        if (match == null)
        {
            return null;
        }
        match.setAccessible(true);
        return new FieldMatch(match, instance);
    }

    private static Field findUnmarkedTesterField(final Class<?> clazz, final Object instance)
    {
        Field match = null;
        for (final Field field : clazz.getDeclaredFields())
        {
            if (!Modifier.isStatic(field.getModifiers())
                    && IDatabaseTester.class.isAssignableFrom(field.getType()))
            {
                if (match != null)
                {
                    throw new IllegalStateException("Multiple IDatabaseTester fields found in "
                            + clazz.getName() + ": '" + match.getName() + "' and '"
                            + field.getName() + "'. Declare exactly one non-static field whose"
                            + " type implements IDatabaseTester in "
                            + instance.getClass().getName() + ".");
                }
                match = field;
            }
        }
        return match;
    }

    private static IDatabaseTester readUnmarkedTesterField(final Field field,
            final Object instance) throws IllegalAccessException
    {
        field.setAccessible(true);
        final IDatabaseTester tester = (IDatabaseTester) field.get(instance);
        if (tester == null)
        {
            throw new IllegalStateException("IDatabaseTester field '" + field.getName() + "' in "
                    + instance.getClass().getName() + " is null.");
        }
        log.debug("Resolved IDatabaseTester '{}' in {}", field.getName(),
                instance.getClass().getName());
        return tester;
    }

    /**
     * The resolved tester, and the test case when a {@code @DbUnitTestCase} field supplied one.
     */
    public static class Resolution
    {
        private final IDatabaseTester tester;
        private final PrepAndExpectedTestCase testCase;

        private Resolution(final IDatabaseTester tester, final PrepAndExpectedTestCase testCase)
        {
            this.tester = tester;
            this.testCase = testCase;
        }

        /**
         * Returns the tester to drive.
         *
         * @return The resolved tester.
         */
        public IDatabaseTester getTester()
        {
            return tester;
        }

        /**
         * Returns the test case a {@code @DbUnitTestCase} field supplied.
         *
         * @return The test case, or {@code null} when no such field exists.
         */
        public PrepAndExpectedTestCase getTestCase()
        {
            return testCase;
        }
    }

    private static class FieldMatch
    {
        private final Field field;
        private final Object instance;

        private FieldMatch(final Field field, final Object instance)
        {
            this.field = field;
            this.instance = instance;
        }

        private Object value() throws IllegalAccessException
        {
            return field.get(instance);
        }

        private String instanceClassName()
        {
            return instance.getClass().getName();
        }

        private String describe()
        {
            return "Field '" + field.getName() + "' in " + instanceClassName();
        }
    }

    private static class MarkedFields
    {
        private final FieldMatch testCaseField;
        private final FieldMatch testerField;

        private MarkedFields(final FieldMatch testCaseField, final FieldMatch testerField)
        {
            this.testCaseField = testCaseField;
            this.testerField = testerField;
        }
    }
}