/*
* Copyright (c) 2017, 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.siddhi.extension.table.rdbms;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.siddhi.annotation.Example;
import org.wso2.siddhi.annotation.Extension;
import org.wso2.siddhi.annotation.Parameter;
import org.wso2.siddhi.annotation.util.DataType;
import org.wso2.siddhi.core.exception.CannotLoadConfigurationException;
import org.wso2.siddhi.core.table.record.AbstractRecordTable;
import org.wso2.siddhi.core.table.record.ConditionBuilder;
import org.wso2.siddhi.core.table.record.RecordIterator;
import org.wso2.siddhi.core.util.SiddhiConstants;
import org.wso2.siddhi.core.util.collection.operator.CompiledCondition;
import org.wso2.siddhi.core.util.config.ConfigReader;
import org.wso2.siddhi.extension.table.rdbms.config.RDBMSQueryConfigurationEntry;
import org.wso2.siddhi.extension.table.rdbms.config.RDBMSTypeMapping;
import org.wso2.siddhi.extension.table.rdbms.exception.RDBMSTableException;
import org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableUtils;
import org.wso2.siddhi.query.api.annotation.Annotation;
import org.wso2.siddhi.query.api.annotation.Element;
import org.wso2.siddhi.query.api.definition.Attribute;
import org.wso2.siddhi.query.api.definition.TableDefinition;
import org.wso2.siddhi.query.api.util.AnnotationHelper;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import static org.wso2.siddhi.core.util.SiddhiConstants.ANNOTATION_STORE;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_FIELD_LENGTHS;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_JNDI_RESOURCE;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_PASSWORD;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_POOL_PROPERTIES;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_TABLE_NAME;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_URL;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.ANNOTATION_ELEMENT_USERNAME;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.CLOSE_PARENTHESIS;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.EQUALS;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.OPEN_PARENTHESIS;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_COLUMNS;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_COLUMNS_VALUES;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_CONDITION;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_INDEX;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_Q;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.PLACEHOLDER_TABLE_NAME;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.QUESTION_MARK;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.SEPARATOR;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.SQL_PRIMARY_KEY_DEF;
import static org.wso2.siddhi.extension.table.rdbms.util.RDBMSTableConstants.WHITESPACE;
/**
* Class representing the RDBMS Event Table implementation.
*/
@Extension(
name = "rdbms",
namespace = "store",
description = "Using this extension the data source or the connection instructions can be " +
"assigned to the event table.",
parameters = {
@Parameter(name = "jdbc.url",
description = "The JDBC URL for the RDBMS data store.",
type = {DataType.STRING}),
@Parameter(name = "username",
description = "The username for accessing the data store.",
type = {DataType.STRING}),
@Parameter(name = "password",
description = "The password for accessing the data store.",
type = {DataType.STRING}),
@Parameter(name = "pool.properties",
description = "Any pool properties for the DB connection, specified as key-value pairs.",
type = {DataType.STRING}),
@Parameter(name = "jndi.resource",
description = "Optional. The name of the JNDI resource (if any). If found, the connection " +
"parameters are not taken into account, and the connection will be attempted through "
+ "JNDI lookup instead.",
type = {DataType.STRING}),
@Parameter(name = "table.name",
description = "Optional. The name of the table in the store this Event Table should be " +
"persisted as. If not specified, the table name will be the same as the Siddhi table.",
type = {DataType.STRING}),
@Parameter(name = "field.length",
description = "Optional. The length of any String fields the table definition contains. If not "
+ "specified, the vendor-specific DB default will be chosen.",
type = {DataType.STRING}),
},
examples = {
@Example(
syntax = "@Store(type=\"rdbms\", jdbc.url=\"jdbc:mysql://localhost:3306/das\", " +
"username=\"root\", password=\"root\",field.length=\"symbol:100\")\n" +
"@PrimaryKey(\"symbol\")" +
"@Index(\"volume\")" +
"define table StockTable (symbol string, price float, volume long);",
description = "The above example will create an Event Table named 'StockTable' on the DB if " +
"it doesn't already exist (with 3 fields 'symbol', 'price', and 'volume' with types" +
"string, float and long respectively). The connection parameters will be as" +
" specified in the '@Store' annotation. The 'symbol' field will be declared a unique " +
"field, and a DB index will be created for the 'symbol' field."
)
}
)
public class RDBMSEventTable extends AbstractRecordTable {
private static final Log log = LogFactory.getLog(RDBMSEventTable.class);
private RDBMSQueryConfigurationEntry queryConfigurationEntry;
private DataSource dataSource;
private String tableName;
private List<Attribute> attributes;
@Override
protected void init(TableDefinition tableDefinition, ConfigReader configReader) {
this.attributes = tableDefinition.getAttributeList();
Annotation storeAnnotation = AnnotationHelper.getAnnotation(ANNOTATION_STORE, tableDefinition.getAnnotations());
Annotation primaryKeys = AnnotationHelper.getAnnotation(SiddhiConstants.ANNOTATION_PRIMARY_KEY,
tableDefinition.getAnnotations());
Annotation indices = AnnotationHelper.getAnnotation(SiddhiConstants.ANNOTATION_INDEX,
tableDefinition.getAnnotations());
RDBMSTableUtils.validateAnnotation(primaryKeys);
RDBMSTableUtils.validateAnnotation(indices);
String jndiResourceName = storeAnnotation.getElement(ANNOTATION_ELEMENT_JNDI_RESOURCE);
if (!RDBMSTableUtils.isEmpty(jndiResourceName)) {
try {
this.lookupDatasource(jndiResourceName);
} catch (NamingException e) {
throw new RDBMSTableException("Failed to lookup datasource with provided JNDI resource name '"
+ jndiResourceName + "': " + e.getMessage(), e);
}
} else {
this.initializeDatasource(storeAnnotation);
}
String tableName = storeAnnotation.getElement(ANNOTATION_ELEMENT_TABLE_NAME);
this.tableName = RDBMSTableUtils.isEmpty(tableName) ? tableDefinition.getId() : tableName;
try {
if (this.queryConfigurationEntry == null) {
this.queryConfigurationEntry = RDBMSTableUtils.lookupCurrentQueryConfigurationEntry(this.dataSource);
}
} catch (CannotLoadConfigurationException e) {
throw new RDBMSTableException("Failed to initialize DB Configuration entry for table '" + this.tableName
+ "': " + e.getMessage(), e);
}
if (!this.tableExists()) {
this.createTable(storeAnnotation, primaryKeys, indices);
}
}
@Override
protected void add(List<Object[]> records) {
String sql = this.composeInsertQuery();
try {
this.batchExecuteQueriesWithRecords(sql, records, false);
} catch (SQLException e) {
throw new RDBMSTableException("Error in adding events to '" + this.tableName + "' store: "
+ e.getMessage(), e);
}
}
@Override
protected RecordIterator<Object[]> find(Map<String, Object> findConditionParameterMap,
CompiledCondition compiledCondition) {
String selectQuery = this.resolveTableName(this.queryConfigurationEntry.getRecordSelectQuery());
String condition = ((RDBMSCompiledCondition) compiledCondition).getCompiledQuery();
Connection conn = this.getConnection();
PreparedStatement stmt = null;
ResultSet rs;
try {
stmt = RDBMSTableUtils.isEmpty(condition) ?
conn.prepareStatement(selectQuery.replace(PLACEHOLDER_CONDITION, "")) :
conn.prepareStatement(RDBMSTableUtils.formatQueryWithCondition(selectQuery, condition));
RDBMSTableUtils.resolveCondition(stmt, (RDBMSCompiledCondition) compiledCondition,
findConditionParameterMap, 0);
rs = stmt.executeQuery();
//Passing all java.sql artifacts to the iterator to ensure everything gets cleaned up at once.
return new RDBMSIterator(conn, stmt, rs, this.attributes, this.tableName);
} catch (SQLException e) {
RDBMSTableUtils.cleanupConnection(null, stmt, conn);
throw new RDBMSTableException("Error retrieving records from table '" + this.tableName + "': "
+ e.getMessage(), e);
}
}
@Override
protected boolean contains(Map<String, Object> containsConditionParameterMap, CompiledCondition compiledCondition) {
String containsQuery = this.resolveTableName(this.queryConfigurationEntry.getRecordExistsQuery());
String condition = ((RDBMSCompiledCondition) compiledCondition).getCompiledQuery();
Connection conn = this.getConnection();
PreparedStatement stmt = null;
ResultSet rs = null;
try {
stmt = RDBMSTableUtils.isEmpty(condition) ?
conn.prepareStatement(containsQuery.replace(PLACEHOLDER_CONDITION, "")) :
conn.prepareStatement(RDBMSTableUtils.formatQueryWithCondition(containsQuery, condition));
RDBMSTableUtils.resolveCondition(stmt, (RDBMSCompiledCondition) compiledCondition,
containsConditionParameterMap, 0);
rs = stmt.executeQuery();
return rs.next() && !rs.isBeforeFirst();
} catch (SQLException e) {
throw new RDBMSTableException("Error performing a contains check on table '" + this.tableName
+ "': " + e.getMessage(), e);
} finally {
RDBMSTableUtils.cleanupConnection(rs, stmt, conn);
}
}
@Override
protected void delete(List<Map<String, Object>> deleteConditionParameterMaps, CompiledCondition compiledCondition) {
String deleteQuery = this.resolveTableName(this.queryConfigurationEntry.getRecordDeleteQuery());
String condition = ((RDBMSCompiledCondition) compiledCondition).getCompiledQuery();
Connection conn = this.getConnection();
PreparedStatement stmt = null;
try {
stmt = RDBMSTableUtils.isEmpty(condition) ?
conn.prepareStatement(deleteQuery.replace(PLACEHOLDER_CONDITION, "")) :
conn.prepareStatement(RDBMSTableUtils.formatQueryWithCondition(deleteQuery, condition));
for (Map<String, Object> deleteConditionParameterMap : deleteConditionParameterMaps) {
RDBMSTableUtils.resolveCondition(stmt, (RDBMSCompiledCondition) compiledCondition,
deleteConditionParameterMap, 0);
stmt.addBatch();
}
stmt.executeBatch();
} catch (SQLException e) {
throw new RDBMSTableException("Error performing record deletion on table '" + this.tableName
+ "': " + e.getMessage(), e);
} finally {
RDBMSTableUtils.cleanupConnection(null, stmt, conn);
}
}
@Override
protected void update(List<Map<String, Object>> updateConditionParameterMaps, CompiledCondition compiledCondition,
List<Map<String, Object>> updateValues) {
String sql = this.composeUpdateQuery(compiledCondition);
try {
this.batchProcessUpdates(sql, updateConditionParameterMaps, compiledCondition, updateValues);
} catch (SQLException e) {
throw new RDBMSTableException("Error performing record update operations on table '" + this.tableName
+ "': " + e.getMessage(), e);
}
}
@Override
protected void updateOrAdd(List<Map<String, Object>> updateConditionParameterMaps,
CompiledCondition compiledCondition, List<Map<String, Object>> updateValues,
List<Object[]> addingRecords) {
this.updateOrInsertRecords(updateConditionParameterMaps, compiledCondition, updateValues, addingRecords);
}
@Override
protected CompiledCondition compileCondition(ConditionBuilder conditionBuilder) {
RDBMSConditionVisitor visitor = new RDBMSConditionVisitor(this.tableName);
conditionBuilder.build(visitor);
return new RDBMSCompiledCondition(visitor.returnCondition(), visitor.getParameters());
}
/**
* Method for looking up a datasource instance through JNDI.
*
* @param resourceName the name of the resource to be looked up.
* @throws NamingException if the lookup fails.
*/
private void lookupDatasource(String resourceName) throws NamingException {
this.dataSource = InitialContext.doLookup(resourceName);
}
/**
* Method for composing the SQL query for INSERT operations with proper placeholders.
*
* @return the composed SQL query in string form.
*/
private String composeInsertQuery() {
String insertQuery = this.resolveTableName(this.queryConfigurationEntry.getRecordInsertQuery());
StringBuilder params = new StringBuilder();
int fieldsLeft = this.attributes.size();
while (fieldsLeft > 0) {
params.append(QUESTION_MARK);
if (fieldsLeft > 1) {
params.append(SEPARATOR);
}
fieldsLeft = fieldsLeft - 1;
}
return insertQuery.replace(PLACEHOLDER_Q, params.toString());
}
/**
* Method for composing the SQL query for UPDATE operations with proper placeholders.
*
* @return the composed SQL query in string form.
*/
private String composeUpdateQuery(CompiledCondition compiledCondition) {
String sql = this.resolveTableName(this.queryConfigurationEntry.getRecordUpdateQuery());
String condition = ((RDBMSCompiledCondition) compiledCondition).getCompiledQuery();
StringBuilder columnsValues = new StringBuilder();
this.attributes.forEach(attribute -> {
columnsValues.append(attribute.getName()).append(EQUALS).append(QUESTION_MARK);
if (this.attributes.indexOf(attribute) != this.attributes.size() - 1) {
columnsValues.append(SEPARATOR);
}
});
sql = sql.replace(PLACEHOLDER_COLUMNS_VALUES, columnsValues.toString());
sql = RDBMSTableUtils.isEmpty(condition) ? sql.replace(PLACEHOLDER_CONDITION, "") :
RDBMSTableUtils.formatQueryWithCondition(sql, condition);
return sql;
}
/**
* Method for processing update operations in a batched manner. This assumes that all update operations will be
* accepted by the database.
*
* @param sql the SQL update operation as string.
* @param updateConditionParameterMaps the runtime parameters that should be populated to the condition.
* @param compiledCondition the condition that was built during compile time.
* @param updateValues the runtime parameters that should be populated to the update statement.
* @throws SQLException if the update operation fails.
*/
private void batchProcessUpdates(String sql, List<Map<String, Object>> updateConditionParameterMaps,
CompiledCondition compiledCondition,
List<Map<String, Object>> updateValues) throws SQLException {
final int seed = this.attributes.size();
Connection conn = this.getConnection();
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(sql);
Iterator<Map<String, Object>> conditionParamIterator = updateConditionParameterMaps.iterator();
Iterator<Map<String, Object>> valueIterator = updateValues.iterator();
while (conditionParamIterator.hasNext() && valueIterator.hasNext()) {
Map<String, Object> conditionParameters = conditionParamIterator.next();
Map<String, Object> values = valueIterator.next();
//Incrementing the ordinals of the conditions in the statement with the # of variables to be updated
RDBMSTableUtils.resolveCondition(stmt, (RDBMSCompiledCondition) compiledCondition, conditionParameters,
seed);
for (Attribute attribute : this.attributes) {
RDBMSTableUtils.populateStatementWithSingleElement(stmt, this.attributes.indexOf(attribute) + 1,
attribute.getType(), values.get(attribute.getName()));
}
stmt.addBatch();
}
stmt.executeBatch();
} finally {
RDBMSTableUtils.cleanupConnection(null, stmt, conn);
}
}
/**
* Method for performing insert/update operations for a given dataset.
*
* @param updateConditionParameterMaps update parameters that should be populated for each condition.
* @param compiledCondition the condition that was built during compile time.
* @param updateValues the values for which the update operation should be done
* (i.e. the new values).
* @param addingRecords the records that should be inserted to the DB should the update operation
* fail.
*/
private void updateOrInsertRecords(List<Map<String, Object>> updateConditionParameterMaps,
CompiledCondition compiledCondition, List<Map<String, Object>> updateValues,
List<Object[]> addingRecords) {
int counter = 0;
final int seed = this.attributes.size();
Connection conn = this.getConnection(false);
PreparedStatement updateStmt = null;
PreparedStatement insertStmt = null;
try {
updateStmt = conn.prepareStatement(this.composeUpdateQuery(compiledCondition));
insertStmt = conn.prepareStatement(this.composeInsertQuery());
while (counter < updateValues.size()) {
int rowsChanged;
Map<String, Object> conditionParameters = updateConditionParameterMaps.get(counter);
Map<String, Object> values = updateValues.get(counter);
//Incrementing the ordinals of the conditions in the statement with the # of variables to be updated
RDBMSTableUtils.resolveCondition(updateStmt, (RDBMSCompiledCondition) compiledCondition,
conditionParameters, seed);
for (Attribute attribute : this.attributes) {
RDBMSTableUtils.populateStatementWithSingleElement(updateStmt,
this.attributes.indexOf(attribute) + 1, attribute.getType(),
values.get(attribute.getName()));
}
rowsChanged = updateStmt.executeUpdate();
conn.commit();
if (rowsChanged < 1) {
Object[] record = addingRecords.get(counter);
try {
this.populateStatement(record, insertStmt);
insertStmt.executeUpdate();
conn.commit();
} catch (SQLException e2) {
RDBMSTableUtils.rollbackConnection(conn);
throw new RDBMSTableException("Error performing update/insert operation (insert) on table '"
+ this.tableName + "': " + e2.getMessage(), e2);
}
}
counter++;
}
} catch (SQLException e) {
throw new RDBMSTableException("Error performing update/insert operation (update) on table '"
+ this.tableName
+ "': " + e.getMessage(), e);
} finally {
RDBMSTableUtils.cleanupConnection(null, updateStmt, null);
RDBMSTableUtils.cleanupConnection(null, insertStmt, conn);
}
}
/**
* Method for creating and initializing the datasource instance given the "@Store" annotation.
*
* @param storeAnnotation the source annotation which contains the needed parameters.
*/
private void initializeDatasource(Annotation storeAnnotation) {
Properties connectionProperties = new Properties();
String poolPropertyString = storeAnnotation.getElement(ANNOTATION_ELEMENT_POOL_PROPERTIES);
String url = storeAnnotation.getElement(ANNOTATION_ELEMENT_URL);
String username = storeAnnotation.getElement(ANNOTATION_ELEMENT_USERNAME);
String password = storeAnnotation.getElement(ANNOTATION_ELEMENT_PASSWORD);
if (RDBMSTableUtils.isEmpty(url)) {
throw new RDBMSTableException("Required parameter '" + ANNOTATION_ELEMENT_URL + "' for DB " +
"connectivity cannot be empty.");
}
if (RDBMSTableUtils.isEmpty(username)) {
throw new RDBMSTableException("Required parameter '" + ANNOTATION_ELEMENT_USERNAME + "' for DB " +
"connectivity cannot be empty.");
}
if (RDBMSTableUtils.isEmpty(password)) {
throw new RDBMSTableException("Required parameter '" + ANNOTATION_ELEMENT_PASSWORD + "' for DB " +
"connectivity cannot be empty.");
}
connectionProperties.setProperty("jdbcUrl", url);
connectionProperties.setProperty("dataSource.user", username);
connectionProperties.setProperty("dataSource.password", password);
if (poolPropertyString != null) {
List<String[]> poolProps = RDBMSTableUtils.processKeyValuePairs(poolPropertyString);
poolProps.forEach(pair -> connectionProperties.setProperty(pair[0], pair[1]));
}
HikariConfig config = new HikariConfig(connectionProperties);
this.dataSource = new HikariDataSource(config);
}
/**
* Returns a connection instance assuming that autocommit should be true.
*
* @return a new {@link Connection} instance from the datasource.
*/
private Connection getConnection() {
return this.getConnection(true);
}
/**
* Returns a connection instance.
*
* @param autoCommit whether or not transactions to the connections should be committed automatically.
* @return a new {@link Connection} instance from the datasource.
*/
private Connection getConnection(boolean autoCommit) {
Connection conn;
try {
conn = this.dataSource.getConnection();
conn.setAutoCommit(autoCommit);
} catch (SQLException e) {
throw new RDBMSTableException("Error initializing connection: " + e.getMessage(), e);
}
return conn;
}
/**
* Method for replacing the placeholder for the table name with the Event Table's name.
*
* @param statement the SQL statement in string form.
* @return the formatted SQL statement.
*/
private String resolveTableName(String statement) {
if (statement == null) {
return null;
}
return statement.replace(PLACEHOLDER_TABLE_NAME, this.tableName);
}
/**
* Method for creating a table on the data store in question, if it does not exist already.
*
* @param storeAnnotation the "@Store" annotation that contains the connection properties.
* @param primaryKeys the unique keys that should be set for the table.
* @param indices the DB indices that should be set for the table.
*/
private void createTable(Annotation storeAnnotation, Annotation primaryKeys, Annotation indices) {
RDBMSTypeMapping typeMapping = this.queryConfigurationEntry.getRdbmsTypeMapping();
StringBuilder builder = new StringBuilder();
List<Element> primaryKeyList = (primaryKeys == null) ? new ArrayList<>() : primaryKeys.getElements();
List<Element> indexElementList = (indices == null) ? new ArrayList<>() : indices.getElements();
List<String> queries = new ArrayList<>();
String createQuery = this.resolveTableName(this.queryConfigurationEntry.getTableCreateQuery());
String indexQuery = this.resolveTableName(this.queryConfigurationEntry.getIndexCreateQuery());
Map<String, String> fieldLengths = RDBMSTableUtils.processFieldLengths(storeAnnotation.getElement(
ANNOTATION_ELEMENT_FIELD_LENGTHS));
this.validateFieldLengths(fieldLengths);
this.attributes.forEach(attribute -> {
builder.append(attribute.getName()).append(WHITESPACE);
switch (attribute.getType()) {
case BOOL:
builder.append(typeMapping.getBooleanType());
break;
case DOUBLE:
builder.append(typeMapping.getDoubleType());
break;
case FLOAT:
builder.append(typeMapping.getFloatType());
break;
case INT:
builder.append(typeMapping.getIntegerType());
break;
case LONG:
builder.append(typeMapping.getLongType());
break;
case OBJECT:
builder.append(typeMapping.getBinaryType());
break;
case STRING:
builder.append(typeMapping.getStringType());
if (this.queryConfigurationEntry.getStringSize() != null) {
builder.append(OPEN_PARENTHESIS);
if (fieldLengths.containsKey(attribute.getName())) {
builder.append(fieldLengths.get(attribute.getName()));
} else {
builder.append(this.queryConfigurationEntry.getStringSize());
}
builder.append(CLOSE_PARENTHESIS);
}
break;
}
if (this.attributes.indexOf(attribute) != this.attributes.size() - 1 || !primaryKeyList.isEmpty()) {
builder.append(SEPARATOR);
}
});
if (primaryKeyList != null && !primaryKeyList.isEmpty()) {
builder.append(SQL_PRIMARY_KEY_DEF)
.append(OPEN_PARENTHESIS)
.append(RDBMSTableUtils.flattenAnnotatedElements(primaryKeyList))
.append(CLOSE_PARENTHESIS);
}
queries.add(createQuery.replace(PLACEHOLDER_COLUMNS, builder.toString()));
if (indexElementList != null && !indexElementList.isEmpty()) {
queries.add(indexQuery.replace(PLACEHOLDER_INDEX,
RDBMSTableUtils.flattenAnnotatedElements(indexElementList)));
}
try {
this.executeDDQueries(queries, false);
} catch (SQLException e) {
throw new RDBMSTableException("Unable to initialize table '" + this.tableName + "': " + e.getMessage(), e);
}
}
/**
* Method used to validate the field length specifications and ensure that the table definition contains them.
*
* @param fieldLengths the specified list of custom string field lengths.
*/
private void validateFieldLengths(Map<String, String> fieldLengths) {
List<String> attributeNames = new ArrayList<>();
this.attributes.forEach(attribute -> attributeNames.add(attribute.getName()));
fieldLengths.keySet().forEach(field -> {
if (!attributeNames.contains(field)) {
throw new RDBMSTableException("Field '" + field + "' (for which a size of " + fieldLengths.get(field)
+ " has been specified) does not exist in the table's list of fields.");
}
});
}
/**
* Method for performing data definition queries for the current datasource.
*
* @param queries the list of queries to be executed.
* @param autocommit whether or not the transactions should automatically be committed.
* @throws SQLException if the query execution fails.
*/
private void executeDDQueries(List<String> queries, boolean autocommit) throws SQLException {
Connection conn = this.getConnection(autocommit);
boolean committed = autocommit;
PreparedStatement stmt;
try {
for (String query : queries) {
stmt = conn.prepareStatement(query);
stmt.execute();
RDBMSTableUtils.cleanupConnection(null, stmt, null);
}
if (!autocommit) {
conn.commit();
committed = true;
}
} catch (SQLException e) {
if (!autocommit) {
RDBMSTableUtils.rollbackConnection(conn);
}
throw e;
} finally {
if (!committed) {
RDBMSTableUtils.rollbackConnection(conn);
}
RDBMSTableUtils.cleanupConnection(null, null, conn);
}
}
/**
* Given a set of records and a query, this method performs that query per each record.
*
* @param query the query to be executed.
* @param records the records to use.
* @param autocommit whether or not the transactions should automatically be committed.
* @throws SQLException if the query execution fails.
*/
private void batchExecuteQueriesWithRecords(String query, List<Object[]> records, boolean autocommit)
throws SQLException {
PreparedStatement stmt = null;
boolean committed = autocommit;
//TODO check if autocommit needs to be false (e.g. for Postgres case)
Connection conn = this.getConnection(autocommit);
try {
stmt = conn.prepareStatement(query);
for (Object[] record : records) {
this.populateStatement(record, stmt);
stmt.addBatch();
}
stmt.executeBatch();
if (!autocommit) {
conn.commit();
committed = true;
}
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Attempted execution of query [" + query + "] produced an exception: " + e.getMessage());
}
if (!autocommit) {
RDBMSTableUtils.rollbackConnection(conn);
}
throw e;
} finally {
if (!committed) {
RDBMSTableUtils.rollbackConnection(conn);
}
RDBMSTableUtils.cleanupConnection(null, stmt, conn);
}
}
/**
* Method for checking whether or not the given table (which reflects the current event table instance) exists.
*
* @return true/false based on the table existence.
*/
private boolean tableExists() {
Connection conn = this.getConnection();
PreparedStatement stmt = null;
ResultSet rs = null;
try {
String query = this.resolveTableName(this.queryConfigurationEntry.getTableCheckQuery());
stmt = conn.prepareStatement(query);
rs = stmt.executeQuery();
return true;
} catch (SQLException e) {
if (log.isDebugEnabled()) {
log.debug("Table '" + this.tableName + "' assumed to not exist since its existence check resulted "
+ "in exception " + e.getMessage());
}
return false;
} finally {
RDBMSTableUtils.cleanupConnection(rs, stmt, conn);
}
}
/**
* Method for populating values to a pre-created SQL prepared statement.
*
* @param record the record whose values should be populated.
* @param stmt the statement to which the values should be set.
*/
private void populateStatement(Object[] record, PreparedStatement stmt) {
Attribute attribute = null;
try {
for (int i = 0; i < this.attributes.size(); i++) {
attribute = this.attributes.get(i);
Object value = record[i];
if (value != null || attribute.getType() == Attribute.Type.STRING) {
RDBMSTableUtils.populateStatementWithSingleElement(stmt, i + 1, attribute.getType(), value);
} else {
throw new RDBMSTableException("Cannot Execute Insert/Update: null value detected for " +
"attribute '" + attribute.getName() + "'");
}
}
} catch (SQLException e) {
throw new RDBMSTableException("Dropping event since value for attribute name " + attribute.getName() +
"cannot be set: " + e.getMessage(), e);
}
}
}