Skip to content

Commit 1d2edb3

Browse files
committed
ISSUE-680 # Add value converter and refactor tests
1 parent 688f967 commit 1d2edb3

File tree

6 files changed

+375
-42
lines changed

6 files changed

+375
-42
lines changed

core/pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@
184184
<artifactId>h2</artifactId>
185185
<scope>test</scope>
186186
</dependency>
187+
<!--uncomment this to test database with postgres
188+
<dependency>
189+
<groupId>org.postgresql</groupId>
190+
<artifactId>postgresql</artifactId>
191+
<version>42.7.4</version>
192+
<scope>test</scope>
193+
</dependency>
194+
-->
187195
<dependency>
188196
<groupId>com.aventstack</groupId>
189197
<artifactId>extentreports</artifactId>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import static org.apache.commons.lang3.time.DateUtils.parseDate;
4+
5+
import java.sql.Connection;
6+
import java.sql.DatabaseMetaData;
7+
import java.sql.ResultSet;
8+
import java.sql.SQLException;
9+
import java.text.ParseException;
10+
import java.util.LinkedHashMap;
11+
import java.util.Map;
12+
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
/**
17+
* Conversion of string values to be inserted in the database
18+
* into objects compatible with the java.sql type of the target columns.
19+
*/
20+
public class DbValueConverter {
21+
private static final Logger LOGGER = LoggerFactory.getLogger(DbSqlExecutor.class);
22+
23+
private Connection conn;
24+
private String table;
25+
private DatabaseMetaData databaseMetaData;
26+
public Map<String, Integer> columnTypes; // java.sql.Types
27+
28+
public DbValueConverter(Connection conn, String table) {
29+
this.conn = conn;
30+
this.table = table;
31+
try {
32+
initializeMetadata();
33+
} catch (Exception e) {
34+
logInitializeError();
35+
}
36+
}
37+
38+
private void initializeMetadata() throws SQLException {
39+
LOGGER.info("Metadata initialization for table: {}", table);
40+
columnTypes = new LinkedHashMap<>(); // must keep column order
41+
databaseMetaData = conn.getMetaData();
42+
43+
table = convertToStoredCase(table); // to locate table name in metadata
44+
LOGGER.info("Database storesLowerCaseIdentifiers={}, storesUpperCaseIdentifiers={}",
45+
databaseMetaData.storesLowerCaseIdentifiers(), databaseMetaData.storesUpperCaseIdentifiers());
46+
47+
try (ResultSet rs = databaseMetaData.getColumns(null, null, table, "%")) {
48+
while (rs.next()) {
49+
String storedName = rs.getString("COLUMN_NAME");
50+
int typeValue = rs.getInt("DATA_TYPE");
51+
// internally, key is lowercase to allow case insensitive lookups
52+
columnTypes.put(storedName.toLowerCase(), typeValue);
53+
}
54+
}
55+
LOGGER.info("Mapping from java columns to sql types: {}", columnTypes.toString());
56+
if (columnTypes.isEmpty())
57+
logInitializeError();
58+
}
59+
60+
private String convertToStoredCase(String identifier) throws SQLException {
61+
if (databaseMetaData.storesLowerCaseIdentifiers())
62+
identifier = identifier.toLowerCase();
63+
else if (databaseMetaData.storesUpperCaseIdentifiers())
64+
identifier = identifier.toUpperCase();
65+
return identifier;
66+
}
67+
68+
private void logInitializeError() {
69+
LOGGER.error("Initialization of metadata for table {} failed. "
70+
+ "Errors may appear when matching query parameters to their data types", table);
71+
}
72+
73+
/**
74+
* Given an array of column names and their corresponding values (as strings)
75+
* transforms each value to the compatible data type that allow to be inserted in the database.
76+
* If the column names are missing, uses all columns in the current table as fallback.
77+
*/
78+
Object[] convertColumnValues(String[] columns, String[] values) {
79+
if (columns == null || columns.length == 0) // if no specified, use all columns in the table
80+
columns = columnTypes.keySet().toArray(new String[0]);
81+
82+
Object[] converted = new Object[values.length];
83+
for (int i = 0; i < values.length; i++) {
84+
converted[i] = i < columns.length && i < values.length
85+
? convertColumnValue(columns[i], values[i])
86+
: values[i];
87+
}
88+
return converted;
89+
}
90+
91+
private Object convertColumnValue(String column, String value) {
92+
try {
93+
return convertColumnValueWithThrow(column, value);
94+
} catch (ParseException e) {
95+
LOGGER.error("Can't convert the data type of value {} at column {}", value, column);
96+
return value;
97+
}
98+
}
99+
100+
/**
101+
* Converts the string representation of a data type value into the appropriate simple sql data type.
102+
* If a data type is not handled by this method, returns the input value as fallback.
103+
*
104+
* See table B-1 in JDBC 4.2 Specification
105+
*/
106+
private Object convertColumnValueWithThrow(String column, String value) throws ParseException {
107+
if (value == null)
108+
return null;
109+
if (!columnTypes.containsKey(column.toLowerCase())) // fallback if no metadata
110+
return value;
111+
112+
int sqlType = columnTypes.get(column.toLowerCase());
113+
return convertColumnValueFromJavaSqlType(sqlType, value);
114+
}
115+
116+
private Object convertColumnValueFromJavaSqlType(int sqlType, String value) throws ParseException {
117+
switch (sqlType) {
118+
case java.sql.Types.NUMERIC:
119+
case java.sql.Types.DECIMAL: return java.math.BigDecimal.valueOf(Double.parseDouble(value));
120+
121+
case java.sql.Types.BIT: //accepts "1" as true (e.g. SqlServer)
122+
case java.sql.Types.BOOLEAN: return Boolean.valueOf("1".equals(value) ? "true" : value);
123+
124+
case java.sql.Types.TINYINT: return Byte.valueOf(value);
125+
case java.sql.Types.SMALLINT: return Short.valueOf(value);
126+
case java.sql.Types.INTEGER: return Integer.valueOf(value);
127+
case java.sql.Types.BIGINT: return Long.valueOf(value);
128+
129+
case java.sql.Types.REAL: return Float.valueOf(value);
130+
case java.sql.Types.FLOAT: return Double.valueOf(value);
131+
case java.sql.Types.DOUBLE: return Double.valueOf(value);
132+
133+
case java.sql.Types.DATE: return new java.sql.Date(parseDate(value, getDateFormats()).getTime());
134+
case java.sql.Types.TIME: return new java.sql.Time(parseDate(value, getTimeFormats()).getTime());
135+
case java.sql.Types.TIMESTAMP: return new java.sql.Timestamp(parseDate(value, getTimestampFormats()).getTime());
136+
default:
137+
return value;
138+
}
139+
}
140+
141+
// Currently, supported date time formats are a few common ISO-8601 formats
142+
// (other common format strings in org.apache.commons.lang3.time.DateFormatUtils)
143+
// This may be made user configurable later, via properties and/or embedded in the payload
144+
145+
private String[] getDateFormats() {
146+
return new String[] {"yyyy-MM-dd"};
147+
}
148+
private String[] getTimeFormats() {
149+
return new String[] {"HH:mm:ssZ", "HH:mm:ss.SSSZ"};
150+
}
151+
private String[] getTimestampFormats() {
152+
return new String[] {"yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss.SSSZ"};
153+
}
154+
155+
}

