All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.finra.herd.service.impl.JdbcServiceImpl Maven / Gradle / Ivy

Go to download

This project contains the business service code. This is a classic service tier where business logic is defined along with it's associated transaction management configuration.

There is a newer version: 0.160.0
Show newest version
/*
* Copyright 2015 herd contributors
*
* Licensed 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.finra.herd.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.Assert;

import org.finra.herd.core.helper.ConfigurationHelper;
import org.finra.herd.dao.JdbcDao;
import org.finra.herd.dao.S3Dao;
import org.finra.herd.model.api.xml.JdbcConnection;
import org.finra.herd.model.api.xml.JdbcDatabaseType;
import org.finra.herd.model.api.xml.JdbcExecutionRequest;
import org.finra.herd.model.api.xml.JdbcExecutionResponse;
import org.finra.herd.model.api.xml.JdbcStatement;
import org.finra.herd.model.api.xml.JdbcStatementResultSet;
import org.finra.herd.model.api.xml.JdbcStatementStatus;
import org.finra.herd.model.api.xml.JdbcStatementType;
import org.finra.herd.model.api.xml.S3PropertiesLocation;
import org.finra.herd.model.dto.ConfigurationValue;
import org.finra.herd.model.dto.S3FileTransferRequestParamsDto;
import org.finra.herd.service.JdbcService;
import org.finra.herd.service.helper.StorageHelper;
import org.finra.herd.service.helper.VelocityHelper;

/**
 * Default implementation of {@link org.finra.herd.service.JdbcService} which uses Spring's JDBC wrapper framework to handle connections and transactions.
 */
@Service
public class JdbcServiceImpl implements JdbcService
{
    public static final String DRIVER_REDSHIFT = "com.amazon.redshift.jdbc41.Driver";
    public static final String DRIVER_POSTGRES = "org.postgresql.Driver";
    public static final String DRIVER_ORACLE = "oracle.jdbc.OracleDriver";
    public static final String DRIVER_MYSQL = "com.mysql.jdbc.Driver";

    @Autowired
    private ConfigurationHelper configurationHelper;

    @Autowired
    private JdbcDao jdbcDao;

    @Autowired
    private S3Dao s3Dao;

    @Autowired
    private StorageHelper storageHelper;

    @Autowired
    private VelocityHelper velocityHelper;

    /**
     * This implementation uses a {@link DriverManagerDataSource} and {@link DefaultTransactionDefinition}. It suspends the existing transaction and purposely
     * runs this logic in "no transaction" to ensure we don't create a connection that would potentially become idle while all JDBC tasks execute. If the
     * underlying connection pool has an abandoned connection timeout, it would reclaim and close the connection. Then when all the JDBC tasks below finish,
     * this transaction would try to commit and would generate a "commit failed" exception because the connection is already closed. This approach is fine since
     * we are not actually doing any "herd" DB operations below. When all the below JDBC operations are finished, nothing would happen here except the callers
     * transaction would pick up where it left off which would be needed to write workflow variables, etc.
     */
    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public JdbcExecutionResponse executeJdbc(JdbcExecutionRequest jdbcExecutionRequest)
    {
        return executeJdbcImpl(jdbcExecutionRequest);
    }

    /**
     * This implementation uses a {@link DriverManagerDataSource}. Uses existing Spring ORM transaction.
     *
     * @param jdbcExecutionRequest JDBC execution request
     *
     * @return {@link JdbcExecutionResponse}
     */
    protected JdbcExecutionResponse executeJdbcImpl(JdbcExecutionRequest jdbcExecutionRequest)
    {
        validateJdbcExecutionRequest(jdbcExecutionRequest);

        // Optionally, get properties from S3
        S3PropertiesLocation s3PropertiesLocation = jdbcExecutionRequest.getS3PropertiesLocation();
        Map variables = getVariablesFromS3(s3PropertiesLocation);

        // Create data source
        DataSource dataSource = createDataSource(jdbcExecutionRequest.getConnection(), variables);

        // Execute the requested statements
        List requestJdbcStatements = jdbcExecutionRequest.getStatements();
        List responseJdbcStatements = executeStatements(requestJdbcStatements, dataSource, variables);

        // Create and return the execution result
        return new JdbcExecutionResponse(null, responseJdbcStatements);
    }

