/*
* Copyright 2002-2016 the original author or authors.
*
* 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.springframework.integration.jdbc;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapperResultSetExtractor;
import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
import org.springframework.util.LinkedCaseInsensitiveMap;
/**
* A message handler that executes an SQL update. Dynamic query parameters are supported through the
* {@link SqlParameterSourceFactory} abstraction, the default implementation of which wraps the message so that its bean
* properties can be referred to by name in the query string E.g.
*
* <pre class="code">
* INSERT INTO FOOS (MESSAGE_ID, PAYLOAD) VALUES (:headers[id], :payload)
* </pre>
*
* N.B. do not use quotes to escape the header keys. The default SQL parameter source (from Spring JDBC) can also handle
* headers with dotted names (e.g. <code>business.id</code>)
*
* @author Dave Syer
* @author Artem Bilan
* @since 2.0
*/
public class JdbcMessageHandler extends AbstractMessageHandler {
private final ResultSetExtractor<List<Map<String, Object>>> generatedKeysResultSetExtractor =
new RowMapperResultSetExtractor<Map<String, Object>>(new ColumnMapRowMapper(), 1);
private final NamedParameterJdbcOperations jdbcOperations;
private final PreparedStatementCreator generatedKeysStatementCreator = con ->
con.prepareStatement(JdbcMessageHandler.this.updateSql, Statement.RETURN_GENERATED_KEYS);
private volatile String updateSql;
private volatile SqlParameterSourceFactory sqlParameterSourceFactory;
private volatile boolean keysGenerated;
private MessagePreparedStatementSetter preparedStatementSetter;
/**
* Constructor taking {@link DataSource} from which the DB Connection can be obtained and the select query to
* execute to retrieve new rows.
*
* @param dataSource Must not be null
* @param updateSql query to execute
*/
public JdbcMessageHandler(DataSource dataSource, String updateSql) {
this.jdbcOperations = new NamedParameterJdbcTemplate(dataSource);
this.updateSql = updateSql;
}
/**
* Constructor taking {@link JdbcOperations} instance to use for query execution and the select query to execute to
* retrieve new rows.
*
* @param jdbcOperations instance to use for query execution
* @param updateSql query to execute
*/
public JdbcMessageHandler(JdbcOperations jdbcOperations, String updateSql) {
this.jdbcOperations = new NamedParameterJdbcTemplate(jdbcOperations);
this.updateSql = updateSql;
}
/**
* Flag to indicate that the update query is an insert with auto-generated keys,
* which will be logged at debug level.
* @param keysGenerated the flag value to set
*/
public void setKeysGenerated(boolean keysGenerated) {
this.keysGenerated = keysGenerated;
}
public void setUpdateSql(String updateSql) {
this.updateSql = updateSql;
}
public void setSqlParameterSourceFactory(SqlParameterSourceFactory sqlParameterSourceFactory) {
this.sqlParameterSourceFactory = sqlParameterSourceFactory;
}
/**
* Specify a {@link MessagePreparedStatementSetter} to populate parameters on the
* {@link PreparedStatement} with the {@link Message} context.
* <p>This is a low-level alternative to the {@link SqlParameterSourceFactory}.
* @param preparedStatementSetter the {@link MessagePreparedStatementSetter} to set.
* @since 4.2
*/
public void setPreparedStatementSetter(MessagePreparedStatementSetter preparedStatementSetter) {
this.preparedStatementSetter = preparedStatementSetter;
}
@Override
public String getComponentType() {
return "jdbc:outbound-channel-adapter";
}
@Override
protected void onInit() throws Exception {
super.onInit();
Assert.state(!(this.sqlParameterSourceFactory != null && this.preparedStatementSetter != null),
"'sqlParameterSourceFactory' and 'preparedStatementSetter' are mutually exclusive.");
if (this.sqlParameterSourceFactory == null && this.preparedStatementSetter == null) {
this.sqlParameterSourceFactory = new BeanPropertySqlParameterSourceFactory();
}
}
/**
* Executes the update, passing the message into the {@link SqlParameterSourceFactory}.
*/
@Override
protected void handleMessageInternal(Message<?> message) throws Exception {
List<? extends Map<String, Object>> keys = executeUpdateQuery(message, this.keysGenerated);
if (!keys.isEmpty() && logger.isDebugEnabled()) {
logger.debug("Generated keys: " + keys);
}
}
protected List<? extends Map<String, Object>> executeUpdateQuery(final Message<?> message, boolean keysGenerated) {
SqlParameterSource updateParameterSource = EmptySqlParameterSource.INSTANCE;
if (this.preparedStatementSetter == null) {
if (this.sqlParameterSourceFactory != null) {
updateParameterSource = this.sqlParameterSourceFactory.createParameterSource(message);
}
}
if (keysGenerated) {
if (this.preparedStatementSetter != null) {
return this.jdbcOperations.getJdbcOperations().execute(this.generatedKeysStatementCreator,
(PreparedStatementCallback<List<Map<String, Object>>>) ps -> {
JdbcMessageHandler.this.preparedStatementSetter.setValues(ps, message);
ps.executeUpdate();
ResultSet keys = ps.getGeneratedKeys(); // NOSONAR closed in JdbcUtils
if (keys != null) {
try {
return JdbcMessageHandler.this.generatedKeysResultSetExtractor.extractData(keys);
}
finally {
JdbcUtils.closeResultSet(keys);
}
}
return new LinkedList<Map<String, Object>>();
});
}
else {
KeyHolder keyHolder = new GeneratedKeyHolder();
this.jdbcOperations.update(this.updateSql, updateParameterSource, keyHolder);
return keyHolder.getKeyList();
}
}
else {
int updated;
if (this.preparedStatementSetter != null) {
updated = this.jdbcOperations.getJdbcOperations().update(this.updateSql,
ps -> JdbcMessageHandler.this.preparedStatementSetter.setValues(ps, message));
}
else {
updated = this.jdbcOperations.update(this.updateSql, updateParameterSource);
}
LinkedCaseInsensitiveMap<Object> map = new LinkedCaseInsensitiveMap<Object>();
map.put("UPDATED", updated);
return Collections.singletonList(map);
}
}
}