core/src/test/java/org/jsmart/zerocode/core/db/DbSqlRunnerTest.java

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,62 +6,29 @@
66

77
import java.io.FileNotFoundException;
88
import java.io.IOException;
9-
import java.sql.Connection;
10-
import java.sql.DriverManager;
119
import java.sql.SQLException;
1210
import java.util.List;
1311
import java.util.Map;
14-
import java.util.Properties;
1512

16-
import org.apache.commons.dbutils.DbUtils;
1713
import org.apache.commons.dbutils.QueryRunner;
18-
import org.jsmart.zerocode.core.utils.PropertiesProviderUtils;
19-
import org.junit.After;
2014
import org.junit.Before;
21-
import org.junit.BeforeClass;
2215
import org.junit.Test;
2316

24-
public class DbSqlRunnerTest {
25-
26-
private static final String DB_PROPERTIES_RESOURCE = "db_test.properties";
27-
private Connection conn;
28-
29-
@BeforeClass
30-
public static void classSetUp() throws FileNotFoundException, SQLException, IOException {
31-
Connection createConn = connect();
32-
new QueryRunner().update(createConn, "DROP TABLE IF EXISTS SQLTABLE; "
33-
+ "CREATE TABLE SQLTABLE (ID INTEGER, NAME VARCHAR(20)); ");
34-
DbUtils.closeQuietly(createConn);
35-
}
17+
public class DbSqlRunnerTest extends DbTestBase {
3618

3719
@Before
3820
public void setUp() throws ClassNotFoundException, SQLException, FileNotFoundException, IOException {
39-
conn = connect();
40-
new QueryRunner().update(conn, "DELETE FROM SQLTABLE; "
21+
super.setUp();
22+
new QueryRunner().update(conn, "DROP TABLE IF EXISTS SQLTABLE; "
23+
+ "CREATE TABLE SQLTABLE (ID INTEGER, NAME VARCHAR(20)); "
4124
+ "INSERT INTO SQLTABLE VALUES (1, 'string 1'); "
4225
+ "INSERT INTO SQLTABLE VALUES (2, 'string 2');");
4326
}
4427

45-
@After
46-
public void tearDown() throws Exception {
47-
DbUtils.closeQuietly(conn);
48-
}
49-
50-
private static Connection connect() throws SQLException, FileNotFoundException, IOException {
51-
Properties prop = PropertiesProviderUtils.getProperties(DB_PROPERTIES_RESOURCE);
52-
return DriverManager.getConnection(
53-
prop.getProperty("db.driver.url"), prop.getProperty("db.driver.user"), prop.getProperty("db.driver.password") );
54-
}
55-
56-
private List<Map<String, Object>> execute(String sql, Object[] params) throws SQLException {
57-
DbSqlRunner runner = new DbSqlRunner(conn);
58-
return runner.execute(sql, params);
59-
}
60-
6128
@Test
6229
public void sqlSelectQueryShouldReturnListOfMap() throws ClassNotFoundException, SQLException {
6330
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID DESC", null);
64-
assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}, {ID=1, NAME=string 1}]"));
31+
assertThat(rows.toString(), equalTo(convertDbCase("[{ID=2, NAME=string 2}, {ID=1, NAME=string 1}]")));
6532
}
6633

6734
@Test
@@ -73,7 +40,7 @@ public void sqlSelectWithoutResultsShouldReturnEmptyList() throws ClassNotFoundE
7340
@Test
7441
public void multipleSqlSelectShouldReturnTheFirstResultSet() throws ClassNotFoundException, SQLException {
7542
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID=2; SELECT ID, NAME FROM SQLTABLE where ID=1;", null);
76-
assertThat(rows.toString(), equalTo("[{ID=2, NAME=string 2}]"));
43+
assertThat(rows.toString(), equalTo(convertDbCase("[{ID=2, NAME=string 2}]")));
7744
}
7845

7946
@Test
@@ -82,14 +49,14 @@ public void sqlInsertShouldReturnNull() throws ClassNotFoundException, SQLExcept
8249
assertThat(nullRows, nullValue());
8350
// check rows are inserted
8451
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE ORDER BY ID", new Object[] {});
85-
assertThat(rows.toString(), equalTo("[{ID=1, NAME=string 1}, {ID=2, NAME=string 2}, {ID=3, NAME=string 3}]"));
52+
assertThat(rows.toString(), equalTo(convertDbCase("[{ID=1, NAME=string 1}, {ID=2, NAME=string 2}, {ID=3, NAME=string 3}]")));
8653
}
8754