    /**
     * Returns a map of key-value from the specified S3 properties location. Returns null if the specified location is null.
     *
     * @param s3PropertiesLocation the location of a Java properties file in S3
     *
     * @return {@link Map} of key-values
     */
    private Map getVariablesFromS3(S3PropertiesLocation s3PropertiesLocation)
    {
        Map variables = null;
        if (s3PropertiesLocation != null)
        {
            Properties properties = getProperties(s3PropertiesLocation);
            variables = new HashMap<>();
            for (Map.Entry e : properties.entrySet())
            {
                variables.put(e.getKey().toString(), e.getValue());
            }
        }
        return variables;
    }

    /**
     * Gets an S3 object from the specified location, and parses it as a Java properties.
     *
     * @param s3PropertiesLocation {@link S3PropertiesLocation}
     *
     * @return {@link Properties}
     */
    private Properties getProperties(S3PropertiesLocation s3PropertiesLocation)
    {
        String s3BucketName = s3PropertiesLocation.getBucketName().trim();
        String s3ObjectKey = s3PropertiesLocation.getKey().trim();
        S3FileTransferRequestParamsDto s3FileTransferRequestParamsDto = storageHelper.getS3FileTransferRequestParamsDto();
        return s3Dao.getProperties(s3BucketName, s3ObjectKey, s3FileTransferRequestParamsDto);
    }

    /**
     * Validates parameters specified in the request.
     *
     * @param jdbcExecutionRequest the request to validate
     *
     * @throws IllegalArgumentException when there are validation errors in any of the parameters
     */
    private void validateJdbcExecutionRequest(JdbcExecutionRequest jdbcExecutionRequest)
    {
        Assert.notNull(jdbcExecutionRequest, "JDBC execution request is required");
        validateJdbcConnection(jdbcExecutionRequest.getConnection());
        validateJdbcStatements(jdbcExecutionRequest.getStatements());
        validateS3PropertiesLocation(jdbcExecutionRequest.getS3PropertiesLocation());
    }

    /**
     * Validates the specified S3 properties location. Asserts that if the given location is not null, bucket name and key are not blank.
     *
     * @param s3PropertiesLocation the {@link S3PropertiesLocation} to validate
     */
    private void validateS3PropertiesLocation(S3PropertiesLocation s3PropertiesLocation)
    {
        if (s3PropertiesLocation != null)
        {
            Assert.isTrue(StringUtils.isNotBlank(s3PropertiesLocation.getBucketName()), "S3 properties location bucket name is required");
            Assert.isTrue(StringUtils.isNotBlank(s3PropertiesLocation.getKey()), "S3 properties location key is required");
        }
    }

    /**
     * Validates parameters specified in the given statements. The statements must not be null, and must not be empty.
     *
     * @param jdbcStatements the list of statements to validate
     */
    private void validateJdbcStatements(List jdbcStatements)
    {
        Assert.notNull(jdbcStatements, "JDBC statements are required");
        Assert.isTrue(!jdbcStatements.isEmpty(), "JDBC statements are required");
        Integer jdbcMaxStatements = configurationHelper.getProperty(ConfigurationValue.JDBC_MAX_STATEMENTS, Integer.class);
        if (jdbcMaxStatements != null)
        {
            Assert.isTrue(jdbcStatements.size() <= jdbcMaxStatements, "The number of JDBC statements exceeded the maximum allowed " + jdbcMaxStatements + ".");
        }

        for (int i = 0; i < jdbcStatements.size(); i++)
        {
            JdbcStatement jdbcStatement = jdbcStatements.get(i);
            validateJdbcStatement(jdbcStatement, i);
        }
    }

    /**
     * Validates parameters specified in the given statement.
     *
     * @param jdbcStatement statement to validate
     * @param jdbcStatementIndex the index number of the statement in the list
     */
    private void validateJdbcStatement(JdbcStatement jdbcStatement, int jdbcStatementIndex)
    {
        Assert.notNull(jdbcStatement, "JDBC statement [" + jdbcStatementIndex + "] is required");
        Assert.notNull(jdbcStatement.getType(), "JDBC statement [" + jdbcStatementIndex + "] type is required");
        validateSqlStatement(jdbcStatement.getSql(), jdbcStatementIndex);
    }

