VerifyTableDefinitionResolver.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.reflect.InvocationTargetException;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.dbunit.VerifyTableDefinition;
import org.dbunit.annotation.DbUnitColumnComparer;
import org.dbunit.annotation.DbUnitConfig;
import org.dbunit.annotation.DbUnitExpected;
import org.dbunit.annotation.DbUnitVerifyTable;
import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.util.fileloader.DataFileLoader;
/**
* Resolves {@code @DbUnitExpected}'s verification spec - its inline {@code verify()}
* {@link DbUnitVerifyTable}s, its {@code verifyTables()} names, its {@code verifyDefinitions()}
* catalog classes, or {@link DbUnitConfig#verifyDefinitions()} class-level catalogs - into the
* {@link VerifyTableDefinition}[] {@link AnnotatedTestConfiguration} carries.
*
* <p>Precedence, first match wins: {@code @DbUnitExpected.verifyDefinitions()}, then
* {@code verify()}, then {@code @DbUnitConfig.verifyDefinitions()}, then bare
* {@code verifyTables()}, then one definition per table found in the expected datasets.
* {@code verifyTables()} narrows a catalog rather than conflicting with it; declaring both
* {@code verify()} and either catalog form, or the same table twice, is rejected.
*
* @author Jeff Jensen
* @since 3.6.0
*/
final class VerifyTableDefinitionResolver
{
private static final Map<Class<? extends ValueComparer>, ValueComparer> COMPARER_CACHE =
new ConcurrentHashMap<>();
/**
* Resolves the verification spec.
*
* @param config The resolved {@code @DbUnitConfig}, or {@code null} if absent.
* @param expected The resolved {@code @DbUnitExpected} (never {@code null} - this runs only
* when it is present).
* @param dataFileLoader The loader used to enumerate expected-dataset table names for the
* default case.
* @param expectedDataFiles The resolved expected dataset paths.
* @return The table definitions to verify.
*/
VerifyTableDefinition[] resolve(final DbUnitConfig config, final DbUnitExpected expected,
final DataFileLoader dataFileLoader, final String[] expectedDataFiles)
{
final Class<?>[] classLevelCatalogs =
config == null ? new Class<?>[0] : config.verifyDefinitions();
final DbUnitVerifyTable[] verify = expected.verify();
final String[] verifyTables = expected.verifyTables();
ProvidedAttribute.rejectBothSet(
verify.length > 0 && expected.verifyDefinitions().length > 0, "@DbUnitExpected",
"verify()", "verifyDefinitions()", "See VerifyTableDefinitionsProvider.");
ProvidedAttribute.rejectBothSet(verify.length > 0 && verifyTables.length > 0,
"@DbUnitExpected", "verify()", "verifyTables()",
"verifyTables() only narrows a catalog.");
requireNoDuplicateTableNames(verifyTables);
final Class<?>[] catalogClasses;
if (expected.verifyDefinitions().length > 0)
{
catalogClasses = expected.verifyDefinitions();
} else if (verify.length > 0)
{
catalogClasses = new Class<?>[0];
} else
{
catalogClasses = classLevelCatalogs;
}
if (catalogClasses.length > 0)
{
return VerifyTableDefinitionCatalog.forClasses(catalogClasses).select(verifyTables);
}
if (verify.length > 0)
{
final VerifyTableDefinition[] definitions =
new VerifyTableDefinition[verify.length];
final Set<String> tableNames = new LinkedHashSet<>(verify.length);
for (int i = 0; i < verify.length; i++)
{
if (!tableNames.add(verify[i].value()))
{
throw new IllegalStateException("@DbUnitExpected declares more than one"
+ " @DbUnitVerifyTable entry for table '" + verify[i].value()
+ "'; declare at most one per table.");
}
definitions[i] = toVerifyTableDefinition(verify[i]);
}
return definitions;
}
if (verifyTables.length > 0)
{
final VerifyTableDefinition[] definitions =
new VerifyTableDefinition[verifyTables.length];
for (int i = 0; i < verifyTables.length; i++)
{
definitions[i] = new VerifyTableDefinition(verifyTables[i], (String[]) null);
}
return definitions;
}
return defaultPerExpectedTable(dataFileLoader, expectedDataFiles);
}
private void requireNoDuplicateTableNames(final String[] verifyTables)
{
final Set<String> tableNames = new LinkedHashSet<>(verifyTables.length);
for (final String tableName : verifyTables)
{
if (!tableNames.add(tableName))
{
throw new IllegalStateException("@DbUnitExpected declares more than one"
+ " verifyTables() entry for table '" + tableName
+ "'; declare at most one per table.");
}
}
}
private VerifyTableDefinition toVerifyTableDefinition(final DbUnitVerifyTable v)
{
final String[] include = v.include().length == 0 ? null : v.include();
final ValueComparer defaultComparer = v.defaultComparer() == ValueComparer.class ? null
: instantiateComparer(v.defaultComparer());
final Map<String, ValueComparer> columnComparers =
new LinkedHashMap<>(v.columnComparers().length);
for (final DbUnitColumnComparer c : v.columnComparers())
{
if (columnComparers.containsKey(c.column()))
{
throw new IllegalStateException("@DbUnitVerifyTable(\"" + v.value()
+ "\") declares more than one @DbUnitColumnComparer for column '"
+ c.column() + "'; declare at most one per column.");
}
columnComparers.put(c.column(), instantiateComparer(c.comparer()));
}
return new VerifyTableDefinition(v.value(), v.exclude(), include, defaultComparer,
columnComparers, v.sortOnFilteredColumnsOnly());
}
/**
* Parses {@code expectedDataFiles} a second time here, just to enumerate table names -
* {@code DefaultPrepAndExpectedTestCase#configureTest()} parses the same files again later
* for the real verification. Left as-is: resolving a configuration is JUnit-free and runs
* before any {@code PrepAndExpectedTestCase} exists to hand a parsed result to (or receive
* one from), so avoiding the second parse needs a real change to that boundary, not a small
* tweak - not worth it for what is normally a small test fixture file.
*/
private VerifyTableDefinition[] defaultPerExpectedTable(final DataFileLoader dataFileLoader,
final String[] expectedDataFiles)
{
final Set<String> tableNames = new LinkedHashSet<>();
for (final String path : expectedDataFiles)
{
final IDataSet dataSet = dataFileLoader.load(path);
try
{
for (final String tableName : dataSet.getTableNames())
{
tableNames.add(tableName);
}
} catch (final DataSetException e)
{
throw new IllegalStateException(
"Failed to read table names from expected dataset '" + path + "'.", e);
}
}
final VerifyTableDefinition[] definitions =
new VerifyTableDefinition[tableNames.size()];
int i = 0;
for (final String tableName : tableNames)
{
definitions[i++] = new VerifyTableDefinition(tableName, (String[]) null);
}
return definitions;
}
/**
* Caches a comparer instance, for the JVM's entire lifetime, keyed by class - the same
* once-and-reused-forever caching {@link VerifyTableDefinitionCatalog#forClasses} already
* applies to a catalog combination, for the same reason: this resolves fresh on every test
* method, and a suite naming the same comparer class from many methods would otherwise
* reflectively construct a new, functionally-identical instance every time. See
* {@link DbUnitColumnComparer#comparer()} for the resulting purity requirement.
*/
private static ValueComparer instantiateComparer(
final Class<? extends ValueComparer> comparerClass)
{
return COMPARER_CACHE.computeIfAbsent(comparerClass,
VerifyTableDefinitionResolver::newComparerInstance);
}
private static ValueComparer newComparerInstance(
final Class<? extends ValueComparer> comparerClass)
{
try
{
return ReflectiveInstantiation.newInstance(comparerClass);
} catch (final InvocationTargetException e)
{
throw new IllegalStateException("ValueComparer " + comparerClass.getName()
+ " threw from its no-arg constructor.", e.getCause());
} catch (final ReflectiveOperationException e)
{
throw new IllegalStateException("ValueComparer " + comparerClass.getName()
+ " has no accessible no-arg constructor - it is likely a configured"
+ " instance built with constructor arguments, which an annotation cannot"
+ " express. Declare it as a VerifyTableDefinition constant in a catalog"
+ " class instead; see VerifyTableDefinitionsProvider.", e);
}
}
}