| // SPDX-License-Identifier: LGPL-2.1-or-later |
| // Copyright (c) 2012-2014 Monty Program Ab |
| // Copyright (c) 2015-2021 MariaDB Corporation Ab |
| |
| package org.mariadb.jdbc.export; |
| |
| import java.sql.*; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| import org.mariadb.jdbc.Configuration; |
| import org.mariadb.jdbc.Connection; |
| import org.mariadb.jdbc.HostAddress; |
| import org.mariadb.jdbc.MariaDbPoolConnection; |
| import org.mariadb.jdbc.client.Completion; |
| import org.mariadb.jdbc.message.server.OkPacket; |
| |
| /** |
| * Exception factory. This permit common error logging, with thread id, dump query, and specific |
| * dead-lock additional information |
| */ |
| public class ExceptionFactory { |
| |
| private static final Set<Integer> LOCK_DEADLOCK_ERROR_CODES = |
| new HashSet<>(Arrays.asList(1205, 1213, 1614)); |
| private final Configuration conf; |
| private final HostAddress hostAddress; |
| private Connection connection; |
| private MariaDbPoolConnection poolConnection; |
| private long threadId; |
| private Statement statement; |
| |
| /** |
| * Connection Exception factory constructor |
| * |
| * @param conf configuration |
| * @param hostAddress current host |
| */ |
| public ExceptionFactory(Configuration conf, HostAddress hostAddress) { |
| this.conf = conf; |
| this.hostAddress = hostAddress; |
| } |
| |
| private ExceptionFactory( |
| Connection connection, |
| MariaDbPoolConnection poolConnection, |
| Configuration conf, |
| HostAddress hostAddress, |
| long threadId, |
| Statement statement) { |
| this.connection = connection; |
| this.poolConnection = poolConnection; |
| this.conf = conf; |
| this.hostAddress = hostAddress; |
| this.threadId = threadId; |
| this.statement = statement; |
| } |
| |
| private static String buildMsgText( |
| String initialMessage, |
| long threadId, |
| Configuration conf, |
| String sql, |
| int errorCode, |
| Connection connection) { |
| |
| StringBuilder msg = new StringBuilder(); |
| |
| if (threadId != 0L) { |
| msg.append("(conn=").append(threadId).append(") "); |
| } |
| msg.append(initialMessage); |
| |
| if (conf.dumpQueriesOnException() && sql != null) { |
| if (conf.maxQuerySizeToLog() != 0 && sql.length() > conf.maxQuerySizeToLog() - 3) { |
| msg.append("\nQuery is: ").append(sql, 0, conf.maxQuerySizeToLog() - 3).append("..."); |
| } else { |
| msg.append("\nQuery is: ").append(sql); |
| } |
| } |
| |
| if (conf.includeInnodbStatusInDeadlockExceptions() |
| && LOCK_DEADLOCK_ERROR_CODES.contains(errorCode) |
| && connection != null) { |
| Statement stmt = connection.createStatement(); |
| try { |
| ResultSet rs = stmt.executeQuery("SHOW ENGINE INNODB STATUS"); |
| rs.next(); |
| msg.append("\ndeadlock information: ").append(rs.getString(3)); |
| } catch (SQLException sqle) { |
| // eat |
| } |
| } |
| |
| if (conf.includeThreadDumpInDeadlockExceptions() |
| && LOCK_DEADLOCK_ERROR_CODES.contains(errorCode)) { |
| msg.append("\nthread name: ").append(Thread.currentThread().getName()); |
| msg.append("\ncurrent threads: "); |
| Thread.getAllStackTraces() |
| .forEach( |
| (thread, traces) -> { |
| msg.append("\n name:\"") |
| .append(thread.getName()) |
| .append("\" pid:") |
| .append(thread.getId()) |
| .append(" status:") |
| .append(thread.getState()); |
| for (StackTraceElement trace : traces) { |
| msg.append("\n ").append(trace); |
| } |
| }); |
| } |
| |
| return msg.toString(); |
| } |
| |
| /** |
| * Set connection |
| * |
| * @param oldExceptionFactory previous connection exception factory |
| */ |
| public void setConnection(ExceptionFactory oldExceptionFactory) { |
| this.connection = oldExceptionFactory.connection; |
| } |
| |
| /** |
| * Set connection to factory |
| * |
| * @param connection connection |
| * @return this {@link ExceptionFactory} |
| */ |
| public ExceptionFactory setConnection(Connection connection) { |
| this.connection = connection; |
| return this; |
| } |
| |
| /** |
| * Set pool connection to factory |
| * |
| * @param internalPoolConnection internal pool connection |
| * @return this {@link ExceptionFactory} |
| */ |
| public ExceptionFactory setPoolConnection(MariaDbPoolConnection internalPoolConnection) { |
| this.poolConnection = internalPoolConnection; |
| return this; |
| } |
| |
| /** |
| * Set connection thread id |
| * |
| * @param threadId connection thread id |
| */ |
| public void setThreadId(long threadId) { |
| this.threadId = threadId; |
| } |
| |
| /** |
| * Create a BatchUpdateException, filling successful updates |
| * |
| * @param res completion list |
| * @param length expected size |
| * @param sqle exception |
| * @return BatchUpdateException object |
| */ |
| public BatchUpdateException createBatchUpdate( |
| List<Completion> res, int length, SQLException sqle) { |
| int[] updateCounts = new int[length]; |
| for (int i = 0; i < length; i++) { |
| if (i < res.size()) { |
| if (res.get(i) instanceof OkPacket) { |
| updateCounts[i] = (int) ((OkPacket) res.get(i)).getAffectedRows(); |
| } else { |
| updateCounts[i] = org.mariadb.jdbc.Statement.SUCCESS_NO_INFO; |
| } |
| } else { |
| updateCounts[i] = org.mariadb.jdbc.Statement.EXECUTE_FAILED; |
| } |
| } |
| return new BatchUpdateException( |
| sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle); |
| } |
| |
| /** |
| * Create a BatchUpdateException, filling successful updates |
| * |
| * @param res completion list |
| * @param length expected length |
| * @param responseMsg successful response |
| * @param sqle exception |
| * @return BatchUpdateException object |
| */ |
| public BatchUpdateException createBatchUpdate( |
| List<Completion> res, int length, int[] responseMsg, SQLException sqle) { |
| int[] updateCounts = new int[length]; |
| |
| int responseIncrement = 0; |
| for (int i = 0; i < length; i++) { |
| if (i >= responseMsg.length) { |
| Arrays.fill(updateCounts, i, length, Statement.EXECUTE_FAILED); |
| break; |
| } |
| int MsgResponseNo = responseMsg[i]; |
| if (MsgResponseNo < 1) { |
| updateCounts[responseIncrement++] = Statement.EXECUTE_FAILED; |
| return new BatchUpdateException(updateCounts, sqle); |
| } else if (MsgResponseNo == 1 && res.size() > i && res.get(i) instanceof OkPacket) { |
| updateCounts[i] = (int) ((OkPacket) res.get(i)).getAffectedRows(); |
| } else { |
| // unknown. |
| updateCounts[i] = Statement.SUCCESS_NO_INFO; |
| } |
| } |
| return new BatchUpdateException( |
| sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), updateCounts, sqle); |
| } |
| |
| /** |
| * Construct an Exception factory from this + adding current statement |
| * |
| * @param statement current statement |
| * @return new Exception factory |
| */ |
| public ExceptionFactory of(Statement statement) { |
| return new ExceptionFactory( |
| this.connection, |
| this.poolConnection, |
| this.conf, |
| this.hostAddress, |
| this.threadId, |
| statement); |
| } |
| |
| /** |
| * Construct an Exception factory from this + adding current SQL |
| * |
| * @param sql current sql command |
| * @return new Exception factory |
| */ |
| public ExceptionFactory withSql(String sql) { |
| return new SqlExceptionFactory( |
| this.connection, |
| this.poolConnection, |
| this.conf, |
| this.hostAddress, |
| this.threadId, |
| statement, |
| sql); |
| } |
| |
| private SQLException createException( |
| String initialMessage, String sqlState, int errorCode, Exception cause) { |
| |
| String msg = buildMsgText(initialMessage, threadId, conf, getSql(), errorCode, connection); |
| |
| if ("70100".equals(sqlState)) { // ER_QUERY_INTERRUPTED |
| return new SQLTimeoutException(msg, sqlState, errorCode); |
| } |
| // 4166 : mariadb load data infile disable |
| // 1148 : 10.2 mariadb load data infile disable |
| // 3948 : mysql load data infile disable |
| if ((errorCode == 4166 || errorCode == 3948 || errorCode == 1148) && !conf.allowLocalInfile()) { |
| return new SQLException( |
| "Local infile is disabled by connector. Enable `allowLocalInfile` to allow local infile" |
| + " commands", |
| sqlState, |
| errorCode, |
| cause); |
| } |
| |
| SQLException returnEx; |
| String sqlClass = sqlState == null ? "42" : sqlState.substring(0, 2); |
| switch (sqlClass) { |
| case "0A": |
| returnEx = new SQLFeatureNotSupportedException(msg, sqlState, errorCode, cause); |
| break; |
| case "22": |
| case "26": |
| case "2F": |
| case "20": |
| case "42": |
| case "XA": |
| returnEx = new SQLSyntaxErrorException(msg, sqlState, errorCode, cause); |
| break; |
| case "25": |
| case "28": |
| returnEx = new SQLInvalidAuthorizationSpecException(msg, sqlState, errorCode, cause); |
| break; |
| case "21": |
| case "23": |
| returnEx = new SQLIntegrityConstraintViolationException(msg, sqlState, errorCode, cause); |
| break; |
| case "08": |
| returnEx = new SQLNonTransientConnectionException(msg, sqlState, errorCode, cause); |
| break; |
| case "40": |
| returnEx = new SQLTransactionRollbackException(msg, sqlState, errorCode, cause); |
| break; |
| case "HY": |
| returnEx = new SQLException(msg, sqlState, errorCode, cause); |
| break; |
| default: |
| returnEx = new SQLTransientConnectionException(msg, sqlState, errorCode, cause); |
| break; |
| } |
| |
| if (poolConnection != null) { |
| if (statement != null && statement instanceof PreparedStatement) { |
| poolConnection.fireStatementErrorOccurred((PreparedStatement) statement, returnEx); |
| } |
| if (returnEx instanceof SQLNonTransientConnectionException |
| || returnEx instanceof SQLTransientConnectionException) { |
| poolConnection.fireConnectionErrorOccurred(returnEx); |
| } |
| } |
| |
| return returnEx; |
| } |
| |
| /** |
| * fast creation of SQLFeatureNotSupportedException exception |
| * |
| * @param message error message |
| * @return exception to be thrown |
| */ |
| public SQLException notSupported(String message) { |
| return createException(message, "0A000", -1, null); |
| } |
| |
| /** |
| * Creation of an exception |
| * |
| * @param message error message |
| * @return exception to be thrown |
| */ |
| public SQLException create(String message) { |
| return createException(message, "42000", -1, null); |
| } |
| |
| /** |
| * Creation of an exception |
| * |
| * @param message error message |
| * @param sqlState sql state |
| * @return exception to be thrown |
| */ |
| public SQLException create(String message, String sqlState) { |
| return createException(message, sqlState, -1, null); |
| } |
| |
| /** |
| * Creation of an exception |
| * |
| * @param message error message |
| * @param sqlState sql state |
| * @param cause initial exception |
| * @return exception to be thrown |
| */ |
| public SQLException create(String message, String sqlState, Exception cause) { |
| return createException(message, sqlState, -1, cause); |
| } |
| /** |
| * Creation of an exception |
| * |
| * @param message error message |
| * @param sqlState sql state |
| * @param errorCode error code |
| * @return exception to be thrown |
| */ |
| public SQLException create(String message, String sqlState, int errorCode) { |
| return createException(message, sqlState, errorCode, null); |
| } |
| |
| /** |
| * get SQL command |
| * |
| * @return sql command |
| */ |
| public String getSql() { |
| return null; |
| } |
| |
| /** Exception with SQL command */ |
| public class SqlExceptionFactory extends ExceptionFactory { |
| private final String sql; |
| |
| /** |
| * Constructor of Exception factory with SQL |
| * |
| * @param connection connection |
| * @param poolConnection pool connection |
| * @param conf configuration |
| * @param hostAddress host |
| * @param threadId connection thread id |
| * @param statement statement |
| * @param sql sql |
| */ |
| public SqlExceptionFactory( |
| Connection connection, |
| MariaDbPoolConnection poolConnection, |
| Configuration conf, |
| HostAddress hostAddress, |
| long threadId, |
| Statement statement, |
| String sql) { |
| super(connection, poolConnection, conf, hostAddress, threadId, statement); |
| this.sql = sql; |
| } |
| |
| @Override |
| public String getSql() { |
| return sql; |
| } |
| } |
| } |