com.espertech.esper.epl.db.DatabasePollingViewableFactory.java Source code

Java tutorial

Introduction

Here is the source code for com.espertech.esper.epl.db.DatabasePollingViewableFactory.java

Source

/**************************************************************************************
 * Copyright (C) 2008 EsperTech, Inc. All rights reserved.                            *
 * http://esper.codehaus.org                                                          *
 * http://www.espertech.com                                                           *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the GPL license       *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package com.espertech.esper.epl.db;

import com.espertech.esper.antlr.NoCaseSensitiveStream;
import com.espertech.esper.client.ConfigurationDBRef;
import com.espertech.esper.client.EventType;
import com.espertech.esper.client.hook.SQLColumnTypeContext;
import com.espertech.esper.client.hook.SQLColumnTypeConversion;
import com.espertech.esper.client.hook.SQLOutputRowConversion;
import com.espertech.esper.client.hook.SQLOutputRowTypeContext;
import com.espertech.esper.core.context.util.EPStatementAgentInstanceHandle;
import com.espertech.esper.epl.expression.ExprValidationException;
import com.espertech.esper.epl.generated.EsperEPL2GrammarLexer;
import com.espertech.esper.epl.spec.DBStatementStreamSpec;
import com.espertech.esper.event.EventAdapterService;
import com.espertech.esper.util.*;
import com.espertech.esper.view.HistoricalEventViewable;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.Token;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.sql.*;
import java.util.*;

/**
 * Factory for a view onto historical data via SQL statement.
 */
public class DatabasePollingViewableFactory {
    /**
     * Placeholder name for SQL-where clause substitution.
     */
    public static final String SAMPLE_WHERECLAUSE_PLACEHOLDER = "$ESPER-SAMPLE-WHERE";

