package dbfit.api;
import dbfit.util.*;
import dbfit.fixture.StatementExecution;
import static dbfit.util.Options.OPTION_AUTO_COMMIT;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Properties;
public abstract class AbstractDbEnvironment implements DBEnvironment {
protected Connection currentConnection;
protected String driverClassName;
protected TypeTransformerFactory dbfitToJdbcTransformerFactory = new TypeTransformerFactory();
protected String getDriverClassName() {
return driverClassName;
}
private boolean driverRegistered = false;
protected AbstractDbEnvironment(String driverClassName) {
this.driverClassName = driverClassName;
}
private void registerDriver() throws SQLException {
String driverName = getDriverClassName();
try {
if (driverRegistered)
return;
DriverManager.registerDriver((Driver) Class.forName(driverName)
.newInstance());
driverRegistered = true;
} catch (Exception e) {
throw new Error("Cannot register SQL driver " + driverName);
}
}
/**
* Intended to be overriden for post-connect activities
*/
protected void afterConnectionEstablished() throws SQLException {
setAutoCommit();
}
public void connect(String connectionString, Properties info) throws SQLException {
registerDriver();
closeConnection();
currentConnection = DriverManager.getConnection(connectionString, info);
afterConnectionEstablished();
}
@Override
public void connect(String connectionString) throws SQLException {
connect(connectionString, new Properties());
}
@Override
public void connect(String dataSource, String username, String password)
throws SQLException {
connect(dataSource, username, password, null);
}
@Override
public void connect(String dataSource, String username, String password,
String database) throws SQLException {
String connectionString = (database == null)
? getConnectionString(dataSource)
: getConnectionString(dataSource, database);
Properties props = new Properties();
props.put("user", username);
props.put("password", new PropertiesLoader().parseValue(password));
connect(connectionString, props);
}
@Override
public void connectUsingFile(String file) throws SQLException, IOException,
FileNotFoundException {
DbConnectionProperties dbp = DbConnectionProperties
.CreateFromFile(file);
if (dbp.FullConnectionString != null)
connect(dbp.FullConnectionString);
else if (dbp.DbName != null)
connect(dbp.Service, dbp.Username, dbp.Password, dbp.DbName);
else
connect(dbp.Service, dbp.Username, dbp.Password);
}
/**
* any processing required to turn a string into something jdbc driver can
* process, can be used to clean up CRLF, externalise parameters if required
* etc.
*/
protected String parseCommandText(String commandText) {
commandText = commandText.replace("\n", " ");
commandText = commandText.replace("\r", " ");
return commandText;
}
public final PreparedStatement createStatementWithBoundFixtureSymbols(
TestHost testHost, String commandText) throws SQLException {
String command = Options.isBindSymbols() ? parseCommandText(commandText) : commandText;
PreparedStatement cs = getConnection().prepareStatement(
command);
if (Options.isBindSymbols()) {
String paramNames[] = extractParamNames(commandText);
for (int i = 0; i < paramNames.length; i++) {
Object value = testHost.getSymbolValue(paramNames[i]);
cs.setObject(i + 1, value);
}
}
return cs;
}
@Override
public DdlStatementExecution createDdlStatementExecution(String ddl)
throws SQLException {
return new DdlStatementExecution(getConnection().createStatement(), ddl);
}
@Override
public StatementExecution createStatementExecution(PreparedStatement statement) {
return new StatementExecution(statement);
}
@Override
public StatementExecution createFunctionStatementExecution(PreparedStatement statement) {
return new StatementExecution(statement);
}
protected DbParameterAccessor createDbParameterAccessor(String name, Direction direction, int sqlType, Class javaType, int position) {
return new DbParameterAccessor(name, direction, sqlType, javaType, position, dbfitToJdbcTransformerFactory);
}
public void closeConnection() throws SQLException {
if (currentConnection != null) {
rollback();
currentConnection.close();
currentConnection = null;
}
}
public void commit() throws SQLException {
if (!getConnection().getAutoCommit()) {
currentConnection.commit();
}
}
public void rollback() throws SQLException {
if (!getConnection().getAutoCommit()) {
currentConnection.rollback();
}
}
@Override
public void setAutoCommit() throws SQLException {
String autoCommitMode = Options.get(OPTION_AUTO_COMMIT);
if (!"auto".equals(autoCommitMode) && isConnected(currentConnection)) {
if (currentConnection.getMetaData().supportsTransactions()) {
currentConnection.setAutoCommit(Options.is(OPTION_AUTO_COMMIT));
}
}
}
/*****/
protected abstract String getConnectionString(String dataSource);
protected abstract String getConnectionString(String dataSource,
String database);
public Connection getConnection() throws SQLException {
checkConnectionValid(currentConnection);
return currentConnection;
}
/**
* MUST RETURN PARAMETER NAMES IN EXACT ORDER AS IN STATEMENT. IF SINGLE
* PARAMETER APPEARS MULTIPLE TIMES, MUST BE LISTED MULTIPLE TIMES IN THE
* ARRAY ALSO
*/
public String[] extractParamNames(String commandText) {
ArrayList<String> hs = new ArrayList<String>();
Matcher mc = getParameterPattern().matcher(commandText);
while (mc.find()) {
hs.add(mc.group(1));
}
String[] array = new String[hs.size()];
return hs.toArray(array);
}
protected abstract Pattern getParameterPattern();
/**
* by default, uses a string generated by buildInsertCommand and creates a
* statement that returns generated keys via JDBC
*/
public PreparedStatement buildInsertPreparedStatement(String tableName,
DbParameterAccessor[] accessors) throws SQLException {
return getConnection().prepareStatement(
buildInsertCommand(tableName, accessors),
Statement.RETURN_GENERATED_KEYS);
}
/**
* This method should generate a valid insert statement which is used by
* buildInsertPreparedStatement to create the actual statement. It is
* isolated into a separate method so that subclasses can override one or
* the other depending on db specifics
*/
public String buildInsertCommand(String tableName,
DbParameterAccessor[] accessors) {
StringBuilder sb = new StringBuilder("insert into ");
sb.append(tableName).append("(");
String comma = "";
StringBuilder values = new StringBuilder();
for (DbParameterAccessor accessor : accessors) {
if (accessor.hasDirection(Direction.INPUT)) {
sb.append(comma);
values.append(comma);
sb.append(accessor.getName());
// values.append(":").append(accessor.getName());
values.append("?");
comma = ",";
}
}
sb.append(") values (");
sb.append(values);
sb.append(")");
return sb.toString();
}
public DbStoredProcedureCall newStoredProcedureCall(String name, DbParameterAccessor[] accessors) {
return new DbStoredProcedureCall(this, name, accessors);
}
public DbParameterAccessor createAutogeneratedPrimaryKeyAccessor(
DbParameterAccessor template) {
return new DbAutoGeneratedKeyAccessor(template);
}
/** Check the validity of the supplied connection. */
public static void checkConnectionValid(final Connection conn)
throws SQLException {
if (! isConnected(conn)) {
throw new IllegalArgumentException(
"No open connection to a database is available. "
+ "Make sure your database is running and that you have connected before performing any queries.");
}
}
private static boolean isConnected(final Connection conn) throws SQLException {
return (conn != null && !conn.isClosed());
}
}