org.wso2.andes.store.rdbms.RDBMSStoreUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.wso2.andes.store.rdbms.RDBMSStoreUtils.java

Source

/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 * 
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.wso2.andes.store.rdbms;

import java.sql.BatchUpdateException;
import java.sql.Connection;
import java.sql.DataTruncation;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLDataException;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;
import java.sql.SQLInvalidAuthorizationSpecException;
import java.sql.SQLNonTransientConnectionException;
import java.sql.SQLTimeoutException;
import java.sql.SQLTransactionRollbackException;
import java.sql.SQLTransientConnectionException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.wso2.andes.configuration.util.ConfigurationProperties;
import org.wso2.andes.kernel.AndesException;
import org.wso2.andes.store.AndesBatchUpdateException;
import org.wso2.andes.store.AndesDataIntegrityViolationException;
import org.wso2.andes.store.AndesDataException;
import org.wso2.andes.store.AndesStoreUnavailableException;
import org.wso2.andes.store.AndesTransactionRollbackException;

import com.google.common.base.Splitter;

/**
 * Contains utilility methods required for both {@link RDBMSMessageStoreImpl}
 * and {@link RDBMSAndesContextStoreImpl}
 */
public class RDBMSStoreUtils {

    private static final Logger log = Logger.getLogger(RDBMSStoreUtils.class);

    /**
     * Keep track of SQL state code classes (i.e first two digits)
     * corresponding to database connectivity errors
     */
    private Set<String> storeUnavailableSQLStatesClassCodes;

    /**
     * Keep track of SQL state code classes (i.e first two digits)
     * corresponding to integrity violation errors
     */

    private Set<String> dataIntegrityViolationSQLStateClassCodes;

    /**
     * Keep track of SQL state code classes (i.e first two digits)
     * corresponding to various database server generated errors similar to
     * {@link DataTruncation}, {@link SQLDataException} (but driver didn't
     * differentiated and just choose set the sql state only)
     */
    private Set<String> dataErrorSQLStateClassCodes;

    private Set<String> transactionRollbackSQLStateClassCodes;

    public RDBMSStoreUtils(ConfigurationProperties connectionProperties) {

        // extract the configurations set in broker.xml
        storeUnavailableSQLStatesClassCodes = extractAndFill(connectionProperties,
                RDBMSConstants.STORE_UNAVAILABLE_SQL_STATE_CLASSES);

        dataIntegrityViolationSQLStateClassCodes = extractAndFill(connectionProperties,
                RDBMSConstants.DATA_INTEGRITY_VIOLATION_SQL_STATE_CLASSES);

        dataErrorSQLStateClassCodes = extractAndFill(connectionProperties,
                RDBMSConstants.DATA_ERROR_SQL_STATE_CLASSES);

        transactionRollbackSQLStateClassCodes = extractAndFill(connectionProperties,
                RDBMSConstants.TRANSACTION_ROLLBACK_ERROR_SQL_STATE_CLASSES);

    }

    public AndesException convertSQLException(String message, SQLException sqlException) {

        // try using SQLException subclasses (works for mysql)
        AndesException convertedException = convertBySQLException(message, sqlException);

        if (null == convertedException) {
            // trying using sql state flags ( works for jtds, mssql etc)
            convertedException = convertBySQLState(message, sqlException);
        }

        if (null == convertedException) {
            // if unable to determine exact error then throw a generic exception.
            convertedException = new AndesException(message, sqlException.getSQLState(), sqlException);
        }

        return convertedException;

    }

    private AndesException convertBySQLException(String message, SQLException sqlException) {
        AndesException convertedException = null;

        if (SQLNonTransientConnectionException.class.isInstance(sqlException)
                || SQLTransientConnectionException.class.isInstance(sqlException)
                || SQLInvalidAuthorizationSpecException.class.isInstance(sqlException)
                || SQLClientInfoException.class.isInstance(sqlException)
                || SQLTimeoutException.class.isInstance(sqlException)) {

            String sqlState = extractSqlState(sqlException);
            convertedException = new AndesStoreUnavailableException(message, sqlState, sqlException);

        } else if (SQLIntegrityConstraintViolationException.class.isInstance(sqlException)) {
            String sqlState = extractSqlState(sqlException);
            convertedException = new AndesDataIntegrityViolationException(message, sqlState, sqlException);
        } else if (SQLTransactionRollbackException.class.isInstance(sqlException)) {
            String sqlState = extractSqlState(sqlException);
            convertedException = new AndesTransactionRollbackException(message, sqlState, sqlException);
        } else if (DataTruncation.class.isInstance(sqlException)
                || SQLDataException.class.isInstance(sqlException)) {
            String sqlState = extractSqlState(sqlException);
            convertedException = new AndesDataException(message, sqlState, sqlException);
        }

        return convertedException;
    }

