/*
 * Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0,
 * and the EPL 1.0 (https://h2database.com/html/license.html).
 * Initial Developer: H2 Group
 */
package org.h2.message;

import static org.h2.api.ErrorCode.*;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Properties;

import org.h2.engine.Constants;
import org.h2.jdbc.JdbcException;
import org.h2.jdbc.JdbcSQLDataException;
import org.h2.jdbc.JdbcSQLException;
import org.h2.jdbc.JdbcSQLFeatureNotSupportedException;
import org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException;
import org.h2.jdbc.JdbcSQLInvalidAuthorizationSpecException;
import org.h2.jdbc.JdbcSQLNonTransientConnectionException;
import org.h2.jdbc.JdbcSQLNonTransientException;
import org.h2.jdbc.JdbcSQLSyntaxErrorException;
import org.h2.jdbc.JdbcSQLTimeoutException;
import org.h2.jdbc.JdbcSQLTransactionRollbackException;
import org.h2.jdbc.JdbcSQLTransientException;
import org.h2.util.HasSQL;
import org.h2.util.SortedProperties;
import org.h2.util.StringUtils;
import org.h2.util.Utils;
import org.h2.value.TypeInfo;
import org.h2.value.Typed;
import org.h2.value.Value;

/**
 * This exception wraps a checked exception.
 * It is used in methods where checked exceptions are not supported,
 * for example in a Comparator.
 */