    /**
     * Validates parameters specified in the given connection. This method does not validate whether the connection can be established.
     *
     * @param jdbcConnection the JDBC connection to validate
     */
    private void validateJdbcConnection(JdbcConnection jdbcConnection)
    {
        Assert.notNull(jdbcConnection, "JDBC connection is required");
        validateUrl(jdbcConnection.getUrl());
        Assert.notNull(jdbcConnection.getUsername(), "JDBC connection user name is required");
        Assert.notNull(jdbcConnection.getPassword(), "JDBC connection password is required");
        Assert.notNull(jdbcConnection.getDatabaseType(), "JDBC connection database type is required");
    }

    /**
     * Executes the requested statements in order. Returns the result of the execution.
     *
     * @param requestJdbcStatements the list of statements to execute, in order
     * @param dataSource the data source
     * @param variables the mapping of variables
     *
     * @return List of response {@link JdbcStatement}
     */
    private List executeStatements(List requestJdbcStatements, DataSource dataSource, Map variables)
    {
        List responseJdbcStatements = new ArrayList<>();

        /*
         * Create a copy of all the request statements.
         * The copied statements are the response statements. The response statements are defaulted to SKIPPED.
         */
        for (JdbcStatement requestJdbcStatement : requestJdbcStatements)
        {
            JdbcStatement responseJdbcStatement = createDefaultResponseJdbcStatement(requestJdbcStatement);
            responseJdbcStatements.add(responseJdbcStatement);
        }

        // We will reuse this template for all executions
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);

        /*
         * Execute each statement.
         * If there were any errors, and continueOnError is not TRUE, then the execution will stop.
         * The un-executed response statements will remain in their SKIPPED status.
         */
        for (int i = 0; i < responseJdbcStatements.size(); i++)
        {
            JdbcStatement jdbcStatement = responseJdbcStatements.get(i);
            executeStatement(jdbcTemplate, jdbcStatement, variables, i);

            if (JdbcStatementStatus.ERROR.equals(jdbcStatement.getStatus()) && !Boolean.TRUE.equals(jdbcStatement.isContinueOnError()))
            {
                break;
            }
        }

