CachingFileExtensionDataFileLoader.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.util.fileloader;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
/**
* {@link FileExtensionDataFileLoader} that parses each file once and shares the parsed
* dataset with every later {@link #load(String)} of the same resource, instead of parsing it
* again for each test. A suite whose tests share the same reference files otherwise pays for
* a full parse of every prep and expected file in every test: a SAX parse and the building of
* the tables for flat XML, and much more for Excel files.
*
* <p>Only the parsed dataset is shared. Every load still wraps it in a new
* {@link org.dbunit.dataset.ReplacementDataSet} built from the replacement objects and
* substrings of the loader instance that was asked, so those stay per instance.
*
* <p>The annotation runtime builds a new loader instance for each test, so the parsed datasets
* are held per loader class, not per instance, and every instance of a class reuses what the
* others parsed. A subclass keeps its own. They stay until {@link #clearCache()} drops them or
* the loader class is unloaded, so for the class itself, for the rest of the run. Share one
* loader class only among tests that read the same resources, and keep the number of distinct
* files in mind when they are large.
*
* <p>This suits immutable classpath fixtures: a resource that changes while the JVM keeps
* running, as in a test daemon or an IDE runner, is not read again until
* {@link #clearCache()}. The shared datasets must be treated as read-only, as they are shared
* by every test.
*
* <p>Use it by passing the class to {@code @DbUnitConfig(dataFileLoader = ...)}, which needs
* the public no-argument constructor, or by setting an instance on a
* {@link org.dbunit.PrepAndExpectedTestCase}.
*
* @author Jeff Jensen
* @since 3.6.0
*/
public class CachingFileExtensionDataFileLoader extends FileExtensionDataFileLoader
{
private static final ClassValue<ConcurrentMap<String, IDataSet>> PARSED_DATA_SETS =
new ClassValue<ConcurrentMap<String, IDataSet>>()
{
@Override
protected ConcurrentMap<String, IDataSet> computeValue(final Class<?> loaderClass)
{
return new ConcurrentHashMap<>();
}
};
/** Create new instance. */
public CachingFileExtensionDataFileLoader()
{
}
/**
* Create new instance with replacement objects.
*
* @param ro
* The replacement objects for use with
* {@link org.dbunit.dataset.ReplacementDataSet}.
*/
public CachingFileExtensionDataFileLoader(final Map<?, ?> ro)
{
super(ro);
}
/**
* Create new instance with replacement objects and replacement substrings.
*
* @param ro
* The replacement objects for use with
* {@link org.dbunit.dataset.ReplacementDataSet}.
* @param rs
* The replacement substrings for use with
* {@link org.dbunit.dataset.ReplacementDataSet}.
*/
public CachingFileExtensionDataFileLoader(final Map<?, ?> ro, final Map<?, ?> rs)
{
super(ro, rs);
}
/**
* {@inheritDoc}
*
* <p>Returns the dataset an earlier call parsed from the same resource, parsing it with
* {@link #loadUncachedDataSet(URL)} the first time. The cache is keyed by the URL's string
* form, as {@link URL#equals(Object)} and {@link URL#hashCode()} can do blocking name
* lookups. A resource that fails to parse is not remembered.
*/
@Override
public IDataSet loadDataSet(final URL url) throws DataSetException, IOException
{
final ConcurrentMap<String, IDataSet> cache = PARSED_DATA_SETS.get(getClass());
final String key = url.toExternalForm();
final IDataSet cached = cache.get(key);
if (cached != null)
{
return cached;
}
final IDataSet parsed = loadUncachedDataSet(url);
final IDataSet raced = cache.putIfAbsent(key, parsed);
return raced == null ? parsed : raced;
}
/**
* Parses the resource without consulting or filling the cache, the way
* {@link FileExtensionDataFileLoader} does.
*
* @param url
* The resource to parse.
* @return The parsed dataset.
* @throws DataSetException
* If the resource cannot be parsed as a dataset.
* @throws IOException
* If the resource cannot be read.
*/
protected IDataSet loadUncachedDataSet(final URL url) throws DataSetException, IOException
{
return super.loadDataSet(url);
}
/**
* Drops the parsed datasets of this loader's class, so the next load of each resource
* parses it again. For a test that changes a resource it loads, or a daemon that outlives
* the change.
*/
public void clearCache()
{
PARSED_DATA_SETS.remove(getClass());
}
}