public class DbException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    /**
     * If the SQL statement contains this text, then it is never added to the
     * SQL exception. Hiding the SQL statement may be important if it contains a
     * passwords, such as a CREATE LINKED TABLE statement.
     */
    public static final String HIDE_SQL = "--hide--";

    private static final Properties MESSAGES = new Properties();

    /**
     * Thrown when OOME exception happens on handle error
     * inside {@link #convert(java.lang.Throwable)}.
     */
    public static final SQLException SQL_OOME =
            new SQLException("OutOfMemoryError", "HY000", OUT_OF_MEMORY, new OutOfMemoryError());
    private static final DbException OOME = new DbException(SQL_OOME);

    private Object source;

    static {
        try {
            byte[] messages = Utils.getResource("/org/h2/res/_messages_en.prop");
            if (messages != null) {
                MESSAGES.load(new ByteArrayInputStream(messages));
            }
            String language = Locale.getDefault().getLanguage();
            if (!"en".equals(language)) {
                byte[] translations = Utils.getResource(
                        "/org/h2/res/_messages_" + language + ".prop");
                // message: translated message + english
                // (otherwise certain applications don't work)
                if (translations != null) {
                    Properties p = SortedProperties.fromLines(
                            new String(translations, StandardCharsets.UTF_8));
                    for (Entry<Object, Object> e : p.entrySet()) {
                        String key = (String) e.getKey();
                        String translation = (String) e.getValue();
                        if (translation != null && !translation.startsWith("#")) {
                            String original = MESSAGES.getProperty(key);
                            String message = translation + "\n" + original;
                            MESSAGES.put(key, message);
                        }
                    }
                }
            }
        } catch (OutOfMemoryError | IOException e) {
            DbException.traceThrowable(e);
        }
    }

    private DbException(SQLException e) {
        super(e.getMessage(), e);
    }

    private static String translate(String key, String... params) {
        String message = MESSAGES.getProperty(key);
        if (message == null) {
            message = "(Message " + key + " not found)";
        }
        if (params != null) {
            for (int i = 0; i < params.length; i++) {
                String s = params[i];
                if (s != null && s.length() > 0) {
                    params[i] = quote(s);
                }
            }
            message = MessageFormat.format(message, (Object[]) params);
        }
        return message;
    }

    private static String quote(String s) {
        int l = s.length();
        StringBuilder builder = new StringBuilder(l + 2).append('"');
        for (int i = 0; i < l;) {
            int cp = s.codePointAt(i);
            i += Character.charCount(cp);
            int t = Character.getType(cp);
            if (t == 0 || t >= Character.SPACE_SEPARATOR && t <= Character.SURROGATE && cp != ' ') {
                if (cp <= 0xffff) {
                    StringUtils.appendHex(builder.append('\\'), cp, 2);
                } else {
                    StringUtils.appendHex(builder.append("\\+"), cp, 3);
                }
            } else {
                if (cp == '"' || cp == '\\') {
                    builder.append((char) cp);
                }
                builder.appendCodePoint(cp);
            }
        }
        return builder.append('"').toString();
    }

    /**
     * Get the SQLException object.
     *
     * @return the exception
     */
    public SQLException getSQLException() {
        return (SQLException) getCause();
    }

    /**
     * Get the error code.
     *
     * @return the error code
     */
    public int getErrorCode() {
        return getSQLException().getErrorCode();
    }

    /**
     * Set the SQL statement of the given exception.
     * This method may create a new object.
     *
     * @param sql the SQL statement
     * @return the exception
     */
    public DbException addSQL(String sql) {
        SQLException e = getSQLException();
        if (e instanceof JdbcException) {
            JdbcException j = (JdbcException) e;
            if (j.getSQL() == null) {
                j.setSQL(filterSQL(sql));
            }
            return this;
        }
        e = getJdbcSQLException(e.getMessage(), sql, e.getSQLState(), e.getErrorCode(), e, null);
        return new DbException(e);
    }

    /**
     * Create a database exception for a specific error code.
     *
     * @param errorCode the error code
     * @return the exception
     */
    public static DbException get(int errorCode) {
        return get(errorCode, (String) null);
    }

    /**
     * Create a database exception for a specific error code.
     *
     * @param errorCode the error code
     * @param p1 the first parameter of the message
     * @return the exception
     */
    public static DbException get(int errorCode, String p1) {
        return get(errorCode, new String[] { p1 });
    }

    /**
     * Create a database exception for a specific error code.
     *
     * @param errorCode the error code
     * @param cause the cause of the exception
     * @param params the list of parameters of the message
     * @return the exception
     */
    public static DbException get(int errorCode, Throwable cause,
            String... params) {
        return new DbException(getJdbcSQLException(errorCode, cause, params));
    }

    /**
     * Create a database exception for a specific error code.
     *
     * @param errorCode the error code
     * @param params the list of parameters of the message
     * @return the exception
     */
    public static DbException get(int errorCode, String... params) {
        return new DbException(getJdbcSQLException(errorCode, null, params));
    }

    /**
     * Create a database exception for an arbitrary SQLState.
     *
     * @param sqlstate the state to use
     * @param message the message to use
     * @return the exception
     */
    public static DbException fromUser(String sqlstate, String message) {
        // do not translate as sqlstate is arbitrary : avoid "message not found"
        return new DbException(getJdbcSQLException(message, null, sqlstate, 0, null, null));
    }

    /**
     * Create a syntax error exception.
     *
     * @param sql the SQL statement
     * @param index the position of the error in the SQL statement
     * @return the exception
     */
    public static DbException getSyntaxError(String sql, int index) {
        sql = StringUtils.addAsterisk(sql, index);
        return get(SYNTAX_ERROR_1, sql);
    }

    /**
     * Create a syntax error exception.
     *
     * @param sql the SQL statement
     * @param index the position of the error in the SQL statement
     * @param message the message
     * @return the exception
     */
    public static DbException getSyntaxError(String sql, int index,
            String message) {
        sql = StringUtils.addAsterisk(sql, index);
        return new DbException(getJdbcSQLException(SYNTAX_ERROR_2, null, sql, message));
    }

    /**
     * Create a syntax error exception for a specific error code.
     *
     * @param errorCode the error code
     * @param sql the SQL statement
     * @param index the position of the error in the SQL statement
     * @param params the list of parameters of the message
     * @return the exception
     */
    public static DbException getSyntaxError(int errorCode, String sql, int index, String... params) {
        sql = StringUtils.addAsterisk(sql, index);
        String sqlstate = getState(errorCode);
        String message = translate(sqlstate, params);
        return new DbException(getJdbcSQLException(message, sql, sqlstate, errorCode, null, null));
    }

    /**
     * Gets a SQL exception meaning this feature is not supported.
     *
     * @param message what exactly is not supported
     * @return the exception
     */
    public static DbException getUnsupportedException(String message) {
        return get(FEATURE_NOT_SUPPORTED_1, message);
    }

    /**
     * Gets a SQL exception meaning this value is invalid.
     *
     * @param param the name of the parameter
     * @param value the value passed
     * @return the exception
     */
    public static DbException getInvalidValueException(String param, Object value) {
        return get(INVALID_VALUE_2, value == null ? "null" : value.toString(), param);
    }

    /**
     * Gets a SQL exception meaning the type of expression is invalid or unknown.
     *
     * @param param the name of the parameter
     * @param e the expression
     * @return the exception
     */
    public static DbException getInvalidExpressionTypeException(String param, Typed e) {
        TypeInfo type = e.getType();
        if (type.getValueType() == Value.UNKNOWN) {
            return get(UNKNOWN_DATA_TYPE_1, (e instanceof HasSQL ? (HasSQL) e : type).getTraceSQL());
        }
        return get(INVALID_VALUE_2, type.getTraceSQL(), param);
    }

    /**
     * Gets a SQL exception meaning this value is too long.
     *
     * @param columnOrType
     *            column with data type or data type name
     * @param value
     *            string representation of value, will be truncated to 80
     *            characters
     * @param valueLength
     *            the actual length of value, {@code -1L} if unknown
     * @return the exception
     */
    public static DbException getValueTooLongException(String columnOrType, String value, long valueLength) {
        int length = value.length();
        int m = valueLength >= 0 ? 22 : 0;
        StringBuilder builder = length > 80 //
                ? new StringBuilder(83 + m).append(value, 0, 80).append("...")
                : new StringBuilder(length + m).append(value);
        if (valueLength >= 0) {
            builder.append(" (").append(valueLength).append(')');
        }
        return get(VALUE_TOO_LONG_2, columnOrType, builder.toString());
    }

    /**
     * Gets a file version exception.
     *
     * @param dataFileName the name of the database
     * @return the exception
     */
    public static DbException getFileVersionError(String dataFileName) {
        return DbException.get(FILE_VERSION_ERROR_1, "Old database: " + dataFileName
                + " - please convert the database to a SQL script and re-create it.");
    }

    /**
     * Gets an internal error.
     *
     * @param s the message
     * @return the RuntimeException object
     */
    public static RuntimeException getInternalError(String s) {
        RuntimeException e = new RuntimeException(s);
        DbException.traceThrowable(e);
        return e;
    }

    /**
     * Gets an internal error.
     *
     * @return the RuntimeException object
     */
    public static RuntimeException getInternalError() {
        return getInternalError("Unexpected code path");
    }

    /**
     * Convert an exception to a SQL exception using the default mapping.
     *
     * @param e the root cause
     * @return the SQL exception object
     */
    public static SQLException toSQLException(Throwable e) {
        if (e instanceof SQLException) {
            return (SQLException) e;
        }
        return convert(e).getSQLException();
    }

    /**
     * Convert a throwable to an SQL exception using the default mapping. All
     * errors except the following are re-thrown: StackOverflowError,
     * LinkageError.
     *
     * @param e the root cause
     * @return the exception object
     */
    public static DbException convert(Throwable e) {
        try {
            if (e instanceof DbException) {
                return (DbException) e;
            } else if (e instanceof SQLException) {
                return new DbException((SQLException) e);
            } else if (e instanceof InvocationTargetException) {
                return convertInvocation((InvocationTargetException) e, null);
            } else if (e instanceof IOException) {
                return get(IO_EXCEPTION_1, e, e.toString());
            } else if (e instanceof OutOfMemoryError) {
                return get(OUT_OF_MEMORY, e);
            } else if (e instanceof StackOverflowError || e instanceof LinkageError) {
                return get(GENERAL_ERROR_1, e, e.toString());
            } else if (e instanceof Error) {
                throw (Error) e;
            }
            return get(GENERAL_ERROR_1, e, e.toString());
        } catch (OutOfMemoryError ignore) {
            return OOME;
        } catch (Throwable ex) {
            try {
                DbException dbException = new DbException(
                        new SQLException("GeneralError", "HY000", GENERAL_ERROR_1, e));
                dbException.addSuppressed(ex);
                return dbException;
            } catch (OutOfMemoryError ignore) {
                return OOME;
            }
        }
    }

    /**
     * Convert an InvocationTarget exception to a database exception.
     *
     * @param te the root cause
     * @param message the added message or null
     * @return the database exception object
     */
    public static DbException convertInvocation(InvocationTargetException te,
            String message) {
        Throwable t = te.getTargetException();
        if (t instanceof SQLException || t instanceof DbException) {
            return convert(t);
        }
        message = message == null ? t.getMessage() : message + ": " + t.getMessage();
        return get(EXCEPTION_IN_FUNCTION_1, t, message);
    }

    /**
     * Convert an IO exception to a database exception.
     *
     * @param e the root cause
     * @param message the message or null
     * @return the database exception object
     */
    public static DbException convertIOException(IOException e, String message) {
        if (message == null) {
            Throwable t = e.getCause();
            if (t instanceof DbException) {
                return (DbException) t;
            }
            return get(IO_EXCEPTION_1, e, e.toString());
        }
        return get(IO_EXCEPTION_2, e, e.toString(), message);
    }

    /**
     * Gets the SQL exception object for a specific error code.
     *
     * @param errorCode the error code
     * @return the SQLException object
     */
    public static SQLException getJdbcSQLException(int errorCode) {
        return getJdbcSQLException(errorCode, (Throwable)null);
    }

    /**
     * Gets the SQL exception object for a specific error code.
     *
     * @param errorCode the error code
     * @param p1 the first parameter of the message
     * @return the SQLException object
     */
    public static SQLException getJdbcSQLException(int errorCode, String p1) {
        return getJdbcSQLException(errorCode, null, p1);
    }

    /**
     * Gets the SQL exception object for a specific error code.
     *
     * @param errorCode the error code
     * @param cause the cause of the exception
     * @param params the list of parameters of the message
     * @return the SQLException object
     */
    public static SQLException getJdbcSQLException(int errorCode, Throwable cause, String... params) {
        String sqlstate = getState(errorCode);
        String message = translate(sqlstate, params);
        return getJdbcSQLException(message, null, sqlstate, errorCode, cause, null);
    }

    /**
     * Creates a SQLException.
     *
     * @param message the reason
     * @param sql the SQL statement
     * @param state the SQL state
     * @param errorCode the error code
     * @param cause the exception that was the reason for this exception
     * @param stackTrace the stack trace
     * @return the SQLException object
     */
    public static SQLException getJdbcSQLException(String message, String sql, String state, int errorCode,
            Throwable cause, String stackTrace) {
        sql = filterSQL(sql);
        // Use SQLState class value to detect type
        switch (errorCode / 1_000) {
        case 2:
            return new JdbcSQLNonTransientException(message, sql, state, errorCode, cause, stackTrace);
        case 7:
        case 21:
        case 42:
        case 54:
            return new JdbcSQLSyntaxErrorException(message, sql, state, errorCode, cause, stackTrace);
        case 8:
            return new JdbcSQLNonTransientConnectionException(message, sql, state, errorCode, cause, stackTrace);
        case 22:
            return new JdbcSQLDataException(message, sql, state, errorCode, cause, stackTrace);
        case 23:
            return new JdbcSQLIntegrityConstraintViolationException(message, sql, state, errorCode, cause, stackTrace);
        case 28:
            return new JdbcSQLInvalidAuthorizationSpecException(message, sql, state, errorCode, cause, stackTrace);
        case 40:
            return new JdbcSQLTransactionRollbackException(message, sql, state, errorCode, cause, stackTrace);
        }
        // Check error code
        switch (errorCode){
        case GENERAL_ERROR_1:
        case UNKNOWN_DATA_TYPE_1:
        case METHOD_NOT_ALLOWED_FOR_QUERY:
        case METHOD_ONLY_ALLOWED_FOR_QUERY:
        case SEQUENCE_EXHAUSTED:
        case OBJECT_CLOSED:
        case CANNOT_DROP_CURRENT_USER:
        case UNSUPPORTED_SETTING_COMBINATION:
        case FILE_RENAME_FAILED_2:
        case FILE_DELETE_FAILED_1:
        case IO_EXCEPTION_1:
        case NOT_ON_UPDATABLE_ROW:
        case IO_EXCEPTION_2:
        case TRACE_FILE_ERROR_2:
        case ADMIN_RIGHTS_REQUIRED:
        case ERROR_EXECUTING_TRIGGER_3:
        case COMMIT_ROLLBACK_NOT_ALLOWED:
        case FILE_CREATION_FAILED_1:
        case SAVEPOINT_IS_INVALID_1:
        case SAVEPOINT_IS_UNNAMED:
        case SAVEPOINT_IS_NAMED:
        case NOT_ENOUGH_RIGHTS_FOR_1:
        case DATABASE_IS_READ_ONLY:
        case WRONG_XID_FORMAT_1:
        case UNSUPPORTED_COMPRESSION_OPTIONS_1:
        case UNSUPPORTED_COMPRESSION_ALGORITHM_1:
        case COMPRESSION_ERROR:
        case EXCEPTION_IN_FUNCTION_1:
        case ERROR_ACCESSING_LINKED_TABLE_2:
        case FILE_NOT_FOUND_1:
        case INVALID_CLASS_2:
        case DATABASE_IS_NOT_PERSISTENT:
        case RESULT_SET_NOT_UPDATABLE:
        case RESULT_SET_NOT_SCROLLABLE:
        case METHOD_NOT_ALLOWED_FOR_PREPARED_STATEMENT:
        case ACCESS_DENIED_TO_CLASS_1:
        case RESULT_SET_READONLY:
        case CURRENT_SEQUENCE_VALUE_IS_NOT_DEFINED_IN_SESSION_1:
            return new JdbcSQLNonTransientException(message, sql, state, errorCode, cause, stackTrace);
        case FEATURE_NOT_SUPPORTED_1:
            return new JdbcSQLFeatureNotSupportedException(message, sql, state, errorCode, cause, stackTrace);
        case LOCK_TIMEOUT_1:
        case STATEMENT_WAS_CANCELED:
        case LOB_CLOSED_ON_TIMEOUT_1:
            return new JdbcSQLTimeoutException(message, sql, state, errorCode, cause, stackTrace);
        case FUNCTION_MUST_RETURN_RESULT_SET_1:
        case INVALID_TRIGGER_FLAGS_1:
        case SUM_OR_AVG_ON_WRONG_DATATYPE_1:
        case MUST_GROUP_BY_COLUMN_1:
        case SECOND_PRIMARY_KEY:
        case FUNCTION_NOT_FOUND_1:
        case COLUMN_MUST_NOT_BE_NULLABLE_1:
        case USER_NOT_FOUND_1:
        case USER_ALREADY_EXISTS_1:
        case SEQUENCE_ALREADY_EXISTS_1:
        case SEQUENCE_NOT_FOUND_1:
        case VIEW_NOT_FOUND_1:
        case VIEW_ALREADY_EXISTS_1:
        case TRIGGER_ALREADY_EXISTS_1:
        case TRIGGER_NOT_FOUND_1:
        case ERROR_CREATING_TRIGGER_OBJECT_3:
        case CONSTRAINT_ALREADY_EXISTS_1:
        case SUBQUERY_IS_NOT_SINGLE_COLUMN:
        case INVALID_USE_OF_AGGREGATE_FUNCTION_1:
        case CONSTRAINT_NOT_FOUND_1:
        case AMBIGUOUS_COLUMN_NAME_1:
        case ORDER_BY_NOT_IN_RESULT:
        case ROLE_ALREADY_EXISTS_1:
        case ROLE_NOT_FOUND_1:
        case USER_OR_ROLE_NOT_FOUND_1:
        case ROLES_AND_RIGHT_CANNOT_BE_MIXED:
        case METHODS_MUST_HAVE_DIFFERENT_PARAMETER_COUNTS_2:
        case ROLE_ALREADY_GRANTED_1:
        case COLUMN_IS_PART_OF_INDEX_1:
        case FUNCTION_ALIAS_ALREADY_EXISTS_1:
        case FUNCTION_ALIAS_NOT_FOUND_1:
        case SCHEMA_ALREADY_EXISTS_1:
        case SCHEMA_NOT_FOUND_1:
        case SCHEMA_NAME_MUST_MATCH:
        case COLUMN_CONTAINS_NULL_VALUES_1:
        case SEQUENCE_BELONGS_TO_A_TABLE_1:
        case COLUMN_IS_REFERENCED_1:
        case CANNOT_DROP_LAST_COLUMN:
        case INDEX_BELONGS_TO_CONSTRAINT_2:
        case CLASS_NOT_FOUND_1:
        case METHOD_NOT_FOUND_1:
        case COLLATION_CHANGE_WITH_DATA_TABLE_1:
        case SCHEMA_CAN_NOT_BE_DROPPED_1:
        case ROLE_CAN_NOT_BE_DROPPED_1:
        case CANNOT_TRUNCATE_1:
        case CANNOT_DROP_2:
        case VIEW_IS_INVALID_2:
        case TYPES_ARE_NOT_COMPARABLE_2:
        case CONSTANT_ALREADY_EXISTS_1:
        case CONSTANT_NOT_FOUND_1:
        case LITERALS_ARE_NOT_ALLOWED:
        case CANNOT_DROP_TABLE_1:
        case DOMAIN_ALREADY_EXISTS_1:
        case DOMAIN_NOT_FOUND_1:
        case WITH_TIES_WITHOUT_ORDER_BY:
        case CANNOT_MIX_INDEXED_AND_UNINDEXED_PARAMS:
        case TRANSACTION_NOT_FOUND_1:
        case AGGREGATE_NOT_FOUND_1:
        case WINDOW_NOT_FOUND_1:
        case CAN_ONLY_ASSIGN_TO_VARIABLE_1:
        case PUBLIC_STATIC_JAVA_METHOD_NOT_FOUND_1:
        case JAVA_OBJECT_SERIALIZER_CHANGE_WITH_DATA_TABLE:
        case FOR_UPDATE_IS_NOT_ALLOWED_IN_DISTINCT_OR_GROUPED_SELECT:
        case INVALID_VALUE_PRECISION:
        case INVALID_VALUE_SCALE:
        case CONSTRAINT_IS_USED_BY_CONSTRAINT_2:
        case UNCOMPARABLE_REFERENCED_COLUMN_2:
        case GENERATED_COLUMN_CANNOT_BE_ASSIGNED_1:
        case GENERATED_COLUMN_CANNOT_BE_UPDATABLE_BY_CONSTRAINT_2:
        case COLUMN_ALIAS_IS_NOT_SPECIFIED_1:
        case GROUP_BY_NOT_IN_THE_RESULT:
            return new JdbcSQLSyntaxErrorException(message, sql, state, errorCode, cause, stackTrace);
        case HEX_STRING_ODD_1:
        case HEX_STRING_WRONG_1:
        case INVALID_VALUE_2:
        case SEQUENCE_ATTRIBUTES_INVALID_7:
        case INVALID_TO_CHAR_FORMAT:
        case PARAMETER_NOT_SET_1:
        case PARSE_ERROR_1:
        case INVALID_TO_DATE_FORMAT:
        case STRING_FORMAT_ERROR_1:
        case SERIALIZATION_FAILED_1:
        case DESERIALIZATION_FAILED_1:
        case SCALAR_SUBQUERY_CONTAINS_MORE_THAN_ONE_ROW:
        case STEP_SIZE_MUST_NOT_BE_ZERO:
            return new JdbcSQLDataException(message, sql, state, errorCode, cause, stackTrace);
        case URL_RELATIVE_TO_CWD:
        case DATABASE_NOT_FOUND_1:
        case DATABASE_NOT_FOUND_WITH_IF_EXISTS_1:
        case REMOTE_DATABASE_NOT_FOUND_1:
        case TRACE_CONNECTION_NOT_CLOSED:
        case DATABASE_ALREADY_OPEN_1:
        case FILE_CORRUPTED_1:
        case URL_FORMAT_ERROR_2:
        case DRIVER_VERSION_ERROR_2:
        case FILE_VERSION_ERROR_1:
        case FILE_ENCRYPTION_ERROR_1:
        case WRONG_PASSWORD_FORMAT:
        case UNSUPPORTED_CIPHER:
        case UNSUPPORTED_LOCK_METHOD_1:
        case EXCEPTION_OPENING_PORT_2:
        case DUPLICATE_PROPERTY_1:
        case CONNECTION_BROKEN_1:
        case UNKNOWN_MODE_1:
        case CLUSTER_ERROR_DATABASE_RUNS_ALONE:
        case CLUSTER_ERROR_DATABASE_RUNS_CLUSTERED_1:
        case DATABASE_IS_CLOSED:
        case ERROR_SETTING_DATABASE_EVENT_LISTENER_2:
        case OUT_OF_MEMORY:
        case UNSUPPORTED_SETTING_1:
        case REMOTE_CONNECTION_NOT_ALLOWED:
        case DATABASE_CALLED_AT_SHUTDOWN:
        case CANNOT_CHANGE_SETTING_WHEN_OPEN_1:
        case DATABASE_IS_IN_EXCLUSIVE_MODE:
        case INVALID_DATABASE_NAME_1:
        case AUTHENTICATOR_NOT_AVAILABLE:
        case METHOD_DISABLED_ON_AUTOCOMMIT_TRUE:
            return new JdbcSQLNonTransientConnectionException(message, sql, state, errorCode, cause, stackTrace);
        case ROW_NOT_FOUND_WHEN_DELETING_1:
        case CONCURRENT_UPDATE_1:
        case ROW_NOT_FOUND_IN_PRIMARY_INDEX:
            return new JdbcSQLTransientException(message, sql, state, errorCode, cause, stackTrace);
        }
        // Default
        return new JdbcSQLException(message, sql, state, errorCode, cause, stackTrace);
    }

    private static String filterSQL(String sql) {
        return sql == null || !sql.contains(HIDE_SQL) ? sql : "-";
    }

    /**
     * Builds message for an exception.
     *
     * @param e exception
     * @return message
     */
    public static String buildMessageForException(JdbcException e) {
        String s = e.getOriginalMessage();
        StringBuilder buff = new StringBuilder(s != null ? s : "- ");
        s = e.getSQL();
        if (s != null) {
            buff.append("; SQL statement:\n").append(s);
        }
        buff.append(" [").append(e.getErrorCode()).append('-').append(Constants.BUILD_ID).append(']');
        return buff.toString();
    }

    /**
     * Prints up to 100 next exceptions for a specified SQL exception.
     *
     * @param e SQL exception
     * @param s print writer
     */
    public static void printNextExceptions(SQLException e, PrintWriter s) {
        // getNextException().printStackTrace(s) would be very slow
        // if many exceptions are joined
        int i = 0;
        while ((e = e.getNextException()) != null) {
            if (i++ == 100) {
                s.println("(truncated)");
                return;
            }
            s.println(e.toString());
        }
    }

    /**
     * Prints up to 100 next exceptions for a specified SQL exception.
     *
     * @param e SQL exception
     * @param s print stream
     */
    public static void printNextExceptions(SQLException e, PrintStream s) {
        // getNextException().printStackTrace(s) would be very slow
        // if many exceptions are joined
        int i = 0;
        while ((e = e.getNextException()) != null) {
            if (i++ == 100) {
                s.println("(truncated)");
                return;
            }
            s.println(e.toString());
        }
    }

    public Object getSource() {
        return source;
    }

    public void setSource(Object source) {
        this.source = source;
    }

    /**
     * Write the exception to the driver manager log writer if configured.
     *
     * @param e the exception
     */
    public static void traceThrowable(Throwable e) {
        PrintWriter writer = DriverManager.getLogWriter();
        if (writer != null) {
            e.printStackTrace(writer);
        }
    }

}
