change flavor from enum to interface, fix int/long bug in JDBC pool, add queue implementation

main
Jörg Prante 2 years ago
parent 7198e766c3
commit 1047be8456

@ -1,5 +1,5 @@
group = org.xbib
name = database
version = 0.0.5
version = 1.0.0
org.gradle.warning.mode = ALL

@ -20,4 +20,5 @@ test {
"${result.skippedTestCount} skipped"
}
}
systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties'
}

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

@ -407,15 +407,17 @@ public class Pool implements BagStateListener {
private void initializeDataSource()
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String jdbcUrl = config.getProperties().getProperty("url");
String dsClassName = config.getDataSourceClassName();
String url = config.getUrl();
DataSource ds = config.getDataSource();
if (ds == null) {
String dsClassName = config.getDataSourceClassName();
if (dsClassName != null) {
Class<?> clazz = Class.forName(dsClassName, true, ClassLoader.getSystemClassLoader());
ds = (DataSource) clazz.getDeclaredConstructor().newInstance();
} else if (jdbcUrl != null) {
ds = new DriverDataSource(jdbcUrl, config.getDriverClassName(), config.getProperties(), config.getUsername(), config.getPassword());
} else if (url != null) {
ds = new DriverDataSource(url, config.getDriverClassName(), config.getProperties(), config.getUsername(), config.getPassword());
} else {
throw new IllegalStateException("no dataSource configured?");
}
}
setTargetFromProperties(ds, config.getProperties());

@ -56,7 +56,7 @@ public class PoolConfig {
private String driverClassName;
private String jdbcUrl;
private String url;
private String poolName;
@ -106,11 +106,18 @@ public class PoolConfig {
this.idleTimeout = IDLE_TIMEOUT;
this.initializationFailTimeout = -1;
this.isAutoCommit = true;
this.jdbcUrl = properties.getProperty("url");
this.aliveBypassWindowMs = TimeUnit.MILLISECONDS.toMillis(500);
this.housekeepingPeriodMs = TimeUnit.SECONDS.toMillis(30);
}
public void setUrl(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public String getCatalog() {
return catalog;
}
@ -588,7 +595,7 @@ public class PoolConfig {
transactionIsolationName = getNullIfEmpty(transactionIsolationName);
dataSourceClassName = getNullIfEmpty(dataSourceClassName);
driverClassName = getNullIfEmpty(driverClassName);
jdbcUrl = getNullIfEmpty(jdbcUrl);
url = getNullIfEmpty(url);
if (dataSource != null) {
if (dataSourceClassName != null) {
logger.log(Level.WARNING, "using dataSource and ignoring dataSourceClassName: " + poolName);
@ -597,17 +604,17 @@ public class PoolConfig {
if (driverClassName != null) {
logger.log(Level.SEVERE, "cannot use driverClassName and dataSourceClassName together: " + poolName);
throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together.");
} else if (jdbcUrl != null) {
logger.log(Level.WARNING, "using dataSourceClassName and ignoring jdbcUrl: " + poolName);
} else if (url != null) {
logger.log(Level.WARNING, "using dataSourceClassName and ignoring url: " + poolName);
}
} else if (jdbcUrl != null) {
} else if (url != null) {
// ok
} else if (driverClassName != null) {
logger.log(Level.SEVERE, "jdbcUrl is required with driverClassName: " + poolName);
throw new IllegalArgumentException("jdbcUrl is required with driverClassName.");
logger.log(Level.SEVERE, "url is required with driverClassName: " + poolName);
throw new IllegalArgumentException("url is required with driverClassName");
} else {
logger.log(Level.SEVERE, "dataSource or dataSourceClassName or jdbcUrl is required: " + poolName);
throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required.");
logger.log(Level.SEVERE, "dataSource or dataSourceClassName or url is required: " + poolName);
throw new IllegalArgumentException("dataSource or dataSourceClassName or url is required");
}
validateNumerics();
}

@ -481,7 +481,7 @@ public class ProxyResultSet implements ResultSet {
@Override
public long getLong(int columnIndex) throws SQLException {
return delegate.getInt(columnIndex);
return delegate.getLong(columnIndex);
}
@Override

@ -136,12 +136,21 @@ public class DriverDataSource implements DataSource {
@Override
public void setLoginTimeout(int seconds) {
DriverManager.setLoginTimeout(seconds);
try {
DriverManager.setLoginTimeout(seconds);
} catch (Exception e) {
logger.log(Level.WARNING, "setLoginTimeout failed");
}
}
@Override
public int getLoginTimeout() {
return DriverManager.getLoginTimeout();
try {
return DriverManager.getLoginTimeout();
} catch (Exception e) {
logger.log(Level.WARNING, "getLoginTimeout failed");
return 0;
}
}
@Override

@ -63,7 +63,6 @@ public class BagTest {
bag.remove(inuse);
bag.remove(inuse);
assertTrue(bag.getLastMessage().contains("not borrowed or reserved"));
bag.close();
try {
PoolEntry bagEntry = pool.newPoolEntry();
bag.add(bagEntry);

@ -29,12 +29,11 @@ public class ConnectionStateTest {
config.setConnectionTestQuery("VALUES 1");
config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource");
try (PoolDataSource ds = new PoolDataSource(config)) {
try (Connection connection = ds.getConnection()) {
Connection unwrap = connection.unwrap(Connection.class);
unwrap.setAutoCommit(false);
connection.close();
assertFalse(unwrap.getAutoCommit());
}
Connection connection = ds.getConnection();
Connection unwrap = connection.unwrap(Connection.class);
unwrap.setAutoCommit(false);
connection.close();
assertFalse(unwrap.getAutoCommit());
}
}
@ -48,12 +47,11 @@ public class ConnectionStateTest {
config.setConnectionTestQuery("VALUES 1");
config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource");
try (PoolDataSource ds = new PoolDataSource(config)) {
try (Connection connection = ds.getConnection()) {
Connection unwrap = connection.unwrap(Connection.class);
unwrap.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
connection.close();
assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, unwrap.getTransactionIsolation());
}
Connection connection = ds.getConnection();
Connection unwrap = connection.unwrap(Connection.class);
unwrap.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
connection.close();
assertEquals(Connection.TRANSACTION_READ_UNCOMMITTED, unwrap.getTransactionIsolation());
}
}
@ -76,12 +74,11 @@ public class ConnectionStateTest {
config.setConnectionTestQuery("VALUES 1");
config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource");
try (PoolDataSource ds = new PoolDataSource(config)) {
try (Connection connection = ds.getConnection()) {
Connection unwrap = connection.unwrap(Connection.class);
connection.setReadOnly(true);
connection.close();
assertFalse(unwrap.isReadOnly());
}
Connection connection = ds.getConnection();
Connection unwrap = connection.unwrap(Connection.class);
connection.setReadOnly(true);
connection.close();
assertFalse(unwrap.isReadOnly());
}
}

@ -453,6 +453,7 @@ public class ConnectionTest {
config.setDataSource(stubDataSource);
try (PoolDataSource ds = new PoolDataSource(config);
Connection ignored = ds.getConnection()) {
assertNotNull(ignored);
fail("Initialization should have failed");
} catch (SQLTransientConnectionException e) {
// passed
@ -472,6 +473,7 @@ public class ConnectionTest {
config.setDataSource(stubDataSource);
try (PoolDataSource ds = new PoolDataSource(config)) {
try (Connection ignored = ds.getConnection()) {
assertNotNull(ignored);
stubDataSource.setErrorOnConnection(true);
fail("SQLException should occur!");
} catch (Exception e) {
@ -520,6 +522,7 @@ public class ConnectionTest {
config.setDataSourceClassName("org.xbib.io.pool.jdbc.mock.StubDataSource");
try (PoolDataSource ds = new PoolDataSource(config);
Connection ignored = ds.getConnection()) {
assertNotNull(ignored);
// passed
} catch (SQLTransientConnectionException sqle) {
fail("Failed to obtain connection");

@ -1,5 +1,6 @@
package org.xbib.io.pool.jdbc;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.AfterEach;
@ -38,6 +39,7 @@ public class JdbcDriverTest {
assertNotNull(unwrap);
try (Connection connection = ds.getConnection()) {
// test that getConnection() succeeds
assertFalse(connection.isClosed());
}
}

@ -1,6 +1,7 @@
dependencies {
api project(":jdbc-connection-pool")
testImplementation libs.derby
testImplementation libs.hsqldb
testImplementation libs.testcontainers
testImplementation libs.testcontainers.junit.jupiter
testImplementation libs.testcontainers.oracle.xe

@ -1,4 +1,13 @@
import org.xbib.jdbc.query.Flavor;
import org.xbib.jdbc.query.flavor.Derby;
import org.xbib.jdbc.query.flavor.Hsql;
import org.xbib.jdbc.query.flavor.Oracle;
import org.xbib.jdbc.query.flavor.Postgresql;
import org.xbib.jdbc.query.flavor.SqlServer;
module org.xbib.jdbc.query {
uses Flavor;
requires transitive org.xbib.jdbc.connection.pool;
exports org.xbib.jdbc.query;
provides Flavor with Derby, Hsql, Oracle, Postgresql, SqlServer;
}

@ -1,6 +1,6 @@
package org.xbib.jdbc.query;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.CharsetDecoder;
import java.util.Properties;
import java.util.function.Function;
@ -47,7 +47,7 @@ public interface ConfigFrom extends Supplier<Config> {
* defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties")
* </pre>
*/
ConfigFrom defaultPropertyFiles();
ConfigFrom defaultProperties();
/**
* Adds a set of properties files to read from, which can be overridden by a specified system property.
@ -56,7 +56,7 @@ public interface ConfigFrom extends Supplier<Config> {
* defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames)
* </pre>
*/
ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames);
ConfigFrom defaultProperties(String systemPropertyKey, String... filenames);
/**
* Adds a set of properties files to read from, which can be overridden by a specified system property.
@ -67,15 +67,15 @@ public interface ConfigFrom extends Supplier<Config> {
* .split(File.pathSeparator));
* </pre>
*/
ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames);
ConfigFrom defaultProperties(String systemPropertyKey, CharsetDecoder decoder, String... filenames);
ConfigFrom propertyFile(String... filenames);
ConfigFrom properties(String... filenames);
ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames);
ConfigFrom properties(CharsetDecoder decoder, String... filenames);
ConfigFrom propertyFile(File... files);
ConfigFrom properties(InputStream... inputStreams);
ConfigFrom propertyFile(CharsetDecoder decoder, File... files);
ConfigFrom properties(CharsetDecoder decoder, InputStream... inputStreams);
ConfigFrom rename(String key, String newKey);

@ -1,16 +1,22 @@
package org.xbib.jdbc.query;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -20,6 +26,10 @@ import java.util.regex.Pattern;
*/
public class ConfigFromImpl implements ConfigFrom {
private static final Logger logger = Logger.getLogger(ConfigFromImpl.class.getName());
private static final String SEPARATOR = FileSystems.getDefault().getSeparator();
private final List<Config> searchPath = new ArrayList<>();
public ConfigFromImpl() {
@ -72,64 +82,59 @@ public class ConfigFromImpl implements ConfigFrom {
}
@Override
public ConfigFrom defaultPropertyFiles() {
return defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties");
public ConfigFrom defaultProperties() {
return defaultProperties("properties", "conf/app.properties", "local.properties", "sample.properties");
}
@Override
public ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames) {
return defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames);
public ConfigFrom defaultProperties(String systemPropertyKey, String... filenames) {
return defaultProperties(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames);
}
@Override
public ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames) {
String properties = System.getProperty(systemPropertyKey, String.join(File.pathSeparator, filenames));
return propertyFile(Charset.defaultCharset().newDecoder(), properties.split(File.pathSeparator));
public ConfigFrom defaultProperties(String systemPropertyKey, CharsetDecoder decoder, String... filenames) {
String properties = System.getProperty(systemPropertyKey, String.join(SEPARATOR, filenames));
return properties(Charset.defaultCharset().newDecoder(), properties.split(SEPARATOR));
}
@Override
public ConfigFrom propertyFile(String... filenames) {
return propertyFile(Charset.defaultCharset().newDecoder(), filenames);
public ConfigFrom properties(String... filenames) {
return properties(Charset.defaultCharset().newDecoder(), filenames);
}
@Override
public ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames) {
public ConfigFrom properties(CharsetDecoder decoder, String... filenames) {
for (String filename : filenames) {
if (filename != null) {
propertyFile(decoder, new File(filename));
Path path = Paths.get(filename);
if (Files.exists(path)) {
try (InputStream inputStream = Files.newInputStream(path)) {
properties(decoder, inputStream);
} catch (IOException e) {
custom(k -> null, "Ignored: propertyFile(" + filename + ") " + e.getClass().getSimpleName());
}
} else {
properties(decoder, getClass().getResourceAsStream(filename));
}
}
}
return this;
}
@Override
public ConfigFrom propertyFile(File... files) {
return propertyFile(Charset.defaultCharset().newDecoder(), files);
public ConfigFrom properties(InputStream... inputStreams) {
return properties(Charset.defaultCharset().newDecoder(), inputStreams);
}
@Override
public ConfigFrom propertyFile(CharsetDecoder decoder, File... files) {
for (File file : files) {
if (file != null) {
try {
Properties properties = new Properties();
try (
FileInputStream fis = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(fis, decoder)
) {
properties.load(reader);
}
searchPath.add(new ConfigImpl(properties::getProperty, "propertyFile(" + file.getAbsolutePath() + ")"));
} catch (Exception e) {
// Put a "fake" provider in so we can see it failed
String fileName = file.getName();
try {
fileName = file.getAbsolutePath();
} catch (Exception ignored) {
// Fall back to relative name
}
custom(k -> null, "Ignored: propertyFile(" + fileName + ") " + e.getClass().getSimpleName());
}
public ConfigFrom properties(CharsetDecoder decoder, InputStream... inputStreams) {
for (InputStream inputStream : inputStreams) {
Properties properties = new Properties();
try (InputStreamReader reader = new InputStreamReader(inputStream, decoder)) {
properties.load(reader);
searchPath.add(new ConfigImpl(properties::getProperty, "propertyFile(" + inputStream + ")"));
} catch (Exception e) {
custom(k -> null, "Ignored: propertyFile(" + inputStream + ") " + e.getClass().getSimpleName());
}
}
return this;

@ -169,7 +169,6 @@ public class ConfigImpl implements Config {
return null;
}
}
@Override
public BigDecimal getBigDecimal(String key, BigDecimal defaultValue) {
@ -177,7 +176,6 @@ public class ConfigImpl implements Config {
return value == null ? defaultValue : value;
}
@Override
public BigDecimal getBigDecimalOrThrow(String key) {
return nonnull(key, getBigDecimal(key));

@ -1,8 +1,10 @@
package org.xbib.jdbc.query;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@ -216,4 +218,27 @@ public interface Database extends Supplier<Database> {
* with millisToWarn=10000 and millisToError=30000.
*/
void assertTimeSynchronized();
/**
* Write data into a queue table, select a channel, and consume the returned primary keys.
*
* @param table the queue table
* @param channel the queue table channel
* @param data the data for the queue table
* @param consumer the consumer for the primary keys
* @throws SQLException if writing fails
*/
void writeQueue(String table, String channel, String data, Consumer<Long> consumer) throws SQLException;
/**
* Read from a queue table and lock rows in a transaction until processing by a consumer
* has succeeded or failed.
* Commit only if processing was successful, otherwise roll back the transaction.
* The connection is switched into manual commit and afterwards, auto-commit is enabled again.
* @param table the queue table
* @param channel the queue table channel. A queue table can have many channels
* @param consumer a consumer for the queue table data column or null
* @throws SQLException if rollback fails
*/
public void readQueue(String table, String channel, Consumer<String> consumer) throws SQLException;
}

@ -1,24 +1,27 @@
package org.xbib.jdbc.query;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.logging.Logger;
/**
* Primary class for accessing a relational (SQL) database.
* Primary class for accessing a relational database.
*/
public class DatabaseImpl implements Database {
private static final Logger log = Logger.getLogger(Database.class.getName());
private static final Logger logger = Logger.getLogger(Database.class.getName());
private final Connection connection;
@ -88,7 +91,7 @@ public class DatabaseImpl implements Database {
}
@Override
public Long nextSequenceValue(/*@Untainted*/ String sequenceName) {
public Long nextSequenceValue(String sequenceName) {
return toSelect(flavor().sequenceSelectNextVal(sequenceName)).queryLongOrNull();
}
@ -97,9 +100,10 @@ public class DatabaseImpl implements Database {
return options.currentDate();
}
@Override
public void commitNow() {
if (options.ignoreTransactionControl()) {
log.fine("Ignoring call to commitNow()");
logger.fine("Ignoring call to commitNow()");
return;
}
if (!options.allowTransactionControl()) {
@ -112,9 +116,10 @@ public class DatabaseImpl implements Database {
}
}
@Override
public void rollbackNow() {
if (options.ignoreTransactionControl()) {
log.fine("Ignoring call to rollbackNow()");
logger.fine("Ignoring call to rollbackNow()");
return;
}
if (!options.allowTransactionControl()) {
@ -153,53 +158,22 @@ public class DatabaseImpl implements Database {
}
@Override
public void dropSequenceQuietly(/*@Untainted*/ String sequenceName) {
public void dropSequenceQuietly(String sequenceName) {
ddl(flavor().sequenceDrop(sequenceName)).executeQuietly();
}
@Override
public void dropTableQuietly(/*@Untainted*/ String tableName) {
if (flavor() == Flavor.postgresql || flavor() == Flavor.hsqldb) {
ddl("drop table if exists " + tableName).executeQuietly();
} else {
ddl("drop table " + tableName).executeQuietly();
}
public void dropTableQuietly(String tableName) {
ddl(flavor().tableDrop(tableName)).executeQuietly();
}
@Override
public boolean tableExists(String tableName) throws DatabaseException {
String schemaName = null;
Method getSchema = null;
try {
// Use reflections to see if connection.getSchema API exists. It should exist for any JDBC7 or later implementation
// We still support Oracle 11 with odbc6, however, so we can't assume it's there.
getSchema = connection.getClass().getDeclaredMethod("getSchema");
} catch (NoSuchMethodException noMethodExc) {
// Expected if method does not exist - just let it go
}
try {
if (getSchema != null) {
schemaName = ((String) getSchema.invoke(connection, new Object[0]));
} else if (flavor() == Flavor.oracle) {
// Oracle defaults to user name schema - use that.
log.warning("Connection getSchema API was not found. Defaulting to Oracle user name schema." +
"If this is not appropriate, please use tableExists(tableName, schemaName) API or upgrade to ojdbc7 or later");
schemaName = connection.getMetaData().getUserName();
}
if (schemaName == null) {
// connection.getSchema API was supported starting at JDK1.7. Method should not be null.
throw new NullPointerException("Unable to retrieve schema name.");
}
} catch (Exception exc) {
throw new DatabaseException("Unable to determine the schema. " +
"Please use tableExists(tableName, schemaName API) or upgrade to a JDBC7 driver or later.", exc);
return tableExists(tableName, connection.getSchema());
} catch (SQLException e) {
throw new DatabaseException("Unable to getSchema()", e);
}
return tableExists(tableName, schemaName);
}
@Override
@ -230,24 +204,24 @@ public class DatabaseImpl implements Database {
try {
DatabaseMetaData metaData = connection.getMetaData();
String normalizedTable = normalizeTableName(tableName);
ResultSet rs = metaData.getColumns(null, null, normalizedTable, "%");
ResultSetMetaData rsmd = rs.getMetaData();
ResultSet resultSet = metaData.getColumns(null, null, normalizedTable, "%");
ResultSetMetaData rsmd = resultSet.getMetaData();
int cols = rsmd.getColumnCount();
while (rs.next()) {
while (resultSet.next()) {
String name = "";
int size = 0;
for (int i = 1; i <= cols; i++) {
String label = rsmd.getColumnName(i);
if ("COLUMN_NAME".equalsIgnoreCase(label)) {
name = rs.getString(i);
name = resultSet.getString(i);
}
if ("COLUMN_SIZE".equalsIgnoreCase(label)) {
size = rs.getBigDecimal(i).intValue();
size = resultSet.getBigDecimal(i).intValue();
}
}
map.put(name, size);
}
rs.close();
resultSet.close();
} catch (SQLException exc) {
throw new DatabaseException("Unable to look up table " + tableName + " : " + exc.getMessage(), exc);
}
@ -260,7 +234,6 @@ public class DatabaseImpl implements Database {
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("\"")) {
@ -268,11 +241,9 @@ public class DatabaseImpl implements Database {
return tableName.substring(1, tableName.length() - 1);
}
}
if (flavor().isNormalizedUpperCase()) {
return tableName.toUpperCase();
}
return tableName.toLowerCase();
}
@ -288,18 +259,18 @@ public class DatabaseImpl implements Database {
Duration duration = Duration.between(appDate, dbDate).abs();
if (duration.getSeconds() > 3600) {
throw new DatabaseException("App and db time are over an hour apart (check your timezones) app: "
+ DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: "
+ DateTimeFormatter.ISO_INSTANT.format(dbDate));
+ appDate + " db: "
+ dbDate);
}
if (duration.getSeconds() * 1000 > millisToError) {
throw new DatabaseException("App and db time over " + millisToError + " millis apart (check your clocks) app: "
+ DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: "
+ DateTimeFormatter.ISO_INSTANT.format(dbDate));
+ appDate + " db: "
+ dbDate);
}
if (duration.getSeconds() * 1000 > millisToWarn) {
log.warning("App and db time are over " + millisToWarn + " millis apart (check your clocks) app: "
+ DateTimeFormatter.ISO_INSTANT.format(appDate) + " db: "
+ DateTimeFormatter.ISO_INSTANT.format(dbDate));
logger.warning("App and db time are over " + millisToWarn + " millis apart (check your clocks) app: "
+ appDate + " db: "
+ dbDate);
}
return null;
});
@ -309,4 +280,83 @@ public class DatabaseImpl implements Database {
public void assertTimeSynchronized() {
assertTimeSynchronized(10000, 30000);
}
@Override
public void writeQueue(String table, String channel, String data, Consumer<Long> consumer) throws SQLException {
writeNextIntoQueue(connection, table, channel, data, consumer);
}
@Override
public void readQueue(String table, String channel, Consumer<String> consumer) throws SQLException {
try {
connection.setAutoCommit(false);
while (true) {
try (ResultSet resultSet = readNextFromQueue(connection, table, channel)) {
if (resultSet.next()) {
Long key = resultSet.getLong("key");
try {
if (consumer != null) {
consumer.accept(resultSet.getString("data"));
}
succeedInQueue(connection, table, key);
} catch (Exception e) {
failInQueue(connection, table, key);
throw e;
}
}
} finally {
connection.commit();
}
}
} catch (Exception e) {
connection.rollback();
throw e;
} finally {
connection.setAutoCommit(true);
}
}
private void writeNextIntoQueue(Connection connection,
String table,
String channel,
String data,
Consumer<Long> consumer) throws SQLException {
try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().writeNextIntoQueue(table),
Statement.RETURN_GENERATED_KEYS)) {
preparedStatement.setString(1, channel);
preparedStatement.setString(2, data);
preparedStatement.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now()));
int rows = preparedStatement.executeUpdate();
if (rows > 0) {
try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) {
if (resultSet.next()) {
if (consumer != null) {
consumer.accept(resultSet.getLong(1));
}
}
}
}
}
}
private ResultSet readNextFromQueue(Connection connection, String table, String channel) throws SQLException {
PreparedStatement preparedStatement = connection.prepareStatement(flavor().readNextFromQueue(table));
preparedStatement.setString(1, channel);
preparedStatement.setTimestamp(2, Timestamp.valueOf(LocalDateTime.now()));
return preparedStatement.executeQuery();
}
private void succeedInQueue(Connection connection, String table, Long key) throws SQLException {
try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().succeedInQueue(table))) {
preparedStatement.setLong(1, key);
preparedStatement.executeUpdate();
}
}
private void failInQueue(Connection connection, String table, Long key) throws SQLException {
try (PreparedStatement preparedStatement = connection.prepareStatement(flavor().succeedInQueue(table))) {
preparedStatement.setLong(1, key);
preparedStatement.executeUpdate();
}
}
}

@ -67,8 +67,7 @@ public final class DatabaseProvider implements Supplier<Database> {
* database.pool.size=... How many connections in the connection pool (default 10).
* database.driver.class The driver to initialize with Class.forName(). This will
* be guessed from the database.url if not provided.
* database.flavor One of the enumerated values in {@link Flavor}. If this
* is not provided the flavor will be guessed based on the
* database.flavor A {@link Flavor}. If this is not provided the flavor will be guessed based on the
* value for database.url, if possible.
* </pre>
*
@ -167,10 +166,12 @@ public final class DatabaseProvider implements Supplier<Database> {
try {
DriverManager.getDriver(url);
} catch (SQLException e) {
try {
Class.forName(Flavor.driverForJdbcUrl(url));
} catch (ClassNotFoundException e1) {
throw new DatabaseException("couldn't locate JDBC driver - try setting -Djdbc.drivers=some.Driver", e1);
if (flavor.driverClass() != null) {
try {
Class.forName(flavor.driverClass());
} catch (ClassNotFoundException e1) {
throw new DatabaseException("couldn't locate JDBC driver", e1);
}
}
}
return new BuilderImpl(null, () -> {
@ -531,7 +532,6 @@ public final class DatabaseProvider implements Supplier<Database> {
if (propertyPrefix == null) {
propertyPrefix = "";
}
String driver;
String flavorStr;
String url;
@ -561,11 +561,9 @@ public final class DatabaseProvider implements Supplier<Database> {
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];
@ -576,28 +574,22 @@ public final class DatabaseProvider implements Supplier<Database> {
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) {
if (flavor == Flavor.oracle) {
driver = "oracle.jdbc.OracleDriver";
} else if (flavor == Flavor.postgresql) {
driver = "org.postgresql.Driver";
} else if (flavor == Flavor.derby) {
driver = "org.apache.derby.jdbc.EmbeddedDriver";
}
}
if (driver != null) {
try {
Class.forName(driver).getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new DatabaseException("Unable to load JDBC driver: " + driver, 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) {
@ -616,7 +608,12 @@ public final class DatabaseProvider implements Supplier<Database> {
}
PoolConfig poolConfig = new PoolConfig();
poolConfig.setPoolName(config.getString("database.pool.name", "database-pool-" + poolNameCounter.getAndAdd(1)));
poolConfig.setDriverClassName(config.getString("database.driver.class", Flavor.driverForJdbcUrl(url)));
poolConfig.setUrl(config.getString("database.url"));
// provide driver class only if in config
String driverClassName = config.getString("database.driver.class");
if (driverClassName != null) {
poolConfig.setDriverClassName(config.getString("database.driver.class"));
}
poolConfig.setUsername(config.getString("database.user"));
poolConfig.setPassword(config.getString("database.password"));
poolConfig.setMaximumPoolSize(config.getInteger("database.pool.size", 8));
@ -774,7 +771,7 @@ public final class DatabaseProvider implements Supplier<Database> {
try {
// JDBC specifies that autoCommit is the default for all new connections.
// Don't try to be clever about clearing it conditionally.
if (!options.flavor().autoCommitOnly()) {
if (!options.flavor().isAutoCommitOnly()) {
connection.setAutoCommit(false);
metric.checkpoint("setAutoCommit");
}
@ -868,7 +865,7 @@ public final class DatabaseProvider implements Supplier<Database> {
}
if (connection != null) {
try {
if (!options.flavor().autoCommitOnly()) {
if (!options.flavor().isAutoCommitOnly()) {
connection.commit();
}
} catch (Exception e) {
@ -884,7 +881,7 @@ public final class DatabaseProvider implements Supplier<Database> {
}
if (connection != null) {
try {
if (!options.flavor().autoCommitOnly()) {
if (!options.flavor().isAutoCommitOnly()) {
connection.commit();
}
} catch (Exception e) {
@ -902,7 +899,7 @@ public final class DatabaseProvider implements Supplier<Database> {
if (connection != null) {
try {
if (!options.flavor().autoCommitOnly()) {
if (!options.flavor().isAutoCommitOnly()) {
connection.rollback();
}
} catch (Exception e) {
@ -919,7 +916,7 @@ public final class DatabaseProvider implements Supplier<Database> {
if (connection != null) {
try {
if (!options.flavor().autoCommitOnly()) {
if (!options.flavor().isAutoCommitOnly()) {
connection.rollback();
}
} catch (Exception e) {

File diff suppressed because it is too large Load Diff

@ -114,9 +114,6 @@ public interface Options {
*
* <p>It is strongly recommended to always run your database in GMT timezone, and
* leave this set to the default.</p>
*
* <p>Behavior in releases 1.3 and prior was to use the JVM default TimeZone, and
* this was not configurable.</p>
*/
Calendar calendarForTimestamps();

@ -1,5 +1,7 @@
package org.xbib.jdbc.query;
import org.xbib.jdbc.query.flavor.Postgresql;
import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.Date;
@ -23,7 +25,7 @@ public class OptionsOverride implements Options {
* Defer to OptionsDefault for anything that is not specified, and use postgresql flavor.
*/
public OptionsOverride() {
parent = new OptionsDefault(Flavor.postgresql);
parent = new OptionsDefault(new Postgresql());
}
/**

@ -5,6 +5,8 @@ import org.xbib.jdbc.query.Schema.Table.Column;
import org.xbib.jdbc.query.Schema.Table.ForeignKey;
import org.xbib.jdbc.query.Schema.Table.Index;
import org.xbib.jdbc.query.Schema.Table.Unique;
import org.xbib.jdbc.query.flavor.Oracle;
import org.xbib.jdbc.query.flavor.Postgresql;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
@ -274,15 +276,13 @@ public class Schema {
sql.append(check.expression);
sql.append(")");
}
sql.append("\n)");
if (table.customClauses.containsKey(flavor)) {
sql.append(" ").append(table.customClauses.get(flavor));
}
executeOrPrint(sql, db, script);
sql = new Sql();
if (flavor == Flavor.oracle || flavor == Flavor.postgresql) {
if (flavor instanceof Oracle || flavor instanceof Postgresql) {
if (table.comment != null) {
sql.append("comment on table ");
sql.append(table.name);
@ -292,7 +292,6 @@ public class Schema {
executeOrPrint(sql, db, script);
sql = new Sql();
}
for (Column c : table.columns) {
if (c.comment != null) {
sql.append("comment on column ");
@ -308,7 +307,6 @@ public class Schema {
}
}
}
for (Table table : tables) {
for (ForeignKey fk : table.foreignKeys) {
Sql sql = new Sql();
@ -329,7 +327,6 @@ public class Schema {
executeOrPrint(sql, db, script);
}
}
for (Table table : tables) {
for (Index index : table.indexes) {
Sql sql = new Sql();

@ -22,18 +22,17 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
public Sql() {
}
public Sql(/*@Untainted*/ String sql) {
public Sql(String sql) {
this.sql.append(sql);
}
public static Sql insert(/*@Untainted*/ String table, SqlArgs args) {
public static Sql insert(String table, SqlArgs args) {
return insert(table, Collections.singletonList(args));
}
public static Sql insert(/*@Untainted*/ String table, List<SqlArgs> args) {
public static Sql insert(String table, List<SqlArgs> args) {
Sql sql = null;
List<String> expectedColumns = null;
for (SqlArgs arg : args) {
if (arg.positionalCount() > 0) {
throw new DatabaseException("The SqlArgs must all be named to do this");
@ -98,7 +97,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
return this;
}
public Sql append(/*@Untainted*/ String sql) {
public Sql append(String sql) {
this.sql.append(sql);
return this;
}
@ -133,12 +132,12 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
return this;
}
public Sql replace(int start, int end, /*@Untainted*/ String str) {
public Sql replace(int start, int end, String str) {
this.sql.replace(start, end, str);
return this;
}
public Sql insert(int offset, /*@Untainted*/ String str) {
public Sql insert(int offset, String str) {
this.sql.insert(offset, str);
return this;
}
@ -189,7 +188,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
* <p>
* Each list started must have be ended. "Lists" are only to support using listSeparator(sep)
*/
public Sql listStart(/*@Untainted*/ String sql) {
public Sql listStart(String sql) {
listFirstItem.push(true);
return append(sql);
}
@ -198,7 +197,7 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
* Appends the passed bit of sql only if a previous item has already been appended,
* and notes that the list is not empty.
*/
public Sql listSeparator(/*@Untainted*/ String sql) {
public Sql listSeparator(String sql) {
if (listFirstItem.peek()) {
listFirstItem.pop();
listFirstItem.push(false);
@ -209,12 +208,11 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
}
public Sql listEnd(/*@Untainted*/ String sql) {
public Sql listEnd(String sql) {
listFirstItem.pop();
return append(sql);
}
/*@Untainted*/
public String sql() {
return sql.toString();
}
@ -222,7 +220,6 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
/**
* Same as sql(), provided for drop-in compatibility with StringBuilder.
*/
/*@Untainted*/
public String toString() {
return sql();
}
@ -231,81 +228,68 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
sqlArgs.argBoolean(arg);
return this;
}
public Sql argBoolean( String argName, Boolean arg) {
sqlArgs.argBoolean(argName, arg);
return this;
}
public Sql argInteger(Integer arg) {
sqlArgs.argInteger(arg);
return this;
}
public Sql argInteger( String argName, Integer arg) {
sqlArgs.argInteger(argName, arg);
return this;
}
public Sql argLong(Long arg) {
sqlArgs.argLong(arg);
return this;
}
public Sql argLong(String argName, Long arg) {
sqlArgs.argLong(argName, arg);
return this;
}
public Sql argFloat(Float arg) {
sqlArgs.argFloat(arg);
return this;
}
public Sql argFloat( String argName, Float arg) {
sqlArgs.argFloat(argName, arg);
return this;
}
public Sql argDouble(Double arg) {
sqlArgs.argDouble(arg);
return this;
}
public Sql argDouble( String argName, Double arg) {
sqlArgs.argDouble(argName, arg);
return this;
}
public Sql argBigDecimal(BigDecimal arg) {
sqlArgs.argBigDecimal(arg);
return this;
}
public Sql argBigDecimal( String argName, BigDecimal arg) {
sqlArgs.argBigDecimal(argName, arg);
return this;
}
public Sql argString(String arg) {
sqlArgs.argString(arg);
return this;
}
public Sql argString( String argName, String arg) {
public Sql argString(String argName, String arg) {
sqlArgs.argString(argName, arg);
return this;
}
@ -314,33 +298,28 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
sqlArgs.argDate(arg);
return this;
}
public Sql argDate( String argName, LocalDateTime arg) {
public Sql argDate(String argName, LocalDateTime arg) {
sqlArgs.argDate(argName, arg);
return this;
}
public Sql argDateNowPerApp() {
sqlArgs.argDateNowPerApp();
return this;
}
public Sql argDateNowPerApp( String argName) {
public Sql argDateNowPerApp(String argName) {
sqlArgs.argDateNowPerApp(argName);
return this;
}
public Sql argDateNowPerDb() {
sqlArgs.argDateNowPerDb();
return this;
}
public Sql argDateNowPerDb( String argName) {
public Sql argDateNowPerDb(String argName) {
sqlArgs.argDateNowPerDb(argName);
return this;
}
@ -352,35 +331,30 @@ public class Sql implements SqlInsert.Apply, SqlUpdate.Apply, SqlSelect.Apply {
}
public Sql argBlobBytes( String argName, byte[] arg) {
public Sql argBlobBytes(String argName, byte[] arg) {
sqlArgs.argBlobBytes(argName, arg);
return this;
}