From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from localhost (localhost [127.0.0.1]) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTP id E53F62D4ED for ; Fri, 12 Oct 2018 11:47:41 -0400 (EDT) Received: from turing.freelists.org ([127.0.0.1]) by localhost (turing.freelists.org [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id mteE8T8ChGjs for ; Fri, 12 Oct 2018 11:47:41 -0400 (EDT) Received: from mail-lj1-f193.google.com (mail-lj1-f193.google.com [209.85.208.193]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by turing.freelists.org (Avenir Technologies Mail Multiplex) with ESMTPS id EF1B8297FF for ; Fri, 12 Oct 2018 11:47:40 -0400 (EDT) Received: by mail-lj1-f193.google.com with SMTP id v6-v6so11750093ljc.11 for ; Fri, 12 Oct 2018 08:47:40 -0700 (PDT) From: Sergei Kalashnikov Subject: [tarantool-patches] [PATCH] jdbc: add connection timeout configuration and handling Date: Fri, 12 Oct 2018 18:47:29 +0300 Message-Id: <1539359249-27397-1-git-send-email-ztarvos@gmail.com> Sender: tarantool-patches-bounce@freelists.org Errors-to: tarantool-patches-bounce@freelists.org Reply-To: tarantool-patches@freelists.org List-help: List-unsubscribe: List-software: Ecartis version 1.0.0 List-Id: tarantool-patches List-subscribe: List-owner: List-post: List-archive: To: tarantool-patches@freelists.org Cc: alexander.turenko@tarantool.org, Sergei Kalashnikov Added connection property `socketTimeout` to allow user control over network timeout before actual connection is returned by the driver. This is only done for default socket provider. The default timeout is is left to be infinite. Implemented `Connection.setNetworkTimeout` API to make it possible to change the maximum amount of time to wait for server replies after the connection is established. The connection that has timed out is forced to close. New subsequent operations requested on such connection must fail right away. The corresponding checks are embedded into relevant APIs. Closes #38 --- https://github.com/tarantool/tarantool-java/issues/38 https://github.com/ztarvos/tarantool-java/commits/gh-38-add-connect-timeout .../java/org/tarantool/TarantoolConnection.java | 21 ++ .../java/org/tarantool/jdbc/SQLConnection.java | 53 +++- .../org/tarantool/jdbc/SQLDatabaseMetadata.java | 97 +++++--- src/main/java/org/tarantool/jdbc/SQLDriver.java | 169 +++++++++++-- .../org/tarantool/jdbc/SQLPreparedStatement.java | 66 ++++- src/main/java/org/tarantool/jdbc/SQLStatement.java | 84 +++++-- .../java/org/tarantool/jdbc/JdbcConnectionIT.java | 61 +++++ .../org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java | 30 +++ .../java/org/tarantool/jdbc/JdbcDriverTest.java | 276 +++++++++++++++++++++ .../tarantool/jdbc/JdbcExceptionHandlingTest.java | 158 ++++++++++++ .../tarantool/jdbc/JdbcPreparedStatementIT.java | 94 +++++++ .../java/org/tarantool/jdbc/JdbcStatementIT.java | 30 +++ 12 files changed, 1036 insertions(+), 103 deletions(-) create mode 100644 src/test/java/org/tarantool/jdbc/JdbcDriverTest.java diff --git a/src/main/java/org/tarantool/TarantoolConnection.java b/src/main/java/org/tarantool/TarantoolConnection.java index be94995..b817988 100644 --- a/src/main/java/org/tarantool/TarantoolConnection.java +++ b/src/main/java/org/tarantool/TarantoolConnection.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; +import java.net.SocketException; import java.nio.ByteBuffer; import java.util.List; import java.util.Map; @@ -80,4 +81,24 @@ public class TarantoolConnection extends TarantoolBase> implements Taran public boolean isClosed() { return socket.isClosed(); } + + /** + * Sets given timeout value on underlying socket. + * + * @param timeout Timeout in milliseconds. + * @throws SocketException If failed. + */ + public void setSocketTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + /** + * Retrieves timeout value from underlying socket. + * + * @return Timeout in milliseconds. + * @throws SocketException If failed. + */ + public int getSocketTimeout() throws SocketException { + return socket.getSoTimeout(); + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLConnection.java b/src/main/java/org/tarantool/jdbc/SQLConnection.java index de28850..16ffa4e 100644 --- a/src/main/java/org/tarantool/jdbc/SQLConnection.java +++ b/src/main/java/org/tarantool/jdbc/SQLConnection.java @@ -1,5 +1,7 @@ package org.tarantool.jdbc; +import java.io.IOException; +import java.net.SocketException; import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; @@ -20,6 +22,7 @@ import java.util.Map; import java.util.Properties; import java.util.concurrent.Executor; +import org.tarantool.CommunicationException; import org.tarantool.TarantoolConnection; @SuppressWarnings("Since15") @@ -36,12 +39,14 @@ public class SQLConnection implements Connection { @Override public Statement createStatement() throws SQLException { - return new SQLStatement(connection, this); + checkNotClosed(); + return new SQLStatement(this); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { - return new SQLPreparedStatement(connection, this, sql); + checkNotClosed(); + return new SQLPreparedStatement(this, sql); } @Override @@ -89,6 +94,7 @@ public class SQLConnection implements Connection { @Override public DatabaseMetaData getMetaData() throws SQLException { + checkNotClosed(); return new SQLDatabaseMetadata(this); } @@ -293,15 +299,28 @@ public class SQLConnection implements Connection { @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + + if (milliseconds < 0) + throw new SQLException("Network timeout cannot be negative."); + + try { + connection.setSocketTimeout(milliseconds); + } catch (SocketException e) { + throw new SQLException("Failed to set socket timeout: timeout=" + milliseconds, e); + } } @Override public int getNetworkTimeout() throws SQLException { - throw new SQLFeatureNotSupportedException(); + checkNotClosed(); + try { + return connection.getSocketTimeout(); + } catch (SocketException e) { + throw new SQLException("Failed to retrieve socket timeout", e); + } } - @Override public T unwrap(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); @@ -311,4 +330,28 @@ public class SQLConnection implements Connection { public boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); } + + /** + * @throws SQLException If connection is closed. + */ + protected void checkNotClosed() throws SQLException { + if (isClosed()) + throw new SQLException("Connection is closed."); + } + + /** + * Inspects passed exception and closes the connection if appropriate. + * + * @param e Exception to process. + */ + protected void handleException(Exception e) { + if (CommunicationException.class.isAssignableFrom(e.getClass()) || + IOException.class.isAssignableFrom(e.getClass())) { + try { + close(); + } catch (SQLException ignored) { + // No-op. + } + } + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java index c8879c9..04c598d 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java +++ b/src/main/java/org/tarantool/jdbc/SQLDatabaseMetadata.java @@ -672,23 +672,30 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { @Override public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] types) - throws SQLException { - if (types != null && !Arrays.asList(types).contains("TABLE")) { - return new SQLResultSet(JDBCBridge.EMPTY); - } - String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - List> spaces = (List>) connection.connection.select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); - List> rows = new ArrayList>(); - for (List space : spaces) { - String name = (String) space.get(NAME_IDX); - Map flags = (Map) space.get(FLAGS_IDX); - if (flags != null && flags.containsKey("sql") && like(name, parts)) { - rows.add(Arrays.asList(name, "TABLE", flags.get("sql"))); + throws SQLException { + connection.checkNotClosed(); + try { + if (types != null && !Arrays.asList(types).contains("TABLE")) { + return new SQLResultSet(JDBCBridge.EMPTY); + } + String[] parts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + List> spaces = (List>) connection.connection.select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + List> rows = new ArrayList>(); + for (List space : spaces) { + String name = (String) space.get(NAME_IDX); + Map flags = (Map) space.get(FLAGS_IDX); + if (flags != null && flags.containsKey("sql") && like(name, parts)) { + rows.add(Arrays.asList(name, "TABLE", flags.get("sql"))); + } } + return new SQLNullResultSet(JDBCBridge.mock(Arrays.asList("TABLE_NAME", "TABLE_TYPE", "REMARKS", + //nulls + "TABLE_CAT", "TABLE_SCHEM", "TABLE_TYPE", "TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "SELF_REFERENCING_COL_NAME", "REF_GENERATION"), rows)); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Failed to retrieve table(s) description: " + + "tableNamePattern=\"" + tableNamePattern + "\".", e); } - return new SQLNullResultSet(JDBCBridge.mock(Arrays.asList("TABLE_NAME", "TABLE_TYPE", "REMARKS", - //nulls - "TABLE_CAT", "TABLE_SCHEM", "TABLE_TYPE", "TYPE_CAT", "TYPE_SCHEM", "TYPE_NAME", "SELF_REFERENCING_COL_NAME", "REF_GENERATION"), rows)); } @Override @@ -713,32 +720,40 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { @Override public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); - String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); - List> spaces = (List>) connection.connection.select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); - List> rows = new ArrayList>(); - for (List space : spaces) { - String tableName = (String) space.get(NAME_IDX); - Map flags = (Map) space.get(FLAGS_IDX); - if (flags != null && flags.containsKey("sql") && like(tableName, tableParts)) { - List> format = (List>) space.get(FORMAT_IDX); - for (int columnIdx = 1; columnIdx <= format.size(); columnIdx++) { - Map f = format.get(columnIdx - 1); - String columnName = (String) f.get("name"); - String dbType = (String) f.get("type"); - if (like(columnName, colParts)) { - rows.add(Arrays.asList(tableName, columnName, columnIdx, Types.OTHER, dbType, 10, 1, "YES", Types.OTHER, "NO", "NO")); + connection.checkNotClosed(); + try { + String[] tableParts = tableNamePattern == null ? new String[]{""} : tableNamePattern.split("%"); + String[] colParts = columnNamePattern == null ? new String[]{""} : columnNamePattern.split("%"); + List> spaces = (List>) connection.connection.select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + List> rows = new ArrayList>(); + for (List space : spaces) { + String tableName = (String) space.get(NAME_IDX); + Map flags = (Map) space.get(FLAGS_IDX); + if (flags != null && flags.containsKey("sql") && like(tableName, tableParts)) { + List> format = (List>) space.get(FORMAT_IDX); + for (int columnIdx = 1; columnIdx <= format.size(); columnIdx++) { + Map f = format.get(columnIdx - 1); + String columnName = (String) f.get("name"); + String dbType = (String) f.get("type"); + if (like(columnName, colParts)) { + rows.add(Arrays.asList(tableName, columnName, columnIdx, Types.OTHER, dbType, 10, 1, "YES", Types.OTHER, "NO", "NO")); + } } } } - } - return new SQLNullResultSet((JDBCBridge.mock( - Arrays.asList("TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "DATA_TYPE", "TYPE_NAME", "NUM_PREC_RADIX", "NULLABLE", "IS_NULLABLE", "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", "IS_GENERATEDCOLUMN", - //nulls - "TABLE_CAT", "TABLE_SCHEM", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "SCOPE_CATALOG", "SCOPE_SCHEMA", "SCOPE_TABLE" - ), - rows))); + return new SQLNullResultSet((JDBCBridge.mock( + Arrays.asList("TABLE_NAME", "COLUMN_NAME", "ORDINAL_POSITION", "DATA_TYPE", "TYPE_NAME", "NUM_PREC_RADIX", "NULLABLE", "IS_NULLABLE", "SOURCE_DATA_TYPE", "IS_AUTOINCREMENT", "IS_GENERATEDCOLUMN", + //nulls + "TABLE_CAT", "TABLE_SCHEM", "COLUMN_SIZE", "BUFFER_LENGTH", "DECIMAL_DIGITS", "REMARKS", "COLUMN_DEF", "SQL_DATA_TYPE", "SQL_DATETIME_SUB", "CHAR_OCTET_LENGTH", "SCOPE_CATALOG", "SCOPE_SCHEMA", "SCOPE_TABLE" + ), + rows))); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Error processing table column metadata: " + + "tableNamePattern=\"" + tableNamePattern + "\"; " + + "columnNamePattern=\"" + columnNamePattern + "\".", e); + } } @Override @@ -766,6 +781,8 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { @Override public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { + connection.checkNotClosed(); + final List colNames = Arrays.asList( "TABLE_CAT", "TABLE_SCHEM", "TABLE_NAME", "COLUMN_NAME", "KEY_SEQ", "PK_NAME"); @@ -809,9 +826,9 @@ public class SQLDatabaseMetadata implements DatabaseMetaData { } }); return new SQLNullResultSet((JDBCBridge.mock(colNames, rows))); - } - catch (Throwable t) { - throw new SQLException("Error processing metadata for table \"" + table + "\".", t); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Error processing metadata for table \"" + table + "\".", e); } } diff --git a/src/main/java/org/tarantool/jdbc/SQLDriver.java b/src/main/java/org/tarantool/jdbc/SQLDriver.java index 6867997..87e7dca 100644 --- a/src/main/java/org/tarantool/jdbc/SQLDriver.java +++ b/src/main/java/org/tarantool/jdbc/SQLDriver.java @@ -3,6 +3,7 @@ package org.tarantool.jdbc; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketException; import java.net.URI; import java.sql.Connection; import java.sql.Driver; @@ -32,7 +33,14 @@ public class SQLDriver implements Driver { public static final String PROP_SOCKET_PROVIDER = "socketProvider"; public static final String PROP_USER = "user"; public static final String PROP_PASSWORD = "password"; + public static final String PROP_SOCKET_TIMEOUT = "socketTimeout"; + // Define default values once here. + final static Properties defaults = new Properties() {{ + setProperty(PROP_HOST, "localhost"); + setProperty(PROP_PORT, "3301"); + setProperty(PROP_SOCKET_TIMEOUT, "0"); + }}; protected Map providerCache = new ConcurrentHashMap(); @@ -45,22 +53,42 @@ public class SQLDriver implements Driver { if (providerClassName != null) { socket = getSocketFromProvider(uri, urlProperties, providerClassName); } else { - socket = createAndConnectDefaultSocket(urlProperties); + // Passing the socket to allow unit tests to mock it. + socket = connectAndSetupDefaultSocket(urlProperties, new Socket()); } try { - TarantoolConnection connection = new TarantoolConnection(urlProperties.getProperty(PROP_USER), urlProperties.getProperty(PROP_PASSWORD), socket) {{ + TarantoolConnection connection = new TarantoolConnection( + urlProperties.getProperty(PROP_USER), + urlProperties.getProperty(PROP_PASSWORD), + socket) {{ msgPackLite = SQLMsgPackLite.INSTANCE; }}; - return new SQLConnection(connection, url, info); - } catch (IOException e) { - throw new SQLException("Couldn't initiate connection. Provider class name is " + providerClassName, e); + return new SQLConnection(connection, url, urlProperties); + } catch (Exception e) { + try { + socket.close(); + } catch (IOException ignored) { + // No-op. + } + throw new SQLException("Couldn't initiate connection using " + diagProperties(urlProperties), e); } - } - protected Properties parseQueryString(URI uri, Properties info) { - Properties urlProperties = new Properties(info); + protected Properties parseQueryString(URI uri, Properties info) throws SQLException { + Properties urlProperties = new Properties(defaults); + + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + // Get user and password from the corresponding part of the URI, i.e. before @ sign. + int i = userInfo.indexOf(':'); + if (i < 0) { + urlProperties.setProperty(PROP_USER, userInfo); + } else { + urlProperties.setProperty(PROP_USER, userInfo.substring(0, i)); + urlProperties.setProperty(PROP_PASSWORD, userInfo.substring(i + 1)); + } + } if (uri.getQuery() != null) { String[] parts = uri.getQuery().split("&"); for (String part : parts) { @@ -72,24 +100,77 @@ public class SQLDriver implements Driver { } } } - urlProperties.put(PROP_HOST, uri.getHost() == null ? "localhost" : uri.getHost()); - urlProperties.put(PROP_PORT, uri.getPort() < 1 ? "3301" : uri.getPort()); + if (uri.getHost() != null) { + // Default values are pre-put above. + urlProperties.setProperty(PROP_HOST, uri.getHost()); + } + if (uri.getPort() >= 0) { + // We need to convert port to string otherwise getProperty() will not see it. + urlProperties.setProperty(PROP_PORT, String.valueOf(uri.getPort())); + } + if (info != null) + urlProperties.putAll(info); + + // Validate properties. + int port; + try { + port = Integer.parseInt(urlProperties.getProperty(PROP_PORT)); + } catch (Exception e) { + throw new SQLException("Port must be a valid number."); + } + if (port <= 0 || port > 65535) { + throw new SQLException("Port is out of range: " + port); + } + int timeout; + try { + timeout = Integer.parseInt(urlProperties.getProperty(PROP_SOCKET_TIMEOUT)); + } catch (Exception e) { + throw new SQLException("Timeout must be a valid number."); + } + if (timeout < 0) { + throw new SQLException("Timeout must not be negative."); + } return urlProperties; } - protected Socket createAndConnectDefaultSocket(Properties properties) throws SQLException { - Socket socket; - socket = new Socket(); + /** + * Connects and setup given socket according to connection properties. + * + * Note: socket parameter mainly exists to enable mocking for unit testing. + * + * @param properties Connection properties. + * @param socket Fresh socket instance to setup. + * @return Connected socket. + * @throws SQLException If failed. + */ + protected Socket connectAndSetupDefaultSocket(Properties properties, Socket socket) throws SQLException { + int timeout = Integer.parseInt(properties.getProperty(PROP_SOCKET_TIMEOUT)); try { - socket.connect(new InetSocketAddress(properties.getProperty(PROP_HOST, "localhost"), Integer.parseInt(properties.getProperty(PROP_PORT, "3301")))); - } catch (Exception e) { - throw new SQLException("Couldn't connect to tarantool using" + properties, e); + socket.connect(new InetSocketAddress( + properties.getProperty(PROP_HOST), + Integer.parseInt(properties.getProperty(PROP_PORT))), + timeout); + } catch (IOException e) { + throw new SQLException("Couldn't connect to tarantool using " + diagProperties(properties), e); + } + // Setup socket further. + if (timeout > 0) { + try { + socket.setSoTimeout(timeout); + } catch (SocketException e) { + try { + socket.close(); + } catch (IOException ignored) { + // No-op. + } + throw new SQLException("Couldn't set socket timeout. timeout=" + timeout, e); + } } return socket; } protected Socket getSocketFromProvider(URI uri, Properties urlProperties, String providerClassName) - throws SQLException { + throws SQLException { Socket socket; SQLSocketProvider sqlSocketProvider = providerCache.get(providerClassName); if (sqlSocketProvider == null) { @@ -103,12 +184,23 @@ public class SQLDriver implements Driver { providerCache.put(providerClassName, sqlSocketProvider); } } catch (Exception e) { - throw new SQLException("Couldn't initiate socket provider " + providerClassName, e); + throw new SQLException("Couldn't instantiate socket provider: " + providerClassName, e); + } + if (sqlSocketProvider == null) { + throw new SQLException(String.format("Socket provider %s does not implement %s", + providerClassName, SQLSocketProvider.class.getCanonicalName())); } } } } - socket = sqlSocketProvider.getConnectedSocket(uri, urlProperties); + try { + socket = sqlSocketProvider.getConnectedSocket(uri, urlProperties); + } catch (Exception e) { + throw new SQLException("Socket provider has failed to connect using " + diagProperties(urlProperties), e); + } + if (socket == null) + throw new SQLException("Socket provider returned null socket: " + diagProperties(urlProperties)); + return socket; } @@ -121,16 +213,15 @@ public class SQLDriver implements Driver { public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { try { URI uri = new URI(url); - Properties properties = parseQueryString(uri, new Properties(info == null ? new Properties() : info)); + Properties properties = parseQueryString(uri, info); DriverPropertyInfo host = new DriverPropertyInfo(PROP_HOST, properties.getProperty(PROP_HOST)); host.required = true; - host.description = "Tarantool sever host"; + host.description = "Tarantool server host"; DriverPropertyInfo port = new DriverPropertyInfo(PROP_PORT, properties.getProperty(PROP_PORT)); port.required = true; - port.description = "Tarantool sever port"; - + port.description = "Tarantool server port"; DriverPropertyInfo user = new DriverPropertyInfo(PROP_USER, properties.getProperty(PROP_USER)); user.required = false; @@ -140,12 +231,20 @@ public class SQLDriver implements Driver { password.required = false; password.description = "password"; - DriverPropertyInfo socketProvider = new DriverPropertyInfo(PROP_SOCKET_PROVIDER, properties.getProperty(PROP_SOCKET_PROVIDER)); + DriverPropertyInfo socketProvider = new DriverPropertyInfo( + PROP_SOCKET_PROVIDER, properties.getProperty(PROP_SOCKET_PROVIDER)); + socketProvider.required = false; socketProvider.description = "SocketProvider class implements org.tarantool.jdbc.SQLSocketProvider"; + DriverPropertyInfo socketTimeout = new DriverPropertyInfo( + PROP_SOCKET_TIMEOUT, properties.getProperty(PROP_SOCKET_TIMEOUT)); - return new DriverPropertyInfo[]{host, port, user, password, socketProvider}; + socketTimeout.required = false; + socketTimeout.description = "The number of milliseconds to wait before a timeout is occurred on a socket" + + " connect or read. The default value is 0, which means infinite timeout."; + + return new DriverPropertyInfo[]{host, port, user, password, socketProvider, socketTimeout}; } catch (Exception e) { throw new SQLException(e); } @@ -171,5 +270,23 @@ public class SQLDriver implements Driver { throw new SQLFeatureNotSupportedException(); } - + /** + * Builds a string representation of given connection properties + * along with their sanitized values. + * + * @param props Connection properties. + * @return Comma-separated pairs of property names and values. + */ + protected static String diagProperties(Properties props) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry e : props.entrySet()) { + if (sb.length() > 0) + sb.append(", "); + sb.append(e.getKey()); + sb.append('='); + sb.append(PROP_USER.equals(e.getKey()) || PROP_PASSWORD.equals(e.getKey()) ? + "*****" : e.getValue().toString()); + } + return sb.toString(); + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java index 23d1073..c7a3961 100644 --- a/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLPreparedStatement.java @@ -20,27 +20,36 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; +import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.Map; import org.tarantool.JDBCBridge; -import org.tarantool.TarantoolConnection; public class SQLPreparedStatement extends SQLStatement implements PreparedStatement { + final static String INVALID_CALL_MSG = "The method cannot be called on a PreparedStatement."; final String sql; final Map params; - public SQLPreparedStatement(TarantoolConnection connection, SQLConnection sqlConnection, String sql) { - super(connection, sqlConnection); + public SQLPreparedStatement(SQLConnection connection, String sql) { + super(connection); this.sql = sql; this.params = new HashMap(); } @Override public ResultSet executeQuery() throws SQLException { - return new SQLResultSet(JDBCBridge.query(connection, sql, getParams())); + connection.checkNotClosed(); + discardLastResults(); + Object[] args = getParams(); + try { + return new SQLResultSet(JDBCBridge.query(connection.connection, sql, args)); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException(formatError(sql, args), e); + } } protected Object[] getParams() throws SQLException { @@ -49,7 +58,7 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem if (params.containsKey(i)) { objects[i - 1] = params.get(i); } else { - throw new SQLException("Parameter " + i + "is not"); + throw new SQLException("Parameter " + i + " is missing"); } } return objects; @@ -57,8 +66,15 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem @Override public int executeUpdate() throws SQLException { - return JDBCBridge.update(connection, sql, getParams()); - + connection.checkNotClosed(); + discardLastResults(); + Object[] args = getParams(); + try { + return JDBCBridge.update(connection.connection, sql, args); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException(formatError(sql, args), e); + } } @Override @@ -163,7 +179,15 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem @Override public boolean execute() throws SQLException { - return false; + connection.checkNotClosed(); + discardLastResults(); + Object[] args = getParams(); + try { + return handleResult(JDBCBridge.execute(connection.connection, sql, args)); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException(formatError(sql, args), e); + } } @Override @@ -336,10 +360,34 @@ public class SQLPreparedStatement extends SQLStatement implements PreparedStatem throw new SQLFeatureNotSupportedException(); } - @Override public void addBatch() throws SQLException { throw new SQLFeatureNotSupportedException(); } + @Override + public ResultSet executeQuery(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + + @Override + public boolean execute(String sql) throws SQLException { + throw new SQLException(INVALID_CALL_MSG); + } + + /** + * Provides error message that contains parameters of failed SQL statement. + * + * @param sql SQL Text. + * @param params Parameters of the SQL statement. + * @return Formatted error message. + */ + private static String formatError(String sql, Object[] params) { + return "Failed to execute SQL: " + sql + ", params: " + Arrays.deepToString(params); + } } diff --git a/src/main/java/org/tarantool/jdbc/SQLStatement.java b/src/main/java/org/tarantool/jdbc/SQLStatement.java index 141ae52..8142687 100644 --- a/src/main/java/org/tarantool/jdbc/SQLStatement.java +++ b/src/main/java/org/tarantool/jdbc/SQLStatement.java @@ -8,33 +8,40 @@ import java.sql.SQLWarning; import java.sql.Statement; import org.tarantool.JDBCBridge; -import org.tarantool.TarantoolConnection; @SuppressWarnings("Since15") public class SQLStatement implements Statement { - protected final TarantoolConnection connection; - protected final SQLConnection sqlConnection; + protected final SQLConnection connection; private SQLResultSet resultSet; private int updateCount; private int maxRows; - protected SQLStatement(TarantoolConnection connection, SQLConnection sqlConnection) { - this.connection = connection; - this.sqlConnection = sqlConnection; + protected SQLStatement(SQLConnection sqlConnection) { + this.connection = sqlConnection; } @Override public ResultSet executeQuery(String sql) throws SQLException { - resultSet = new SQLResultSet(JDBCBridge.query(connection, sql)); - updateCount = -1; - return resultSet; + connection.checkNotClosed(); + discardLastResults(); + try { + return new SQLResultSet(JDBCBridge.query(connection.connection, sql)); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Failed to execute SQL: " + sql, e); + } } @Override public int executeUpdate(String sql) throws SQLException { - int update = JDBCBridge.update(connection, sql); - resultSet = null; - return update; + connection.checkNotClosed(); + discardLastResults(); + try { + return JDBCBridge.update(connection.connection, sql); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Failed to execute SQL: " + sql, e); + } } @Override @@ -102,16 +109,13 @@ public class SQLStatement implements Statement { @Override public boolean execute(String sql) throws SQLException { - Object result = JDBCBridge.execute(connection, sql); - if (result instanceof SQLResultSet) { - resultSet = (SQLResultSet) result; - resultSet.maxRows = maxRows; - updateCount = -1; - return true; - } else { - resultSet = null; - updateCount = (Integer) result; - return false; + connection.checkNotClosed(); + discardLastResults(); + try { + return handleResult(JDBCBridge.execute(connection.connection, sql)); + } catch (Exception e) { + connection.handleException(e); + throw new SQLException("Failed to execute SQL: " + sql, e); } } @@ -186,7 +190,7 @@ public class SQLStatement implements Statement { @Override public Connection getConnection() throws SQLException { - return sqlConnection; + return connection; } @Override @@ -268,4 +272,38 @@ public class SQLStatement implements Statement { public boolean isWrapperFor(Class iface) throws SQLException { throw new SQLFeatureNotSupportedException(); } + + /** + * Clears the results of the most recent execution. + */ + protected void discardLastResults() { + updateCount = -1; + if (resultSet != null) { + try { + resultSet.close(); + } catch (Exception ignored) { + // No-op. + } + resultSet = null; + } + } + + /** + * Sets the internals according to the result of last execution. + * + * @param result The result of SQL statement execution. + * @return {@code true}, if the result is a ResultSet object. + */ + protected boolean handleResult(Object result) { + if (result instanceof SQLResultSet) { + resultSet = (SQLResultSet) result; + resultSet.maxRows = maxRows; + updateCount = -1; + return true; + } else { + resultSet = null; + updateCount = (Integer) result; + return false; + } + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java index cc6bfb9..ec16898 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcConnectionIT.java @@ -1,16 +1,24 @@ package org.tarantool.jdbc; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.TarantoolConnection; +import java.lang.reflect.Field; +import java.net.Socket; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +@SuppressWarnings("Since15") public class JdbcConnectionIT extends AbstractJdbcIT { @Test public void testCreateStatement() throws SQLException { @@ -39,4 +47,57 @@ public class JdbcConnectionIT extends AbstractJdbcIT { DatabaseMetaData meta = conn.getMetaData(); assertNotNull(meta); } + + @Test + public void testGetSetNetworkTimeout() throws Exception { + assertEquals(0, conn.getNetworkTimeout()); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + conn.setNetworkTimeout(null, -1); + } + }); + assertEquals("Network timeout cannot be negative.", e.getMessage()); + + conn.setNetworkTimeout(null, 3000); + + assertEquals(3000, conn.getNetworkTimeout()); + + // Check that timeout gets propagated to the socket. + Field sock = TarantoolConnection.class.getDeclaredField("socket"); + sock.setAccessible(true); + assertEquals(3000, ((Socket)sock.get(((SQLConnection)conn).connection)).getSoTimeout()); + } + + @Test + public void testClosedConnection() throws SQLException { + conn.close(); + + int i = 0; + for (; i < 5; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: conn.createStatement(); + break; + case 1: conn.prepareStatement("TEST"); + break; + case 2: conn.getMetaData(); + break; + case 3: conn.getNetworkTimeout(); + break; + case 4: conn.setNetworkTimeout(null, 1000); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(5, i); + } } \ No newline at end of file diff --git a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java index 17ab086..e8244b0 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcDatabaseMetaDataIT.java @@ -2,6 +2,7 @@ package org.tarantool.jdbc; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.function.Executable; import java.sql.DatabaseMetaData; import java.sql.ResultSet; @@ -12,7 +13,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { private DatabaseMetaData meta; @@ -183,4 +186,31 @@ public class JdbcDatabaseMetaDataIT extends AbstractJdbcIT { assertEquals(seq, rs.getInt(5)); assertEquals(pkName, rs.getString(6)); } + + @Test + public void testClosedConnection() throws SQLException { + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: meta.getTables(null, null, null, new String[]{"TABLE"}); + break; + case 1: meta.getColumns(null, null, "TEST", null); + break; + case 2: meta.getPrimaryKeys(null, null, "TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(3, i); + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java new file mode 100644 index 0000000..25d26f3 --- /dev/null +++ b/src/test/java/org/tarantool/jdbc/JdbcDriverTest.java @@ -0,0 +1,276 @@ +package org.tarantool.jdbc; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.tarantool.CommunicationException; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URI; + +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.tarantool.jdbc.SQLDriver.PROP_HOST; +import static org.tarantool.jdbc.SQLDriver.PROP_PASSWORD; +import static org.tarantool.jdbc.SQLDriver.PROP_PORT; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_PROVIDER; +import static org.tarantool.jdbc.SQLDriver.PROP_SOCKET_TIMEOUT; +import static org.tarantool.jdbc.SQLDriver.PROP_USER; + +public class JdbcDriverTest { + @Test + public void testParseQueryString() throws Exception { + SQLDriver drv = new SQLDriver(); + + Properties prop = new Properties(); + prop.setProperty(PROP_USER, "adm"); + prop.setProperty(PROP_PASSWORD, "secret"); + + URI uri = new URI(String.format( + "tarantool://server.local:3302?%s=%s&%s=%d", + PROP_SOCKET_PROVIDER, "some.class", + PROP_SOCKET_TIMEOUT, 5000)); + + Properties res = drv.parseQueryString(uri, prop); + assertNotNull(res); + + assertEquals("server.local", res.getProperty(PROP_HOST)); + assertEquals("3302", res.getProperty(PROP_PORT)); + assertEquals("adm", res.getProperty(PROP_USER)); + assertEquals("secret", res.getProperty(PROP_PASSWORD)); + assertEquals("some.class", res.getProperty(PROP_SOCKET_PROVIDER)); + assertEquals("5000", res.getProperty(PROP_SOCKET_TIMEOUT)); + } + + @Test + public void testParseQueryStringUserInfoInURI() throws Exception { + SQLDriver drv = new SQLDriver(); + Properties res = drv.parseQueryString(new URI("tarantool://adm:secret@server.local"), null); + assertNotNull(res); + assertEquals("server.local", res.getProperty(PROP_HOST)); + assertEquals("3301", res.getProperty(PROP_PORT)); + assertEquals("adm", res.getProperty(PROP_USER)); + assertEquals("secret", res.getProperty(PROP_PASSWORD)); + } + + @Test + public void testParseQueryStringValidations() { + // Check non-number port + checkParseQueryStringValidation("tarantool://0", + new Properties() {{setProperty(PROP_PORT, "nan");}}, + "Port must be a valid number."); + + // Check zero port + checkParseQueryStringValidation("tarantool://0:0", null, "Port is out of range: 0"); + + // Check high port + checkParseQueryStringValidation("tarantool://0:65536", null, "Port is out of range: 65536"); + + // Check non-number timeout + checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=nan", PROP_SOCKET_TIMEOUT), null, + "Timeout must be a valid number."); + + // Check negative timeout + checkParseQueryStringValidation(String.format("tarantool://0:3301?%s=-100", PROP_SOCKET_TIMEOUT), null, + "Timeout must not be negative."); + } + + @Test + public void testGetPropertyInfo() throws SQLException { + Driver drv = new SQLDriver(); + Properties props = new Properties(); + DriverPropertyInfo[] info = drv.getPropertyInfo("tarantool://server.local:3302", props); + assertNotNull(info); + + for (DriverPropertyInfo e: info) { + assertNotNull(e.name); + assertNull(e.choices); + assertNotNull(e.description); + + if (PROP_HOST.equals(e.name)) { + assertTrue(e.required); + assertEquals("server.local", e.value); + } else if (PROP_PORT.equals(e.name)) { + assertTrue(e.required); + assertEquals("3302", e.value); + } else if (PROP_USER.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_PASSWORD.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_SOCKET_PROVIDER.equals(e.name)) { + assertFalse(e.required); + assertNull(e.value); + } else if (PROP_SOCKET_TIMEOUT.equals(e.name)) { + assertFalse(e.required); + assertEquals("0", e.value); + } else + fail("Unknown property '" + e.name + "'"); + } + } + + @Test + public void testDefaultSocketProviderConnectTimeoutError() throws IOException { + final int socketTimeout = 3000; + final Socket mockSocket = mock(Socket.class); + final SQLDriver drv = new TestSQLDriverThatPassesSocket(mockSocket); + + SocketTimeoutException timeoutEx = new SocketTimeoutException(); + doThrow(timeoutEx) + .when(mockSocket) + .connect(new InetSocketAddress("localhost", 3301), socketTimeout); + + final Properties prop = new Properties(); + prop.setProperty(PROP_SOCKET_TIMEOUT, String.valueOf(socketTimeout)); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.connect("tarantool://localhost:3301", prop); + } + }); + + assertTrue(e.getMessage().startsWith("Couldn't connect to tarantool using"), e.getMessage()); + assertEquals(timeoutEx, e.getCause()); + } + + @Test + public void testDefaultSocketProviderSetSocketTimeoutError() throws IOException { + final int socketTimeout = 3000; + final Socket mockSocket = mock(Socket.class); + final SQLDriver drv = new TestSQLDriverThatPassesSocket(mockSocket); + + // Check error setting socket timeout + reset(mockSocket); + doNothing() + .when(mockSocket) + .connect(new InetSocketAddress("localhost", 3301), socketTimeout); + + SocketException sockEx = new SocketException("TEST"); + doThrow(sockEx) + .when(mockSocket) + .setSoTimeout(socketTimeout); + + final Properties prop = new Properties(); + prop.setProperty(PROP_SOCKET_TIMEOUT, String.valueOf(socketTimeout)); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.connect("tarantool://localhost:3301", prop); + } + }); + + assertTrue(e.getMessage().startsWith("Couldn't set socket timeout."), e.getMessage()); + assertEquals(sockEx, e.getCause()); + } + + @Test + public void testCustomSocketProviderFail() throws SQLException { + checkCustomSocketProviderFail("nosuchclassexists", + "Couldn't instantiate socket provider"); + + checkCustomSocketProviderFail(Integer.class.getName(), + "Socket provider java.lang.Integer does not implement org.tarantool.jdbc.SQLSocketProvider"); + + checkCustomSocketProviderFail(TestSQLProviderThatReturnsNull.class.getName(), + "Socket provider returned null socket"); + + checkCustomSocketProviderFail(TestSQLProviderThatThrows.class.getName(), + "Socket provider has failed to connect using"); + } + + @Test + public void testNoResponseAfterInitialConnect() throws IOException { + ServerSocket socket = new ServerSocket(); + socket.bind(null, 0); + try { + final String url = "tarantool://localhost:" + socket.getLocalPort(); + final Properties prop = new Properties(); + prop.setProperty(PROP_SOCKET_TIMEOUT, "3000"); + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + DriverManager.getConnection(url, prop); + } + }); + assertTrue(e.getMessage().startsWith("Couldn't initiate connection using "), e.getMessage()); + assertTrue(e.getCause() instanceof CommunicationException); + assertTrue(e.getCause().getCause() instanceof SocketTimeoutException); + } finally { + socket.close(); + } + } + + private void checkCustomSocketProviderFail(String providerClassName, String errMsg) throws SQLException { + final Driver drv = DriverManager.getDriver("tarantool:"); + final Properties prop = new Properties(); + prop.setProperty(PROP_SOCKET_PROVIDER, providerClassName); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.connect("tarantool://0:3301", prop); + } + }); + assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); + } + + private void checkParseQueryStringValidation(final String uri, final Properties prop, String errMsg) { + final SQLDriver drv = new SQLDriver(); + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + drv.parseQueryString(new URI(uri), prop); + } + }); + assertTrue(e.getMessage().startsWith(errMsg), e.getMessage()); + } + + static class TestSQLDriverThatPassesSocket extends SQLDriver { + private Socket mockSocket; + + TestSQLDriverThatPassesSocket(Socket mockSocket) { + this.mockSocket = mockSocket; + } + + @Override + protected Socket connectAndSetupDefaultSocket(Properties properties, Socket s) throws SQLException { + return super.connectAndSetupDefaultSocket(properties, mockSocket); + } + } + + static class TestSQLProviderThatReturnsNull implements SQLSocketProvider { + @Override + public Socket getConnectedSocket(URI uri, Properties params) { + return null; + } + } + + static class TestSQLProviderThatThrows implements SQLSocketProvider { + @Override + public Socket getConnectedSocket(URI uri, Properties params) { + throw new RuntimeException("ERROR"); + } + } +} diff --git a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java index 8cc7acc..0f74965 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java +++ b/src/test/java/org/tarantool/jdbc/JdbcExceptionHandlingTest.java @@ -2,22 +2,33 @@ package org.tarantool.jdbc; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.tarantool.CommunicationException; import org.tarantool.TarantoolConnection; +import java.io.IOException; +import java.net.Socket; import java.sql.DatabaseMetaData; +import java.sql.PreparedStatement; import java.sql.SQLException; +import java.sql.Statement; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.tarantool.jdbc.SQLDatabaseMetadata.FORMAT_IDX; import static org.tarantool.jdbc.SQLDatabaseMetadata.INDEX_FORMAT_IDX; import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACE_ID_IDX; +import static org.tarantool.jdbc.SQLDatabaseMetadata.SPACES_MAX; import static org.tarantool.jdbc.SQLDatabaseMetadata._VINDEX; import static org.tarantool.jdbc.SQLDatabaseMetadata._VSPACE; @@ -57,4 +68,151 @@ public class JdbcExceptionHandlingTest { assertTrue(t.getCause().getMessage().contains("Wrong value type")); } + + @Test + public void testStatementCommunicationException() throws SQLException { + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.executeQuery("TEST"); + } + }); + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.executeUpdate("TEST"); + } + }); + checkStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(Statement statement) throws Throwable { + statement.execute("TEST"); + } + }); + } + + @Test + public void testPreparedStatementCommunicationException() throws SQLException { + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.executeQuery(); + } + }); + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.executeUpdate(); + } + }); + checkPreparedStatementCommunicationException(new ThrowingConsumer() { + @Override + public void accept(PreparedStatement prep) throws Throwable { + prep.execute(); + } + }); + } + + @Test + public void testDatabaseMetaDataCommunicationException() throws SQLException { + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getTables(null, null, null, new String[] {"TABLE"}); + } + }, "Failed to retrieve table(s) description: tableNamePattern=\"null\"."); + + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getColumns(null, null, "TEST", "ID"); + } + }, "Error processing table column metadata: tableNamePattern=\"TEST\"; columnNamePattern=\"ID\"."); + + checkDatabaseMetaDataCommunicationException(new ThrowingConsumer() { + @Override + public void accept(DatabaseMetaData meta) throws Throwable { + meta.getPrimaryKeys(null, null, "TEST"); + } + }, "Error processing metadata for table \"TEST\"."); + } + + private void checkStatementCommunicationException(final ThrowingConsumer consumer) throws SQLException { + TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); + final Statement stmt = new SQLStatement(new SQLConnection(mockCon, "tarantool://0:0", new Properties())); + + Exception ex = new CommunicationException("TEST"); + + doThrow(ex).when(mockCon).sql("TEST", new Object[0]); + doThrow(ex).when(mockCon).update("TEST"); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + consumer.accept(stmt); + } + }); + assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); + + assertEquals(ex, e.getCause()); + + verify(((SQLConnection)stmt.getConnection()).connection, times(1)).close(); + } + + private void checkPreparedStatementCommunicationException(final ThrowingConsumer consumer) + throws SQLException { + TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); + + final PreparedStatement prep = new SQLPreparedStatement( + new SQLConnection(mockCon, "tarantool://0:0", new Properties()), "TEST"); + + Exception ex = new CommunicationException("TEST"); + doThrow(ex).when(mockCon).sql("TEST", new Object[0]); + doThrow(ex).when(mockCon).update("TEST"); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + consumer.accept(prep); + } + }); + assertTrue(e.getMessage().startsWith("Failed to execute"), e.getMessage()); + + assertEquals(ex, e.getCause()); + + verify(mockCon, times(1)).close(); + } + + private void checkDatabaseMetaDataCommunicationException(final ThrowingConsumer consumer, + String msg) throws SQLException { + TestTarantoolConnection mockCon = mock(TestTarantoolConnection.class); + SQLConnection conn = new SQLConnection(mockCon, "tarantool://0:0", new Properties()); + final DatabaseMetaData meta = conn.getMetaData(); + + Exception ex = new CommunicationException("TEST"); + doThrow(ex).when(mockCon).select(_VSPACE, 0, Arrays.asList(), 0, SPACES_MAX, 0); + doThrow(ex).when(mockCon).select(_VSPACE, 2, Arrays.asList("TEST"), 0, 1, 0); + + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + consumer.accept(meta); + } + }); + assertTrue(e.getMessage().startsWith(msg), e.getMessage()); + + assertEquals(ex, e.getCause()); + + verify(mockCon, times(1)).close(); + } + + class TestTarantoolConnection extends TarantoolConnection { + TestTarantoolConnection() throws IOException { + super(null, null, mock(Socket.class)); + } + @Override + protected void sql(String sql, Object[] bind) { + super.sql(sql, bind); + } + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java index f356f6b..3976d5b 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcPreparedStatementIT.java @@ -2,6 +2,7 @@ package org.tarantool.jdbc; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.function.Executable; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -10,7 +11,10 @@ import java.sql.SQLException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class JdbcPreparedStatementIT extends AbstractJdbcIT { private PreparedStatement prep; @@ -64,4 +68,94 @@ public class JdbcPreparedStatementIT extends AbstractJdbcIT { assertEquals("thousand", getRow("test", 1000).get(1)); } + + @Test + public void testExecuteReturnsResultSet() throws SQLException { + prep = conn.prepareStatement("SELECT val FROM test WHERE id=?"); + assertNotNull(prep); + prep.setInt(1, 1); + + assertTrue(prep.execute()); + assertEquals(-1, prep.getUpdateCount()); + + ResultSet rs = prep.getResultSet(); + assertNotNull(rs); + assertTrue(rs.next()); + assertEquals("one", rs.getString(1)); + assertFalse(rs.next()); + rs.close(); + } + + @Test + public void testExecuteReturnsUpdateCount() throws Exception { + prep = conn.prepareStatement("INSERT INTO test VALUES(?, ?), (?, ?)"); + assertNotNull(prep); + + prep.setInt(1, 10); + prep.setString(2, "ten"); + prep.setInt(3, 20); + prep.setString(4, "twenty"); + + assertFalse(prep.execute()); + assertNull(prep.getResultSet()); + assertEquals(2, prep.getUpdateCount()); + + assertEquals("ten", getRow("test", 10).get(1)); + assertEquals("twenty", getRow("test", 20).get(1)); + } + + @Test void testForbiddenMethods() throws SQLException { + prep = conn.prepareStatement("TEST"); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: prep.executeQuery("TEST"); + break; + case 1: prep.executeUpdate("TEST"); + break; + case 2: prep.execute("TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("The method cannot be called on a PreparedStatement.", e.getMessage()); + } + assertEquals(3, i); + } + + @Test + public void testClosedConnection() throws SQLException { + prep = conn.prepareStatement("TEST"); + + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: prep.executeQuery(); + break; + case 1: prep.executeUpdate(); + break; + case 2: prep.execute(); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(3, i); + } } diff --git a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java index 925556d..735f326 100644 --- a/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java +++ b/src/test/java/org/tarantool/jdbc/JdbcStatementIT.java @@ -3,6 +3,7 @@ package org.tarantool.jdbc; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; import java.sql.ResultSet; import java.sql.SQLException; @@ -10,8 +11,10 @@ import java.sql.Statement; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; public class JdbcStatementIT extends AbstractJdbcIT { private Statement stmt; @@ -63,4 +66,31 @@ public class JdbcStatementIT extends AbstractJdbcIT { assertEquals("hundred", getRow("test", 100).get(1)); assertEquals("thousand", getRow("test", 1000).get(1)); } + + @Test + public void testClosedConnection() throws Exception { + conn.close(); + + int i = 0; + for (; i < 3; i++) { + final int step = i; + SQLException e = assertThrows(SQLException.class, new Executable() { + @Override + public void execute() throws Throwable { + switch (step) { + case 0: stmt.executeQuery("TEST"); + break; + case 1: stmt.executeUpdate("TEST"); + break; + case 2: stmt.execute("TEST"); + break; + default: + fail(); + } + } + }); + assertEquals("Connection is closed.", e.getMessage()); + } + assertEquals(3, i); + } } \ No newline at end of file -- 1.8.3.1