    /**
     * Creates the viewable for polling via database SQL query.
     * @param streamNumber is the stream number of the view
     * @param databaseStreamSpec provides the SQL statement, database name and additional info
     * @param databaseConfigService for getting database connection and settings
     * @param eventAdapterService for generating event beans from database information
     * @param epStatementAgentInstanceHandle is the statements-own handle for use in registering callbacks with services
     * @param columnTypeConversionHook hook for statement-specific column conversion
     * @param outputRowConversionHook hook for statement-specific row conversion
     * @param enableJDBCLogging indicator to enable JDBC logging
     * @return viewable providing poll functionality
     * @throws ExprValidationException if the validation failed
     */
    public static HistoricalEventViewable createDBStatementView(String statementId, int streamNumber,
            DBStatementStreamSpec databaseStreamSpec, DatabaseConfigService databaseConfigService,
            EventAdapterService eventAdapterService, EPStatementAgentInstanceHandle epStatementAgentInstanceHandle,
            SQLColumnTypeConversion columnTypeConversionHook, SQLOutputRowConversion outputRowConversionHook,
            boolean enableJDBCLogging) throws ExprValidationException {
        // Parse the SQL for placeholders and text fragments
        List<PlaceholderParser.Fragment> sqlFragments;
        try {
            sqlFragments = PlaceholderParser.parsePlaceholder(databaseStreamSpec.getSqlWithSubsParams());
        } catch (PlaceholderParseException ex) {
            String text = "Error parsing SQL";
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        // Assemble a PreparedStatement and parameter list
        String preparedStatementText = createPreparedStatement(sqlFragments);
        SQLParameterDesc parameterDesc = getParameters(sqlFragments);
        if (log.isDebugEnabled()) {
            log.debug(".createDBEventStream preparedStatementText=" + preparedStatementText + " parameterDesc="
                    + parameterDesc);
        }

        // Get a database connection
        String databaseName = databaseStreamSpec.getDatabaseName();
        DatabaseConnectionFactory databaseConnectionFactory;
        ColumnSettings metadataSetting;
        try {
            databaseConnectionFactory = databaseConfigService.getConnectionFactory(databaseName);
            metadataSetting = databaseConfigService.getQuerySetting(databaseName);
        } catch (DatabaseConfigException ex) {
            String text = "Error connecting to database '" + databaseName + '\'';
            log.error(text, ex);
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        Connection connection;
        try {
            connection = databaseConnectionFactory.getConnection();
        } catch (DatabaseConfigException ex) {
            String text = "Error connecting to database '" + databaseName + '\'';
            log.error(text, ex);
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        // On default setting, if we detect Oracle in the connection then don't query metadata from prepared statement
        ConfigurationDBRef.MetadataOriginEnum metaOriginPolicy = metadataSetting.getMetadataRetrievalEnum();
        if (metaOriginPolicy == ConfigurationDBRef.MetadataOriginEnum.DEFAULT) {
            String connectionClass = connection.getClass().getName();
            if ((connectionClass.toLowerCase().contains("oracle")
                    || (connectionClass.toLowerCase().contains("timesten")))) {
                // switch to sample statement if we are dealing with an oracle connection
                metaOriginPolicy = ConfigurationDBRef.MetadataOriginEnum.SAMPLE;
            }
        }

        QueryMetaData queryMetaData;
        try {
            if ((metaOriginPolicy == ConfigurationDBRef.MetadataOriginEnum.METADATA)
                    || (metaOriginPolicy == ConfigurationDBRef.MetadataOriginEnum.DEFAULT)) {
                queryMetaData = getPreparedStmtMetadata(connection, parameterDesc.getParameters(),
                        preparedStatementText, metadataSetting);
            } else {
                String sampleSQL;
                boolean isGivenMetadataSQL = true;
                if (databaseStreamSpec.getMetadataSQL() != null) {
                    sampleSQL = databaseStreamSpec.getMetadataSQL();
                    isGivenMetadataSQL = true;
                    if (log.isInfoEnabled()) {
                        log.info(".createDBStatementView Using provided sample SQL '" + sampleSQL + "'");
                    }
                } else {
                    // Create the sample SQL by replacing placeholders with null and
                    // SAMPLE_WHERECLAUSE_PLACEHOLDER with a "where 1=0" clause
                    sampleSQL = createSamplePlaceholderStatement(sqlFragments);

                    if (log.isInfoEnabled()) {
                        log.info(".createDBStatementView Using un-lexed sample SQL '" + sampleSQL + "'");
                    }

                    // If there is no SAMPLE_WHERECLAUSE_PLACEHOLDER, lexical analyse the SQL
                    // adding a "where 1=0" clause.
                    if (parameterDesc.getBuiltinIdentifiers().length != 1) {
                        sampleSQL = lexSampleSQL(sampleSQL);
                        if (log.isInfoEnabled()) {
                            log.info(".createDBStatementView Using lexed sample SQL '" + sampleSQL + "'");
                        }
                    }
                }

                // finally get the metadata by firing the sample SQL
                queryMetaData = getExampleQueryMetaData(connection, parameterDesc.getParameters(), sampleSQL,
                        metadataSetting, isGivenMetadataSQL);
            }
        } catch (ExprValidationException ex) {
            try {
                connection.close();
            } catch (SQLException e) {
                // don't handle
            }
            throw ex;
        }

        // Close connection
        try {
            connection.close();
        } catch (SQLException e) {
            String text = "Error closing connection";
            log.error(text, e);
            throw new ExprValidationException(text + ", reason: " + e.getMessage());
        }

        // Create event type
        // Construct an event type from SQL query result metadata
        Map<String, Object> eventTypeFields = new HashMap<String, Object>();
        int columnNum = 1;
        for (Map.Entry<String, DBOutputTypeDesc> entry : queryMetaData.getOutputParameters().entrySet()) {
            String name = entry.getKey();
            DBOutputTypeDesc dbOutputDesc = entry.getValue();

            Class clazz;
            if (dbOutputDesc.getOptionalBinding() != null) {
                clazz = dbOutputDesc.getOptionalBinding().getType();
            } else {
                clazz = SQLTypeMapUtil.sqlTypeToClass(dbOutputDesc.getSqlType(), dbOutputDesc.getClassName());
            }

            if (columnTypeConversionHook != null) {

                Class newValue = columnTypeConversionHook.getColumnType(new SQLColumnTypeContext(
                        databaseStreamSpec.getDatabaseName(), databaseStreamSpec.getSqlWithSubsParams(), name,
                        clazz, dbOutputDesc.getSqlType(), columnNum));
                if (newValue != null) {
                    clazz = newValue;
                }

            }
            eventTypeFields.put(name, clazz);
            columnNum++;
        }

        EventType eventType;
        if (outputRowConversionHook == null) {
            String outputEventType = statementId + "_dbpoll_" + streamNumber;
            eventType = eventAdapterService.createAnonymousMapType(outputEventType, eventTypeFields);
        } else {
            Class carrierClass = outputRowConversionHook
                    .getOutputRowType(new SQLOutputRowTypeContext(databaseStreamSpec.getDatabaseName(),
                            databaseStreamSpec.getSqlWithSubsParams(), eventTypeFields));
            if (carrierClass == null) {
                throw new ExprValidationException("Output row conversion hook returned no type");
            }
            eventType = eventAdapterService.addBeanType(carrierClass.getName(), carrierClass, false, false, false);
        }

        // Get a proper connection and data cache
        ConnectionCache connectionCache;
        DataCache dataCache;
        try {
            connectionCache = databaseConfigService.getConnectionCache(databaseName, preparedStatementText);
            dataCache = databaseConfigService.getDataCache(databaseName, epStatementAgentInstanceHandle);
        } catch (DatabaseConfigException e) {
            String text = "Error obtaining cache configuration";
            log.error(text, e);
            throw new ExprValidationException(text + ", reason: " + e.getMessage());
        }

        PollExecStrategyDBQuery dbPollStrategy = new PollExecStrategyDBQuery(eventAdapterService, eventType,
                connectionCache, preparedStatementText, queryMetaData.getOutputParameters(),
                columnTypeConversionHook, outputRowConversionHook, enableJDBCLogging);

        return new DatabasePollingViewable(streamNumber, queryMetaData.getInputParameters(), dbPollStrategy,
                dataCache, eventType);
    }

    private static QueryMetaData getExampleQueryMetaData(Connection connection, String[] parameters,
            String sampleSQL, ColumnSettings metadataSetting, boolean isUsingMetadataSQL)
            throws ExprValidationException {
        // Simply add up all input parameters
        List<String> inputParameters = new LinkedList<String>();
        inputParameters.addAll(Arrays.asList(parameters));

        Statement statement;
        try {
            statement = connection.createStatement();
        } catch (SQLException ex) {
            String text = "Error creating statement";
            log.error(text, ex);
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        ResultSet result = null;
        try {
            result = statement.executeQuery(sampleSQL);
        } catch (SQLException ex) {
            try {
                statement.close();
            } catch (SQLException e) {
                log.info("Error closing statement: " + e.getMessage(), e);
            }

            String text;
            if (isUsingMetadataSQL) {
                text = "Error compiling metadata SQL to retrieve statement metadata, using sql text '" + sampleSQL
                        + "'";
            } else {
                text = "Error compiling metadata SQL to retrieve statement metadata, consider using the 'metadatasql' syntax, using sql text '"
                        + sampleSQL + "'";
            }

            log.error(text, ex);
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        Map<String, DBOutputTypeDesc> outputProperties;
        try {
            outputProperties = compileResultMetaData(result.getMetaData(), metadataSetting);
        } catch (SQLException ex) {
            try {
                result.close();
            } catch (SQLException e) {
                // don't handle
            }
            try {
                statement.close();
            } catch (SQLException e) {
                // don't handle
            }
            String text = "Error in statement '" + sampleSQL + "', failed to obtain result metadata";
            log.error(text, ex);
            throw new ExprValidationException(text + ", please check the statement, reason: " + ex.getMessage());
        } finally {
            if (result != null) {
                try {
                    result.close();
                } catch (SQLException e) {
                    log.warn("Exception closing result set: " + e.getMessage());
                }
            }
            if (statement != null) {
                try {
                    statement.close();
                } catch (SQLException e) {
                    log.warn("Exception closing result set: " + e.getMessage());
                }
            }
        }

        return new QueryMetaData(inputParameters, outputProperties);
    }

    /**
     * Lexes the sample SQL and inserts a "where 1=0" where-clause.
     * @param querySQL to inspect using lexer
     * @return sample SQL with where-clause inserted
     * @throws ExprValidationException to indicate a lexer problem
     */
    protected static String lexSampleSQL(String querySQL) throws ExprValidationException {
        querySQL = querySQL.replaceAll("\\s\\s+|\\n|\\r", " ");
        StringReader reader = new StringReader(querySQL);
        CharStream input;
        try {
            input = new NoCaseSensitiveStream(reader);
        } catch (IOException ex) {
            throw new ExprValidationException("IOException lexing query SQL '" + querySQL + '\'', ex);
        }

        int whereIndex = -1;
        int groupbyIndex = -1;
        int havingIndex = -1;
        int orderByIndex = -1;
        List<Integer> unionIndexes = new ArrayList<Integer>();

        EsperEPL2GrammarLexer lex = new EsperEPL2GrammarLexer(input);
        CommonTokenStream tokens = new CommonTokenStream(lex);
        List tokenList = tokens.getTokens();

        for (int i = 0; i < tokenList.size(); i++) {
            Token token = (Token) tokenList.get(i);
            if ((token == null) || token.getText() == null) {
                break;
            }
            String text = token.getText().toLowerCase().trim();
            if (text.equals("where")) {
                whereIndex = token.getCharPositionInLine() + 1;
            }
            if (text.equals("group")) {
                groupbyIndex = token.getCharPositionInLine() + 1;
            }
            if (text.equals("having")) {
                havingIndex = token.getCharPositionInLine() + 1;
            }
            if (text.equals("order")) {
                orderByIndex = token.getCharPositionInLine() + 1;
            }
            if (text.equals("union")) {
                unionIndexes.add(token.getCharPositionInLine() + 1);
            }
        }

        // If we have a union, break string into subselects and process each
        if (unionIndexes.size() != 0) {
            StringWriter changedSQL = new StringWriter();
            int lastIndex = 0;
            for (int i = 0; i < unionIndexes.size(); i++) {
                int index = unionIndexes.get(i);
                String fragment;
                if (i > 0) {
                    fragment = querySQL.substring(lastIndex + 5, index - 1);
                } else {
                    fragment = querySQL.substring(lastIndex, index - 1);
                }
                String lexedFragment = lexSampleSQL(fragment);

                if (i > 0) {
                    changedSQL.append("union ");
                }
                changedSQL.append(lexedFragment);
                lastIndex = index - 1;
            }

            // last part after last union
            String fragment = querySQL.substring(lastIndex + 5, querySQL.length());
            String lexedFragment = lexSampleSQL(fragment);
            changedSQL.append("union ");
            changedSQL.append(lexedFragment);

            return changedSQL.toString();
        }

        // Found a where clause, simplest cases
        if (whereIndex != -1) {
            StringWriter changedSQL = new StringWriter();
            String prefix = querySQL.substring(0, whereIndex + 5);
            String suffix = querySQL.substring(whereIndex + 5, querySQL.length());
            changedSQL.write(prefix);
            changedSQL.write("1=0 and ");
            changedSQL.write(suffix);
            return changedSQL.toString();
        }

        // No where clause, find group-by
        int insertIndex;
        if (groupbyIndex != -1) {
            insertIndex = groupbyIndex;
        } else if (havingIndex != -1) {
            insertIndex = havingIndex;
        } else if (orderByIndex != -1) {
            insertIndex = orderByIndex;
        } else {
            StringWriter changedSQL = new StringWriter();
            changedSQL.write(querySQL);
            changedSQL.write(" where 1=0 ");
            return changedSQL.toString();
        }

        try {
            StringWriter changedSQL = new StringWriter();
            String prefix = querySQL.substring(0, insertIndex - 1);
            changedSQL.write(prefix);
            changedSQL.write("where 1=0 ");
            String suffix = querySQL.substring(insertIndex - 1, querySQL.length());
            changedSQL.write(suffix);
            return changedSQL.toString();
        } catch (Exception ex) {
            String text = "Error constructing sample SQL to retrieve metadata for JDBC-drivers that don't support metadata, consider using the "
                    + SAMPLE_WHERECLAUSE_PLACEHOLDER + " placeholder or providing a sample SQL";
            log.error(text, ex);
            throw new ExprValidationException(text, ex);
        }
    }

    private static QueryMetaData getPreparedStmtMetadata(Connection connection, String[] parameters,
            String preparedStatementText, ColumnSettings metadataSetting) throws ExprValidationException {
        PreparedStatement prepared;
        try {
            if (log.isInfoEnabled()) {
                log.info(".getPreparedStmtMetadata Preparing statement '" + preparedStatementText + "'");
            }
            prepared = connection.prepareStatement(preparedStatementText);
        } catch (SQLException ex) {
            String text = "Error preparing statement '" + preparedStatementText + '\'';
            log.error(text, ex);
            throw new ExprValidationException(text + ", reason: " + ex.getMessage());
        }

        // Interrogate prepared statement - parameters and result
        List<String> inputParameters = new LinkedList<String>();
        try {
            ParameterMetaData parameterMetaData = prepared.getParameterMetaData();
            inputParameters.addAll(Arrays.asList(parameters).subList(0, parameterMetaData.getParameterCount()));
        } catch (Exception ex) {
            try {
                prepared.close();
            } catch (SQLException e) {
                // don't handle
            }
            String text = "Error obtaining parameter metadata from prepared statement, consider turning off metadata interrogation via configuration, for statement '"
                    + preparedStatementText + '\'';
            log.error(text, ex);
            throw new ExprValidationException(text + ", please check the statement, reason: " + ex.getMessage());
        }

        Map<String, DBOutputTypeDesc> outputProperties;
        try {
            outputProperties = compileResultMetaData(prepared.getMetaData(), metadataSetting);
        } catch (SQLException ex) {
            try {
                prepared.close();
            } catch (SQLException e) {
                // don't handle
            }
            String text = "Error in statement '" + preparedStatementText
                    + "', failed to obtain result metadata, consider turning off metadata interrogation via configuration";
            log.error(text, ex);
            throw new ExprValidationException(text + ", please check the statement, reason: " + ex.getMessage());
        }

        if (log.isDebugEnabled()) {
            log.debug(".createDBEventStream in=" + inputParameters.toString() + " out="
                    + outputProperties.toString());
        }

        // Close statement
        try {
            prepared.close();
        } catch (SQLException e) {
            String text = "Error closing prepared statement";
            log.error(text, e);
            throw new ExprValidationException(text + ", reason: " + e.getMessage());
        }

        return new QueryMetaData(inputParameters, outputProperties);
    }

    private static String createPreparedStatement(List<PlaceholderParser.Fragment> parseFragements) {
        StringBuilder buffer = new StringBuilder();
        for (PlaceholderParser.Fragment fragment : parseFragements) {
            if (!fragment.isParameter()) {
                buffer.append(fragment.getValue());
            } else {
                if (fragment.getValue().equals(SAMPLE_WHERECLAUSE_PLACEHOLDER)) {
                    continue;
                }
                buffer.append('?');
            }
        }
        return buffer.toString();
    }

    private static String createSamplePlaceholderStatement(List<PlaceholderParser.Fragment> parseFragements) {
        StringBuilder buffer = new StringBuilder();
        for (PlaceholderParser.Fragment fragment : parseFragements) {
            if (!fragment.isParameter()) {
                buffer.append(fragment.getValue());
            } else {
                if (fragment.getValue().equals(SAMPLE_WHERECLAUSE_PLACEHOLDER)) {
                    buffer.append(" where 1=0 ");
                    break;
                } else {
                    buffer.append("null");
                }
            }
        }
        return buffer.toString();
    }

    private static SQLParameterDesc getParameters(List<PlaceholderParser.Fragment> parseFragements) {
        List<String> eventPropertyParams = new LinkedList<String>();
        for (PlaceholderParser.Fragment fragment : parseFragements) {
            if (fragment.isParameter()) {
                if (!fragment.getValue().equals(SAMPLE_WHERECLAUSE_PLACEHOLDER)) {
                    eventPropertyParams.add(fragment.getValue());
                }
            }
        }
        String[] parameters = eventPropertyParams.toArray(new String[eventPropertyParams.size()]);
        String[] builtin = eventPropertyParams.toArray(new String[eventPropertyParams.size()]);
        return new SQLParameterDesc(parameters, builtin);
    }

    private static Map<String, DBOutputTypeDesc> compileResultMetaData(ResultSetMetaData resultMetaData,
            ColumnSettings columnSettings) throws SQLException {
        Map<String, DBOutputTypeDesc> outputProperties = new HashMap<String, DBOutputTypeDesc>();
        for (int i = 0; i < resultMetaData.getColumnCount(); i++) {
            String columnName = resultMetaData.getColumnLabel(i + 1);
            if (columnName == null) {
                columnName = resultMetaData.getColumnName(i + 1);
            }
            int columnType = resultMetaData.getColumnType(i + 1);
            String javaClass = resultMetaData.getColumnTypeName(i + 1);

            ConfigurationDBRef.ColumnChangeCaseEnum caseEnum = columnSettings.getColumnCaseConversionEnum();
            if ((caseEnum != null) && (caseEnum == ConfigurationDBRef.ColumnChangeCaseEnum.LOWERCASE)) {
                columnName = columnName.toLowerCase();
            }
            if ((caseEnum != null) && (caseEnum == ConfigurationDBRef.ColumnChangeCaseEnum.UPPERCASE)) {
                columnName = columnName.toUpperCase();
            }

            DatabaseTypeBinding binding = null;
            String javaTypeBinding = null;
            if (columnSettings.getJavaSqlTypeBinding() != null) {
                javaTypeBinding = columnSettings.getJavaSqlTypeBinding().get(columnType);
            }
            if (javaTypeBinding != null) {
                binding = DatabaseTypeEnum.getEnum(javaTypeBinding).getBinding();
            }
            DBOutputTypeDesc outputType = new DBOutputTypeDesc(columnType, javaClass, binding);
            outputProperties.put(columnName, outputType);
        }
        return outputProperties;
    }

    private static final Log log = LogFactory.getLog(DatabasePollingViewableFactory.class);
}