/*
* Copyright 2015 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.cloud.stream.module.jdbc;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ResourceLoader;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.jdbc.JdbcMessageHandler;
import org.springframework.integration.jdbc.SqlParameterSourceFactory;
import org.springframework.integration.json.JsonPropertyAccessor;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.messaging.Message;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* A module that writes its incoming payload to an RDBMS using JDBC.
*
* @author Eric Bottard
* @author Thomas Risberg
*/
@EnableBinding(Sink.class)
@EnableConfigurationProperties(JdbcSinkProperties.class)
public class JdbcSinkConfiguration {
private static final Logger logger = LoggerFactory.getLogger(JdbcSinkConfiguration.class);
public static final Object NOT_SET = new Object();
private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
@Autowired
private BeanFactory beanFactory;
protected EvaluationContext evaluationContext;
@Autowired
private JdbcSinkProperties properties;
@Bean
@ServiceActivator(autoStartup = "false", inputChannel = Sink.INPUT)
public JdbcMessageHandler jdbcMessageHandler(DataSource dataSource) {
final MultiValueMap<String, Expression> columnExpressionVariations = new LinkedMultiValueMap<>();
for (Map.Entry<String, String> entry : properties.getColumns().entrySet()) {
String value = entry.getValue();
columnExpressionVariations.add(entry.getKey(), spelExpressionParser.parseExpression(value));
if (!value.startsWith("payload")) {
columnExpressionVariations.add(entry.getKey(), spelExpressionParser.parseExpression("payload." + value));
}
}
JdbcMessageHandler jdbcMessageHandler = new JdbcMessageHandler(dataSource,
generateSql(properties.getTableName(),columnExpressionVariations.keySet()));
jdbcMessageHandler.setSqlParameterSourceFactory(
new SqlParameterSourceFactory() {
@Override
public SqlParameterSource createParameterSource(Object o) {
if (!(o instanceof Message)) {
throw new IllegalArgumentException("Unable to handle type " + o.getClass().getName());
}
Message<?> message = (Message<?>) o;
MapSqlParameterSource parameterSource = new MapSqlParameterSource();
for (String key: columnExpressionVariations.keySet()) {
List<Expression> spels = columnExpressionVariations.get(key);
Object value = NOT_SET;
EvaluationException lastException = null;
for (Expression spel : spels) {
try {
value = spel.getValue(evaluationContext, message);
break;
}
catch (EvaluationException e) {
lastException = e;
}
}
if (value == NOT_SET) {
if (lastException != null) {
logger.info("Could not find value for column '" + key + "': " + lastException.getMessage());
}
parameterSource.addValue(key, null);
}
else {
if (value instanceof JsonPropertyAccessor.ToStringFriendlyJsonNode) {
// Need to do some reflection until we have a getter for the Node
DirectFieldAccessor dfa = new DirectFieldAccessor(value);
JsonNode node = (JsonNode) dfa.getPropertyValue("node");
Object valueToUse;
if (node == null || node.isNull()) {
valueToUse = null;
}
else if (node.isNumber()) {
valueToUse = node.numberValue();
}
else if (node.isBoolean()) {
valueToUse = node.booleanValue();
}
else {
valueToUse = node.textValue();
}
parameterSource.addValue(key, valueToUse);
}
else {
parameterSource.addValue(key, value);
}
}
}
return parameterSource;
}
});
return jdbcMessageHandler;
}
@ConditionalOnProperty("initialize")
@Bean
public DataSourceInitializer nonBootDataSourceInitializer(DataSource dataSource, ResourceLoader resourceLoader) {
DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
dataSourceInitializer.setDataSource(dataSource);
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
databasePopulator.setIgnoreFailedDrops(true);
dataSourceInitializer.setDatabasePopulator(databasePopulator);
if ("true".equals(properties.getInitialize())) {
databasePopulator.addScript(new DefaultInitializationScriptResource(properties));
}
else {
databasePopulator.addScript(resourceLoader.getResource(properties.getInitialize()));
}
return dataSourceInitializer;
}
/*
* This is needed to prevent a circular dependency issue with the creation of the converter.
*/
public static class Nested {
@Bean
@ConfigurationPropertiesBinding
public ShorthandMapConverter shorthandMapConverter() {
return new ShorthandMapConverter();
}
}
@PostConstruct
public void afterPropertiesSet() {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
}
private String generateSql(String tableName, Set<String> columns) {
StringBuilder builder = new StringBuilder("INSERT INTO ");
StringBuilder questionMarks = new StringBuilder(") VALUES (");
builder.append(tableName).append("(");
int i = 0;
for (String column : columns) {
if (i++ > 0) {
builder.append(", ");
questionMarks.append(", ");
}
builder.append(column);
questionMarks.append(':' + column);
}
builder.append(questionMarks).append(")");
return builder.toString();
}
}