    private AndesException convertBySQLState(String message, SQLException sqlException) {

        AndesException convertedException = null;
        String sqlState = extractSqlState(sqlException);

        String sqlStateClassCode = determineSqlStateClassCode(sqlState);

        if (storeUnavailableSQLStatesClassCodes.contains(sqlStateClassCode)) {
            convertedException = new AndesStoreUnavailableException(message, sqlState, sqlException);
        } else if (dataIntegrityViolationSQLStateClassCodes.contains(sqlStateClassCode)) {
            convertedException = new AndesDataIntegrityViolationException(message, sqlState, sqlException);
        } else if (transactionRollbackSQLStateClassCodes.contains(sqlStateClassCode)) {
            convertedException = new AndesTransactionRollbackException(message, sqlState, sqlException);
        } else if (dataErrorSQLStateClassCodes.contains(sqlStateClassCode)) {
            convertedException = new AndesDataException(message, sqlState, sqlException);
        }

        return convertedException;
    }

    /**
     * Inserts a test record
     * 
     * @param testString
     *            a string value
     * @param testTime
     *            a time value
     * @return true if the test was successful.
     */
    public boolean testInsert(Connection connection, String testString, long testTime) throws SQLException {

        boolean canInsert = false;
        PreparedStatement preparedStatement = null;

        try {
            connection.setAutoCommit(false);

            preparedStatement = connection.prepareStatement(RDBMSConstants.PS_TEST_MSG_STORE_INSERT);

            preparedStatement.setString(1, testString);
            preparedStatement.setLong(2, testTime);

            preparedStatement.executeUpdate();
            connection.commit();
            canInsert = true;
        } catch (SQLException e) {
            rollback(connection, RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_INSERT
                    + String.format("test data : [%s, %d]", testString, testTime));

        } finally {
            String msg = RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_INSERT
                    + String.format("test data : [%s, %d]", testString, testTime);
            close(preparedStatement, msg);
            close(connection, msg);
        }

        return canInsert;
    }

    /**
     * Reads a test record
     * 
     * @param testString
     *            a string value
     * @param testTime
     *            a time value
     * @return true if the test was successful.
     */
    public boolean testRead(Connection connection, String testString, long testTime) throws SQLException {

        boolean canRead = false;

        PreparedStatement selectPreparedStatement = null;

        ResultSet results = null;

        try {
            selectPreparedStatement = connection.prepareStatement(RDBMSConstants.PS_TEST_MSG_STORE_SELECT);
            selectPreparedStatement.setString(1, testString);
            selectPreparedStatement.setLong(2, testTime);

            results = selectPreparedStatement.executeQuery();

            if (results.next()) {
                canRead = true;
            }

        } catch (SQLException e) {
            log.error(RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_READ
                    + String.format("test data : [%s, %d]", testString, testTime), e);

        } finally {
            String msg = RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_READ
                    + String.format("test data : [%s, %d]", testString, testTime);

            close(results, msg);
            close(selectPreparedStatement, msg);
            close(connection, msg);
        }

        return canRead;
    }

    /**
     * Delete a test record
     * 
     * @param testString
     *            a string value
     * @param testTime
     *            a time value
     * @return true if the test was successful.
     */
    public boolean testDelete(Connection connection, String testString, long testTime) throws SQLException {

        boolean canDelete = false;
        PreparedStatement preparedStatement = null;

        ResultSet results = null;

        try {
            connection.setAutoCommit(false);

            preparedStatement = connection.prepareStatement(RDBMSConstants.PS_TEST_MSG_STORE_DELETE);
            preparedStatement.setString(1, testString);
            preparedStatement.setLong(2, testTime);
            preparedStatement.executeUpdate();
            connection.commit();
            canDelete = true;
        } catch (SQLException e) {
            rollback(connection, RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_DELETE
                    + String.format("test data : [%s, %d]", testString, testTime));
        } finally {

            String msg = RDBMSConstants.TASK_TEST_MESSAGE_STORE_OPERATIONAL_DELETE
                    + String.format("test data : [%s, %d]", testString, testTime);
            close(results, msg);
            close(preparedStatement, msg);
            close(connection, msg);
        }

        return canDelete;
    }

