cleaning the code, move sqlserver, table name normalization in flavor, get rid of properties/file handling in DatabaseProvider

main
Jörg Prante 2 years ago
parent ca784dc6cf
commit 7378b91c28

@ -1,4 +1,12 @@
This work is based upon
The work in jdbc-connection-pool is base upon
https://github.com/brettwooldridge/HikariCP
as of 28 Dec 2021
License: Apache 2.0
The work in jdbc-query is based upon
https://github.com/susom/database

@ -20,9 +20,9 @@ import org.xbib.jdbc.connection.pool.PoolConfig;
import org.xbib.jdbc.connection.pool.PoolDataSource;
@ExtendWith(PoolTestExtension.class)
public class SaturatedPoolTest830 {
public class SaturatedPoolTest {
private static final Logger logger = Logger.getLogger(SaturatedPoolTest830.class.getName());
private static final Logger logger = Logger.getLogger(SaturatedPoolTest.class.getName());
private static final int MAX_POOL_SIZE = 10;

@ -4,6 +4,7 @@ import org.xbib.jdbc.query.Flavor;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Locale;
public class MariaDB implements Flavor {
@ -26,8 +27,16 @@ public class MariaDB implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return false;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toLowerCase(Locale.ROOT);
}
@Override
@ -52,7 +61,7 @@ public class MariaDB implements Flavor {
@Override
public void setFloat(PreparedStatement preparedStatement, int i, Float floatValue) throws SQLException {
preparedStatement.setFloat(i, floatValue);
preparedStatement.setDouble(i, floatValue);
}
@Override
@ -98,6 +107,11 @@ public class MariaDB implements Flavor {
return "datetime(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "DATETIME";
}
@Override
public String typeLocalDate() {
return "date";

@ -1,6 +1,7 @@
package org.xbib.jdbc.mariadb.test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@ -9,13 +10,24 @@ import org.xbib.jdbc.query.Config;
import org.xbib.jdbc.query.ConfigSupplier;
import org.xbib.jdbc.query.DatabaseProvider;
import org.xbib.jdbc.query.OptionsOverride;
import org.xbib.jdbc.query.Schema;
import org.xbib.jdbc.query.Sql;
import org.xbib.jdbc.query.SqlArgs;
import org.xbib.jdbc.test.CommonTest;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MariaDBTest extends CommonTest {
static MariaDBContainer<?> mariaDBContainer;
static {
// mariadb 10.3.6
mariaDBContainer = new MariaDBContainer<>("mariadb")
.withDatabaseName("testDB")
.withUsername("testUser")
@ -88,21 +100,156 @@ public class MariaDBTest extends CommonTest {
super.intervals();
}
@Disabled("MariaDB temporarily disabled")
@Override
public void metadataColumnNames() {
super.intervals();
}
@Disabled("MariaDB temporarily disabled")
@Override
public void metadataColumnTypes() {
super.metadataColumnTypes();
}
@Disabled("MariaDB temporarily disabled")
/**
* MariaDB seems to have different behavior in that is does not convert
* column names.
* I haven't figured out how to smooth over this difference, since all databases
* seem to respect the provided case when it is inside quotes, but don't provide
* a way to tell whether a particular parameter was quoted.
*/
@Override
@Test
public void metadataColumnNames() {
db.dropTableQuietly("dbtest");
new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db);
db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest")
.query(rs -> {
Assertions.assertArrayEquals(new String[]{"Pk", "Foo", "Foo"}, rs.getColumnLabels());
return null;
});
}
/**
* This one is adjusted in that the float values are passed as double, because
* the database stores them both as double and there doesn't appear to be a way
* to tell that one was actually declared as a float.
*
* Also, CLOBs are stored as strings ("mediumtext" is just an alias).
*/
@Test
public void saveResultAsTable() {
super.saveResultAsTable();
new Schema().addTable("dbtest")
.addColumn("nbr_integer").asInteger().primaryKey().table()
.addColumn("nbr_long").asLong().table()
.addColumn("nbr_float").asFloat().table()
.addColumn("nbr_double").asDouble().table()
.addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
.addColumn("str_varchar").asString(80).table()
.addColumn("str_fixed").asStringFixed(1).table()
.addColumn("str_lob").asClob().table()
.addColumn("bin_blob").asBlob().table()
.addColumn("boolean_flag").asBoolean().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate().schema().execute(db);
db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar,"
+ " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")
.argInteger(Integer.MAX_VALUE)
.argLong(Long.MAX_VALUE)
.argDouble((double) Float.MAX_VALUE)
.argDouble(Double.MAX_VALUE)
.argBigDecimal(new BigDecimal("123.456"))
.argString("hello")
.argString("Z")
.argClobString("hello again")
.argBlobBytes(new byte[]{'1', '2'})
.argBoolean(true)
.argLocalDateTime(now)
.argLocalDate(localDateNow)
.insert(1);
db.toInsert("insert into dbtest (nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal, str_varchar,"
+ " str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date) values (?,?,?,?,?,?,?,?,?,?,?,?)")
.argInteger(Integer.MIN_VALUE)
.argLong(Long.MIN_VALUE)
.argDouble((double) Float.MIN_VALUE)
.argDouble(Double.MIN_VALUE)
.argBigDecimal(new BigDecimal("-123.456"))
.argString("goodbye")
.argString("A")
.argClobString("bye again")
.argBlobBytes(new byte[]{'3', '4'})
.argBoolean(false)
.argLocalDateTime(now)
.argLocalDate(localDateNow)
.insert(1);
String expectedSchema = new Schema().addTable("dbtest2")
.addColumn("nbr_integer").asInteger().table()
.addColumn("nbr_long").asLong().table()
.addColumn("nbr_float").asFloat().table()
.addColumn("nbr_double").asDouble().table()
.addColumn("nbr_big_decimal").asBigDecimal(19, 9).table()
.addColumn("str_varchar").asString(80).table()
.addColumn("str_fixed").asStringFixed(1).table()
.addColumn("str_lob").asClob().table()
.addColumn("bin_blob").asBlob().table()
.addColumn("boolean_flag").asBoolean().table()
.addColumn("date_millis").asLocalDateTime().table()
.addColumn("local_date").asLocalDate()
.schema()
.print(db.flavor());
List<SqlArgs> args = db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest")
.query(rs -> {
List<SqlArgs> result = new ArrayList<>();
while (rs.next()) {
if (result.size() == 0) {
db.dropTableQuietly("dbtest2");
Schema schema = new Schema().addTableFromRow("dbtest2", rs).schema();
assertEquals(expectedSchema, schema.print(db.flavor()));
schema.execute(db);
}
result.add(SqlArgs.readRow(rs));
}
return result;
});
db.toInsert(Sql.insert("dbtest2", args)).insertBatch();
assertEquals(2, db.toSelect("select count(*) from dbtest2").queryIntegerOrZero());
assertEquals(db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest order by 1")
.queryMany(SqlArgs::readRow),
db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1")
.queryMany(SqlArgs::readRow));
assertEquals(Arrays.asList(
new SqlArgs()
.argInteger("nbr_integer", Integer.MIN_VALUE)
.argLong("nbr_long", Long.MIN_VALUE)
.argDouble("nbr_float", (double) Float.MIN_VALUE)
.argDouble("nbr_double", Double.MIN_VALUE)
.argBigDecimal("nbr_big_decimal", new BigDecimal("-123.456"))
.argString("str_varchar", "goodbye")
.argString("str_fixed", "A")
.argString("str_lob", "bye again")
.argBlobBytes("bin_blob", new byte[]{'3', '4'})
.argString("boolean_flag", "N")//.argBoolean("boolean_flag", false)
.argLocalDateTime("date_millis", now)
.argLocalDate("local_date", localDateNow),
new SqlArgs()
.argInteger("nbr_integer", Integer.MAX_VALUE)
.argLong("nbr_long", Long.MAX_VALUE)
.argDouble("nbr_float", (double) Float.MAX_VALUE)
.argDouble("nbr_double", Double.MAX_VALUE)
.argBigDecimal("nbr_big_decimal", new BigDecimal("123.456"))
.argString("str_varchar", "hello")
.argString("str_fixed", "Z")
.argString("str_lob", "hello again")
.argBlobBytes("bin_blob", new byte[]{'1', '2'})
.argString("boolean_flag", "Y")//.argBoolean("boolean_flag", true)
.argLocalDateTime("date_millis", now)
.argLocalDate("local_date", localDateNow)),
db.toSelect("select nbr_integer, nbr_long, nbr_float, nbr_double, nbr_big_decimal,"
+ " str_varchar, str_fixed, str_lob, bin_blob, boolean_flag, date_millis, local_date from dbtest2 order by 1")
.queryMany(SqlArgs::readRow));
}
}

@ -28,8 +28,16 @@ public class Oracle implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return true;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toUpperCase();
}
@Override
@ -87,6 +95,11 @@ public class Oracle implements Flavor {
return "timestamp(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "TIMESTAMP";
}
@Override
public String typeLocalDate() {
return "date";

@ -4,6 +4,7 @@ import org.xbib.jdbc.query.Flavor;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Locale;
public class Postgresql implements Flavor {
@ -26,8 +27,16 @@ public class Postgresql implements Flavor {
}
@Override
public boolean isNormalizedUpperCase() {
return false;
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
return tableName.substring(1, tableName.length() - 1);
}
}
return tableName.toLowerCase(Locale.ROOT);
}
@Override
@ -95,6 +104,11 @@ public class Postgresql implements Flavor {
return "timestamp(3)";
}
@Override
public String columnTypeLocalDateTime() {
return "timestamp";
}
@Override
public String typeLocalDate() {
return "date";

@ -20,6 +20,7 @@ public class PostgresqlTest extends CommonTest {
static PostgreSQLContainer<?> postgreSQLContainer;
static {
// postgresql 9.6.12
postgreSQLContainer = new PostgreSQLContainer<>("postgres")
.withDatabaseName("testDB")
.withUsername("testUser")

@ -8,3 +8,4 @@ java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.FileHandler.pattern=build/database.log
jdk.event.security.level=INFO
javax.management.level=INFO
org.postgresql.level=INFO

@ -1,7 +1,13 @@
dependencies {
api project(":jdbc-connection-pool")
api project(':jdbc-connection-pool')
testImplementation project(':jdbc-test')
testImplementation libs.derby
testImplementation libs.hsqldb
testImplementation libs.h2
testImplementation libs.testcontainers
testImplementation libs.testcontainers.junit.jupiter
}
test {
systemProperty 'user.timezone', 'GMT'
}

@ -1,12 +1,12 @@
import org.xbib.jdbc.query.Flavor;
import org.xbib.jdbc.query.flavor.Derby;
import org.xbib.jdbc.query.flavor.H2;
import org.xbib.jdbc.query.flavor.Hsql;
import org.xbib.jdbc.query.flavor.SqlServer;
module org.xbib.jdbc.query {
uses Flavor;
requires org.xbib.jdbc.connection.pool;
requires java.sql;
exports org.xbib.jdbc.query;
provides Flavor with Derby, Hsql, SqlServer;
provides Flavor with Derby, Hsql, H2;
}

@ -11,6 +11,11 @@ import java.util.function.Supplier;
* Primary class for accessing a relational (SQL) database.
*/
public interface Database extends Supplier<Database> {
int jdbcMajorVersion();
int jdbcMinorVersion();
/**
* Create a SQL "insert" statement for further manipulation and execution.
* Note this call does not actually execute the SQL.
@ -181,18 +186,6 @@ public interface Database extends Supplier<Database> {
*/
Map<String, Integer> getColumnSizesOfTable(String tableName);
/**
* Return the DB table name in the normalized form in which it is stored.
* Databases like Oracle, Derby, HSQL store their tables in upper case.
* Databases like postgres and sqlserver use lower case unless configured otherwise.
* If the caller passes in a quoted string, we will leave the name as is, removing
* the quotes.
*
* @param tableName this should be a name, not a pattern
* @return table name in appropriate format for DB lookup - original case, uppercase, or lowercase
*/
String normalizeTableName(String tableName);
/**
* Check the JVM time (and timezone) against the database and log a warning
* or throw an error if they are too far apart. It is a good idea to do this

@ -131,16 +131,15 @@ public class DatabaseImpl implements Database {
throw new DatabaseException("Unable to rollback transaction", e);
}
}
@Override
public Connection underlyingConnection() {
if (!options.allowConnectionAccess()) {
throw new DatabaseException("Calls to underlyingConnection() are not allowed");
}
return connection;
}
@Override
public Options options() {
return options;
@ -151,7 +150,24 @@ public class DatabaseImpl implements Database {
return options.flavor();
}
@Override
public int jdbcMajorVersion() {
try {
return connection.getMetaData().getJDBCMajorVersion();
} catch (SQLException e) {
throw new DatabaseException(e);
}
}
@Override
public int jdbcMinorVersion() {
try {
return connection.getMetaData().getJDBCMinorVersion();
} catch (SQLException e) {
throw new DatabaseException(e);
}
}
@Override
public When when() {
return new When(options.flavor());
@ -181,7 +197,7 @@ public class DatabaseImpl implements Database {
if (tableName != null && connection != null) {
try {
DatabaseMetaData metadata = connection.getMetaData();
String normalizedTable = normalizeTableName(tableName);
String normalizedTable = flavor().normalizeTableName(tableName);
ResultSet resultSet = metadata.getTables(connection.getCatalog(), schemaName, normalizedTable, new String[]{"TABLE", "VIEW"});
while (resultSet.next()) {
if (normalizedTable.equals(resultSet.getString("TABLE_NAME"))) {
@ -203,7 +219,7 @@ public class DatabaseImpl implements Database {
if (tableName != null && connection != null) {
try {
DatabaseMetaData metaData = connection.getMetaData();
String normalizedTable = normalizeTableName(tableName);
String normalizedTable = flavor().normalizeTableName(tableName);
ResultSet resultSet = metaData.getColumns(null, null, normalizedTable, "%");
ResultSetMetaData rsmd = resultSet.getMetaData();
int cols = rsmd.getColumnCount();
@ -229,24 +245,6 @@ public class DatabaseImpl implements Database {
return map;
}
@Override
public String normalizeTableName(String tableName) {
if (tableName == null) {
return tableName;
}
// If user gave us a quoted string, leave it alone for look up
if (tableName.length() > 2) {
if (tableName.startsWith("\"") && tableName.endsWith("\"")) {
// Remove quotes and return as is.
return tableName.substring(1, tableName.length() - 1);
}
}
if (flavor().isNormalizedUpperCase()) {
return tableName.toUpperCase();
}
return tableName.toLowerCase();
}
@Override
public void assertTimeSynchronized(long millisToWarn, long millisToError) {
toSelect("select ?" + flavor().fromAny())

@ -6,12 +6,8 @@ import org.xbib.jdbc.query.util.Metric;
import javax.sql.DataSource;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ -30,7 +26,7 @@ import java.util.logging.Logger;
*/
public final class DatabaseProvider implements Supplier<Database>, Closeable {
private static final Logger log = Logger.getLogger(DatabaseProvider.class.getName());
private static final Logger logger = Logger.getLogger(DatabaseProvider.class.getName());
private static final AtomicInteger poolNameCounter = new AtomicInteger(1);
@ -63,7 +59,8 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
*
* <p>A database pool will be created using jdbc-connection-pool.</p>
*/
public static DatabaseProviderBuilder builder(Config config) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
public static DatabaseProviderBuilder builder(Config config)
throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return builder(createDataSource(config), getFlavor(config));
}
@ -85,67 +82,12 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url) {
return builder(url, Flavor.fromJdbcUrl(url), null, null, null);
public static DatabaseProviderBuilder builder(ClassLoader classLoader, String url) {
return builder(classLoader, url, Flavor.fromJdbcUrl(url), null, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url, Flavor flavor) {
return builder(url, flavor, null, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url, Properties info) {
return builder(url, Flavor.fromJdbcUrl(url), info, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url, Flavor flavor, Properties info) {
return builder(url, flavor, info, null, null);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method. The url parameter will be inspected
* to determine the Flavor for this database.
*/
public static DatabaseProviderBuilder builder(String url, String user, String password) {
return builder(url, Flavor.fromJdbcUrl(url), null, user, password);
}
/**
* Builder method to create and initialize an instance of this class using
* the JDBC standard DriverManager method.
*
* @param flavor use this flavor rather than guessing based on the url
*/
public static DatabaseProviderBuilder builder(String url,
Flavor flavor,
String user,
String password) {
return builder(url, flavor, null, user, password);
}
private static DatabaseProviderBuilder builder(String url,
private static DatabaseProviderBuilder builder(ClassLoader classLoader,
String url,
Flavor flavor,
Properties info,
String user,
@ -156,7 +98,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
} catch (SQLException e) {
if (flavor.driverClass() != null) {
try {
Class.forName(flavor.driverClass());
Class.forName(flavor.driverClass(), true, classLoader);
} catch (ClassNotFoundException e1) {
throw new DatabaseException("couldn't locate JDBC driver", e1);
}
@ -176,417 +118,6 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
}, options);
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param propertyFileName path to the properties file we will attempt to read
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String propertyFileName) {
return fromPropertyFile(propertyFileName, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param propertyFileName path to the properties file we will attempt to read
* @param decoder character encoding to use when reading the property file
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String propertyFileName, CharsetDecoder decoder) {
Properties properties = new Properties();
if (propertyFileName != null && propertyFileName.length() > 0) {
try (
FileInputStream fis = new FileInputStream(propertyFileName);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
throw new DatabaseException("Unable to read properties file: " + propertyFileName, e);
}
}
return fromProperties(properties, "", true);
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String filename, String propertyPrefix) {
return fromPropertyFile(filename, propertyPrefix, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from a file:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @param decoder character encoding to use when reading the property file
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromPropertyFile(String filename, String propertyPrefix, CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
throw new DatabaseException("Unable to read properties file: " + filename, e);
}
}
return fromProperties(properties, propertyPrefix, true);
}
/**
* Configure the database from up to five properties read from the provided properties:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param properties properties will be read from here
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromProperties(Properties properties) {
return fromProperties(properties, "", false);
}
/**
* Configure the database from up to five properties read from the provided properties:
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param properties properties will be read from here
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @throws DatabaseException if the property file could not be read for any reason
*/
public static DatabaseProviderBuilder fromProperties(Properties properties, String propertyPrefix) {
return fromProperties(properties, propertyPrefix, false);
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename) {
return fromPropertyFileOrSystemProperties(filename, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param decoder character encoding to use when reading the property file
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
log.fine("Trying system properties - unable to read properties file: " + filename);
}
}
return fromProperties(properties, "", true);
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
* <p>This will use the JVM default character encoding to read the property file.</p>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix) {
return fromPropertyFileOrSystemProperties(filename, propertyPrefix, Charset.defaultCharset().newDecoder());
}
/**
* Configure the database from up to five properties read from the specified
* properties file, or from the system properties (system properties will take
* precedence over the file):
* <pre>
* database.url=... Database connect string (required)
* database.user=... Authenticate as this user (optional if provided in url)
* database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param filename path to the properties file we will attempt to read; if the file
* cannot be read for any reason (e.g. does not exist) a debug level
* log entry will be entered, but it will attempt to proceed using
* solely the system properties
* @param propertyPrefix if this is null or empty the properties above will be read;
* if a value is provided it will be prefixed to each property
* (exactly, so if you want to use "my.database.url" you must
* pass "my." as the prefix)
* @param decoder character encoding to use when reading the property file
*/
public static DatabaseProviderBuilder fromPropertyFileOrSystemProperties(String filename, String propertyPrefix,
CharsetDecoder decoder) {
Properties properties = new Properties();
if (filename != null && filename.length() > 0) {
try (
FileInputStream fis = new FileInputStream(filename);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
} catch (Exception e) {
log.fine("Trying system properties - unable to read properties file: " + filename);
}
}
return fromProperties(properties, propertyPrefix, true);
}
/**
* Configure the database from up to five system properties:
* <pre>
* -Ddatabase.url=... Database connect string (required)
* -Ddatabase.user=... Authenticate as this user (optional if provided in url)
* -Ddatabase.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* -Ddatabase.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* -Ddatabase.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*/
public static DatabaseProviderBuilder fromSystemProperties() {
return fromProperties(null, "", true);
}
/**
* Configure the database from up to five system properties:
* <pre>
* -D{prefix}database.url=... Database connect string (required)
* -D{prefix}database.user=... Authenticate as this user (optional if provided in url)
* -D{prefix}database.password=... User password (optional if user and password provided in
* url; prompted on standard input if user is provided and
* password is not)
* -D{prefix}database.flavor=... What kind of database it is (optional, will guess based
* on the url if this is not provided)
* -D{prefix}database.driver=... The Java class of the JDBC driver to load (optional, will
* guess based on the flavor if this is not provided)
* </pre>
*
* @param propertyPrefix a prefix to attach to each system property - be sure to include the
* dot if desired (e.g. "mydb." for properties like -Dmydb.database.url)
*/
public static DatabaseProviderBuilder fromSystemProperties(String propertyPrefix) {
return fromProperties(null, propertyPrefix, true);
}
private static DatabaseProviderBuilder fromProperties(Properties properties, String propertyPrefix, boolean useSystemProperties) {
if (propertyPrefix == null) {
propertyPrefix = "";
}
String driver;
String flavorStr;
String url;
String user;
String password;
if (useSystemProperties) {
if (properties == null) {
properties = new Properties();
}
driver = System.getProperty(propertyPrefix + "database.driver",
properties.getProperty(propertyPrefix + "database.driver"));
flavorStr = System.getProperty(propertyPrefix + "database.flavor",
properties.getProperty(propertyPrefix + "database.flavor"));
url = System.getProperty(propertyPrefix + "database.url",
properties.getProperty(propertyPrefix + "database.url"));
user = System.getProperty(propertyPrefix + "database.user",
properties.getProperty(propertyPrefix + "database.user"));
password = System.getProperty(propertyPrefix + "database.password",
properties.getProperty(propertyPrefix + "database.password"));
} else {
if (properties == null) {
throw new DatabaseException("No properties were provided");
}
driver = properties.getProperty(propertyPrefix + "database.driver");
flavorStr = properties.getProperty(propertyPrefix + "database.flavor");
url = properties.getProperty(propertyPrefix + "database.url");
user = properties.getProperty(propertyPrefix + "database.user");
password = properties.getProperty(propertyPrefix + "database.password");
}
if (url == null) {
throw new DatabaseException("You must use -D" + propertyPrefix + "database.url=...");
}
if (user != null && password == null) {
System.out.println("Enter database password for user " + user + ":");
byte[] input = new byte[256];
try {
int bytesRead = System.in.read(input);
password = new String(input, 0, bytesRead - 1, Charset.defaultCharset());
} catch (IOException e) {
throw new DatabaseException("Error reading password from standard input", e);
}
}
Flavor flavor;
if (flavorStr != null) {
flavor = Flavor.valueOf(flavorStr);
} else {
flavor = Flavor.fromJdbcUrl(url);
}
if (driver != null) {
try {
Class.forName(driver).getDeclaredConstructor().newInstance();
} catch (Exception e) {
driver = flavor.driverClass();
try {
Class.forName(driver).getDeclaredConstructor().newInstance();
} catch (Exception ee) {
throw new DatabaseException("Unable to load JDBC driver: " + driver, ee);
}
}
}
if (user == null) {
return builder(url, flavor);
} else {
return builder(url, flavor, user, password);
}
}
private static DataSource createDataSource(Config config)
throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException,
IllegalAccessException {
@ -749,7 +280,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
if (builder.isClosed()) {
throw new DatabaseException("Called get() on a DatabaseProvider after close()");
}
Metric metric = new Metric(log.isLoggable(Level.FINE));
Metric metric = new Metric(logger.isLoggable(Level.FINE));
try {
connection = builder.connectionProvider.get();
metric.checkpoint("getConn");
@ -770,10 +301,10 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
throw e;
} finally {
metric.done();
if (log.isLoggable(Level.FINE)) {
if (logger.isLoggable(Level.FINE)) {
StringBuilder buf = new StringBuilder("Get ").append(builder.options.flavor()).append(" database: ");
metric.printMessage(buf);
log.fine(buf.toString());
logger.fine(buf.toString());
}
}
return database;
@ -811,7 +342,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
connection.rollback();
}
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to rollback the transaction", e);
logger.log(Level.SEVERE, "Unable to rollback the transaction", e);
}
}
}
@ -825,7 +356,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
}
close();
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to rollback the transaction", e);
logger.log(Level.SEVERE, "Unable to rollback the transaction", e);
}
}
@ -835,7 +366,7 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
try {
connection.close();
} catch (Exception e) {
log.log(Level.SEVERE, "Unable to close the database connection", e);
logger.log(Level.SEVERE, "Unable to close the database connection", e);
}
}
connection = null;
@ -843,129 +374,6 @@ public final class DatabaseProvider implements Supplier<Database>, Closeable {
builder.close();
}
/**
* This builder is immutable, so setting various options does not affect
* the previous instance. This is intended to make it safe to pass builders
* around without risk someone will reconfigure it.
*/
public interface DatabaseProviderBuilder {
DatabaseProviderBuilder withOptions(OptionsOverride options);
/**
* Enable logging of parameter values along with the SQL.
*/
DatabaseProviderBuilder withSqlParameterLogging();
/**
* Include SQL in exception messages. This will also include parameters in the
* exception messages if SQL parameter logging is enabled. This is handy for
* development, but be careful as this is an information disclosure risk,
* dependent on how the exception are caught and handled.
*/
DatabaseProviderBuilder withSqlInExceptionMessages();
/**
* Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is
* useful for testing purposes as you can use OptionsOverride to provide your
* own system clock that will be used for time travel.
*/
DatabaseProviderBuilder withDatePerAppOnly();
/**
* Allow provided Database instances to explicitly control transactions using the
* commitNow() and rollbackNow() methods. Otherwise calling those methods would
* throw an exception.
*/
DatabaseProviderBuilder withTransactionControl();
/**
* This can be useful when testing code, as it can pretend to use transactions,
* while giving you control over whether it actually commits or rolls back.
*/
DatabaseProviderBuilder withTransactionControlSilentlyIgnored();
/**
* Allow direct access to the underlying database connection. Normally this is
* not allowed, and is a bad idea, but it can be helpful when migrating from
* legacy code that works with raw JDBC.
*/
DatabaseProviderBuilder withConnectionAccess();
/**
* WARNING: You should try to avoid using this method. If you use it more
* that once or twice in your entire codebase you are probably doing
* something wrong.
*
* <p>If you use this method you are responsible for managing
* the transaction and commit/rollback/close.</p>
*/
DatabaseProvider build();
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete the transaction will commit unless the
* {@link DbCode#run(Supplier) run(Supplier)} method threw a {@link Throwable}.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact(dbs -&gt; {
* List<String> r = dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* ... do something with the results ...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
* @see #transact(DbCodeTx)
*/
void transact(DbCode code);
/**
* This method is the same as {@link #transact(DbCode)} but allows a return value.
*
* <p>Here is a typical usage:
* <pre>
* List<String> r = dbp.transact(dbs -&gt; {
* return dbs.get().toSelect("select a from b where c=?").argInteger(1).queryStrings();
* });
* </pre>
* </p>
*/
<T> T transactReturning(DbCodeTyped<T> code);
/**
* This is a convenience method to eliminate the need for explicitly
* managing the resources (and error handling) for this class. After
* the run block is complete commit() will be called unless either the
* {@link DbCodeTx#run(Supplier, Transaction)} method threw a {@link Throwable}
* while {@link Transaction#isRollbackOnError()} returns true, or
* {@link Transaction#isRollbackOnly()} returns a true value.
*
* <p>Here is a typical usage:
* <pre>
* dbp.transact((dbs, tx) -&gt; {
* tx.setRollbackOnError(false);
* dbs.get().toInsert("...").argInteger(1).insert(1);
* ...some stuff that might fail...
* });
* </pre>
* </p>
*
* @param code the code you want to run as a transaction with a Database
*/
void transact(DbCodeTx code);
}
private static class DatabaseProviderBuilderImpl implements DatabaseProviderBuilder, Closeable {
private DataSource dataSource;

@ -0,0 +1,126 @@
package org.xbib.jdbc.query;
import java.util.function.Supplier;
/**
* This builder is immutable, so setting various options does not affect
* the previous instance. This is intended to make it safe to pass builders
* around without risk someone will reconfigure it.
*/
public interface DatabaseProviderBuilder {
DatabaseProviderBuilder withOptions(OptionsOverride options);
/**
* Enable logging of parameter values along with the SQL.
*/
DatabaseProviderBuilder withSqlParameterLogging();
/**
* Include SQL in exception messages. This will also include parameters in the
* exception messages if SQL parameter logging is enabled. This is handy for
* development, but be careful as this is an information disclosure risk,
* dependent on how the exception are caught and handled.
*/
DatabaseProviderBuilder withSqlInExceptionMessages();
/**
* Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is
* useful for testing purposes as you can use OptionsOverride to provide your
* own system clock that will be used for time travel.
*/
DatabaseProviderBuilder withDatePerAppOnly();
/**
* Allow provided Database instances to explicitly control transactions using the
* commitNow() and rollbackNow() methods. Otherwise calling those methods would
* throw an exception.
*/
DatabaseProviderBuilder withTransactionControl();
/**
* This can be useful when testing code, as it can pretend to use transactions,
* while giving you control over whether it actually commits or rolls back.
*/
DatabaseProviderBuilder withTransactionControlSilentlyIgnored();
/**
* Allow direct access to the underlying database connection. Normally this is
* not allowed, and is a bad idea, but it can be helpful when migrating from
* legacy code that works with raw JDBC.
*/
DatabaseProviderBuilder withConnectionAccess();
/**
* WARNING: You should try to avoid using this method. If you use it more
* that once or twice in your entire codebase you are probably doing
* something wrong.
*
* <p>If you use this method you are responsible for managing
* the transaction and commit/rollback/close.</p>