package dbfit.environment;
import dbfit.annotations.DatabaseEnvironment;
import dbfit.api.AbstractDbEnvironment;
import dbfit.util.DbParameterAccessor;
import dbfit.util.Direction;
import static dbfit.util.NameNormaliser.normaliseName;
import java.math.BigDecimal;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Time;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
/**
* Encapsulates support for the Derby database (also known as JavaDB). Operates
* in Client mode.
*
* @see EmbeddedDerbyEnvironment
* @author Pål Brattberg, pal.brattberg@acando.com
*/
@DatabaseEnvironment(name="Derby", driver="org.apache.derby.jdbc.ClientDriver")
public class DerbyEnvironment extends AbstractDbEnvironment {
private TypeMapper typeMapper = new DerbyTypeMapper();
public DerbyEnvironment(String driverClassName) {
super(driverClassName);
}
@Override
protected String getConnectionString(String dataSource) {
return String.format("jdbc:derby://%s", dataSource);
}
@Override
protected String getConnectionString(String dataSource, String database) {
return String.format("jdbc:derby://%s/%s", dataSource, database);
}
private static final String paramNamePattern = "@([A-Za-z0-9_]+)";
private static final Pattern paramRegex = Pattern.compile(paramNamePattern);
@Override
public Pattern getParameterPattern() {
return paramRegex;
}
@Override
public Map<String, DbParameterAccessor> getAllColumns(String tableName)
throws SQLException {
return new DatabaseTable(buildDatabaseObjectName(tableName)).getParams();
}
private abstract class DatabaseObject {
protected DatabaseObjectName name;
private DatabaseObject(DatabaseObjectName objName) {
name = objName;
}
protected DatabaseMetaData getDbMetaData() throws SQLException {
return getConnection().getMetaData();
}
abstract protected ResultSet getObjectMetaData() throws SQLException;
public boolean exists() throws SQLException {
try (ResultSet rs = getObjectMetaData()) {
return rs.next();
}
}
abstract protected ResultSet getColumns() throws SQLException;
public Map<String, DbParameterAccessor> getParams() throws SQLException {
Map<String, DbParameterAccessor> allParams = new HashMap<>();
try (ResultSet rs = getColumns()) {
while (rs.next()) {
DbParameterAccessor accessor = createColumnAccessor(rs);
allParams.put(normaliseName(accessor.getName()), accessor);
}
}
return allParams;
}
protected DbParameterAccessor createColumnAccessor(ResultSet rs)
throws SQLException {
String paramTypeName = rs.getString("TYPE_NAME");
return createDbParameterAccessor(getColumnName(rs), columnDirection(rs),
typeMapper.getJDBCSQLTypeForDBType(paramTypeName),
getJavaClass(paramTypeName), rs.getInt("ORDINAL_POSITION") - 1);
}
protected String getColumnName(ResultSet rs) throws SQLException {
return defaultIfNull(rs.getString("COLUMN_NAME"), "");
}
protected Direction columnDirection(ResultSet rs) throws SQLException {
return requireNonNull(getColumnDirection(rs),
"Invalid type for column/parameter " + getColumnName(rs));
}
abstract protected Direction getColumnDirection(ResultSet rs) throws SQLException;
}
private class DatabaseTable extends DatabaseObject {
private DatabaseTable(DatabaseObjectName objName) {
super(objName);
}
@Override
protected ResultSet getObjectMetaData() throws SQLException {
return getDbMetaData().getTables(null, name.getSchemaName(), name.getObjectName(), null);
}
@Override
public ResultSet getColumns() throws SQLException {
return getDbMetaData().getColumns(null, name.getSchemaName(), name.getObjectName(), "%");
}
@Override
protected Direction getColumnDirection(ResultSet rs) {
return Direction.INPUT;
}
}
private class DatabaseProcedure extends DatabaseObject {
private DatabaseProcedure(DatabaseObjectName objName) {
super(objName);
}
@Override
protected ResultSet getObjectMetaData() throws SQLException {
return getDbMetaData().getProcedures(null, name.getSchemaName(), name.getObjectName());
}
@Override
public ResultSet getColumns() throws SQLException {
return getDbMetaData().getProcedureColumns(null, name.getSchemaName(), name.getObjectName(), "%");
}
@Override
protected Direction getColumnDirection(ResultSet rs) throws SQLException {
switch (rs.getInt("COLUMN_TYPE")) {
case DatabaseMetaData.procedureColumnIn:
return Direction.INPUT;
case DatabaseMetaData.procedureColumnInOut:
return Direction.INPUT_OUTPUT;
case DatabaseMetaData.procedureColumnOut:
return Direction.OUTPUT;
default:
return null;
}
}
}
private class DatabaseFunction extends DatabaseObject {
private DatabaseFunction(DatabaseObjectName objName) {
super(objName);
}
@Override
protected ResultSet getObjectMetaData() throws SQLException {
return getDbMetaData().getFunctions(null, name.getSchemaName(), name.getObjectName());
}
@Override
public ResultSet getColumns() throws SQLException {
return getDbMetaData().getFunctionColumns(null, name.getSchemaName(), name.getObjectName(), "%");
}
@Override
protected Direction getColumnDirection(ResultSet rs) throws SQLException {
switch (rs.getInt("COLUMN_TYPE")) {
case DatabaseMetaData.functionColumnIn:
return Direction.INPUT;
case DatabaseMetaData.functionReturn:
return Direction.RETURN_VALUE;
default:
return null;
}
}
}
private static class DatabaseObjectName {
String schemaName, objectName;
private DatabaseObjectName(String objSchemaName, String objName) {
schemaName = objSchemaName;
objectName = objName;
}
private String getSchemaName() {
return schemaName;
}
String getObjectName() {
return objectName;
}
}
DatabaseObjectName buildDatabaseObjectName(String objName) throws SQLException {
String[] qualifiers = objName.toUpperCase().split("\\.");
String schemaName = (qualifiers.length == 2) ? qualifiers[0] : getConnection().getSchema();
String objectName = (qualifiers.length == 2) ? qualifiers[1] : qualifiers[0];
return new DatabaseObjectName(schemaName, objectName);
}
DatabaseObject findStoredRoutine(DatabaseObjectName objName) throws SQLException {
List<DatabaseObject> prioritisedCandidates = Arrays.asList(
new DatabaseProcedure(objName),
new DatabaseFunction(objName));
for (DatabaseObject object : prioritisedCandidates) {
if (object.exists()) {
return object;
}
}
throw new SQLException("Procedure/function " + objName.getObjectName() + " does not exist");
}
@Override
public Map<String, DbParameterAccessor> getAllProcedureParameters(String callableName)
throws SQLException {
return findStoredRoutine(buildDatabaseObjectName(callableName)).getParams();
}
@Override
public Class<?> getJavaClass(final String dataType) {
return typeMapper.getJavaClassForDBType(dataType);
}
/**
* Interface for mapping of db types to java types.
*/
public static interface TypeMapper {
Class<?> getJavaClassForDBType(final String dbDataType);
int getJDBCSQLTypeForDBType(final String dbDataType);
}
/**
* From http://db.apache.org/derby/docs/10.4/ref/ref-single.html
*/
public static class DerbyTypeMapper implements TypeMapper {
private static final List<String> stringTypes = Arrays
.asList(new String[] { "CHAR", "CHARACTER", "LONG VARCHAR",
"VARCHAR", "XML", "CHAR VARYING", "CHARACTER VARYING",
"LONG VARCHAR FOR BIT DATA", "VARCHAR FOR BIT DATA" });
private static final List<String> intTypes = Arrays
.asList(new String[] { "INTEGER", "INT" });
private static final List<String> longTypes = Arrays
.asList(new String[] { "BIGINT", });
private static final List<String> doubleTypes = Arrays
.asList(new String[] { "DOUBLE", "DOUBLE PRECISION", "FLOAT" });
private static final List<String> floatTypes = Arrays
.asList(new String[] { "REAL" });
private static final List<String> shortTypes = Arrays
.asList(new String[] { "SMALLINT" });
private static final List<String> decimalTypes = Arrays
.asList(new String[] { "DECIMAL", "DEC", "NUMERIC" });
private static final List<String> dateTypes = Arrays
.asList(new String[] { "DATE" });
private static final List<String> timestampTypes = Arrays
.asList(new String[] { "TIMESTAMP", });
private static final List<String> timeTypes = Arrays
.asList(new String[] { "TIME" });
@Override
public Class<?> getJavaClassForDBType(final String dbDataType) {
String dataType = normaliseTypeName(dbDataType);
if (stringTypes.contains(dataType))
return String.class;
if (decimalTypes.contains(dataType))
return BigDecimal.class;
if (intTypes.contains(dataType))
return Integer.class;
if (timeTypes.contains(dataType))
return Time.class;
if (dateTypes.contains(dataType))
return java.sql.Date.class;
if (floatTypes.contains(dataType))
return Float.class;
if (shortTypes.contains(dataType))
return Short.class;
if (doubleTypes.contains(dataType))
return Double.class;
if (longTypes.contains(dataType))
return Long.class;
if (timestampTypes.contains(dataType))
return java.sql.Timestamp.class;
throw new UnsupportedOperationException("Type '" + dbDataType
+ "' is not supported for Derby");
}
@Override
public int getJDBCSQLTypeForDBType(final String dbDataType) {
String dataType = normaliseTypeName(dbDataType);
if (stringTypes.contains(dataType))
return java.sql.Types.VARCHAR;
if (decimalTypes.contains(dataType))
return java.sql.Types.DECIMAL;
if (intTypes.contains(dataType) || shortTypes.contains(dataType))
return java.sql.Types.INTEGER;
if (floatTypes.contains(dataType))
return java.sql.Types.FLOAT;
if (doubleTypes.contains(dataType))
return java.sql.Types.DOUBLE;
if (longTypes.contains(dataType))
return java.sql.Types.BIGINT;
if (timestampTypes.contains(dataType))
return java.sql.Types.TIMESTAMP;
if (timeTypes.contains(dataType))
return java.sql.Types.TIME;
if (dateTypes.contains(dataType))
return java.sql.Types.DATE;
throw new UnsupportedOperationException("Type '" + dbDataType
+ "' is not supported for Derby");
}
private static String normaliseTypeName(String type) {
if (type != null && !"".equals(type)) {
String dataType = type.toUpperCase().trim();
// remove any size declarations such as CHAR(nn)
int idxLeftPara = dataType.indexOf('(');
if (idxLeftPara > 0) {
dataType = dataType.substring(0, idxLeftPara);
}
// remove any modifiers such as CHAR NOT NULL, but keep support
// for INTEGER UNSIGNED. Yes, I know this is funky coding, but
// it works, just see the unit tests! ;)
idxLeftPara = dataType.indexOf(" NOT NULL");
if (idxLeftPara > 0) {
dataType = dataType.substring(0, idxLeftPara);
}
idxLeftPara = dataType.indexOf(" NULL");
if (idxLeftPara > 0) {
dataType = dataType.substring(0, idxLeftPara);
}
return dataType;
} else {
throw new IllegalArgumentException(
"You must specify a valid type for conversions");
}
}
}
}