    /**
     * close the prepared statement resource
     *
     * @param preparedStatement
     *            PreparedStatement
     * @param task
     *            task that was done by the closed prepared statement.
     */
    private void close(PreparedStatement preparedStatement, String task) {
        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                log.error("Closing prepared statement failed after " + task, e);
            }
        }
    }

    /**
     * closes the result set resources
     *
     * @param resultSet
     *            ResultSet
     * @param task
     *            task that was done by the closed result set.
     */
    private void close(ResultSet resultSet, String task) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                log.error("Closing result set failed after " + task, e);
            }
        }
    }

    /**
     * Closes the provided connection. on failure log the error;
     *
     * @param connection
     *            Connection
     * @param task
     *            task that was done before closing
     */
    private void close(Connection connection, String task) {
        if (connection != null) {
            try {
                connection.setAutoCommit(true);
                connection.close();
            } catch (SQLException e) {
                log.error("Failed to close connection after " + task, e);
            }
        }
    }

    /**
     * On database update failure tries to rollback
     *
     * @param connection
     *            database connection
     * @param task
     *            explanation of the task done when the rollback was triggered
     */
    private void rollback(Connection connection, String task) {
        if (connection != null) {
            try {
                connection.rollback();
            } catch (SQLException e) {
                log.warn("Rollback failed on " + task, e);
            }
        }
    }

    /**
     * Throws an {@link AndesBatchUpdateException} with specified
     * information. and rollback the tried batch operation.
     * 
     * @param dataList
     *            data objects will was used for batch operation.
     * @param connection
     *            SQL connection used.
     * @param bue
     *            the original SQL batch update exception.
     * @param task
     *            the string indicating the task tried out (in failed batch
     *            update operation)
     * @throws AndesBatchUpdateException
     *             the error.
     */
    public <T> void raiseBatchUpdateException(List<T> dataList, Connection connection, BatchUpdateException bue,
            String task) throws AndesBatchUpdateException {

        int[] updateCountsOfFailedBatch = bue.getUpdateCounts();
        List<T> failed = new ArrayList<T>();
        List<T> succeded = new ArrayList<T>();

        for (int i = 0; i < updateCountsOfFailedBatch.length; i++) {
            T msgPart = dataList.get(i);
            if (Statement.EXECUTE_FAILED == updateCountsOfFailedBatch[i]) {
                failed.add(msgPart);
            } else {
                succeded.add(msgPart);
            }
        }

        rollback(connection, task); // try to rollback the batch operation.

        AndesBatchUpdateException insertEx = new AndesBatchUpdateException(task + " failed", bue.getSQLState(), bue,
                failed, succeded);
        throw insertEx;
    }

    /**
     * A Utility method which will extract a configuration, tokenize and
     *  return a {@link Set} with values.
     * 
     * @param connectionProperties
     *            All configurations
     * @param configParam
     *            specific configuration name (to extract value)
     * @return parsed configurations
     */
    private Set<String> extractAndFill(ConfigurationProperties connectionProperties, String configParam) {
        Set<String> configValues = Collections.emptySet();

        String value = connectionProperties.getProperty(configParam);

        if (StringUtils.isNotBlank(value)) {
            Iterable<String> splitValues = Splitter.on(',').trimResults().omitEmptyStrings().split(value);

            configValues = new HashSet<String>();
            for (String state : splitValues) {
                configValues.add(state);
            }
        }

        return configValues;
    }

    /**
     * Extract the vendor specific error code from a sql exception
     * 
     * @param sqlException
     *            the error
     * @return the error code supplied by the vendor
     */
    private int extractErrorCode(SQLException sqlException) {
        int errorCode = sqlException.getErrorCode();
        SQLException nextEx = sqlException.getNextException();
        while (errorCode == 0 && nextEx != null) {
            errorCode = nextEx.getErrorCode();
            nextEx = nextEx.getNextException();
        }
        return errorCode;
    }

    /**
     * Extracts SQL State from a given sql exception
     * 
     * @param sqlException
     *            the error
     * @return sql state
     */
    private String extractSqlState(SQLException sqlException) {
        String sqlState = sqlException.getSQLState();
        SQLException nextEx = sqlException.getNextException();
        while (sqlState == null && nextEx != null) {
            sqlState = nextEx.getSQLState();
            nextEx = nextEx.getNextException();
        }
        return sqlState;
    }

    /**
     * Extracts the Sql state class ( the first two digits of the sql state)
     * 
     * @param sqlState
     *            the sql state given in a sql exception
     * @return the sql state class
     */
    private String determineSqlStateClassCode(String sqlState) {
        if (sqlState == null || sqlState.length() < 2) {
            return sqlState;
        }
        return sqlState.substring(0, 2);
    }

}