ch.inftec.ju.testing.db.DbDataUtil Maven / Gradle / Ivy
package ch.inftec.ju.testing.db;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.EntityManager;
import org.apache.commons.lang3.StringUtils;
import org.dbunit.Assertion;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.DatabaseSequenceFilter;
import org.dbunit.database.DefaultMetadataHandler;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.IMetadataHandler;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.FilteredDataSet;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.datatype.DefaultDataTypeFactory;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.ext.h2.H2DataTypeFactory;
import org.dbunit.ext.hsqldb.HsqldbDataTypeFactory;
import org.dbunit.ext.mysql.MySqlDataTypeFactory;
import org.dbunit.ext.mysql.MySqlMetadataHandler;
import org.dbunit.ext.oracle.Oracle10DataTypeFactory;
import org.dbunit.operation.DatabaseOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import ch.inftec.ju.db.ConnectionInfo;
import ch.inftec.ju.db.DbWork;
import ch.inftec.ju.db.JuConnUtil;
import ch.inftec.ju.db.JuConnUtil.DbType;
import ch.inftec.ju.db.JuConnUtils;
import ch.inftec.ju.db.JuDbException;
import ch.inftec.ju.db.JuEmUtil;
import ch.inftec.ju.util.AssertUtil;
import ch.inftec.ju.util.DataHolder;
import ch.inftec.ju.util.JuCollectionUtils;
import ch.inftec.ju.util.JuUrl;
import ch.inftec.ju.util.ReflectUtils;
import ch.inftec.ju.util.XString;
import ch.inftec.ju.util.xml.XPathGetter;
import ch.inftec.ju.util.xml.XmlOutputConverter;
import ch.inftec.ju.util.xml.XmlUtils;
/**
* Utility class containing methods to import and export data from a DB.
*
* The class needs a JuConnUtil Connection wrapper to access the DB.
* @author Martin
*
*/
public class DbDataUtil {
private final static Logger logger = LoggerFactory.getLogger(DbDataUtil.class);
private final JuConnUtil connUtil;
private String schemaName = null;
private Map configProperties = new HashMap<>();
private IDatabaseConnection dbConn;
/**
* Cache for PrimaryKeyColumns. These are evaluated through MetaData and this results in a
* DatabaseMetaData query every time.
*/
private final Map primaryKeyColumns = new HashMap<>();
/**
* Creates a DbDataUtil that will use the internal "raw" DB connection
* of the EntityManager to perform data export and import.
* @param emUtil Wrapper around an EntityManager instance
*/
public DbDataUtil(JuEmUtil emUtil) {
this(emUtil.asConnUtil());
}
/**
* Creates a DbDataUtil that will use the specified "raw" JDBC connection
* to perform data export and import.
* @param conn JDBC connection
*/
public DbDataUtil(Connection conn) {
this(JuConnUtils.createByConnection(conn));
}
/**
* Creates a new DbDataUtil instance using the specifiec JuConnUtil.
*
* If you need to specify a DB Schema, use the DbDataUtil(JuConnUtil, String) constructor.
* @param connUtil DB Connection instance
*/
public DbDataUtil(JuConnUtil connUtil) {
this(connUtil, (String)null);
}
/**
* Creates a new DbDataUtil instance using the specified Connection and the Schema
* from the ConnectionInfo
* @param connection Connection instance
* @param ConnectionInfo to get the Schema to use
* @Deprecated Use constructor with EntityManager
*/
public DbDataUtil(JuConnUtil connUtil, ConnectionInfo connectionInfo) {
this(connUtil, connectionInfo.getSchema());
}
/**
* Executes a DbUnit dataset import through a JDBC connection.
* @param conn JDBC connection
* @param dataSetUrl DataSet URL
* @param cleanInsert If true, a clean insert is performed. Otherwise, an insert is performed.
*/
public static void executeInsert(Connection conn, URL dataSetUrl, boolean cleanInsert) {
ImportBuilder ib = new DbDataUtil(conn).buildImport().from(dataSetUrl);
if (cleanInsert) {
ib.executeCleanInsert();
} else {
ib.executeInsert();
}
}
/**
* Create a new DbDataUtil that will use the specified EntityManager to get
* a raw connection to the DB and execute SQL queries.
* @param em EntityManager instance to execute SQL in a JDBC connection
*/
public DbDataUtil(EntityManager em) {
this(new JuEmUtil(em));
}
/**
* Create a DbDataUtil using the specified connection and Schema name.
* @param connUtil DB connection instance
* @param schema Explicit Schema name
*/
public DbDataUtil(JuConnUtil connUtil, String schema) {
this.connUtil = connUtil;
this.schemaName = schema;
// Initialize
DefaultDataTypeFactory dataTypeFactory = null;
IMetadataHandler metadataHandler = new DefaultMetadataHandler();
switch (this.connUtil.getDbType()) {
case DERBY:
dataTypeFactory = new DefaultDataTypeFactory();
break;
case H2:
dataTypeFactory = new H2DataTypeFactory();
break;
case HSQL:
dataTypeFactory = new HsqldbDataTypeFactory();
break;
case MYSQL:
dataTypeFactory = new MySqlDataTypeFactory();
metadataHandler = new MySqlMetadataHandler();
break;
case ORACLE:
// XXX: Enable for other DBs?
this.setConfigProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true);
if (StringUtils.isEmpty(this.schemaName)) {
this.schemaName = this.connUtil.getMetaDataInfo().getUserName();
}
dataTypeFactory = new Oracle10DataTypeFactory();
break;
default:
throw new JuDbException("Unsupported DB: " + this.connUtil.getDbType());
}
this.setConfigProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, dataTypeFactory);
this.setConfigProperty(DatabaseConfig.PROPERTY_METADATA_HANDLER, metadataHandler);
}
/**
* Sets the DB schema name to work with.
*
* May be necessary for DBs like oracle to avoid duplicate name problems.
* @param schemaName DB schema name
* @return This util to allow for chaining
*/
public DbDataUtil setSchema(String schemaName) {
this.schemaName = schemaName;
return this;
}
/**
* Sets a config attribute of the underlying DbUnit IDatabaseConnection instance.
* @param name Name of the attribute
* @param value Value of the attribute
* @return This instance to allow for chaining
*/
public DbDataUtil setConfigProperty(String name, Object value) {
this.configProperties.put(name, value);
return this;
}
private void execute(final DbUnitWork work) {
this.connUtil.doWork(new DbWork() {
@Override
public void execute(Connection conn) {
doExecute(conn, work);
}
});
}
private void doExecute(Connection connection, DbUnitWork work) {
/**
* Due to a JDBC 1.4 spec imcompatibility of the Oracle driver
* (doesn't return IS_AUTOINCREMENT in table meta data), we need
* to unwrap the actual JDBC connection in case this is a (Hibernate)
* proxy.
*/
Connection unwrappedConn = null;
if (this.connUtil.getDbType() == DbType.ORACLE && connection instanceof Proxy) {
try {
unwrappedConn = connection.unwrap(Connection.class);
} catch (Exception ex) {
throw new JuDbException("Couldn't unwrap Connection", ex);
}
}
final Connection realConn = unwrappedConn != null
? unwrappedConn
: connection;
try {
// Check if we get the same connection. If so, we'll recycle the DatabaseConnection to avoid
// requerying of all the DB meta data
if (this.dbConn == null || this.dbConn.getConnection() != connection) {
this.dbConn = new DatabaseConnection(realConn, this.schemaName);
for (String key : this.configProperties.keySet()) {
this.dbConn.getConfig().setProperty(key, this.configProperties.get(key));
}
logger.debug("Created DatabaseConnection: {}", this.dbConn);
} else {
// Use existing DatabaseConnection
logger.info("Reusing DatabaseConnection {}", this.dbConn.getConnection());
}
work.execute(this.dbConn);
} catch (Exception ex) {
throw new JuDbException("Couldn't execute DbUnitWork", ex);
}
}
/**
* Shortcut to execute a clean import from a dataset resource file.
* @param resourcePath Path to dataset resource
*/
public void cleanImport(String resourcePath) {
this.buildImport().from(resourcePath).executeCleanInsert();
}
/**
* Returns a new ExportBuilder to configure and execute DB data exports.
* @return ExportBuilder instance
*/
public ExportBuilder buildExport() {
return new ExportBuilder(this);
}
/**
* Returns a new ImportBuilder to import data from XML resources into the DB.
* @return ImportBuilder instance
*/
public ImportBuilder buildImport() {
return new ImportBuilder(this);
}
/**
* Returns a new AssertBuilder to assert that table data equals expected data
* specified in an XML file.
* @return AssertBuilder instance
*/
public AssertBuilder buildAssert() {
return new AssertBuilder(this);
}
/**
* Helper callback interface to execute code that needs a IDatabaseConnection
* instance.
* @author tgdmemae
* Return value
*
*/
private static interface DbUnitWork {
public void execute(IDatabaseConnection conn);
}
/**
* Builder class to configure and execute DB data exports.
* @author Martin
*
*/
public static class ExportBuilder {
// private Logger logger = LoggerFactory.getLogger(ExportBuilder.class);
private final DbDataUtil dbDataUtil;
private final ExportItems exportItems = new ExportItems();
private ExportBuilder(DbDataUtil dbDataUtil) {
this.dbDataUtil = dbDataUtil;
}
/**
* Loads the table names from the specified dataset XML resource and uses it as a template
* of how to case any table name that will be exported.
*
* Note that calling this method doesn't actually ADD a table.
* @param resourcePath Resource path to dataset XML
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder setTableNamesCasingByDataSet(String resourcePath) {
try {
this.exportItems.setCasedTableNames(new XPathGetter(XmlUtils.loadXml(
JuUrl.resource().relativeTo(DbDataUtil.class).get(resourcePath))).getNodeNames("dataset/*"));
} catch (Exception ex) {
throw new JuDbException("Couldn't load table names data set " + resourcePath, ex);
}
return this;
}
/**
* Adds the specific table to the builder, exporting the table data.
* @param tableName Table name
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder addTable(String tableName) {
return this.addTable(tableName, null);
}
private String[] getPrimaryKeyColumns(String tableName) {
if (!dbDataUtil.primaryKeyColumns.containsKey(tableName)) {
List primaryKeyColumnsList = this.dbDataUtil.connUtil.getMetaDataInfo().getPrimaryKeyColumns(tableName);
dbDataUtil.primaryKeyColumns.put(tableName, primaryKeyColumnsList.toArray(new String[0]));
}
return dbDataUtil.primaryKeyColumns.get(tableName);
}
/**
* Adds the specific table to the builder, exporting the table data.
*
* The data will be sorted by the tables primary key column.
*
* Note: This works only if the DbDataUtil was initialized with an emUtil instance.
* @param tableName Table name
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder addTableSorted(String tableName) {
AssertUtil.assertNotNull(
"Sorting by primary key only works with DbDataUtils that were initialized with an JuConnUtil instance"
, this.dbDataUtil.connUtil);
return this.addTableSorted(tableName, this.getPrimaryKeyColumns(tableName));
}
/**
* Adds the specified table to the builder, exporting the table data.
*
* If no query is specified (null), all table data is exported. Otherwise, only
* the data returned by the query is exported.
*
* The query has to be a full SQL query like select * from table where id=7
* @param tableName TableName
* @param query Optional query to select sub data
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder addTable(final String tableName, final String query) {
this.exportItems.add(tableName, query);
return this;
}
/**
* Adds the data of the specified table, ordering by the specified columns.
* @param tableName Table names
* @param orderColumns List of columns to order by
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder addTableSorted(String tableName, String... orderColumns) {
if (orderColumns.length == 0) {
return this.addTable(tableName);
} else {
XString xs = new XString();
xs.addFormatted("SELECT * FROM %s ORDER BY ", tableName);
for (String orderColumn : orderColumns) {
xs.assertText("ORDER BY ", ", ");
xs.addText(orderColumn);
}
return this.addTable(tableName, xs.toString());
}
}
/**
* Adds the data of the tables contained in the specified data set.
*
* It doesn't matter what kind of dataset we got, we're just extracting the table names.
* @param resourceUrl
* @param sortedByPrimaryKey If true, the entries will be sorted by primary key
* @return ExportBuilder to allow for chaining
*/
public ExportBuilder addTablesByDataSet(URL resourceUrl, boolean sortedByPrimaryKey) {
try {
Set tableNames = JuCollectionUtils.asSameOrderSet(new XPathGetter(XmlUtils.loadXml(resourceUrl)).getNodeNames("dataset/*"));
for (String tableName : tableNames) {
if (sortedByPrimaryKey) {
this.addTableSorted(tableName);
} else {
this.addTable(tableName);
}
}
return this;
} catch (Exception ex) {
throw new JuDbException("Couldn't add tables by dataset " + resourceUrl, ex);
}
}
private void doWork(final DataSetWork dataSetWork) {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
dataSetWork.execute(createDataSet(conn));
};
});
}
private IDataSet createDataSet(IDatabaseConnection conn) {
IDataSet dataSet = exportItems.createDataSet(conn);
return dataSet;
}
/**
* Writes the DB data to an XML Output Converter that can convert or write it to different
* output formats without having to re-execute the whole DbUnit export.
* @return
*/
public XmlOutputConverter writeToXml() {
final DataHolder doc = new DataHolder<>();
this.doWork(new DataSetWork() {
@Override
public void execute(IDataSet dataSet) {
try {
XmlOutputConverter xmlConv = new XmlOutputConverter();
ExportBuilder.writeToXml(dataSet, xmlConv.getOutputStream());
doc.setValue(xmlConv);
} catch (Exception ex) {
throw new JuDbException("Couldn't write DB data to XML Output Converter", ex);
}
}
});
return doc.getValue();
}
/**
* Writes the DB data to an (in-memory) XML Document.
* @return Xml Document instance
*/
public Document writeToXmlDocument() {
return this.writeToXml().getDocument();
}
/**
* Writes the DB data to a String.
* @return Xml Document instance
*/
public String writeToXmlString() {
return this.writeToXml().getXmlString();
}
/**
* Write the DB data to an XML file.
*
* File name will be resolved relatively to the current working directory.
* @param fileName File name
* @deprecated Use writeToXmlFile(Path) instead
*/
@Deprecated
public void writeToXmlFile(String fileName) {
this.writeToXmlFile(Paths.get(fileName));
}
/**
* Write the DB data to an XML file.
* @param file Path to the file
*/
public void writeToXmlFile(final Path file) {
this.writeToXml().writeToXmlFile(file);
}
private interface DataSetWork {
void execute(IDataSet dataSet);
}
/**
* Custom implementation of FlatXmlDataSet.write so we can enforce column casing
* @param dataSet
* @param out
* @throws IOException
* @throws DataSetException
*/
private static void writeToXml(IDataSet dataSet, OutputStream out) throws IOException, DataSetException {
CaseAwareFlatXmlWriter datasetWriter = new CaseAwareFlatXmlWriter(out, "utf-8");
datasetWriter.setIncludeEmptyTable(true);
datasetWriter.write(dataSet);
}
}
/**
* Builder class to configure and execute DB data imports.
* @author Martin
*
*/
public static class ImportBuilder {
private Logger logger = LoggerFactory.getLogger(ImportBuilder.class);
private final DbDataUtil dbDataUtil;
private FlatXmlDataSet flatXmlDataSet;
private URL dataSetUrl;
private boolean automatedTableOrder = false;
private ImportBuilder(DbDataUtil dbDataUtil) {
this.dbDataUtil = dbDataUtil;
}
/**
* If true, Import tries to order tables in change set automatically according to
* foreign key constraints.
*
* Default value is false.
* @param automatedTableOrder
* @return
*/
public ImportBuilder automatedTableOrder(boolean automatedTableOrder) {
this.automatedTableOrder = automatedTableOrder;
return this;
}
/**
* Imports DB data from the specified XML.
*
* Only one 'from' is possible per import.
* @param resourcePath Resource path, either absolute or relative to the current class
* @return ImportBuilder
*/
public ImportBuilder from(String resourcePath) {
URL url = JuUrl.resource().relativeTo(ReflectUtils.getCallingClass()).get(resourcePath);
return from(url);
}
/**
* Imports DB data from the specified XML
* @param xmlUrl URL to XML file location
*/
public ImportBuilder from(URL xmlUrl) {
try {
flatXmlDataSet = new FlatXmlDataSetBuilder()
.setColumnSensing(true)
.setCaseSensitiveTableNames(false)
.build(xmlUrl);
this.dataSetUrl = xmlUrl;
return this;
} catch (Exception ex) {
throw new JuDbException("Couldn't import data from XML: " + xmlUrl, ex);
}
}
/**
* Performs a clean import of the data into the DB, i.e. cleans any existing
* data in affected tables and imports the rows specified in in this builder.
*/
public void executeCleanInsert() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
logger.debug("Executing Clean-Insert from: " + dataSetUrl);
DatabaseOperation.CLEAN_INSERT.execute(conn, getDataSet(conn));
} catch (Exception ex) {
throw new JuDbException("Couldn't clean and insert data into DB", ex);
}
}
});
}
private IDataSet getDataSet(IDatabaseConnection conn) throws Exception {
IDataSet ds = flatXmlDataSet;
// If automated table order is enabled, decorate the table set with a database sequence filter
if (this.automatedTableOrder) {
ds = new FilteredDataSet(new DatabaseSequenceFilter(conn), flatXmlDataSet);
}
return ds;
}
/**
* Truncates all tables included in the data set.
*/
public void executeDeleteAll() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
logger.debug("Executing Delete-All from: " + dataSetUrl);
DatabaseOperation.DELETE_ALL.execute(conn, getDataSet(conn));
} catch (Exception ex) {
throw new JuDbException("Couldnt truncate data in DB", ex);
}
};
});
}
/**
* Performs an import of the data into the DB, without cleaning any data
* previously.
*/
public void executeInsert() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
logger.debug("Executing Insert from: " + dataSetUrl);
DatabaseOperation.INSERT.execute(conn, getDataSet(conn));
} catch (Exception ex) {
throw new JuDbException("Couldnt insert data into DB", ex);
}
};
});
}
/**
* Performs an update of the existing data in the DB, without inserting new data.
*/
public void executeUpdate() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
logger.debug("Executing Update from: " + dataSetUrl);
DatabaseOperation.UPDATE.execute(conn, getDataSet(conn));
} catch (Exception ex) {
throw new JuDbException("Couldnt update data in DB", ex);
}
};
});
}
}
/**
* Builder class to configure and execute DB data asserts.
* @author Martin
*
*/
public static class AssertBuilder {
private final DbDataUtil dbDataUtil;
private FlatXmlDataSet flatXmlDataSet;
private URL dataSetUrl;
private AssertBuilder(DbDataUtil dbDataUtil) {
this.dbDataUtil = dbDataUtil;
}
/**
* Path to XML of expected data.
* @param resourcePath Resource path, either absolute or relative to the current class
* @return AssertBuilder
*/
public AssertBuilder expected(String resourcePath) {
URL url = JuUrl.resource().relativeTo(ReflectUtils.getCallingClass()).get(resourcePath);
return expected(url);
}
/**
* URL to XML of expected data.
* @param xmlUrl URL to XML file location
* @return This builder to allow for chaining
*/
public AssertBuilder expected(URL xmlUrl) {
try {
this.dataSetUrl = xmlUrl;
flatXmlDataSet = new FlatXmlDataSetBuilder().build(xmlUrl);
return this;
} catch (Exception ex) {
throw new JuDbException("Couldn't import data from XML: xmlUrl", ex);
}
}
/**
* Asserts that the data exported based on the result data set (i.e. all tables contained in the dataset, sorted
* by primary key) equals the result data set.
*/
public void assertEquals() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
IDataSet dbDataSet = dbDataUtil.buildExport()
.addTablesByDataSet(dataSetUrl, true)
.createDataSet(conn);
Assertion.assertEquals(flatXmlDataSet, dbDataSet);
} catch (Exception ex) {
throw new JuDbException("Couldn't assert DB data", ex);
}
}
});
}
/**
* Asserts that the whole data set in the DB equals the expected data.
* TODO: Add functionality to exclude (system) tables
*/
public void assertEqualsAll() {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
IDataSet dbDataSet = conn.createDataSet();
Assertion.assertEquals(flatXmlDataSet, dbDataSet);
} catch (Exception ex) {
throw new JuDbException("Couldn't assert DB data", ex);
}
}
});
}
/**
* Asserts that the export from the specified table equals the expected data.
* @param tableName Name of the table to assert
* @param orderColumnName Name of the column to order data by for the export
*/
public void assertEqualsTable(final String tableName, final String orderColumnName) {
this.dbDataUtil.execute(new DbUnitWork() {
@Override
public void execute(IDatabaseConnection conn) {
try {
QueryDataSet tableDataSet = new QueryDataSet(conn);
tableDataSet.addTable(tableName, String.format("select * from %s order by %s", tableName, orderColumnName));
Assertion.assertEquals(flatXmlDataSet, tableDataSet);
} catch (Exception ex) {
throw new JuDbException("Couldn't assert DB data", ex);
}
}
});
}
}
}