/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.storage.rdbms;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.List;
import mockit.Invocation;
import mockit.Mock;
import mockit.MockUp;
import org.apache.log4j.Logger;
import org.dspace.core.Context;
import static org.dspace.storage.rdbms.DatabaseManager.getColumnNames;
/**
* Mocks a DatabaseManager to add some custom logic / queries to support the
* H2 in-memory database for Unit Testing. By default DSpace does not fully
* support H2, so this MockDatabaseManager modifies some methods of DatabaseManager
* which are currently problematic in H2.
* <P>
* Any methods which are NOT "mocked" below are just used as-is from the
* original DatabaseManager class. Also note that, in order to support running
* Unit Tests on PostgreSQL & Oracle, any H2-specific logic is only executed
* after verifying your Unit Testing database reports that it is indeed H2.
*
* @author pvillega
* @author tdonohue
*/
public final class MockDatabaseManager
extends MockUp<DatabaseManager>
{
// Set our logger to specify the Mock class, so we know which logs are from the "real" vs "mock" class
private static final Logger log = Logger.getLogger(MockDatabaseManager.class);
/**
* Override/Mock the default "setConstraintDeferred()" method in order to
* add some custom H2-specific code (look for the comments with "H2" in them).
*
* Set the constraint check to deferred (commit time)
*
* @param context
* The context object
* @param constraintName
* the constraint name to deferred
* @throws SQLException
*/
@Mock
public static void setConstraintDeferred(Invocation inv, Context context,
String constraintName) throws SQLException
{
// What type of database is this?
String databaseType = DatabaseManager.getDbKeyword();
if(databaseType!=null && !databaseType.equals(DatabaseManager.DBMS_H2))
{
// If we are unit testing with a non-H2 database, just proceed to
// DatabaseManager method of the same name
inv.proceed(context, constraintName);
}
else
{
// Otherwise, we'll run slightly customized code in order to support H2
log.debug("Mocked setContraintDeferred() method for H2 database");
Statement statement = null;
try
{
statement = context.getDBConnection().createStatement();
//statement.execute("SET CONSTRAINTS " + constraintName + " DEFERRED");
// H2 does NOT support "SET CONSTRAINTS" syntax.
// Instead it requires the following SQL
statement.execute("SET REFERENTIAL_INTEGRITY FALSE");
statement.close();
}
finally
{
if (statement != null)
{
try
{
statement.close();
}
catch (SQLException sqle)
{
}
}
}
}
}
/**
* Override/Mock the default "setConstraintImmediate()" method in order to
* add some custom H2-specific code (look for the comments with "H2" in them).
*
* Set the constraint check to immediate (every query)
*
* @param context
* The context object
* @param constraintName
* the constraint name to check immediately after every query
* @throws SQLException
*/
@Mock
public static void setConstraintImmediate(Invocation inv, Context context,
String constraintName) throws SQLException
{
// What type of database is this?
String databaseType = DatabaseManager.getDbKeyword();
if(databaseType!=null && !databaseType.equals(DatabaseManager.DBMS_H2))
{
// If we are unit testing with a non-H2 database, just proceed to
// DatabaseManager method of the same name
inv.proceed(context, constraintName);
}
else
{
// Otherwise, we'll run slightly customized code in order to support H2
log.debug("Mocked setContraintImmediate() method for H2 database");
Statement statement = null;
try
{
statement = context.getDBConnection().createStatement();
//statement.execute("SET CONSTRAINTS " + constraintName + " IMMEDIATE");
// H2 does NOT support "SET CONSTRAINTS" syntax.
// Instead it requires the following SQL
statement.execute("SET REFERENTIAL_INTEGRITY TRUE");
statement.close();
}
finally
{
if (statement != null)
{
try
{
statement.close();
}
catch (SQLException sqle)
{
}
}
}
}
}
/**
* Override/Mock the default "process()" method in order to add some custom
* H2-specific code (look for the comments with "H2" in them below).
*
* Convert the current row in a ResultSet into a TableRow object.
*
* @param results
* A ResultSet to process
* @param table
* The name of the table
* @param pColumnNames
* The name of the columns in this resultset
* @return A TableRow object with the data from the ResultSet
* @exception SQLException
* If a database error occurs
*/
@Mock
static TableRow process(Invocation inv, ResultSet results, String table, List<String> pColumnNames) throws SQLException
{
// What type of database is this?
String databaseType = DatabaseManager.getDbKeyword();
// Also, is it Oracle-like?
boolean isOracle = DatabaseManager.isOracle();
if(databaseType!=null && !databaseType.equals(DatabaseManager.DBMS_H2))
{
// If we are unit testing with a non-H2 database, just proceed to
// DatabaseManager method of the same name
return inv.proceed(results, table, pColumnNames);
}
else
{
// Otherwise, we'll run slightly customized code in order to support H2
log.debug("Mocked process() method for H2 database");
ResultSetMetaData meta = results.getMetaData();
int columns = meta.getColumnCount() + 1;
// If we haven't been passed the column names try to generate them from the metadata / table
List<String> columnNames = pColumnNames != null ? pColumnNames :
((table == null) ? getColumnNames(meta) : getColumnNames(table));
TableRow row = new TableRow(DatabaseManager.canonicalize(table), columnNames);
// Process the columns in order
// (This ensures maximum backwards compatibility with
// old JDBC drivers)
for (int i = 1; i < columns; i++)
{
String name = meta.getColumnName(i);
int jdbctype = meta.getColumnType(i);
// Added for H2 debugging
log.debug("In mocked process(), column '" + name + "' is of SQL Type " + jdbctype);
switch (jdbctype)
{
case Types.BOOLEAN:
case Types.BIT:
row.setColumn(name, results.getBoolean(i));
break;
case Types.INTEGER:
if (isOracle)
{
long longValue = results.getLong(i);
if (longValue <= (long)Integer.MAX_VALUE)
{
row.setColumn(name, (int) longValue);
}
else
{
row.setColumn(name, longValue);
}
}
else
{
row.setColumn(name, results.getInt(i));
}
break;
case Types.NUMERIC:
case Types.DECIMAL:
row.setColumn(name, results.getBigDecimal(i));
break;
case Types.BIGINT:
row.setColumn(name, results.getLong(i));
break;
case Types.DOUBLE:
row.setColumn(name, results.getDouble(i));
break;
case Types.CLOB:
if (isOracle)
{
row.setColumn(name, results.getString(i));
}
else
{
throw new IllegalArgumentException("Unsupported JDBC type: " + jdbctype);
}
break;
case Types.VARCHAR:
/*try
{
byte[] bytes = results.getBytes(i);
if (bytes != null)
{
String mystring = new String(results.getBytes(i), "UTF-8");
row.setColumn(name, mystring);
}
else
{
row.setColumn(name, results.getString(i));
}
}
catch (UnsupportedEncodingException e)
{
log.error("Unable to parse text from database", e);
}*/
// H2 assumes that "getBytes()" should return hexidecimal.
// So, the above commented out code will throw a JdbcSQLException:
// "Hexadecimal string contains non-hex character"
// Instead, we just want to get the value as a string.
row.setColumn(name, results.getString(i));
// END ADDED for H2
break;
case Types.DATE:
row.setColumn(name, results.getDate(i));
break;
case Types.TIME:
row.setColumn(name, results.getTime(i));
break;
case Types.TIMESTAMP:
row.setColumn(name, results.getTimestamp(i));
break;
default:
throw new IllegalArgumentException("Unsupported JDBC type: " + jdbctype);
}
// Determines if the last column was null, and sets the tablerow accordingly
if (results.wasNull())
{
row.setColumnNull(name);
}
}
// Now that we've prepped the TableRow, reset the flags so that we can detect which columns have changed
row.resetChanged();
return row;
}
}
}