        return responseJdbcStatements;
    }

    /**
     * Executes a single statement using the given JDBC template. The given statement will be updated with the result and status.
     *
     * @param jdbcTemplate the JDBC template
     * @param jdbcStatement the JDBC statement to execute
     * @param variables the mapping of variables
     * @param jdbcStatementIndex the index of the statement
     */
    private void executeStatement(JdbcTemplate jdbcTemplate, JdbcStatement jdbcStatement, Map variables, int jdbcStatementIndex)
    {
        // This is the exception to be set as the error message in the response
        Throwable exception = null;
        try
        {
            String sql = evaluate(jdbcStatement.getSql(), variables, "jdbc statement sql");
            validateSqlStatement(sql, jdbcStatementIndex);

            // Process UPDATE type statements
            if (JdbcStatementType.UPDATE.equals(jdbcStatement.getType()))
            {
                int result = jdbcDao.update(jdbcTemplate, sql);

                jdbcStatement.setStatus(JdbcStatementStatus.SUCCESS);
                jdbcStatement.setResult(String.valueOf(result));
            }
            // Process QUERY type statements
            else if (JdbcStatementType.QUERY.equals(jdbcStatement.getType()))
            {
                Integer maxResults = configurationHelper.getProperty(ConfigurationValue.JDBC_RESULT_MAX_ROWS, Integer.class);

                JdbcStatementResultSet jdbcStatementResultSet = jdbcDao.query(jdbcTemplate, sql, maxResults);

                jdbcStatement.setStatus(JdbcStatementStatus.SUCCESS);
                jdbcStatement.setResultSet(jdbcStatementResultSet);
            }
            // Any other statement types are unrecognized. This case should not be possible unless developer error.
            else
            {
                throw new IllegalStateException("Unsupported JDBC statement type '" + jdbcStatement.getType() + "'");
            }
        }
        catch (CannotGetJdbcConnectionException cannotGetJdbcConnectionException)
        {
            /*
             * When the statement fails to execute due to connection errors. This usually indicates that the connection information which was specified is
             * wrong, or there is a network issue. Either way, it would indicate user error.
             * We get the wrapped exception and throw again as an IllegalArgumentException.
             */
            Throwable causeThrowable = cannotGetJdbcConnectionException.getCause();
            throw new IllegalArgumentException(String.valueOf(causeThrowable).trim(), cannotGetJdbcConnectionException);
        }
        catch (DataAccessException dataAccessException)
        {
            // DataAccessException's cause is a SQLException which is thrown by driver
            // We will use the SQLException message result
            exception = dataAccessException.getCause();
        }

        // If there was an error
        if (exception != null)
        {
            // Set status to error and result as message
            jdbcStatement.setStatus(JdbcStatementStatus.ERROR);
            jdbcStatement.setErrorMessage(maskSensitiveInformation(exception, variables));
        }
    }

    /**
     * Returns the message of the given exception, masking any sensitive information indicated by the given collection of sensitive data. If the variables is
     * null, no masking will occur.
     *
     * @param exception the exception message to mask
     * @param variables the mapping of variables with sensitive information
     *
     * @return The exception message with masked data.
     */
    private String maskSensitiveInformation(Throwable exception, Map variables)
    {
        String message = String.valueOf(exception).trim();
        if (variables != null)
        {
            for (Object sensitiveData : variables.values())
            {
                String sensitiveDataString = String.valueOf(sensitiveData);
                message = message.replace(sensitiveDataString, "****");
            }
        }
        return message;
    }

    /**
     * Validates the given SQL statement where its position in the list of statement is the given index. This method does not validate SQL syntax.
     *
     * @param sql the SQL statement to validate
     * @param jdbcStatementIndex the index of the statement to validate in the list of statements
     */
    private void validateSqlStatement(String sql, int jdbcStatementIndex)
    {
        Assert.isTrue(StringUtils.isNotBlank(sql), "JDBC statement [" + jdbcStatementIndex + "] SQL is required");
    }

    /**
     * Creates and returns a {@link JdbcStatement} at a state which has not yet been executed based on the given request.
     * 

* The status will be set to {@link JdbcStatementStatus#SKIPPED} and result null. * * @param requestJdbcStatement the requested JDBC statement * * @return a new {@link JdbcStatement} */ private JdbcStatement createDefaultResponseJdbcStatement(JdbcStatement requestJdbcStatement) { JdbcStatement responseJdbcStatement = new JdbcStatement(); responseJdbcStatement.setType(requestJdbcStatement.getType()); responseJdbcStatement.setSql(requestJdbcStatement.getSql()); responseJdbcStatement.setContinueOnError(requestJdbcStatement.isContinueOnError()); responseJdbcStatement.setStatus(JdbcStatementStatus.SKIPPED); return responseJdbcStatement; } /** * Creates and returns a new data source from the given connection information. Creates a new {@link DriverManagerDataSource}. * * @param jdbcConnection the JDBC connection * @param variables the optional map of key-value for expression evaluation * * @return a new {@link DataSource} */ private DataSource createDataSource(JdbcConnection jdbcConnection, Map variables) { String url = evaluate(jdbcConnection.getUrl(), variables, "jdbc connection url"); String username = evaluate(jdbcConnection.getUsername(), variables, "jdbc connection username"); String password = evaluate(jdbcConnection.getPassword(), variables, "jdbc connection password"); validateUrl(url); DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource(); driverManagerDataSource.setUrl(url); driverManagerDataSource.setUsername(username); driverManagerDataSource.setPassword(password); driverManagerDataSource.setDriverClassName(getDriverClassName(jdbcConnection.getDatabaseType())); return driverManagerDataSource; } /** * Validates the given URL. Does not validate URL syntax or whether the URL is accessible. * * @param url the URL string to validate */ private void validateUrl(String url) { Assert.isTrue(StringUtils.isNotBlank(url), "JDBC connection URL is required"); } /** * Evaluates the given expression as a Velocity template using the given variables. Returns the expression as-is if the variables is null. The given * variable name will be used as the log tag. * * @param expression the expression * @param variables the mapping of variables * @param variableName the variable name * * @return the expression evaluated as a Velocity template */ private String evaluate(String expression, Map variables, String variableName) { String result = expression; if (variables != null) { result = velocityHelper.evaluate(expression, variables, variableName); } return result; } /** * Returns the fully qualified driver class name of the given JDBC database type. * * @param jdbcDatabaseType the JDBC database type * * @return fully qualified driver class name * @throws IllegalArgumentException when the database type is not supported. */ private String getDriverClassName(JdbcDatabaseType jdbcDatabaseType) { switch (jdbcDatabaseType) { case ORACLE: return DRIVER_ORACLE; case POSTGRES: return DRIVER_POSTGRES; case REDSHIFT: return DRIVER_REDSHIFT; case MYSQL: return DRIVER_MYSQL; default: throw new IllegalArgumentException("Unsupported database type '" + jdbcDatabaseType + "'"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy