diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index 2f54a98ab..a6054b1fc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -483,6 +483,19 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold, */ void setAccessTokenCallbackClass(String accessTokenCallbackClass); + /** + * Get Currently installed message handler on the connection + * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} + * @return + */ + ISQLServerMessageHandler getServerMessageHandler(); + + /** + * Set message handler on the connection + * @see {@link ISQLServerMessageHandler#messageHandler(ISQLServerMessage)} + */ + ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler); + /** * Returns the current flag for calcBigDecimalPrecision. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java new file mode 100644 index 000000000..055e812b2 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessage.java @@ -0,0 +1,93 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +import java.sql.SQLException; + +public interface ISQLServerMessage +{ + /** + * Returns SQLServerError containing detailed info about SQL Server Message as received from SQL Server. + * + * @return SQLServerError + */ + public SQLServerError getSQLServerMessage(); + + /** + * Returns error message as received from SQL Server + * + * @return Error Message + */ + public String getErrorMessage(); + + /** + * Returns error number as received from SQL Server + * + * @return Error Number + */ + public int getErrorNumber(); + + /** + * Returns error state as received from SQL Server + * + * @return Error State + */ + public int getErrorState(); + + /** + * Returns Severity of error (as int value) as received from SQL Server + * + * @return Error Severity + */ + public int getErrorSeverity(); + + /** + * Returns name of the server where exception occurs as received from SQL Server + * + * @return Server Name + */ + public String getServerName(); + + /** + * Returns name of the stored procedure where exception occurs as received from SQL Server + * + * @return Procedure Name + */ + public String getProcedureName(); + + /** + * Returns line number where the error occurred in Stored Procedure returned by getProcedureName() as + * received from SQL Server + * + * @return Line Number + */ + public long getLineNumber(); + + /** + * Creates a SQLServerException or SQLServerWarning from this SQLServerMessage
+ * @return + * + */ + public SQLException toSqlExceptionOrSqlWarning(); + + /** + * Check if this is a isErrorMessage + * @return true if it's an instance of SQLServerError + */ + public default boolean isErrorMessage() { + return this instanceof SQLServerError; + } + + /** + * Check if this is a SQLServerInfoMessage + * @return true if it's an instance of SQLServerInfoMessage + */ + public default boolean isInfoMessage() { + return this instanceof SQLServerInfoMessage; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java new file mode 100644 index 000000000..74d7d7518 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerMessageHandler.java @@ -0,0 +1,103 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +/** + * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server. + * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits: + * + *

+ * For example code, see {@link #messageHandler(ISQLServerMessage)} + */ +public interface ISQLServerMessageHandler +{ + /** + * You can use the ISQLServerMessageHandler interface to customize the way JDBC handles error messages generated by the SQL Server. + * Implementing ISQLServerMessageHandler in your own class for handling error messages can provide the following benefits: + *

+ * + * Example code: + *
+     *  public ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning)
+     *  {
+     *      ISQLServerMessage retObj = serverErrorOrWarning;
+     *
+     *      if (serverErrorOrWarning.isErrorMessage()) {
+     *
+     *          // Downgrade: 2601 -- Cannot insert duplicate key row...
+     *          if (2601 == serverErrorOrWarning.getErrorNumber()) {
+     *              retObj = serverErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage();
+     *          }
+     *
+     *          // Discard: 3701 -- Cannot drop the table ...
+     *          if (3701 == serverErrorOrWarning.getErrorNumber()) {
+     *              retObj = null;
+     *          }
+     *      }
+     *
+     *      return retObj;
+     *  }
+    
+     * 
+ * + * @param serverErrorOrWarning + * @return + * + */ + ISQLServerMessage messageHandler(ISQLServerMessage serverErrorOrWarning); +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 982cf0e8e..b4a663c20 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -4928,6 +4928,27 @@ void addWarning(String warningString) { } } + // Any changes to SQLWarnings should be synchronized. + /** Used to add plain SQLWarning messages (if they do not hold extended information, like: ErrorSeverity, ServerName, ProcName etc */ + void addWarning(SQLWarning sqlWarning) { + warningSynchronization.lock(); + try { + if (null == sqlWarnings) { + sqlWarnings = sqlWarning; + } else { + sqlWarnings.setNextWarning(sqlWarning); + } + } finally { + warningSynchronization.unlock(); + } + } + + // Any changes to SQLWarnings should be synchronized. + /** Used to add messages that holds extended information, like: ErrorSeverity, ServerName, ProcName etc */ + void addWarning(ISQLServerMessage sqlServerMessage) { + addWarning(new SQLServerWarning(sqlServerMessage.getSQLServerMessage())); + } + @Override public void clearWarnings() throws SQLServerException { warningSynchronization.lock(); @@ -8500,6 +8521,34 @@ public void setIPAddressPreference(String iPAddressPreference) { public String getIPAddressPreference() { return activeConnectionProperties.getProperty(SQLServerDriverStringProperty.IPADDRESS_PREFERENCE.toString()); } + + + + /** Message handler */ + private transient ISQLServerMessageHandler serverMessageHandler; + + /** + * Set current message handler + * + * @param messageHandler + * @return The previously installed message handler (null if none) + */ + @Override + public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) + { + ISQLServerMessageHandler installedMessageHandler = this.serverMessageHandler; + this.serverMessageHandler = messageHandler; + return installedMessageHandler; + } + + /** + * @return Get Currently installed message handler on the connection + */ + @Override + public ISQLServerMessageHandler getServerMessageHandler() + { + return this.serverMessageHandler; + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index ffda2dd6b..5ed4b7b06 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -692,6 +692,16 @@ public void setAccessTokenCallbackClass(String accessTokenCallbackClass) { wrappedConnection.setAccessTokenCallbackClass(accessTokenCallbackClass); } + @Override + public ISQLServerMessageHandler getServerMessageHandler() { + return wrappedConnection.getServerMessageHandler(); + } + + @Override + public ISQLServerMessageHandler setServerMessageHandler(ISQLServerMessageHandler messageHandler) { + return wrappedConnection.setServerMessageHandler(messageHandler); + } + /** * Returns the current value for 'calcBigDecimalPrecision'. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java index 3454e5aef..1b4c1e836 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerError.java @@ -6,12 +6,15 @@ package com.microsoft.sqlserver.jdbc; import java.io.Serializable; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; /** * SQLServerError represents a TDS error or message event. */ -public final class SQLServerError extends StreamPacket implements Serializable { +public final class SQLServerError extends StreamPacket implements Serializable, ISQLServerMessage { /** * List SQL Server transient errors drivers will retry on from @@ -221,6 +224,18 @@ public long getLineNumber() { super(TDS.TDS_ERR); } + SQLServerError(SQLServerError errorMsg) { + super(TDS.TDS_ERR); + this.errorNumber = errorMsg.errorNumber; + this.errorState = errorMsg.errorState; + this.errorSeverity = errorMsg.errorSeverity; + this.errorMessage = errorMsg.errorMessage; + this.serverName = errorMsg.serverName; + this.procName = errorMsg.procName; + this.lineNumber = errorMsg.lineNumber; + } + + @Override void setFromTDS(TDSReader tdsReader) throws SQLServerException { if (TDS.TDS_ERR != tdsReader.readUnsignedByte()) assert false; @@ -237,4 +252,104 @@ void setContentsFromTDS(TDSReader tdsReader) throws SQLServerException { procName = tdsReader.readUnicodeString(tdsReader.readUnsignedByte()); lineNumber = tdsReader.readUnsignedInt(); } + + + + /** + * Holds any "overflow messages", or messages that has been added after the first message. + *

+ * This is later on used when creating a SQLServerException.
+ * Where all entries in the errorChain will be added {@link java.sql.SQLException#setNextException(SQLException)} + */ + private List errorChain; + + void addError(SQLServerError sqlServerError) { + if (errorChain == null) { + errorChain = new ArrayList<>(); + } + errorChain.add(sqlServerError); + } + + List getErrorChain() { + return errorChain; + } + + @Override + public SQLServerError getSQLServerMessage() + { + return this; + } + + /** + * Downgrade a Error message into a Info message + *

+ * This simply create a SQLServerInfoMessage from this SQLServerError, + * without changing the message content. + * @return + */ + public ISQLServerMessage toSQLServerInfoMessage() + { + return toSQLServerInfoMessage(-1, -1); + } + + /** + * Downgrade a Error message into a Info message + *

+ * This simply create a SQLServerInfoMessage from this SQLServerError, + * + * @param newErrorSeverity - The new ErrorSeverity + * + * @return + */ + public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity) + { + return toSQLServerInfoMessage(newErrorSeverity, -1); + } + + /** + * Downgrade a Error message into a Info message + *

+ * This simply create a SQLServerInfoMessage from this SQLServerError, + * + * @param newErrorSeverity - If you want to change the ErrorSeverity (-1: leave unchanged) + * @param newErrorNumber - If you want to change the ErrorNumber (-1: leave unchanged) + * + * @return + */ + public ISQLServerMessage toSQLServerInfoMessage(int newErrorSeverity, int newErrorNumber) + { + if (newErrorSeverity != -1) { + this.setErrorSeverity(newErrorSeverity); + } + + if (newErrorNumber != -1) { + this.setErrorNumber(newErrorNumber); + } + + return new SQLServerInfoMessage(this); + } + + /** + * Set a new ErrorSeverity for this Message + * @param newSeverity + */ + public void setErrorSeverity(int newSeverity) + { + this.errorSeverity = newSeverity; + } + + /** + * Set a new ErrorNumber for this Message + * @param newSeverity + */ + public void setErrorNumber(int newErrorNumber) + { + this.errorNumber = newErrorNumber; + } + + @Override + public SQLException toSqlExceptionOrSqlWarning() + { + return new SQLServerException(this); + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 943804dcf..77367708a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -7,6 +7,7 @@ import java.sql.SQLFeatureNotSupportedException; import java.text.MessageFormat; +import java.util.List; import java.util.UUID; import java.util.logging.Level; @@ -178,6 +179,15 @@ static String getErrString(String errCode) { logException(obj, errText, bStack); } + public SQLServerException(SQLServerError sqlServerError) { + super(sqlServerError.getErrorMessage(), + generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), + sqlServerError.getErrorNumber(), + null); + + this.sqlServerError = sqlServerError; + } + /** * Constructs a new SQLServerException. * @@ -261,6 +271,21 @@ static void makeFromDatabaseError(SQLServerConnection con, Object obj, String er SQLServerException.checkAndAppendClientConnId(errText, con), state, sqlServerError, bStack); theException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE); + // Add any extra messages to the SQLException error chain + List errorChain = sqlServerError.getErrorChain(); + if (errorChain != null) { + for (SQLServerError srvError : errorChain) { + String state2 = generateStateCode(con, srvError.getErrorNumber(), srvError.getErrorState()); + + SQLServerException chainException = new SQLServerException(obj, + SQLServerException.checkAndAppendClientConnId(srvError.getErrorMessage(), con), + state2, srvError, bStack); + chainException.setDriverErrorCode(DRIVER_ERROR_FROM_DATABASE); + + theException.setNextException(chainException); + } + } + // Close the connection if we get a severity 20 or higher error class (nClass is severity of error). if ((sqlServerError.getErrorSeverity() >= 20) && (null != con)) { con.notifyPooledConnection(theException); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java new file mode 100644 index 000000000..8b6579491 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerInfoMessage.java @@ -0,0 +1,136 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.sql.SQLException; + +/** + * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10) + *

+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the + * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc + *

+ * This enables client to print out extra information about the message.
+ * Like: In what procedure was the message produced. + *

+ * A SQLServerInfoMessage is produced when reading the TDS Stream and added to the Connection as a SQLServerWarning + */ +public final class SQLServerInfoMessage extends StreamPacket implements ISQLServerMessage { + SQLServerError msg = new SQLServerError(); + + SQLServerInfoMessage() { + super(TDS.TDS_MSG); + } + + SQLServerInfoMessage(SQLServerError msg) { + super(TDS.TDS_MSG); + this.msg = msg; + } + + @Override + void setFromTDS(TDSReader tdsReader) throws SQLServerException { + if (TDS.TDS_MSG != tdsReader.readUnsignedByte()) + assert false; + msg.setContentsFromTDS(tdsReader); + } + + @Override + public SQLServerError getSQLServerMessage() { + return msg; + } + + @Override + public String getErrorMessage() { + return msg.getErrorMessage(); + } + + @Override + public int getErrorNumber() { + return msg.getErrorNumber(); + } + + @Override + public int getErrorState() { + return msg.getErrorState(); + } + + @Override + public int getErrorSeverity() { + return msg.getErrorSeverity(); + } + + @Override + public String getServerName() { + return msg.getServerName(); + } + + @Override + public String getProcedureName() + { + return msg.getProcedureName(); + } + + @Override + public long getLineNumber() + { + return msg.getLineNumber(); + } + + /** + * Upgrade a Info message into a Error message + *

+ * This simply create a SQLServerError from this SQLServerInfoMessage, + * without changing the message content. + * @return + */ + public ISQLServerMessage toSQLServerError() + { + return toSQLServerError(-1, -1); + } + + /** + * Upgrade a Info message into a Error message + *

+ * This simply create a SQLServerError from this SQLServerInfoMessage. + * + * @param newErrorSeverity - The new ErrorSeverity + * + * @return + */ + public ISQLServerMessage toSQLServerError(int newErrorSeverity) + { + return toSQLServerError(newErrorSeverity, -1); + } + + /** + * Upgrade a Info message into a Error message + *

+ * This simply create a SQLServerError from this SQLServerInfoMessage. + * + * @param newErrorSeverity - If you want to change the ErrorSeverity (-1: leave unchanged) + * @param newErrorNumber - If you want to change the ErrorNumber (-1: leave unchanged) + * + * @return + */ + public ISQLServerMessage toSQLServerError(int newErrorSeverity, int newErrorNumber) + { + if (newErrorSeverity != -1) { + this.msg.setErrorSeverity(newErrorSeverity); + } + + if (newErrorNumber != -1) { + this.msg.setErrorNumber(newErrorNumber); + } + + return new SQLServerError(this.msg); + } + + @Override + public SQLException toSqlExceptionOrSqlWarning() + { + return new SQLServerWarning(this.msg); + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java index 866b9ea13..cbfe102ca 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerStatement.java @@ -1620,7 +1620,6 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { } } } - // If the current command (whatever it was) produced an error then stop parsing and propagate it up. // In this case, the command is likely to be a RAISERROR, but it could be anything. if (doneToken.isError()) @@ -1690,8 +1689,8 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException { @Override boolean onInfo(TDSReader tdsReader) throws SQLServerException { - StreamInfo infoToken = new StreamInfo(); - infoToken.setFromTDS(tdsReader); + SQLServerInfoMessage infoMessage = new SQLServerInfoMessage(); + infoMessage.setFromTDS(tdsReader); // Under some circumstances the server cannot produce the cursored result set // that we requested, but produces a client-side (default) result set instead. @@ -1705,13 +1704,40 @@ boolean onInfo(TDSReader tdsReader) throws SQLServerException { // ErrorCause: Server cursor is not supported on the specified SQL, falling back to default result set // ErrorCorrectiveAction: None required // - if (16954 == infoToken.msg.getErrorNumber()) + if (16954 == infoMessage.msg.getErrorNumber()) executedSqlDirectly = true; - SQLWarning warning = new SQLWarning( - infoToken.msg.getErrorMessage(), SQLServerException.generateStateCode(connection, - infoToken.msg.getErrorNumber(), infoToken.msg.getErrorState()), - infoToken.msg.getErrorNumber()); + + // Call the message handler to see what that think of the message + // - discard + // - upgrade to Error + // - or simply pass on + ISQLServerMessageHandler msgHandler = ((ISQLServerConnection)getConnection()).getServerMessageHandler(); + if (msgHandler != null) { + + // Let the message handler decide if the error should be unchanged, up/down-graded or ignored + ISQLServerMessage srvMessage = msgHandler.messageHandler(infoMessage); + + // Ignored + if (srvMessage == null) { + return true; + } + + // The message handler changed it to an "Error Message" + if (srvMessage.isErrorMessage()) { + // Set/Add the error message to the "super" + addDatabaseError( (SQLServerError)srvMessage ); + return true; + } + + // Still a "info message", just set infoMessage and the code in the below section will create the Warnings + if (srvMessage.isInfoMessage()) { + infoMessage = (SQLServerInfoMessage)srvMessage; + } + } + + // Create the SQLWarning and add them to the Warning chain + SQLWarning warning = new SQLServerWarning(infoMessage.msg); if (sqlWarnings == null) { sqlWarnings = new Vector<>(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java new file mode 100644 index 000000000..81d588bd9 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerWarning.java @@ -0,0 +1,47 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc; + +import java.sql.SQLWarning; + +/** + * Holds information about SQL Server messages that is considered as Informational Messages (normally if SQL Server Severity is at 10) + *

+ * Instead of just holding the SQL Server message (like a normal SQLWarning, it also holds all the + * SQL Servers extended information, like: ErrorSeverity, ServerName, ProcName etc + *

+ * This enables client to print out extra information about the message.
+ * Like: In what procedure was the message produced. + */ +public class SQLServerWarning +extends SQLWarning +{ + private static final long serialVersionUID = -5212432397705929142L; + + /** SQL server error */ + private SQLServerError sqlServerError; + + /* + * Create a SQLWarning from an SQLServerError object + */ + public SQLServerWarning(SQLServerError sqlServerError) { + super(sqlServerError.getErrorMessage(), + SQLServerException.generateStateCode(null, sqlServerError.getErrorNumber(), sqlServerError.getErrorState()), + sqlServerError.getErrorNumber(), + null); + + this.sqlServerError = sqlServerError; + } + + /** + * Returns SQLServerError object containing detailed info about exception as received from SQL Server. This API + * returns null if no server error has occurred. + * + * @return SQLServerError + */ + public SQLServerError getSQLServerError() { + return sqlServerError; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java b/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java deleted file mode 100644 index 5b4cb571f..000000000 --- a/src/main/java/com/microsoft/sqlserver/jdbc/StreamInfo.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made - * available under the terms of the MIT License. See the LICENSE file in the project root for more information. - */ - -package com.microsoft.sqlserver.jdbc; - -final class StreamInfo extends StreamPacket { - final SQLServerError msg = new SQLServerError(); - - StreamInfo() { - super(TDS.TDS_MSG); - } - - void setFromTDS(TDSReader tdsReader) throws SQLServerException { - if (TDS.TDS_MSG != tdsReader.readUnsignedByte()) - assert false; - msg.setContentsFromTDS(tdsReader); - } -} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java index c18d61a0c..90eebb8df 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java @@ -197,6 +197,14 @@ final SQLServerError getDatabaseError() { return databaseError; } + public void addDatabaseError(SQLServerError databaseError) { + if (this.databaseError == null) { + this.databaseError = databaseError; + } else { + this.databaseError.addError(databaseError); + } + } + TDSTokenHandler(String logContext) { this.logContext = logContext; } @@ -258,13 +266,29 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException { } boolean onError(TDSReader tdsReader) throws SQLServerException { - if (null == databaseError) { - databaseError = new SQLServerError(); - databaseError.setFromTDS(tdsReader); - } else { - (new SQLServerError()).setFromTDS(tdsReader); + SQLServerError tmpDatabaseError = new SQLServerError(); + tmpDatabaseError.setFromTDS(tdsReader); + + ISQLServerMessageHandler msgHandler = tdsReader.getConnection().getServerMessageHandler(); + if (msgHandler != null) { + // Let the message handler decide if the error should be unchanged/down-graded or ignored + ISQLServerMessage srvMessage = msgHandler.messageHandler(tmpDatabaseError); + + // Ignored + if (srvMessage == null) { + return true; + } + + // Down-graded to a SQLWarning + if (srvMessage.isInfoMessage()) { + tdsReader.getConnection().addWarning(srvMessage); + return true; + } } + // set/add the database error + addDatabaseError(tmpDatabaseError); + return true; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java index d758950f8..926b6a766 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -521,6 +521,10 @@ private List getVerifiedMethodNames() { verifiedMethodNames.add("setMsiTokenCacheTtl"); verifiedMethodNames.add("getAccessTokenCallbackClass"); verifiedMethodNames.add("setAccessTokenCallbackClass"); + verifiedMethodNames.add("getServerMessageHandler"); + verifiedMethodNames.add("setServerMessageHandler"); + verifiedMethodNames.add("getCalcBigDecimalScale"); + verifiedMethodNames.add("setCalcBigDecimalScale"); verifiedMethodNames.add("getUseFlexibleCallableStatements"); verifiedMethodNames.add("setUseFlexibleCallableStatements"); verifiedMethodNames.add("getCalcBigDecimalPrecision"); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java new file mode 100644 index 000000000..9e6b1d4b7 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/exception/ChainedExceptionTest.java @@ -0,0 +1,71 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.exception; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class ChainedExceptionTest extends AbstractTest { + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + @Test + public void testTwoExceptions() throws Exception { + try (Connection conn = getConnection()) { + + // The below should yield the following Server Messages: + // 1 : Msg 5074, Level 16, State 1: The object 'DF__#chained_exc__c1__AB25243A' is dependent on column 'c1'. + // 1 : Msg 4922, Level 16, State 9: ALTER TABLE ALTER COLUMN c1 failed because one or more objects access this column. + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate("CREATE TABLE #chained_exception_test_x1(c1 INT DEFAULT(0))"); + stmnt.executeUpdate("ALTER TABLE #chained_exception_test_x1 ALTER COLUMN c1 VARCHAR(10)"); + stmnt.executeUpdate("DROP TABLE IF EXISTS #chained_exception_test_x1"); + } + + fail(TestResource.getResource("R_expectedFailPassed")); + + } catch (SQLException ex) { + + // Check the SQLException and the chain + int exCount = 0; + int firstMsgNum = ex.getErrorCode(); + int lastMsgNum = -1; + + while (ex != null) { + exCount++; + + lastMsgNum = ex.getErrorCode(); + + ex = ex.getNextException(); + } + + // Exception Count should be: 2 + assertEquals(2, exCount, "Number of SQLExceptions in the SQLException chain"); + + // Check first Msg: 5074 + assertEquals(5074, firstMsgNum, "First SQL Server Message"); + + // Check last Msg: 4922 + assertEquals(4922, lastMsgNum, "Last SQL Server Message"); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java new file mode 100644 index 000000000..94042a7d8 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/messageHandler/MessageHandlerTest.java @@ -0,0 +1,497 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.messageHandler; + +import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import java.sql.Types; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ISQLServerMessage; +import com.microsoft.sqlserver.jdbc.ISQLServerMessageHandler; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class MessageHandlerTest extends AbstractTest { + + @BeforeAll + public static void setupTests() throws Exception { + setConnection(); + } + + + /** + * Helper method to count number of SQLWarnings in a chain + * @param str - Debug String, so we can evaluate from where we called it... + * @param sqlw - The SQL Warning chain. (can be null) + * @return A count of warnings + */ + private static int getWarningCount(String str, SQLWarning sqlw) + { + int count = 0; + while(sqlw != null) { + count++; + //System.out.println("DEBUG: getWarningCount(): [" + str + "] SQLWarning: Error=" + sqlw.getErrorCode() + ", Severity=" + ((SQLServerWarning)sqlw).getSQLServerError().getErrorSeverity() + ", Text=|" + sqlw.getMessage() + "|."); + sqlw = sqlw.getNextWarning(); + } + return count; + } + + + /** + * Test message handler with normal Statement + *

+ */ + @Test + public void testMsgHandlerWithStatement() throws Exception { + try (SQLServerConnection conn = getConnection()) { + + class TestMsgHandler implements ISQLServerMessageHandler + { + int numOfCalls = 0; + int numOfDowngrades = 0; + int numOfDiscards = 0; + + @Override + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) + { + numOfCalls++; + ISQLServerMessage retObj = srvErrorOrWarning; + + if (srvErrorOrWarning.isErrorMessage()) { + + // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1) + if (2601 == srvErrorOrWarning.getErrorNumber()) { + retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage(); + numOfDowngrades++; + } + + // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission. + if (3701 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + if (srvErrorOrWarning.isInfoMessage()) { + + // Discard: 3621 -- The statement has been terminated. + if (3621 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + return retObj; + } + } + TestMsgHandler testMsgHandler = new TestMsgHandler(); + + // Create a massage handler + conn.setServerMessageHandler(testMsgHandler); + + try (Statement stmnt = conn.createStatement()) { + + stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement."); + + stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement."); + + stmnt.executeUpdate("INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1')"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement."); + + stmnt.executeUpdate("INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it')"); + assertNotNull(conn .getWarnings(), "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection."); + assertNull (stmnt.getWarnings(), "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement."); + conn.clearWarnings(); // Clear Warnings at Connection level + + stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement."); + + stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); // This should be IGNORED by the message handler + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'second drop table, since it should be IGNORED by the message handler', at Statement."); + + // numOfCalls to the message handler should be: 3 + assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler."); + + // numOfDowngrades in the message handler should be: 1 + assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler."); + + // numOfDiscards in the message handler should be: 2 + assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler."); + } + + } catch (SQLException ex) { + fail(TestResource.getResource("R_unexpectedErrorMessage")); + } + } + + + + /** + * Test message handler with PreparedStatement + * + */ + @Test + public void testMsgHandlerWithPreparedStatement() throws Exception { + try (SQLServerConnection conn = getConnection()) { + + class TestMsgHandler implements ISQLServerMessageHandler + { + int numOfCalls = 0; + int numOfDowngrades = 0; + int numOfDiscards = 0; + + @Override + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) + { + numOfCalls++; + ISQLServerMessage retObj = srvErrorOrWarning; + + if (srvErrorOrWarning.isErrorMessage()) { + + // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1) + if (2601 == srvErrorOrWarning.getErrorNumber()) { + retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage(); + numOfDowngrades++; + } + + // Discard: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission. + if (3701 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + if (srvErrorOrWarning.isInfoMessage()) { + + // Discard: 3621 -- The statement has been terminated. + if (3621 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + return retObj; + } + } + TestMsgHandler testMsgHandler = new TestMsgHandler(); + + // Create a massage handler + conn.setServerMessageHandler(testMsgHandler); + + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate("CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255))"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'create table', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create table', at Statement."); + } + + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate("CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id)"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'create index', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'create index', at Statement."); + } + + try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) { + stmnt.setInt(1, 1); + stmnt.setString(2, "row 1"); + stmnt.executeUpdate(); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first insert', at Statement."); + } + + try (PreparedStatement stmnt = conn.prepareStatement("INSERT INTO #msghandler_tmp_table VALUES(?, ?)")) { + stmnt.setInt(1, 1); + stmnt.setString(2, "row 1 - again - msg handler downgrades it"); + stmnt.executeUpdate(); + assertNotNull(conn .getWarnings(), "Expecting at least ONE SQLWarnings from 'second insert', which is a duplicate row, at Connection."); + assertNull (stmnt.getWarnings(), "Expecting NO SQLWarnings from 'second insert', which is a duplicate row, at Statement."); + conn.clearWarnings(); // Clear Warnings at Connection level + } + + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement."); + } + + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate("DROP TABLE #msghandler_tmp_table"); + assertNull(conn .getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Connection."); + assertNull(stmnt.getWarnings(), "Expecting NO SQLWarnings from 'first drop table', at Statement."); + } + + + // numOfCalls to the message handler should be: 3 + assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler."); + + // numOfDowngrades in the message handler should be: 1 + assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler."); + + // numOfDiscards in the message handler should be: 2 + assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler."); + + } catch (SQLException ex) { + fail(TestResource.getResource("R_unexpectedErrorMessage")); + } + } + + + + /** + * Test message handler with CallableStatement + * + */ + @Test + public void testMsgHandlerWithCallableStatement() throws Exception { + try (SQLServerConnection conn = getConnection()) { + + class TestMsgHandler implements ISQLServerMessageHandler + { + int numOfCalls = 0; + int numOfDowngrades = 0; + int numOfDiscards = 0; + + @Override + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) + { + numOfCalls++; + ISQLServerMessage retObj = srvErrorOrWarning; + + if (srvErrorOrWarning.isErrorMessage()) { + + // Downgrade: 2601 -- Cannot insert duplicate key row in object 'dbo.#msghandler_tmp_table' with unique index 'ix_id'. The duplicate key value is (1) + if (2601 == srvErrorOrWarning.getErrorNumber()) { + retObj = srvErrorOrWarning.getSQLServerMessage().toSQLServerInfoMessage(); + numOfDowngrades++; + } + + // Downgrade: 3701 -- Cannot drop the table '#msghandler_tmp_table', because it does not exist or you do not have permission. + if (3701 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + if (srvErrorOrWarning.isInfoMessage()) { + + // Discard: 3621 -- The statement has been terminated. + if (3621 == srvErrorOrWarning.getErrorNumber()) { + retObj = null; + numOfDiscards++; + } + } + + return retObj; + } + } + TestMsgHandler testMsgHandler = new TestMsgHandler(); + + // Create a massage handler + conn.setServerMessageHandler(testMsgHandler); + + // SQL to create procedure + String sqlCreateProc = "" + + "CREATE PROCEDURE #msghandler_tmp_proc( \n" + + " @out_row_count INT OUTPUT \n" + + ") \n" + + "AS \n" + + "BEGIN \n" + + " -- Create a dummy table, with index \n" + + " CREATE TABLE #msghandler_tmp_table(id int, c1 varchar(255)) \n" + + " CREATE UNIQUE INDEX ix_id ON #msghandler_tmp_table(id) \n" + + " \n" + + " -- Insert records 1 \n" + + " INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1') \n" + + " \n" + + " -- Insert records 1 -- Again, which will FAIL, but the message handler will downgrade it into a INFO Message \n" + + " INSERT INTO #msghandler_tmp_table VALUES(1, 'row 1 - again - msg handler downgrades it') \n" + + " \n" + + " -- Count records \n" + + " SELECT @out_row_count = count(*) \n" + + " FROM #msghandler_tmp_table \n" + + " \n" + + " -- Drop the table \n" + + " DROP TABLE #msghandler_tmp_table \n" + + " \n" + + " -- Drop the table agin... The message handler will DISCARD the error\n" + + " DROP TABLE #msghandler_tmp_table \n" + + " \n" + + " RETURN 1 \n" + + "END \n" + ; + + // Create the proc + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate(sqlCreateProc); + assertEquals(0, getWarningCount("Conn" , conn .getWarnings()), "Expecting NO SQLWarnings from 'create proc', at Connection."); + assertEquals(0, getWarningCount("Stmnt", conn .getWarnings()), "Expecting NO SQLWarnings from 'create proc', at Statement."); + } + + // Execute the proc + try (CallableStatement cstmnt = conn.prepareCall("{ ? =call #msghandler_tmp_proc(?) }")) { + cstmnt.registerOutParameter(1, Types.INTEGER); + cstmnt.registerOutParameter(2, Types.INTEGER); + cstmnt.execute(); + int procReturnCode = cstmnt.getInt(1); + int procRowCount = cstmnt.getInt(2); + + assertEquals(1, procReturnCode, "Expecting ReturnCode 1 from the temp procedure."); + assertEquals(1, procRowCount , "Expecting procRowCount 1 from the temp procedure."); + + assertEquals(1, getWarningCount("Conn" , conn .getWarnings()), "Expecting NO SQLWarnings from 'exec proc', at Connection."); + assertEquals(0, getWarningCount("CStmnt", cstmnt.getWarnings()), "Expecting NO SQLWarnings from 'exec proc', at CallableStatement."); + } + + // numOfCalls to the message handler should be: 3 + assertEquals(3, testMsgHandler.numOfCalls, "Number of message calls to the message handler."); + + // numOfDowngrades in the message handler should be: 1 + assertEquals(1, testMsgHandler.numOfDowngrades, "Number of message Downgrades in the message handler."); + + // numOfDiscards in the message handler should be: 2 + assertEquals(2, testMsgHandler.numOfDiscards, "Number of message Discards in the message handler."); + + } catch (SQLException ex) { + fail(TestResource.getResource("R_unexpectedErrorMessage")); + } + } + + + + + /** + * Test message handler with CallableStatement -- and "feedback" messages + *

+ * Do a "long running procedure" and check that the message handler receives the "feedback" messages + */ + @Test + public void testMsgHandlerWithProcedureFeedback() throws Exception { + try (SQLServerConnection conn = getConnection()) { + + class TestMsgHandler implements ISQLServerMessageHandler + { + int numOfCalls = 0; + Map feedbackMsgTs = new LinkedHashMap<>(); + + @Override + public ISQLServerMessage messageHandler(ISQLServerMessage srvErrorOrWarning) + { + numOfCalls++; + + if (50_000 == srvErrorOrWarning.getErrorNumber()) { + //System.out.println("DEBUG: testMsgHandlerWithProcedureFeedback.messageHandler(): FEEDBACK: " + srvErrorOrWarning.getErrorMessage()); + // Remember when the message was received + feedbackMsgTs.put(srvErrorOrWarning.getErrorMessage(), System.currentTimeMillis()); + } + + return srvErrorOrWarning; + } + } + TestMsgHandler testMsgHandler = new TestMsgHandler(); + + // Create a massage handler + conn.setServerMessageHandler(testMsgHandler); + + int doSqlLoopCount = 4; + // SQL to create procedure + String sqlCreateProc = "" + + "CREATE PROCEDURE #msghandler_feeback_proc \n" + + "AS \n" + + "BEGIN \n" + + " DECLARE @loop_cnt INT = " + doSqlLoopCount + " \n" + + " DECLARE @feedback VARCHAR(255) \n" + + " \n" + + " WHILE (@loop_cnt > 0) \n" + + " BEGIN \n" + + " WAITFOR DELAY '00:00:01' \n" + + " \n" + + " SET @feedback = 'In proc, still looping... waiting for loop_count to reach 0. loop_count is now at: ' + convert(varchar(10), @loop_cnt) \n" + + " RAISERROR(@feedback, 0, 1) WITH NOWAIT \n" + + " \n" + + " SET @loop_cnt = @loop_cnt - 1 \n" + + " END \n" + + " \n" + + " RETURN @loop_cnt \n" + + "END \n" + ; + + // Create the proc + try (Statement stmnt = conn.createStatement()) { + stmnt.executeUpdate(sqlCreateProc); + assertEquals(0, getWarningCount("Conn" , conn .getWarnings()), "Expecting NO SQLWarnings from 'create proc', at Connection."); + assertEquals(0, getWarningCount("Stmnt", conn .getWarnings()), "Expecting NO SQLWarnings from 'create proc', at Statement."); + } + + // Execute the proc + try (CallableStatement cstmnt = conn.prepareCall("{ ? =call #msghandler_feeback_proc }")) { + cstmnt.registerOutParameter(1, Types.INTEGER); + cstmnt.execute(); + int procReturnCode = cstmnt.getInt(1); + + assertEquals(0, procReturnCode, "Unexpected ReturnCode from the temp procedure."); + + assertEquals(0, getWarningCount("conn" , conn .getWarnings()), "Unexpected Number Of SQLWarnings from 'exec proc', at Connection."); + assertEquals(doSqlLoopCount, getWarningCount("cstmnt", cstmnt.getWarnings()), "Unexpected Number Of SQLWarnings from 'exec proc', at CallableStatement."); + } + + // numOfCalls to the message handler should be: # + assertEquals(doSqlLoopCount, testMsgHandler.numOfCalls, "Number of message calls to the message handler."); + + // Loop all received messages and check that they are within a second (+-200ms) + long prevTime = 0; + for (Entry entry : testMsgHandler.feedbackMsgTs.entrySet()) { + if (prevTime == 0) { + prevTime = entry.getValue(); + continue; + } + + long msDiff = entry.getValue() - prevTime; + if (msDiff < 800 || msDiff > 1200) { + fail("Received Messages is to far apart. They should be approx 1000 ms. msDiff=" + msDiff + " Message=|" + entry.getKey() + "|."); + } + + prevTime = entry.getValue(); + } + + } catch (SQLException ex) { + fail(TestResource.getResource("R_unexpectedErrorMessage")); + } + } + +}