8855
@Test
8956
public void executeWithParametersShouldAllowNulls() throws SQLException {
9057
execute("INSERT INTO SQLTABLE VALUES (?, ?)", new Object[] { 4, null });
9158
List<Map<String, Object>> rows = execute("SELECT ID, NAME FROM SQLTABLE where ID = ?", new Object[] { 4 });
92-
assertThat(rows.toString(), equalTo("[{ID=4, NAME=null}]"));
59+
assertThat(rows.toString(), equalTo(convertDbCase("[{ID=4, NAME=null}]")));
9360
}
9461

9562
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.jsmart.zerocode.core.db;
2+
3+
import java.io.FileNotFoundException;
4+
import java.io.IOException;
5+
import java.sql.Connection;
6+
import java.sql.DriverManager;
7+
import java.sql.SQLException;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Properties;
11+
12+
import org.apache.commons.dbutils.DbUtils;
13+
import org.jsmart.zerocode.core.utils.PropertiesProviderUtils;
14+
import org.junit.After;
15+
import org.junit.Before;
16+
17+
/**
18+
* Base class for the unit DB test classes: manages connections,
19+
* execution of queries and DBMS specific features
20+
*/
21+
public class DbTestBase {
22+
23+
private static final String DB_PROPERTIES_RESOURCE = "db_test.properties";
24+
protected Connection conn; // managed connection for each test
25+
protected boolean isPostgres = false; // set by each connection, to allow portable assertions (postgres is lowercase)
26+
27+
@Before
28+
public void setUp() throws ClassNotFoundException, SQLException, FileNotFoundException, IOException {
29+
conn = connect();
30+
}
31+
32+
@After
33+
public void tearDown() throws Exception {
34+
DbUtils.closeQuietly(conn);
35+
}
36+
37+
protected Connection connect() throws SQLException {
38+
Properties prop = PropertiesProviderUtils.getProperties(DB_PROPERTIES_RESOURCE);
39+
isPostgres = prop.getProperty("db.driver.url").startsWith("jdbc:postgresql:");
40+
return DriverManager.getConnection(
41+
prop.getProperty("db.driver.url"), prop.getProperty("db.driver.user"), prop.getProperty("db.driver.password") );
42+
}
43+
44+
protected List<Map<String, Object>> execute(String sql, Object[] params) throws SQLException {
45+
DbSqlRunner runner = new DbSqlRunner(conn);
46+
return runner.execute(sql, params);
47+
}
48+
49+
protected String convertDbCase(String value) {
50+
return isPostgres ? value.toLowerCase() : value;
51+
}
52+
53+
}

0 commit comments

Comments
 (0)