Initial Commit

Migrating the existing structure from the following location:
https://github.com/spring-cloud-stream-app-starters/stream-applications/tree/restructuring
This commit is contained in:
Soby Chacko
2020-05-04 17:50:58 -04:00
commit 6edfb852f9
132 changed files with 8765 additions and 0 deletions

30
consumer/cassandra-consumer/.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/

View File

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>cassandra-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>cassandra-consumer</name>
<description>Cassandra Consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<properties>
<springIntegrationCassandara.version>0.8.0.BUILD-SNAPSHOT</springIntegrationCassandara.version>
<cassandra-unit-spring.version>4.3.1.0</cassandra-unit-spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-cassandra-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-cassandra</artifactId>
<version>${springIntegrationCassandara.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.cassandraunit</groupId>
<artifactId>cassandra-unit-spring</artifactId>
<version>${cassandra-unit-spring.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.addthis.metrics</groupId>
<artifactId>reporter-config3</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2015-2019 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.fn.consumer.cassandra;
import java.sql.Date;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.fn.consumer.cassandra.query.InsertQueryColumnNameExtractor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.cassandra.core.InsertOptions;
import org.springframework.data.cassandra.core.ReactiveCassandraOperations;
import org.springframework.data.cassandra.core.UpdateOptions;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.data.cassandra.core.cql.WriteOptions;
import org.springframework.integration.cassandra.outbound.CassandraMessageHandler;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.integration.transformer.AbstractPayloadTransformer;
import org.springframework.messaging.MessageHandler;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.StdDateFormat;
import org.springframework.cloud.fn.consumer.cassandra.query.ColumnNameExtractor;
import org.springframework.cloud.fn.consumer.cassandra.query.UpdateQueryColumnNameExtractor;
import reactor.core.publisher.Mono;
/**
* @author Artem Bilan
* @author Thomas Risberg
* @author Ashu Gairola
* @author Akos Ratku
*/
@Configuration
@EnableConfigurationProperties(CassandraConsumerProperties.class)
public class CassandraConsumerConfiguration {
@Autowired
private CassandraConsumerProperties cassandraSinkProperties;
@Bean
public IntegrationFlow cassandraConsumerFlow(MessageHandler cassandraSinkMessageHandler,
ObjectMapper objectMapper) {
IntegrationFlowBuilder integrationFlowBuilder =
IntegrationFlows.from(CassandraConsumerFunction.class);
if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) {
integrationFlowBuilder.transform(
new PayloadToMatrixTransformer(objectMapper, this.cassandraSinkProperties.getIngestQuery(),
CassandraMessageHandler.Type.UPDATE == this.cassandraSinkProperties.getQueryType()
? new UpdateQueryColumnNameExtractor()
: new InsertQueryColumnNameExtractor()));
}
return integrationFlowBuilder
.handle(cassandraSinkMessageHandler)
.get();
}
@Bean
public MessageHandler cassandraSinkMessageHandler(ReactiveCassandraOperations cassandraOperations) {
CassandraMessageHandler cassandraMessageHandler =
this.cassandraSinkProperties.getQueryType() != null
? new CassandraMessageHandler(cassandraOperations, this.cassandraSinkProperties.getQueryType())
: new CassandraMessageHandler(cassandraOperations);
cassandraMessageHandler.setProducesReply(true);
cassandraMessageHandler.setAsync(true);
if (this.cassandraSinkProperties.getConsistencyLevel() != null
|| this.cassandraSinkProperties.getTtl() > 0) {
WriteOptions.WriteOptionsBuilder writeOptionsBuilder = WriteOptions.builder();
switch (this.cassandraSinkProperties.getQueryType()) {
case INSERT:
writeOptionsBuilder = InsertOptions.builder();
break;
case UPDATE:
writeOptionsBuilder = UpdateOptions.builder();
break;
}
if (this.cassandraSinkProperties.getConsistencyLevel() != null) {
writeOptionsBuilder.consistencyLevel(this.cassandraSinkProperties.getConsistencyLevel());
}
if (this.cassandraSinkProperties.getTtl() > 0) {
writeOptionsBuilder.ttl(this.cassandraSinkProperties.getTtl());
}
cassandraMessageHandler.setWriteOptions(writeOptionsBuilder.build());
}
if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) {
cassandraMessageHandler.setIngestQuery(this.cassandraSinkProperties.getIngestQuery());
}
else if (this.cassandraSinkProperties.getStatementExpression() != null) {
cassandraMessageHandler.setStatementExpression(this.cassandraSinkProperties.getStatementExpression());
}
return cassandraMessageHandler;
}
private static boolean isUuid(String uuid) {
if (uuid.length() == 36) {
String[] parts = uuid.split("-");
if (parts.length == 5) {
return (parts[0].length() == 8) && (parts[1].length() == 4) &&
(parts[2].length() == 4) && (parts[3].length() == 4) &&
(parts[4].length() == 12);
}
}
return false;
}
private static class PayloadToMatrixTransformer extends AbstractPayloadTransformer<Object, List<List<Object>>> {
private final Jackson2JsonObjectMapper jsonObjectMapper;
private final List<String> columns = new LinkedList<>();
private final ISO8601StdDateFormat dateFormat = new ISO8601StdDateFormat();
PayloadToMatrixTransformer(ObjectMapper objectMapper, String query, ColumnNameExtractor columnNameExtractor) {
this.jsonObjectMapper = new Jackson2JsonObjectMapper(objectMapper);
this.columns.addAll(columnNameExtractor.extract(query));
this.jsonObjectMapper.getObjectMapper()
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
@Override
@SuppressWarnings("unchecked")
protected List<List<Object>> transformPayload(Object payload) {
if (payload instanceof List) {
return (List<List<Object>>) payload;
}
else {
try {
List<Map<String, Object>> model = this.jsonObjectMapper.fromJson(payload, List.class);
List<List<Object>> data = new ArrayList<>(model.size());
for (Map<String, Object> entity : model) {
List<Object> row = new ArrayList<>(this.columns.size());
for (String column : this.columns) {
Object value = entity.get(column);
if (value instanceof String) {
String string = (String) value;
if (this.dateFormat.looksLikeISO8601(string)) {
synchronized (this.dateFormat) {
value = new Date(this.dateFormat.parse(string).getTime()).toLocalDate();
}
}
if (isUuid(string)) {
value = UUID.fromString(string);
}
}
row.add(value);
}
data.add(row);
}
return data;
}
catch (Exception ex) {
throw new IllegalArgumentException("Cannot parse json into matrix", ex);
}
}
}
}
/*
* We need this to provide visibility to the protected method.
*/
@SuppressWarnings("serial")
private static class ISO8601StdDateFormat extends StdDateFormat {
@Override
protected boolean looksLikeISO8601(String dateStr) {
return super.looksLikeISO8601(dateStr);
}
}
interface CassandraConsumerFunction extends Function<Object, Mono<? extends WriteResult>> {
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2019-2020 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.fn.consumer.cassandra;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.integration.cassandra.outbound.CassandraMessageHandler;
import com.datastax.oss.driver.api.core.ConsistencyLevel;
/**
* @author Artem Bilan
* @author Thomas Risberg
*/
@ConfigurationProperties("cassandra")
public class CassandraConsumerProperties {
/**
* Time-to-live option of WriteOptions.
*/
private int ttl;
/**
* QueryType for Cassandra Sink.
*/
private CassandraMessageHandler.Type queryType;
/**
* Ingest Cassandra query.
*/
private String ingestQuery;
/**
* Expression in Cassandra query DSL style.
*/
private Expression statementExpression;
/**
* The consistency level for write operation.
*/
private ConsistencyLevel consistencyLevel;
public int getTtl() {
return this.ttl;
}
public void setTtl(int ttl) {
this.ttl = ttl;
}
public CassandraMessageHandler.Type getQueryType() {
return this.queryType;
}
public void setQueryType(CassandraMessageHandler.Type queryType) {
this.queryType = queryType;
}
public String getIngestQuery() {
return this.ingestQuery;
}
public void setIngestQuery(String ingestQuery) {
this.ingestQuery = ingestQuery;
}
public Expression getStatementExpression() {
return this.statementExpression;
}
public void setStatementExpression(Expression statementExpression) {
this.statementExpression = statementExpression;
}
public ConsistencyLevel getConsistencyLevel() {
return this.consistencyLevel;
}
public void setConsistencyLevel(ConsistencyLevel consistencyLevel) {
this.consistencyLevel = consistencyLevel;
}
}

View File

@@ -0,0 +1,157 @@
/*
* Copyright 2019-2020 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.fn.consumer.cassandra.cluster;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Scanner;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties;
import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.domain.EntityScanPackages;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.cassandra.config.CqlSessionFactoryBean;
import org.springframework.data.cassandra.core.ReactiveCassandraTemplate;
import org.springframework.data.cassandra.core.cql.CqlTemplate;
import org.springframework.data.cassandra.core.cql.ReactiveCqlOperations;
import org.springframework.data.cassandra.core.cql.generator.CreateKeyspaceCqlGenerator;
import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification;
import org.springframework.util.StringUtils;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
import reactor.core.publisher.Flux;
/**
* @author Artem Bilan
* @author Thomas Risberg
* @author Rob Hardt
*/
@Configuration
@EnableConfigurationProperties(CassandraClusterProperties.class)
@Import(CassandraAppClusterConfiguration.CassandraPackageRegistrar.class)
public class CassandraAppClusterConfiguration {
@Bean
public CqlSessionBuilderCustomizer clusterBuilderCustomizer(
CassandraClusterProperties cassandraClusterProperties) {
PropertyMapper map = PropertyMapper.get();
return builder ->
map.from(cassandraClusterProperties::isSkipSslValidation)
.whenTrue()
.toCall(() -> {
try {
builder.withSslContext(TrustAllSSLContextFactory.getSslContext());
}
catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new BeanInitializationException(
"Unable to configure a Cassandra cluster using SSL.", e);
}
});
}
@Bean
@ConditionalOnProperty("cassandra.cluster.create-keyspace")
public Object keyspaceCreator(CassandraProperties cassandraProperties, CqlSessionBuilder cqlSessionBuilder) {
CreateKeyspaceSpecification createKeyspaceSpecification =
CreateKeyspaceSpecification
.createKeyspace(cassandraProperties.getKeyspaceName())
.withSimpleReplication()
.ifNotExists();
String createKeySpaceQuery = new CreateKeyspaceCqlGenerator(createKeyspaceSpecification).toCql();
CqlSession systemSession =
cqlSessionBuilder.withKeyspace(CqlSessionFactoryBean.CASSANDRA_SYSTEM_SESSION).build();
CqlTemplate template = new CqlTemplate(systemSession);
template.execute(createKeySpaceQuery);
return null;
}
@Bean
@Lazy
@DependsOn("keyspaceCreator")
public CqlSession cassandraSession(CqlSessionBuilder cqlSessionBuilder) {
return cqlSessionBuilder.build();
}
@Bean
@ConditionalOnProperty("cassandra.cluster.init-script")
public Object keyspaceInitializer(CassandraClusterProperties cassandraClusterProperties,
ReactiveCassandraTemplate reactiveCassandraTemplate) throws IOException {
String scripts =
new Scanner(cassandraClusterProperties.getInitScript().getInputStream(),
StandardCharsets.UTF_8.name())
.useDelimiter("\\A")
.next();
ReactiveCqlOperations reactiveCqlOperations =
reactiveCassandraTemplate.getReactiveCqlOperations();
Flux.fromArray(StringUtils.delimitedListToStringArray(scripts, ";", "\r\n\f"))
.filter(StringUtils::hasText) // an empty String after the last ';'
.flatMap(script -> reactiveCqlOperations.execute(script + ";"))
.blockLast();
return null;
}
static class CassandraPackageRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
Binder.get(this.environment)
.bind("cassandra.cluster.entity-base-packages", String[].class)
.map(Arrays::asList)
.ifBound(packagesToScan -> EntityScanPackages.register(registry, packagesToScan));
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2015-2020 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.fn.consumer.cassandra.cluster;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
/**
* Common properties for the cassandra modules.
*
* @author Artem Bilan
* @author Thomas Risberg
* @author Rob Hardt
*/
@ConfigurationProperties("cassandra.cluster")
public class CassandraClusterProperties {
/**
* Flag to create (or not) keyspace on application startup.
*/
private boolean createKeyspace;
/**
* Resource with CQL scripts (delimited by ';') to initialize keyspace schema.
*/
private Resource initScript;
/**
* Flag to validate the Servers' SSL certs
*/
private boolean skipSslValidation;
/**
* Base packages to scan for entities annotated with Table annotations.
*/
private String[] entityBasePackages = { };
public void setCreateKeyspace(boolean createKeyspace) {
this.createKeyspace = createKeyspace;
}
public void setInitScript(Resource initScript) {
this.initScript = initScript;
}
public void setSkipSslValidation(boolean skipSslValidation) {
this.skipSslValidation = skipSslValidation;
}
public boolean isCreateKeyspace() {
return this.createKeyspace;
}
public Resource getInitScript() {
return this.initScript;
}
public boolean isSkipSslValidation() {
return this.skipSslValidation;
}
public String[] getEntityBasePackages() {
return this.entityBasePackages;
}
public void setEntityBasePackages(String[] entityBasePackages) {
this.entityBasePackages = entityBasePackages;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2019 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.fn.consumer.cassandra.cluster;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
/**
* Helper to provide an SSL Context that does not validate
* certificates presented in the SSL handshake.
*
* The usual caveats apply.
*
* @author Rob Hardt
* @author Artem Bilan
*/
class TrustAllSSLContextFactory {
static SSLContext getSslContext() throws NoSuchAlgorithmException, KeyManagementException {
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
@Override
public void checkClientTrusted(X509Certificate[] certs, String authType) {
}
@Override
public void checkServerTrusted(X509Certificate[] certs, String authType) {
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
return sc;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2017-2019 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.fn.consumer.cassandra.query;
import java.util.List;
/**
* @author Akos Ratku
* @author Artem Bilan
*/
@FunctionalInterface
public interface ColumnNameExtractor {
List<String> extract(String query);
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017 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.fn.consumer.cassandra.query;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* @author Akos Ratku
* @author Artem Bilan
*/
public class InsertQueryColumnNameExtractor implements ColumnNameExtractor {
private static final Pattern PATTERN = Pattern.compile(".+\\((.+)\\).+(?:values\\s*\\((.+)\\))");
@Override
public List<String> extract(String query) {
List<String> extractedColumns = new LinkedList<>();
Matcher matcher = PATTERN.matcher(query);
if (matcher.matches()) {
String[] columns = StringUtils.delimitedListToStringArray(matcher.group(1), ",", " ");
String[] params = StringUtils.delimitedListToStringArray(matcher.group(2), ",", " ");
for (int i = 0; i < columns.length; i++) {
String param = params[i];
if (param.equals("?")) {
extractedColumns.add(columns[i]);
}
else if (param.startsWith(":")) {
extractedColumns.add(param.substring(1));
}
}
}
else {
throw new IllegalArgumentException("Invalid CQL insert query syntax: " + query);
}
return extractedColumns;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2017 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.fn.consumer.cassandra.query;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.util.StringUtils;
/**
* @author Akos Ratku
* @author Artem Bilan
*/
public class UpdateQueryColumnNameExtractor implements ColumnNameExtractor {
private static final Pattern PATTERN = Pattern.compile("(?i)(?<=set)(.*)(?=where)where(.*)");
@Override
public List<String> extract(String query) {
List<String> extractedColumns = new LinkedList<>();
Matcher matcher = PATTERN.matcher(query);
if (matcher.find()) {
String[] settings = StringUtils.delimitedListToStringArray(matcher.group(1), ",", " ");
String[] where = StringUtils.delimitedListToStringArray(matcher.group(2), ",", " ");
readPairs(extractedColumns, settings);
readPairs(extractedColumns, where);
}
else {
throw new IllegalArgumentException("Invalid CQL update query syntax: " + query);
}
return extractedColumns;
}
protected void readPairs(List<String> extractedColumns, String[] settings) {
for (String setting : settings) {
String[] columnValuePair = StringUtils.delimitedListToStringArray(setting, "=", " ");
if (columnValuePair[1].startsWith(":") || columnValuePair[1].equals("?")) {
extractedColumns.add(columnValuePair[0]);
}
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.cassandra;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
import org.springframework.cloud.fn.consumer.cassandra.domain.Book;
import org.cassandraunit.spring.CassandraUnitDependencyInjectionIntegrationTestExecutionListener;
import org.cassandraunit.spring.EmbeddedCassandra;
import org.cassandraunit.utils.EmbeddedCassandraServerHelper;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.cassandra.core.CassandraOperations;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestExecutionListeners;
/**
* @author Artem Bilan
*/
@TestExecutionListeners(mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS,
listeners = CassandraUnitDependencyInjectionIntegrationTestExecutionListener.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
properties = {
"spring.data.cassandra.keyspaceName=" + CassandraConsumerApplicationTests.CASSANDRA_KEYSPACE,
"spring.data.cassandra.localDatacenter=datacenter1",
"cassandra.cluster.createKeyspace=true" })
@EmbeddedCassandra(configuration = EmbeddedCassandraServerHelper.CASSANDRA_RNDPORT_YML_FILE, timeout = 120000)
@DirtiesContext
abstract class CassandraConsumerApplicationTests {
static final String CASSANDRA_KEYSPACE = "test";
@Autowired
protected CassandraOperations cassandraTemplate;
@Autowired
protected Function<Object, Mono<? extends WriteResult>> cassandraConsumer;
@BeforeAll
static void setUp() {
EmbeddedCassandraServerHelper.getSession();
System.setProperty("spring.data.cassandra.contactPoints",
EmbeddedCassandraServerHelper.getHost() + ':' + EmbeddedCassandraServerHelper.getNativeTransportPort());
}
@AfterAll
static void cleanup() {
System.clearProperty("spring.data.cassandra.contactPoints");
}
@AfterEach
void tearDown() {
this.cassandraTemplate.truncate(Book.class);
}
protected static List<Book> getBookList(int numBooks) {
List<Book> books = new ArrayList<>();
Book b;
for (int i = 0; i < numBooks; i++) {
b = new Book();
b.setIsbn(UUID.randomUUID());
b.setTitle("Spring Cloud Data Flow Guide");
b.setAuthor("SCDF Guru");
b.setPages(i * 10 + 5);
b.setInStock(true);
b.setSaleDate(LocalDate.now());
books.add(b);
}
return books;
}
@SpringBootApplication
static class TestApplication {}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.cassandra;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import java.util.UUID;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.springframework.cloud.fn.consumer.cassandra.domain.Book;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.test.context.TestPropertySource;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
*/
@DisabledOnOs(OS.WINDOWS)
@TestPropertySource(properties = {
"spring.data.cassandra.schema-action=RECREATE",
"cassandra.cluster.entity-base-packages=io.pivotal.java.function.cassandra.consumer.domain" })
class CassandraEntityInsertTests extends CassandraConsumerApplicationTests {
@Test
@Disabled
void testInsert() {
Book book = new Book();
book.setIsbn(UUID.randomUUID());
book.setTitle("Spring Integration Cassandra");
book.setAuthor("Cassandra Guru");
book.setPages(521);
book.setSaleDate(LocalDate.now());
book.setInStock(true);
Mono<? extends WriteResult> result = this.cassandraConsumer.apply(book);
StepVerifier.create(result)
.expectNextCount(1)
.then(() ->
assertThat(this.cassandraTemplate.query(Book.class)
.count())
.isEqualTo(1))
.verifyComplete();
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.cassandra;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.fn.consumer.cassandra.domain.Book;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.test.context.TestPropertySource;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
*/
@DisabledOnOs(OS.WINDOWS)
@TestPropertySource(properties = {
"cassandra.cluster.init-script=init-db.cql",
"cassandra.ingest-query=" +
"insert into book (isbn, title, author, pages, saleDate, inStock) values (?, ?, ?, ?, ?, ?)" })
class CassandraIngestInsertTests extends CassandraConsumerApplicationTests {
@Test
void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception {
List<Book> books = getBookList(5);
Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper);
Mono<? extends WriteResult> result =
this.cassandraConsumer.apply(mapper.toJson(books));
StepVerifier.create(result)
.expectNextCount(1)
.then(() ->
assertThat(this.cassandraTemplate.query(Book.class)
.count())
.isEqualTo(5))
.verifyComplete();
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.cassandra;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.fn.consumer.cassandra.domain.Book;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
*/
@DisabledOnOs(OS.WINDOWS)
@TestPropertySource(properties = {
"cassandra.cluster.init-script=init-db.cql",
"cassandra.ingest-query=" +
"insert into book (isbn, title, author, pages, saleDate, inStock) " +
"values (:myIsbn, :myTitle, :myAuthor, ?, ?, ?)" })
class CassandraIngestNamedParamsTests extends CassandraConsumerApplicationTests {
@Test
void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception {
List<Book> books = getBookList(5);
Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper);
String booksJsonWithNamedParams = mapper.toJson(books);
booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "isbn", "myIsbn");
booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "title", "myTitle");
booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "author", "myAuthor");
Mono<? extends WriteResult> result =
this.cassandraConsumer.apply(booksJsonWithNamedParams);
StepVerifier.create(result)
.expectNextCount(1)
.then(() ->
assertThat(this.cassandraTemplate.query(Book.class)
.count())
.isEqualTo(5))
.verifyComplete();
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.cassandra;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.fn.consumer.cassandra.domain.Book;
import org.springframework.data.cassandra.core.WriteResult;
import org.springframework.integration.support.json.Jackson2JsonObjectMapper;
import org.springframework.test.context.TestPropertySource;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
/**
* @author Artem Bilan
*/
@DisabledOnOs(OS.WINDOWS)
@TestPropertySource(properties = {
"cassandra.cluster.init-script=init-db.cql",
"cassandra.ingest-query=" +
"update book set inStock = :inStock, author = :author, pages = :pages, " +
"saleDate = :saleDate, title = :title where isbn = :isbn",
"cassandra.queryType=UPDATE" })
class CassandraIngestUpdateTests extends CassandraConsumerApplicationTests {
@Test
void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception {
List<Book> books = getBookList(5);
Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper);
Mono<? extends WriteResult> result =
this.cassandraConsumer.apply(mapper.toJson(books));
StepVerifier.create(result)
.expectNextCount(1)
.then(() ->
assertThat(this.cassandraTemplate.query(Book.class)
.count())
.isEqualTo(5))
.verifyComplete();
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2015-2020 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.fn.consumer.cassandra.domain;
import java.time.LocalDate;
import java.util.UUID;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
/**
* Test POJO
*
* @author David Webb
* @author Artem Bilan
*/
@Table("book")
public class Book {
@PrimaryKey
private UUID isbn;
private String title;
private String author;
private int pages;
private LocalDate saleDate;
private boolean inStock;
public Book() {
}
public Book(UUID isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
}
/**
* @return Returns the isbn.
*/
public UUID getIsbn() {
return this.isbn;
}
/**
* @return Returns the saleDate.
*/
public LocalDate getSaleDate() {
return this.saleDate;
}
/**
* @param saleDate The saleDate to set.
*/
public void setSaleDate(LocalDate saleDate) {
this.saleDate = saleDate;
}
/**
* @return Returns the inStock.
*/
public boolean isInStock() {
return this.inStock;
}
/**
* @param inStock The isInStock to set.
*/
public void setInStock(boolean inStock) {
this.inStock = inStock;
}
/**
* @param isbn The isbn to set.
*/
public void setIsbn(UUID isbn) {
this.isbn = isbn;
}
/**
* @return Returns the title.
*/
public String getTitle() {
return this.title;
}
/**
* @param title The title to set.
*/
public void setTitle(String title) {
this.title = title;
}
/**
* @return Returns the author.
*/
public String getAuthor() {
return this.author;
}
/**
* @param author The author to set.
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* @return Returns the pages.
*/
public int getPages() {
return this.pages;
}
/**
* @param pages The pages to set.
*/
public void setPages(int pages) {
this.pages = pages;
}
@Override
public String toString() {
return ("isbn -> " + this.isbn) + "\n" + "tile -> " + this.title + "\n" + "author -> " + this.author
+ "\n" + "pages -> " + this.pages + "\n";
}
}

View File

@@ -0,0 +1,10 @@
DROP TABLE IF EXISTS book;
CREATE TABLE book (
isbn uuid PRIMARY KEY,
author text,
instock boolean,
pages int,
saledate date,
title text
);

28
consumer/counter-consumer/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>counter-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>counter-consumer</name>
<description>Spring Native Consumer for computing counters</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>payload-converter-function</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.ConfigurationPropertiesBinding;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.convert.converter.Converter;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.messaging.Message;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
*
* @author Christian Tzolov
*/
@Configuration
@EnableConfigurationProperties({ CounterConsumerProperties.class })
public class CounterConsumerConfiguration {
@Bean
public Function<String, Expression> stringToSpelFunction(@Lazy EvaluationContext evaluationContext) {
return new StringToSpelConversionFunction(evaluationContext);
}
@Bean
@ConfigurationPropertiesBinding
public Converter<String, Expression> propertiesSpelConverter(Function<String, Expression> stringToSpelFunction) {
return new Converter<String, Expression>() { // NOTE Using lambda causes Java Generics issues.
@Override
public Expression convert(String source) {
return stringToSpelFunction.apply(source);
}
};
}
@Bean(name = "counterConsumer")
public Consumer<Message<?>> counterConsumer(CounterConsumerProperties properties, MeterRegistry[] meterRegistries,
@Qualifier(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME) EvaluationContext context) {
return message -> {
String counterName = properties.getComputedNameExpression().getValue(context, message, CharSequence.class).toString();
// All fixed tags together are passed with every counter increment.
Tags fixedTags = this.toTags(properties.getTag().getFixed());
double amount = properties.getComputedAmountExpression().getValue(context, message, double.class);
Map<String, List<Tag>> allGroupedTags = new HashMap<>();
// Tag Expressions Counter
if (properties.getTag().getExpression() != null) {
Map<String, List<Tag>> groupedTags = properties.getTag().getExpression().entrySet().stream()
// maps a <name, expr> pair into [<name, expr#val_1>, ... <name, expr#val_N>] Tag array.
.map(namedExpression ->
toList(namedExpression.getValue().getValue(context, message)).stream()
.map(tagValue -> Tag.of(namedExpression.getKey(), tagValue))
.collect(Collectors.toList())).flatMap(List::stream)
.collect(Collectors.groupingBy(tag -> tag.getKey(), Collectors.toList()));
allGroupedTags.putAll(groupedTags);
}
this.count(meterRegistries, counterName, fixedTags, allGroupedTags, amount);
};
}
/**
* Converts a key/value Map into Tag(key,value) list. Filters out the empty key/value pairs.
* @param keyValueMap key/value map to convert into tags.
* @return Returns Tags list representing every non-empty key/value pair.
*/
protected Tags toTags(Map<String, String> keyValueMap) {
return CollectionUtils.isEmpty(keyValueMap) ? Tags.empty() :
Tags.of(keyValueMap.entrySet().stream()
.filter(e -> StringUtils.hasText(e.getKey()) && StringUtils.hasText(e.getValue()))
.map(e -> Tag.of(e.getKey(), e.getValue()))
.collect(Collectors.toList()));
}
/**
* Converts the input value into an list of values. If the value is not a collection/array type the result
* is a single element list. For collection/array input value the result is the list of stringifie content of
* this collection.
* @param value input value can be array, collection or single value.
* @return Returns value list.
*/
protected List<String> toList(Object value) {
if (value == null) {
return Collections.emptyList();
}
if ((value instanceof Collection) || ObjectUtils.isArray(value)) {
Collection<?> valueCollection = (value instanceof Collection) ? (Collection<?>) value
: Arrays.asList(ObjectUtils.toObjectArray(value));
return valueCollection.stream()
.filter(v -> v != null)
.map(Object::toString)
.filter(StringUtils::hasText)
.collect(Collectors.toList());
}
else {
return Arrays.asList(value.toString());
}
}
private void count(MeterRegistry[] meterRegistries, String counterName, Tags fixedTags, Map<String, List<Tag>> groupedTags, double amount) {
if (!CollectionUtils.isEmpty(groupedTags)) {
groupedTags.values().stream().map(List::size).max(Integer::compareTo).ifPresent(
max -> {
for (int i = 0; i < max; i++) {
Tags currentTags = Tags.of(fixedTags);
for (Map.Entry<String, List<Tag>> e : groupedTags.entrySet()) {
currentTags = (e.getValue().size() > i) ?
currentTags.and(e.getValue().get(i)) :
currentTags.and(Tags.of(e.getKey(), ""));
}
// Increment the counterName increment for every configured MaterRegistry.
for (MeterRegistry meterRegistry : meterRegistries) {
meterRegistry.counter(counterName, currentTags).increment(amount);
}
}
}
);
}
else {
// Increment the counterName increment for every configured MaterRegistry.
for (MeterRegistry meterRegistry : meterRegistries) {
meterRegistry.counter(counterName, fixedTags).increment(amount);
}
}
}
@Bean
@ConditionalOnMissingBean
public SimpleMeterRegistry simpleMeterRegistry() {
return new SimpleMeterRegistry();
}
}

View File

@@ -0,0 +1,172 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.Map;
import javax.validation.constraints.AssertTrue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.validation.annotation.Validated;
/**
* @author Christian Tzolov
*/
@ConfigurationProperties("counter")
@Validated
public class CounterConsumerProperties {
/**
* The default name of the increment
*/
@Value("${spring.application.name:counts}")
private String defaultName;
/**
* The name of the counter to increment. The 'name' and 'nameExpression' are mutually exclusive.
* Only one can be set.
*/
private String name;
/**
* A SpEL expression (against the incoming Message) to derive the name of the counter to increment.
* The 'name' and 'nameExpression' are mutually exclusive. Only one can be set.
*/
private Expression nameExpression;
/**
* A SpEL expression (against the incoming Message) to derive the amount to add to the counter.
* If not set the counter is incremented by 1.0
*/
private Expression amountExpression;
/**
* Enables counting the number of messages processed. Uses the 'message.' counter name prefix to distinct it
* form the expression based counter. The message counter includes the fixed tags when provided.
*/
private boolean messageCounterEnabled = true;
/**
* Fixed and computed tags to be assignee with the counter increment measurement.
*/
private MetricsTag tag = new MetricsTag();
public static class MetricsTag {
/**
* Custom tags assigned to every counter increment measurements.
* This is a map so the property convention fixed tags is: counter.tag.fixed.[tag-name]=[tag-value]
*/
private Map<String, String> fixed;
/**
* Computes tags from SpEL expression.
* Single SpEL expression can produce an array of values, which in turn means distinct name/value tags.
* Every name/value tag will produce a separate counter increment.
* Tag expression format is: counter.tag.expression.[tag-name]=[SpEL expression]
*/
private Map<String, Expression> expression;
public Map<String, String> getFixed() {
return fixed;
}
public void setFixed(Map<String, String> fixed) {
this.fixed = fixed;
}
public Map<String, Expression> getExpression() {
return expression;
}
public void setExpression(Map<String, Expression> expression) {
this.expression = expression;
}
@Override
public String toString() {
return "MetricsTag{" +
"fixed=" + fixed +
", expression=" + expression +
'}';
}
}
public MetricsTag getTag() {
return tag;
}
public String getName() {
if (name == null && nameExpression == null) {
return defaultName;
}
return name;
}
public void setName(String name) {
this.name = name;
}
public Expression getNameExpression() {
return nameExpression;
}
public void setNameExpression(Expression nameExpression) {
this.nameExpression = nameExpression;
}
public Expression getAmountExpression() {
return amountExpression;
}
public void setAmountExpression(Expression amountExpression) {
this.amountExpression = amountExpression;
}
public Expression getComputedAmountExpression() {
return (amountExpression != null ? amountExpression : new LiteralExpression("1.0"));
}
public Expression getComputedNameExpression() {
return (nameExpression != null ? nameExpression : new LiteralExpression(getName()));
}
public boolean isMessageCounterEnabled() {
return messageCounterEnabled;
}
public void setMessageCounterEnabled(boolean messageCounterEnabled) {
this.messageCounterEnabled = messageCounterEnabled;
}
@AssertTrue(message = "exactly one of 'name' and 'nameExpression' must be set")
public boolean isExclusiveOptions() {
return getName() != null ^ getNameExpression() != null;
}
@Override
public String toString() {
return "CounterFunctionProperties{" +
"defaultName='" + defaultName + '\'' +
", name=" + name +
", tag=" + tag +
'}';
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.function.Function;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Converter from String to Spring Expression.
*
* TODO: This could be a top level project.
*/
public class StringToSpelConversionFunction implements Function<String, Expression> {
private final SpelExpressionParser parser;
private final EvaluationContext evaluationContext;
public StringToSpelConversionFunction(EvaluationContext evaluationContext) {
this(new SpelExpressionParser(), evaluationContext);
}
public StringToSpelConversionFunction(SpelExpressionParser parser, EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
this.parser = parser;
}
@Override
public Expression apply(String source) {
try {
Expression expression = parser.parseExpression(source);
if (expression instanceof SpelExpression) {
((SpelExpression) expression).setEvaluationContext(evaluationContext);
}
return expression;
}
catch (ParseException e) {
throw new IllegalArgumentException(String.format(
"Could not convert '%s' into a SpEL expression", source), e);
}
}
}

View File

@@ -0,0 +1,22 @@
package org.springframework.cloud.fn.consumer.counter;
import java.util.function.Function;
import org.springframework.core.convert.converter.Converter;
/**
* @author Christian Tzolov
*/
public class ConverterFunctionAdapter<S, T> implements Converter<S, T> {
private Function<S, T> function;
public ConverterFunctionAdapter(Function<S, T> function) {
this.function = function;
}
@Override
public T convert(S s) {
return this.function.apply(s);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name=counter666",
"counter.tag.expression.foo='bar'",
"counter.amount-expression=payload.length()"
})
class CountWithAmountTest extends CounterConsumerParentTest {
@Test
void testCounterSink() {
String message = "hello world message";
double messageSize = Long.valueOf(message.length()).doubleValue();
counterConsumer.accept(new GenericMessage(message));
assertThat(meterRegistry.find("counter666").counter().count()).isEqualTo(messageSize);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.consumer.counter;
import java.util.function.Consumer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.annotation.DirtiesContext;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@DirtiesContext
public class CounterConsumerParentTest {
@Autowired
protected SimpleMeterRegistry meterRegistry;
@Autowired
protected Consumer<Message<?>> counterConsumer;
protected Message<byte[]> message(String payload) {
return MessageBuilder.withPayload(payload.getBytes()).build();
}
@SpringBootApplication
static class TestApplication {}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.Collection;
import io.micrometer.core.instrument.Counter;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name=counter666",
"counter.tag.fixed.foo=",
"counter.tag.expression.tag666=#jsonPath(payload,'$..noField')",
"counter.tag.expression.test=#jsonPath(payload,'$..test')",
})
class EmptyTagsTests extends CounterConsumerParentTest {
@Test
void testCounterSink() {
counterConsumer.accept(message("{\"test\": \"Bar\"}"));
Collection<Counter> fixedTagsCounters = meterRegistry.find("counter666").tagKeys("foo").counters();
assertThat(fixedTagsCounters.size()).isEqualTo(0);
Collection<Counter> expressionTagsCounters = meterRegistry.find("counter666").tagKeys("tag666").counters();
assertThat(expressionTagsCounters.size()).isEqualTo(0);
Collection<Counter> testExpTagsCounters = meterRegistry.find("counter666").tagKeys("test").counters();
assertThat(testExpTagsCounters.size()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.stream.IntStream;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name-expression=payload"
})
public class ExpressionCounterNameTests extends CounterConsumerParentTest {
@Test
void testCounterSink() {
IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello")));
assertThat(meterRegistry.find("hello").counter().count()).isEqualTo(13.0);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;
import io.micrometer.core.instrument.Meter;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name=counter666",
"counter.tag.fixed.foo=bar",
"counter.tag.fixed.gork=bork"
})
public class FixedTagsTests extends CounterConsumerParentTest {
@Test
void testCounterSink() {
IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello")));
Meter counterMeter = meterRegistry.find("counter666").meter();
assertThat(StreamSupport.stream(counterMeter.measure().spliterator(), false)
.mapToDouble(m -> m.getValue()).sum()).isEqualTo(13.0);
assertThat(counterMeter.getId().getTags().size()).isEqualTo(2);
assertThat(counterMeter.getId().getTag("foo")).isEqualTo("bar");
assertThat(counterMeter.getId().getTag("gork")).isEqualTo("bork");
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.stream.IntStream;
import io.micrometer.core.instrument.Counter;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name=counter666",
"counter.tag.expression.foo='bar'",
"counter.tag.expression.gork='bork'"
})
public class LiteralTagExpressionsTests extends CounterConsumerParentTest {
@Test
void testCounterSink() {
IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello")));
Counter fooCounter = meterRegistry.find("counter666").tag("foo", "bar").counter();
assertThat(fooCounter.count()).isEqualTo(13.0);
Counter gorkCounter = meterRegistry.find("counter666").tag("gork", "bork").counter();
assertThat(gorkCounter.count()).isEqualTo(13.0);
assertThat(fooCounter.getId()).isEqualTo(gorkCounter.getId());
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.counter;
import java.util.Collection;
import io.micrometer.core.instrument.Counter;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Christian Tzolov
*/
@TestPropertySource(properties = {
"counter.name=counter666",
"counter.tag.fixed.foo=",
"counter.tag.expression.tag666=#jsonPath(payload,'$..noField')",
"counter.tag.expression.test=#jsonPath(payload,'$..test')",
})
public class NullTagsTests extends CounterConsumerParentTest {
@Test
void testCounterSink() {
counterConsumer.accept(message("{\"test\": null}"));
Collection<Counter> fixedTagsCounters = meterRegistry.find("counter666").tagKeys("foo").counters();
assertThat(fixedTagsCounters.size()).isEqualTo(0);
Collection<Counter> expressionTagsCounters = meterRegistry.find("counter666").tagKeys("tag666").counters();
assertThat(expressionTagsCounters.size()).isEqualTo(0);
Collection<Counter> testExpTagsCounters = meterRegistry.find("counter666").tagKeys("test").counters();
assertThat(testExpTagsCounters.size()).isEqualTo(0);
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>file-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>file-consumer</name>
<description>file consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2015-2020 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
*
* https://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.fn.consumer.file;
import java.util.function.Consumer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.file.DefaultFileNameGenerator;
import org.springframework.integration.file.FileNameGenerator;
import org.springframework.integration.file.FileWritingMessageHandler;
import org.springframework.messaging.Message;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Soby Chacko
*/
@Configuration
@EnableConfigurationProperties(FileConsumerProperties.class)
public class FileConsumerConfiguration {
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private final FileConsumerProperties properties;
public FileConsumerConfiguration(FileConsumerProperties properties) {
this.properties = properties;
}
@Bean
public Consumer<Message<?>> fileConsumer() {
return fileWritingMessageHandler()::handleMessage;
}
@Bean
public FileWritingMessageHandler fileWritingMessageHandler() {
FileWritingMessageHandler handler = (properties.getDirectoryExpression() != null)
? new FileWritingMessageHandler(EXPRESSION_PARSER.parseExpression(properties.getDirectoryExpression()))
: new FileWritingMessageHandler(properties.getDirectory());
handler.setAutoCreateDirectory(true);
handler.setAppendNewLine(!properties.isBinary());
handler.setCharset(properties.getCharset());
handler.setExpectReply(false);
handler.setFileExistsMode(properties.getMode());
handler.setFileNameGenerator(fileNameGenerator());
return handler;
}
@Bean
public FileNameGenerator fileNameGenerator() {
DefaultFileNameGenerator fileNameGenerator = new DefaultFileNameGenerator();
fileNameGenerator.setExpression(properties.getNameExpression());
return fileNameGenerator;
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2015-2020 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
*
* https://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.fn.consumer.file;
import java.io.File;
import javax.validation.constraints.AssertTrue;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.integration.file.support.FileExistsMode;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
/**
* Properties for the file sink.
*
* @author Mark Fisher
* @author Gary Russell
*/
@ConfigurationProperties("file.consumer")
@Validated
public class FileConsumerProperties {
static final String DEFAULT_DIR = System.getProperty("java.io.tmpdir") + "file-consumer";
private static final String DEFAULT_NAME = "file-consumer";
/**
* A flag to indicate whether adding a newline after the write should be suppressed.
*/
private boolean binary = false;
/**
* The charset to use when writing text content.
*/
private String charset = "UTF-8";
/**
* The parent directory of the target file.
*/
private File directory = new File(DEFAULT_DIR);
/**
* The expression to evaluate for the parent directory of the target file.
*/
private String directoryExpression;
/**
* The FileExistsMode to use if the target file already exists.
*/
private FileExistsMode mode = FileExistsMode.APPEND;
/**
* The name of the target file.
*/
private String name = DEFAULT_NAME;
/**
* The expression to evaluate for the name of the target file.
*/
private String nameExpression;
/**
* The suffix to append to file name.
*/
private String suffix = "";
public boolean isBinary() {
return binary;
}
public void setBinary(boolean binary) {
this.binary = binary;
}
public String getCharset() {
return charset;
}
public void setCharset(String charset) {
this.charset = charset;
}
public File getDirectory() {
return directory;
}
public void setDirectory(File directory) {
this.directory = directory;
}
public String getDirectoryExpression() {
return directoryExpression;
}
public void setDirectoryExpression(String directoryExpression) {
this.directoryExpression = directoryExpression;
}
public FileExistsMode getMode() {
return mode;
}
public void setMode(FileExistsMode mode) {
this.mode = mode;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public String getNameExpression() {
return (nameExpression != null)
? nameExpression + " + '" + getSuffix() + "'"
: "'" + name + getSuffix() + "'";
}
public void setNameExpression(String nameExpression) {
this.nameExpression = nameExpression;
}
public String getSuffix() {
String suffixWithDotIfNecessary = "";
if (StringUtils.hasText(suffix)) {
suffixWithDotIfNecessary = suffix.startsWith(".") ? suffix : "." + suffix;
}
return suffixWithDotIfNecessary;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
@AssertTrue(message = "Exactly one of 'name' or 'nameExpression' must be set")
public boolean isMutuallyExclusiveNameAndNameExpression() {
return DEFAULT_NAME.equals(name) || nameExpression == null;
}
@AssertTrue(message = "Exactly one of 'directory' or 'directoryExpression' must be set")
public boolean isMutuallyExclusiveDirectoryAndDirectoryExpression() {
return new File(DEFAULT_DIR).equals(directory) || directoryExpression == null;
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.file;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
/**
* @author Soby Chacko
*/
@SpringBootTest
@DirtiesContext
public class AbstractFileConsumerTests {
@TempDir
static Path tempDir;
@Autowired
Consumer<Message<?>> fileConsumer;
@BeforeAll
public static void beforeAll() {
System.setProperty("file.consumer.directory", tempDir.toAbsolutePath().toString());
}
@AfterAll
public static void afterAll() {
System.clearProperty("file.consumer.directory");
}
@SpringBootApplication
static class TestApplication {
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2015-2020 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
*
* https://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.fn.consumer.file;
import java.io.File;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Soby Chacko
*/
@TestPropertySource(properties = "file.consumer.binary = true")
public class BinaryFileTests extends AbstractFileConsumerTests {
@Test
public void test() throws Exception {
fileConsumer.accept(MessageBuilder.withPayload("hello file-consumer".getBytes()).build());
File file = new File(tempDir.toFile(), "file-consumer");
assertThat(file.exists()).isTrue();
byte[] results = FileCopyUtils.copyToByteArray(file);
assertThat("hello file-consumer".getBytes()).isEqualTo(results);
}
}

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2015-2020 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
*
* https://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.fn.consumer.file;
import java.io.File;
import java.io.FileReader;
import java.nio.file.Path;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Soby Chacko
*
* We don't need a separate SpringBootApplication for this test as there is already one available in this package.
* {@link AbstractFileConsumerTests}.
*/
@SpringBootTest(properties = {"file.consumer.nameExpression = payload.substring(0, 4)",
"file.consumer.directoryExpression = '${java.io.tmpdir}'+'/'+headers.dir",
"file.consumer.suffix=out"})
@DirtiesContext
public class ExpressionTests {
@TempDir
static Path tempDir;
@Autowired
Consumer<Message<?>> fileConsumer;
@Test
public void test() throws Exception {
fileConsumer.accept(MessageBuilder.withPayload("this is something").setHeader("dir", "expression").build());
File file = new File(System.getProperty("java.io.tmpdir") + File.separator + "expression", "this.out");
file.deleteOnExit();
assertThat(file.exists()).isTrue();
assertThat("this is something" + System.lineSeparator())
.isEqualTo(FileCopyUtils.copyToString(new FileReader(file)));
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2015-2020 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
*
* https://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.fn.consumer.file;
import java.io.File;
import java.io.FileReader;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Mark Fisher
* @author Artem Bilan
* @author Soby Chacko
*/
@TestPropertySource(properties = {"file.consumer.name = test", "file.consumer.suffix=txt"})
public class TextFileTests extends AbstractFileConsumerTests {
@Test
public void test() throws Exception {
fileConsumer.accept(MessageBuilder.withPayload("hello file-consumer").build());
File file = new File(tempDir.toFile(), "test.txt");
assertThat(file.exists()).isTrue();
assertThat("hello file-consumer" + System.lineSeparator())
.isEqualTo(FileCopyUtils.copyToString(new FileReader(file)));
}
}

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>jdbc-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>jdbc-consumer</name>
<description>jdbc consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ByteArrayResource;
/**
* An in-memory script crafted for dropping-creating the table we're working with.
* All columns are created as VARCHAR(2000).
*
* @author Eric Bottard
* @author Thomas Risberg
*/
public class DefaultInitializationScriptResource extends ByteArrayResource {
private static final Log logger = LogFactory.getLog(DefaultInitializationScriptResource.class);
public DefaultInitializationScriptResource(String tableName, Collection<String> columns) {
super(scriptFor(tableName, columns).getBytes(StandardCharsets.UTF_8));
}
private static String scriptFor(String tableName, Collection<String> columns) {
StringBuilder result = new StringBuilder("DROP TABLE ");
result.append(tableName).append(";\n\n");
result.append("CREATE TABLE ").append(tableName).append('(');
int i = 0;
for (String column : columns) {
if (i++ > 0) {
result.append(", ");
}
result.append(column).append(" VARCHAR(2000)");
}
result.append(");\n");
logger.debug(String.format("Generated the following initializing script for table %s:\n%s", tableName,
result.toString()));
return result.toString();
}
}

View File

@@ -0,0 +1,314 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.sql.DataSource;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.SpelParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.aggregator.DefaultAggregatingMessageGroupProcessor;
import org.springframework.integration.aggregator.MessageCountReleaseStrategy;
import org.springframework.integration.config.AggregatorFactoryBean;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.expression.ValueExpression;
import org.springframework.integration.jdbc.JdbcMessageHandler;
import org.springframework.integration.jdbc.SqlParameterSourceFactory;
import org.springframework.integration.json.JsonPropertyAccessor;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.SimpleMessageStore;
import org.springframework.integration.support.MutableMessage;
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.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.MultiValueMap;
/**
*
* @author Eric Bottard
* @author Thomas Risberg
* @author Robert St. John
* @author Oliver Flasch
* @author Artem Bilan
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@Configuration
@EnableConfigurationProperties(JdbcConsumerProperties.class)
public class JdbcConsumerConfiguration {
private static final Log logger = LogFactory.getLog(JdbcConsumerConfiguration.class);
private static final Object NOT_SET = new Object();
private final JdbcConsumerProperties properties;
private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
private EvaluationContext evaluationContext;
public JdbcConsumerConfiguration(JdbcConsumerProperties properties, BeanFactory beanFactory) {
this.properties = properties;
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
StandardEvaluationContext standardEvaluationContext = (StandardEvaluationContext) this.evaluationContext;
standardEvaluationContext.addPropertyAccessor(new JsonPropertyAccessor());
}
@Bean
IntegrationFlow jdbcConsumerFlow(@Qualifier("aggregator") MessageHandler aggregator,
JdbcMessageHandler jdbcMessageHandler) {
final IntegrationFlowBuilder builder =
IntegrationFlows.from(Consumer.class, gateway -> gateway.beanName("jdbcConsumer"));
if (properties.getBatchSize() > 1 || properties.getIdleTimeout() > 0) {
builder.handle(aggregator);
}
return builder.handle(jdbcMessageHandler).get();
}
@Bean
FactoryBean<MessageHandler> aggregator(MessageGroupStore messageGroupStore) {
AggregatorFactoryBean aggregatorFactoryBean = new AggregatorFactoryBean();
aggregatorFactoryBean.setCorrelationStrategy(message -> message.getPayload().getClass().getName());
aggregatorFactoryBean.setReleaseStrategy(new MessageCountReleaseStrategy(this.properties.getBatchSize()));
if (this.properties.getIdleTimeout() >= 0) {
aggregatorFactoryBean.setGroupTimeoutExpression(new ValueExpression<>(this.properties.getIdleTimeout()));
}
aggregatorFactoryBean.setMessageStore(messageGroupStore);
aggregatorFactoryBean.setProcessorBean(new DefaultAggregatingMessageGroupProcessor());
aggregatorFactoryBean.setExpireGroupsUponCompletion(true);
aggregatorFactoryBean.setSendPartialResultOnExpiry(true);
return aggregatorFactoryBean;
}
@Bean
MessageGroupStore messageGroupStore() {
SimpleMessageStore messageGroupStore = new SimpleMessageStore();
messageGroupStore.setTimeoutOnIdle(true);
messageGroupStore.setCopyOnGet(false);
return messageGroupStore;
}
@Bean
public JdbcMessageHandler jdbcMessageHandler(DataSource dataSource) {
final MultiValueMap<String, Expression> columnExpressionVariations = new LinkedMultiValueMap<>();
for (Map.Entry<String, String> entry : this.properties.getColumnsMap().entrySet()) {
String value = entry.getValue();
columnExpressionVariations.add(entry.getKey(), this.spelExpressionParser.parseExpression(value));
if (!value.startsWith("payload")) {
String qualified = "payload." + value;
try {
columnExpressionVariations.add(entry.getKey(),
this.spelExpressionParser.parseExpression(qualified));
}
catch (SpelParseException e) {
logger.info("failed to parse qualified fallback expression " + qualified +
"; be sure your expression uses the 'payload.' prefix where necessary");
}
}
}
JdbcMessageHandler jdbcMessageHandler = new JdbcMessageHandler(dataSource,
generateSql(this.properties.getTableName(), columnExpressionVariations.keySet())) {
@Override
protected void handleMessageInternal(final Message<?> message) {
Message<?> convertedMessage = message;
if (message.getPayload() instanceof byte[] || message.getPayload() instanceof Iterable) {
final String contentType = message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)
? message.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString()
: MimeTypeUtils.APPLICATION_JSON_VALUE;
if (message.getPayload() instanceof Iterable) {
Stream<Object> messageStream =
StreamSupport.stream(((Iterable<?>) message.getPayload()).spliterator(), false)
.map(payload -> {
if (payload instanceof byte[]) {
return convertibleContentType(contentType) ?
new String(((byte[]) payload)) : payload;
}
else {
return payload;
}
});
convertedMessage = new MutableMessage<>(messageStream.collect(Collectors.toList()),
message.getHeaders());
}
else {
if (convertibleContentType(contentType)) {
convertedMessage = new MutableMessage<>(new String(((byte[]) message.getPayload())),
message.getHeaders());
}
}
}
super.handleMessageInternal(convertedMessage);
}
};
SqlParameterSourceFactory parameterSourceFactory =
new ParameterFactory(columnExpressionVariations, this.evaluationContext);
jdbcMessageHandler.setSqlParameterSourceFactory(parameterSourceFactory);
return jdbcMessageHandler;
}
@ConditionalOnProperty("jdbc.consumer.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(this.properties.getTableName(),
this.properties.getColumnsMap().keySet()));
}
else {
databasePopulator.addScript(resourceLoader.getResource(this.properties.getInitialize()));
}
return dataSourceInitializer;
}
@Bean
public static ShorthandMapConverter shorthandMapConverter() {
return new ShorthandMapConverter();
}
private static boolean convertibleContentType(String contentType) {
return contentType.contains("text") || contentType.contains("json") || contentType.contains("x-spring-tuple");
}
private static 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(':').append(column);
}
builder.append(questionMarks).append(")");
return builder.toString();
}
private static final class ParameterFactory implements SqlParameterSourceFactory {
private final MultiValueMap<String, Expression> columnExpressions;
private final EvaluationContext context;
ParameterFactory(MultiValueMap<String, Expression> columnExpressions, EvaluationContext context) {
this.columnExpressions = columnExpressions;
this.context = context;
}
@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 (Map.Entry<String, List<Expression>> entry : this.columnExpressions.entrySet()) {
String key = entry.getKey();
List<Expression> spels = entry.getValue();
Object value = NOT_SET;
EvaluationException lastException = null;
for (Expression spel : spels) {
try {
value = spel.getValue(context, 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;
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Eric Bottard
* @author Artem Bilan
* @author Oliver Flasch
*/
@ConfigurationProperties("jdbc.consumer")
public class JdbcConsumerProperties {
@Autowired
private ShorthandMapConverter shorthandMapConverter;
/**
* The name of the table to write into.
*/
private String tableName = "messages";
/**
* The comma separated colon-based pairs of column names and SpEL expressions for values to insert/update.
* Names are used at initialization time to issue the DDL.
*/
private String columns = "payload:payload.toString()";
/**
* 'true', 'false' or the location of a custom initialization script for the table.
*/
private String initialize = "false";
/**
* Threshold in number of messages when data will be flushed to database table.
*/
private int batchSize = 1;
/**
* Idle timeout in milliseconds when data is automatically flushed to database table.
*/
private long idleTimeout = -1L;
private Map<String, String> columnsMap;
public String getTableName() {
return this.tableName;
}
public void setTableName(String tableName) {
this.tableName = tableName;
}
public String getColumns() {
return this.columns;
}
public void setColumns(String columns) {
this.columns = columns;
}
public String getInitialize() {
return this.initialize;
}
public void setInitialize(String initialize) {
this.initialize = initialize;
}
public int getBatchSize() {
return this.batchSize;
}
public void setBatchSize(int batchSize) {
this.batchSize = batchSize;
}
public long getIdleTimeout() {
return this.idleTimeout;
}
public void setIdleTimeout(long idleTimeout) {
this.idleTimeout = idleTimeout;
}
Map<String, String> getColumnsMap() {
if (this.columnsMap == null) {
this.columnsMap = this.shorthandMapConverter.convert(this.columns);
}
return this.columnsMap;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.Assert;
/**
* A Converter from String to Map that accepts csv {@literal key:value} pairs
* (similar to what comes out of the box in Spring Core) but also simple
* {@literal key} items, in which case the value is assumed to be equal to the key.
* <p>
* <p>Additionally, commas and colons can be escaped by using a backslash, which is
* useful if said mappings are to be used for SpEL for example.</p>
*
* @author Eric Bottard
* @author Artem Bilan
*/
public class ShorthandMapConverter implements Converter<String, Map<String, String>> {
@Override
public Map<String, String> convert(String source) {
Map<String, String> result = new LinkedHashMap<>();
// Split on comma if not preceded by backslash
String[] mappings = source.split("(?<!\\\\),");
for (String mapping : mappings) {
// Turn backslash-comma back to comma
String unescaped = mapping.trim().replace("\\,", ",");
if (unescaped.length() == 0) {
continue;
}
// Split on colon, if not preceded by backslash
String[] keyValuePair = unescaped.split("(?<!\\\\):");
Assert.isTrue(keyValuePair.length <= 2, "'" + unescaped +
"' could not be parsed to a 'key:value' pair or simple 'key' with implicit value");
String key = keyValuePair[0].trim().replace("\\:", ":");
String value = keyValuePair.length == 2 ? keyValuePair[1].trim().replace("\\:", ":") : key;
result.put(key, value);
}
return result;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = { "jdbc.consumer.batchSize=1000", "jdbc.consumer.idleTimeout=100" })
public class BatchInsertTimeoutTests extends JdbcConsumerApplicationTests {
@Test
public void testBatchInsertionTimeout() {
final int numberOfInserts = 10;
Payload sent = new Payload("hello", 42);
for (int i = 0; i < numberOfInserts; i++) {
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
}
Awaitility.await().until(() -> jdbcOperations
.queryForObject("select count(*) from messages", Integer.class), value -> value == numberOfInserts);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a")
public class DataReceivedAsByteArrayTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertionWhenDataReceivedAsByteArray() {
String hello = "{\"a\": \"hello\"}";
final Message<byte[]> message = MessageBuilder.withPayload(hello.getBytes()).build();
jdbcConsumer.accept(message);
final Integer count =
jdbcOperations.queryForObject("select count(*) from messages where a = ?", Integer.class, "hello");
assertThat(count).isEqualTo(1);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = { "jdbc.consumer.tableName=foobar",
"jdbc.consumer.initialize=classpath:explicit-script.sql",
"jdbc.consumer.columns=a,b" })
public class ExplicitTableCreationTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
Payload result =
jdbcOperations.query("select a, b from foobar", new BeanPropertyRowMapper<>(Payload.class))
.get(0);
assertThat(result).isEqualToComparingFieldByField(sent);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a: headers[foo]")
public class HeaderInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testHeaderInsertion() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent)
.setHeader("foo", "bar").build();
jdbcConsumer.accept(message);
assertThat(jdbcOperations.queryForObject("select count(*) from messages where a = ?",
Integer.class, "bar")).isEqualTo(1);
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = {
"jdbc.consumer.tableName=no_script",
"jdbc.consumer.initialize=true",
"jdbc.consumer.columns=a,b" })
public class ImplicitTableCreationTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
Payload result = jdbcOperations
.query("select a, b from no_script", new BeanPropertyRowMapper<>(Payload.class)).get(0);
assertThat(result).isEqualToComparingFieldByField(sent);
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
/**
* @author Soby Chacko
*/
@SpringBootTest
@DirtiesContext
public class JdbcConsumerApplicationTests {
@Autowired
Consumer<Message<?>> jdbcConsumer;
@Autowired
JdbcOperations jdbcOperations;
@Autowired
JdbcTemplate jdbcTemplate;
@AfterEach
public void cleanup() {
jdbcOperations.execute("DROP TABLE MESSAGES IF EXISTS");
}
static class Payload {
private String a;
private Integer b;
public Payload() {
}
public Payload(String a, Integer b) {
this.a = a;
this.b = b;
}
public String getA() {
return a;
}
public Integer getB() {
return b;
}
public void setA(String a) {
this.a = a;
}
public void setB(Integer b) {
this.b = b;
}
@Override
public String toString() {
return a + b;
}
}
@SpringBootApplication
static class TestApplication {}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a,b")
public class JsonStringPayloadInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
String stringA = "{\"a\": \"hello1\", \"b\": 42}";
String stringB = "{\"a\": \"hello2\", \"b\": null}";
String stringC = "{\"a\": \"hello3\"}";
final Message<String> message1 = MessageBuilder.withPayload(stringA).build();
jdbcConsumer.accept(message1);
final Message<String> message2 = MessageBuilder.withPayload(stringB).build();
jdbcConsumer.accept(message2);
final Message<String> message3 = MessageBuilder.withPayload(stringC).build();
jdbcConsumer.accept(message3);
assertThat(jdbcOperations.queryForObject(
"select count(*) from messages where a = ? and b = ?",
Integer.class, "hello1", 42)).isEqualTo(1);
assertThat(jdbcOperations.queryForObject(
"select count(*) from messages where a = ? and b IS NULL",
Integer.class, "hello2")).isEqualTo(1);
assertThat(jdbcOperations.queryForObject(
"select count(*) from messages where a = ? and b IS NULL",
Integer.class, "hello3")).isEqualTo(1);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a,b")
public class MapPayloadInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
NamedParameterJdbcOperations namedParameterJdbcOperations = new NamedParameterJdbcTemplate(jdbcOperations);
Map<String, Object> mapA = new HashMap<>();
mapA.put("a", "hello1");
mapA.put("b", 42);
Map<String, Object> mapB = new HashMap<>();
mapB.put("a", "hello2");
mapB.put("b", null);
Map<String, Object> mapC = new HashMap<>();
mapC.put("a", "hello3");
final Message<Map<String, Object>> message1 = MessageBuilder.withPayload(mapA).build();
jdbcConsumer.accept(message1);
final Message<Map<String, Object>> message2 = MessageBuilder.withPayload(mapB).build();
jdbcConsumer.accept(message2);
final Message<Map<String, Object>> message3 = MessageBuilder.withPayload(mapC).build();
jdbcConsumer.accept(message3);
assertThat(namedParameterJdbcOperations.queryForObject(
"select count(*) from messages where a = :a and b = :b", mapA, Integer.class)).isEqualTo(1);
assertThat(namedParameterJdbcOperations.queryForObject(
"select count(*) from messages where a = :a and b IS NULL", mapB, Integer.class)).isEqualTo(1);
assertThat(namedParameterJdbcOperations.queryForObject(
"select count(*) from messages where a = :a and b IS NULL", mapC, Integer.class)).isEqualTo(1);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.batchSize=1000")
public class SimpleBatchInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testBatchInsertion() {
final int numberOfInserts = 5000;
Payload sent = new Payload("hello", 42);
for (int i = 0; i < numberOfInserts; i++) {
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
}
int result = jdbcOperations.queryForObject("select count(*) from messages", Integer.class);
assertThat(result).isEqualTo(numberOfInserts);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
public class SimpleInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testSimpleInsert() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
String result = jdbcOperations.queryForObject("select payload from messages", String.class);
assertThat(result).isEqualTo(("hello42"));
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a,b")
public class SimpleMappingTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
Payload result = jdbcOperations
.query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class)).get(0);
assertThat(result).isEqualToComparingFieldByField(sent);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
// annotation below relies on java.util.Properties so backslash needs to be doubled
@TestPropertySource(properties = "jdbc.consumer.columns=a: a.substring(0\\\\, 4), b: b + 624")
public class SpELTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
Payload sent = new Payload("hello", 42);
final Message<Payload> message = MessageBuilder.withPayload(sent).build();
jdbcConsumer.accept(message);
Payload expected = new Payload("hell", 666);
Payload result = jdbcOperations
.query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class)).get(0);
assertThat(result).isEqualToComparingFieldByField(expected);
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a: new StringBuilder(payload.a).reverse().toString(), b")
public class UnqualifiableColumnExpressionTests extends JdbcConsumerApplicationTests {
@Test
public void doesNotFailParsingUnqualifiableExpression() {
// if the app initializes, the test condition passes, but go ahead and apply the column expression anyway
jdbcConsumer.accept(MessageBuilder.withPayload(new Payload("desrever", 123)).build());
assertThat(jdbcOperations.queryForObject("select count(*) from messages where a = ? and b = ?",
Integer.class, "reversed", 123)).isEqualTo(1);
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2020 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
*
* https://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.fn.consumer.jdbc;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
/**
* @author Eric Bottard
* @author Thomas Risberg
* @author Artem Bilan
* @author Robert St. John
* @author Oliver Flasch
* @author Soby Chacko
* @author Szabolcs Stremler
*/
@TestPropertySource(properties = "jdbc.consumer.columns=a,b")
public class VaryingInsertTests extends JdbcConsumerApplicationTests {
@Test
public void testInsertion() {
Payload a = new Payload("hello", 42);
Payload b = new Payload("world", 12);
Payload c = new Payload("bonjour", null);
Payload d = new Payload(null, 22);
final Message<Payload> message1 = MessageBuilder.withPayload(a).build();
jdbcConsumer.accept(message1);
final Message<Payload> message2 = MessageBuilder.withPayload(b).build();
jdbcConsumer.accept(message2);
final Message<Payload> message3 = MessageBuilder.withPayload(c).build();
jdbcConsumer.accept(message3);
final Message<Payload> message4 = MessageBuilder.withPayload(d).build();
jdbcConsumer.accept(message4);
List<Payload> result = jdbcOperations
.query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class));
Assertions.assertThat(result).extracting("a").containsExactly("hello", "world", "bonjour", null);
Assertions.assertThat(result).extracting("b").contains(42, 12, 22, null);
}
}

View File

@@ -0,0 +1,6 @@
-- Used in test for explicit script
create table foobar(
a varchar(2000),
b VARCHAR (2000)
);

View File

@@ -0,0 +1,7 @@
-- Run by default by Boot infrastructure
create table messages(
a varchar(2000),
b VARCHAR (2000),
payload VARCHAR (2000)
);

31
consumer/log-consumer/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>log-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>log-consumer</name>
<description>Log Consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>io.pivotal.java.function</groupId>
<artifactId>payload-converter-function</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2020 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.fn.consumer.log;
import java.util.function.Consumer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.messaging.Message;
/**
* The Configuration class for {@link Consumer} which logs incoming data.
* For the logging logic a Spring Integration {@link org.springframework.integration.handler.LoggingHandler}
* is used.
* If incoming payload is a {@code byte[]} and incoming message {@code contentType} header is text-compatible
* (e.g. {@code application/json}), it is converted into a {@link String}.
* Otherwise the payload is passed to logger as is.
*
* @author Artem Bilan
*/
@Configuration
@EnableConfigurationProperties(LogConsumerProperties.class)
public class LogConsumerConfiguration {
@Bean
IntegrationFlow logConsumerFlow(LogConsumerProperties logSinkProperties) {
return IntegrationFlows.from(MessageConsumer.class, (gateway) -> gateway.beanName("logConsumer"))
.handle((payload, headers) -> payload)
.log(logSinkProperties.getLevel(), logSinkProperties.getName(), logSinkProperties.getExpression())
.get();
}
private interface MessageConsumer extends Consumer<Message<?>> {}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2020 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.fn.consumer.log;
import static org.springframework.integration.handler.LoggingHandler.Level.INFO;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.validation.annotation.Validated;
/**
* Configuration properties for the Log Sink app.
*
* @author Gary Russell
* @author Eric Bottard
* @author Chris Schaefer
* @author Artem Bilan
*/
@ConfigurationProperties("log")
@Validated
public class LogConsumerProperties {
/**
* The name of the logger to use.
*/
@Value("${spring.application.name:log.consumer}")
private String name;
/**
* A SpEL expression (against the incoming message) to evaluate as the logged message.
*/
private String expression = "payload";
/**
* The level at which to log messages.
*/
private LoggingHandler.Level level = INFO;
@NotBlank
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@NotBlank
public String getExpression() {
return expression;
}
public void setExpression(String expression) {
this.expression = expression;
}
@NotNull
public LoggingHandler.Level getLevel() {
return level;
}
public void setLevel(LoggingHandler.Level level) {
this.level = level;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2020 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.fn.consumer.log;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* @author Artem Bilan
*/
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@SpringBootTest({ "log.name=foo", "log.level=warn", "log.expression=payload.toUpperCase()" })
class LogConsumerApplicationTests {
@Autowired
private Consumer<Message<?>> logConsumer;
@Autowired
@Qualifier("logConsumerFlow.logging-channel-adapter#0")
private LoggingHandler loggingHandler;
@Test
public void testJsonContentType() {
Message<String> message = MessageBuilder.withPayload("{\"foo\":\"bar\"}")
.setHeader("contentType", new MimeType("json"))
.build();
testMessage(message, "{\"foo\":\"bar\"}");
}
private void testMessage(Message<?> message, String expectedPayload) {
assertThat(this.loggingHandler.getLevel()).isEqualTo(LoggingHandler.Level.WARN);
Log logger = TestUtils.getPropertyValue(this.loggingHandler, "messageLogger", Log.class);
assertThat(TestUtils.getPropertyValue(logger, "logger.name")).isEqualTo("foo");
logger = spy(logger);
new DirectFieldAccessor(this.loggingHandler).setPropertyValue("messageLogger", logger);
this.logConsumer.accept(message);
ArgumentCaptor<Object> captor = ArgumentCaptor.forClass(Object.class);
verify(logger).warn(captor.capture());
assertThat(captor.getValue()).isEqualTo(expectedPayload.toUpperCase());
this.loggingHandler.setLogExpressionString("#this");
this.logConsumer.accept(message);
verify(logger, times(2)).warn(captor.capture());
Message<?> captorMessage = (Message<?>) captor.getAllValues().get(2);
assertThat(captorMessage.getPayload()).isEqualTo(expectedPayload);
MessageHeaders messageHeaders = captorMessage.getHeaders();
assertThat(messageHeaders).hasSize(3);
assertThat(messageHeaders)
.containsEntry(MessageHeaders.CONTENT_TYPE, message.getHeaders().get(MessageHeaders.CONTENT_TYPE));
}
@SpringBootApplication
static class TestApplication {}
}

31
consumer/mongodb-consumer/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>mongodb-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>mongodb-consumer</name>
<description>Mongo DB consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2017-2020 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
*
* https://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.fn.consumer.mongo;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.integration.mongodb.outbound.ReactiveMongoDbStoringMessageHandler;
import org.springframework.messaging.Message;
import org.springframework.messaging.ReactiveMessageHandler;
/**
* A configuration for MongoDB Consumer function. Uses a
* {@link ReactiveMongoDbStoringMessageHandler} to save payload contents to Mongo DB.
*
* @author Artem Bilan
* @author David Turanski
*
*/
@Configuration
@EnableConfigurationProperties({ MongoDbConsumerProperties.class })
public class MongoDbConsumerConfiguration {
private final MongoDbConsumerProperties properties;
private final ReactiveMongoTemplate mongoTemplate;
public MongoDbConsumerConfiguration(MongoDbConsumerProperties properties, ReactiveMongoTemplate mongoTemplate) {
this.properties = properties;
this.mongoTemplate = mongoTemplate;
}
@Bean
public Function<Message<?>, Mono<Void>> mongodbConsumer(ReactiveMessageHandler mongoConsumerMessageHandler) {
return mongoConsumerMessageHandler::handleMessage;
}
@Bean
public ReactiveMessageHandler mongoConsumerMessageHandler() {
ReactiveMongoDbStoringMessageHandler mongoDbMessageHandler = new ReactiveMongoDbStoringMessageHandler(
this.mongoTemplate);
Expression collectionExpression = this.properties.getCollectionExpression();
if (collectionExpression == null) {
collectionExpression = new LiteralExpression(this.properties.getCollection());
}
mongoDbMessageHandler.setCollectionNameExpression(collectionExpression);
return mongoDbMessageHandler;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.mongo;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
/**
* @author Artem Bilan
* @author David Turanski
*
*/
@ConfigurationProperties("mongodb.consumer")
@Validated
public class MongoDbConsumerProperties {
/**
* The MongoDB collection to store data
*/
private String collection;
/**
* The SpEL expression to evaluate MongoDB collection
*/
private Expression collectionExpression;
public void setCollection(String collection) {
this.collection = collection;
}
public String getCollection() {
return this.collection;
}
public void setCollectionExpression(Expression collectionExpression) {
this.collectionExpression = collectionExpression;
}
public Expression getCollectionExpression() {
return collectionExpression;
}
@AssertTrue(message = "One of 'collection' or 'collectionExpression' is required")
private boolean isValid() {
return StringUtils.hasText(this.collection) || this.collectionExpression != null;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.mongo;
import java.time.Duration;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.bson.Document;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author David Turanski
*/
@SpringBootTest(properties = {
"spring.data.mongodb.port=0",
"mongodb.consumer.collection=testing"})
class MongoDbConsumerApplicationTests {
@Autowired
private MongoDbConsumerProperties properties;
@Autowired
private Function<Message<?>, Mono<Void>> mongoDbConsumer;
@Autowired
private ReactiveMongoTemplate mongoTemplate;
@Test
void testMongodbConsumer() {
Map<String, String> data1 = new HashMap<>();
data1.put("foo", "bar");
Map<String, String> data2 = new HashMap<>();
data2.put("firstName", "Foo");
data2.put("lastName", "Bar");
Flux<Message<?>> messages = Flux.just(
new GenericMessage<>(data1),
new GenericMessage<>(data2),
new GenericMessage<>("{\"my_data\": \"THE DATA\"}")
);
messages.flatMap(mongoDbConsumer::apply).blockLast(Duration.ofSeconds(10));
StepVerifier.create(this.mongoTemplate.findAll(Document.class, properties.getCollection())
.sort(Comparator.comparing(d -> d.get("_id").toString())))
.assertNext(document -> {
assertThat(document.get("foo")).isEqualTo("bar");
})
.assertNext(document-> {
assertThat(document.get("firstName")).isEqualTo("Foo");
assertThat(document.get("lastName")).isEqualTo("Bar");
})
.assertNext(document-> {
assertThat(document.get("my_data")).isEqualTo("THE DATA");
})
.verifyComplete();
}
@SpringBootApplication
static class TestApplication {}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>rabbit-consumer</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>rabbit-consumer</name>
<description>Rabbit consumer</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.rabbit;
import java.util.function.Function;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.Expression;
import org.springframework.integration.amqp.dsl.Amqp;
import org.springframework.integration.amqp.dsl.AmqpOutboundChannelAdapterSpec;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
@EnableConfigurationProperties(RabbitConsumerProperties.class)
@Configuration
public class RabbitConsumerConfiguration implements DisposableBean {
@Autowired
private RabbitProperties bootProperties;
@Autowired
private ObjectProvider<ConnectionNameStrategy> connectionNameStrategy;
@Autowired
private RabbitConsumerProperties properties;
@Value("#{${rabbit.converterBeanName:null}}")
private MessageConverter messageConverter;
private CachingConnectionFactory ownConnectionFactory;
@Bean
public Function<Message<?>, Object> rabbitConsumer(@Qualifier("amqpChannelAdapter") MessageHandler messageHandler) {
return o -> {
messageHandler.handleMessage(o);
return "";
};
}
@Bean
public MessageHandler amqpChannelAdapter(ConnectionFactory rabbitConnectionFactory)
throws Exception {
AmqpOutboundChannelAdapterSpec handler = Amqp
.outboundAdapter(rabbitTemplate(this.properties.isOwnConnection()
? buildLocalConnectionFactory() : rabbitConnectionFactory))
.mappedRequestHeaders(properties.getMappedRequestHeaders())
.defaultDeliveryMode(properties.getPersistentDeliveryMode()
? MessageDeliveryMode.PERSISTENT
: MessageDeliveryMode.NON_PERSISTENT);
Expression exchangeExpression = this.properties.getExchangeExpression();
if (exchangeExpression != null) {
handler.exchangeNameExpression(exchangeExpression);
}
else {
handler.exchangeName(this.properties.getExchange());
}
Expression routingKeyExpression = this.properties.getRoutingKeyExpression();
if (routingKeyExpression != null) {
handler.routingKeyExpression(routingKeyExpression);
}
else {
handler.routingKey(this.properties.getRoutingKey());
}
return handler.get();
}
private ConnectionFactory buildLocalConnectionFactory() throws Exception {
this.ownConnectionFactory = new AutoConfig.Creator().rabbitConnectionFactory(
this.bootProperties, this.connectionNameStrategy);
return this.ownConnectionFactory;
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory rabbitConnectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory);
if (this.messageConverter != null) {
rabbitTemplate.setMessageConverter(this.messageConverter);
}
return rabbitTemplate;
}
@Bean
@ConditionalOnProperty(name = "rabbit.converterBeanName",
havingValue = RabbitConsumerProperties.JSON_CONVERTER)
public Jackson2JsonMessageConverter jsonConverter() {
return new Jackson2JsonMessageConverter();
}
@Override
public void destroy() {
if (this.ownConnectionFactory != null) {
this.ownConnectionFactory.destroy();
}
}
}
class AutoConfig extends RabbitAutoConfiguration {
static class Creator extends RabbitConnectionFactoryCreator {
@Override
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config,
ObjectProvider<ConnectionNameStrategy> connectionNameStrategy)
throws Exception {
CachingConnectionFactory cf = super.rabbitConnectionFactory(config,
connectionNameStrategy);
cf.setConnectionNameStrategy(
connectionFactory -> "rabbit.sink.own.connection");
cf.afterPropertiesSet();
return cf;
}
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2019-2020 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
*
* https://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.fn.consumer.rabbit;
import javax.validation.constraints.AssertTrue;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.validation.annotation.Validated;
@ConfigurationProperties("rabbit")
@Validated
public class RabbitConsumerProperties {
public static final String JSON_CONVERTER = "jsonConverter";
/**
* Exchange name - overridden by exchangeNameExpression, if supplied.
*/
private String exchange = "";
/**
* A SpEL expression that evaluates to an exchange name.
*/
private Expression exchangeExpression;
/**
* Routing key - overridden by routingKeyExpression, if supplied.
*/
private String routingKey;
/**
* A SpEL expression that evaluates to a routing key.
*/
private Expression routingKeyExpression;
/**
* Default delivery mode when 'amqp_deliveryMode' header is not present,
* true for PERSISTENT.
*/
private boolean persistentDeliveryMode;
/**
* Headers that will be mapped.
*/
private String[] mappedRequestHeaders = { "*" };
/**
* The bean name for a custom message converter; if omitted, a SimpleMessageConverter is used.
* If 'jsonConverter', a Jackson2JsonMessageConverter bean will be created for you.
*/
private String converterBeanName;
/**
* When true, use a separate connection based on the boot properties.
*/
private boolean ownConnection;
public String getExchange() {
return this.exchange;
}
public void setExchange(String exchange) {
this.exchange = exchange;
}
public Expression getExchangeExpression() {
return this.exchangeExpression;
}
public void setExchangeExpression(Expression exchangeExpression) {
this.exchangeExpression = exchangeExpression;
}
public String getRoutingKey() {
return this.routingKey;
}
public void setRoutingKey(String routingKey) {
this.routingKey = routingKey;
}
public Expression getRoutingKeyExpression() {
return this.routingKeyExpression;
}
public void setRoutingKeyExpression(Expression routingKeyExpression) {
this.routingKeyExpression = routingKeyExpression;
}
public boolean getPersistentDeliveryMode() {
return this.persistentDeliveryMode;
}
public void setPersistentDeliveryMode(boolean persistentDeliveryMode) {
this.persistentDeliveryMode = persistentDeliveryMode;
}
public String[] getMappedRequestHeaders() {
return this.mappedRequestHeaders;
}
public void setMappedRequestHeaders(String[] mappedRequestHeaders) {
this.mappedRequestHeaders = mappedRequestHeaders;
}
public String getConverterBeanName() {
return this.converterBeanName;
}
public void setConverterBeanName(String converterBeanName) {
this.converterBeanName = converterBeanName;
}
@AssertTrue(message = "routingKey or routingKeyExpression is required")
public boolean isRoutingKeyProvided() {
return this.routingKey != null || this.routingKeyExpression != null;
}
public boolean isOwnConnection() {
return this.ownConnection;
}
public void setOwnConnection(boolean ownConnection) {
this.ownConnection = ownConnection;
}
}

28
function/filter-function/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>filter-function</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>filter-function</name>
<description>Spring Native Function for applying filter SpEL expressions</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spel-function</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.filter;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.fn.spel.SpelFunctionConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.messaging.Message;
@Configuration
@Import(SpelFunctionConfiguration.class)
public class FilterFunctionConfiguration {
@Bean
public Function<Message<?>, Message<?>> filterFunction(
@Qualifier("spelFunction") Function<Message<?>, Message<?>> spelFunction) {
return message ->
Optional.of(message)
.filter(m -> (Boolean) spelFunction.apply(m).getPayload())
.orElse(null);
}
}

View File

@@ -0,0 +1 @@
spel.function.expression=true

View File

@@ -0,0 +1,53 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.filter;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
@SpringBootTest(properties = "spel.function.expression=payload.length() > 5")
@DirtiesContext
public class FilterFunctionApplicationTests {
@Autowired
@Qualifier("filterFunction")
Function<Message<?>, Message<?>> filter;
@Test
public void testFilter() {
Message<?> filtered = this.filter.apply(new GenericMessage<>("hello"));
assertThat(filtered).isNull();
filtered = this.filter.apply(new GenericMessage<>("hello world"));
assertThat(filtered).isNotNull()
.extracting(Message::getPayload)
.isEqualTo("hello world");
}
@SpringBootApplication
static class TestApplication {}
}

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>payload-converter-function</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>payload-converter-function</name>
<description>Utility message conversion functions</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2020 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
*
* https://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 functions;
import java.util.function.Function;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeTypeUtils;
/**
*
* @author Christian Tzolov
*/
public class ByteArrayTextToString implements Function<Message<?>, Message<?>> {
@Override
public Message<?> apply(Message<?> message) {
if (message.getPayload() instanceof byte[]) {
final MessageHeaders headers = message.getHeaders();
String contentType = headers.containsKey(MessageHeaders.CONTENT_TYPE)
? headers.get(MessageHeaders.CONTENT_TYPE).toString()
: MimeTypeUtils.APPLICATION_JSON_VALUE;
if (contentType.contains("text") || contentType.contains("json") || contentType.contains("x-spring-tuple")) {
message = MessageBuilder.withPayload(new String(((byte[]) message.getPayload())))
.copyHeaders(message.getHeaders())
.build();
}
}
return message;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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 functions;
import java.util.Collections;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
public class ByteArrayTextToStringTests {
private static final String MESSAGE = "hello world";
private static Function<Message<?>, Message<?>> converter;
@BeforeAll
static void before() {
converter = new ByteArrayTextToString();
}
@Test
public void testDefaultNoContentType() {
Message<?> converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes()));
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE);
converted = converter.apply(new GenericMessage<>(MESSAGE)); // String
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE);
}
@Test
public void testApplicationJsonContentType() {
Message<?> converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(),
Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON_VALUE)));
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo("hello world");
}
@Test
public void testPlainTextContentType() {
Message<?> converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(),
Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN_VALUE)));
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE);
}
@Test
public void testOctetContentType() {
Message<?> converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(),
Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)));
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE.getBytes());
}
@Test
public void testRandomNonTextContentType() {
Message<?> converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(),
Collections.singletonMap(MessageHeaders.CONTENT_TYPE, "Random Content Type")));
assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE.getBytes());
}
}

28
function/spel-function/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spel-function</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>spel-function</name>
<description>Spring Native Function for applying SpEL expressions</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.function</groupId>
<artifactId>payload-converter-function</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.spel;
import java.util.function.Function;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.transformer.ExpressionEvaluatingTransformer;
import org.springframework.messaging.Message;
@Configuration
@EnableConfigurationProperties(SpelFunctionProperties.class)
public class SpelFunctionConfiguration {
@Bean
public Function<Message<?>, Message<?>> spelFunction(
ExpressionEvaluatingTransformer expressionEvaluatingTransformer) {
return message -> expressionEvaluatingTransformer.transform(message);
}
@Bean
public ExpressionEvaluatingTransformer expressionEvaluatingTransformer(
SpelFunctionProperties spelFunctionProperties) {
return new ExpressionEvaluatingTransformer(new SpelExpressionParser()
.parseExpression(spelFunctionProperties.getExpression()));
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2020 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.fn.spel;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
/**
* Configuration properties for the SpEL function.
*
* @author Gary Russell
* @author Artem Bilan
*/
@ConfigurationProperties("spel.function")
public class SpelFunctionProperties {
private static final Expression DEFAULT_EXPRESSION = new SpelExpressionParser().parseExpression("payload");
/**
* A SpEL expression to apply.
*/
private String expression = DEFAULT_EXPRESSION.getExpressionString();
public void setExpression(String expression) {
this.expression = expression;
}
public String getExpression() {
return this.expression;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.spel;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(properties = "spel.function.expression=payload.toUpperCase()")
@DirtiesContext
public class SpelFunctionApplicationTests {
@Autowired
Function<Message<?>, Message<?>> transformer;
@Test
public void testTransform() {
final Message<?> transformed = this.transformer.apply(new GenericMessage<>("hello,world"));
assertThat(transformed.getPayload()).isEqualTo("HELLO,WORLD");
}
@Test
public void testJson() {
Message<?> message = MessageBuilder.withPayload("{\"foo\":\"bar\"}")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build();
final Message<?> transformed = this.transformer.apply(message);
assertThat(transformed.getPayload()).isEqualTo("{\"FOO\":\"BAR\"}");
}
@SpringBootApplication
static class TestApplication {
}
}

28
function/splitter-function/.gitignore vendored Normal file
View File

@@ -0,0 +1,28 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>splitter-function</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>splitter-function</name>
<description>Spring Native Function for Splitter</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,125 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.splitter;
import java.nio.charset.Charset;
import java.util.List;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.integration.splitter.AbstractMessageSplitter;
import org.springframework.integration.splitter.DefaultMessageSplitter;
import org.springframework.integration.splitter.ExpressionEvaluatingSplitter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import reactor.core.publisher.Flux;
@Configuration
@EnableConfigurationProperties(SplitterFunctionProperties.class)
public class SplitterFunctionConfiguration {
@Bean
public Function<Message<?>, List<Message<?>>> splitterFunction(AbstractMessageSplitter messageSplitter,
SplitterFunctionProperties splitterFunctionProperties) {
messageSplitter.setApplySequence(splitterFunctionProperties.isApplySequence());
ThreadLocalFluxSinkMessageChannel outputChannel = new ThreadLocalFluxSinkMessageChannel();
messageSplitter.setOutputChannel(outputChannel);
return message -> {
messageSplitter.handleMessage(message);
return outputChannel.publisherThreadLocal.get();
};
}
@Bean
@ConditionalOnProperty(prefix = "splitter", name = "expression")
public AbstractMessageSplitter expressionSplitter(SplitterFunctionProperties splitterFunctionProperties) {
return new ExpressionEvaluatingSplitter(
new SpelExpressionParser()
.parseExpression(splitterFunctionProperties.getExpression()));
}
@Bean
@ConditionalOnMissingBean
@Conditional(FileSplitterCondition.class)
public AbstractMessageSplitter fileSplitter(SplitterFunctionProperties splitterFunctionProperties) {
Boolean markers = splitterFunctionProperties.getFileMarkers();
String charset = splitterFunctionProperties.getCharset();
if (markers == null) {
markers = false;
}
FileSplitter fileSplitter = new FileSplitter(true, markers, splitterFunctionProperties.getMarkersJson());
if (charset != null) {
fileSplitter.setCharset(Charset.forName(charset));
}
return fileSplitter;
}
@Bean
@ConditionalOnMissingBean
public AbstractMessageSplitter defaultSplitter(SplitterFunctionProperties splitterFunctionProperties) {
DefaultMessageSplitter defaultMessageSplitter = new DefaultMessageSplitter();
defaultMessageSplitter.setDelimiters(splitterFunctionProperties.getDelimiters());
return defaultMessageSplitter;
}
static class FileSplitterCondition extends AnyNestedCondition {
FileSplitterCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(prefix = "splitter", name = "charset")
static class Charset { }
@ConditionalOnProperty(prefix = "splitter", name = "fileMarkers")
static class FileMarkers { }
}
private static final class ThreadLocalFluxSinkMessageChannel
implements MessageChannel, ReactiveStreamsSubscribableChannel {
private final ThreadLocal<List<Message<?>>> publisherThreadLocal = new ThreadLocal<>();
@Override
@SuppressWarnings("unchecked")
public void subscribeTo(Publisher<? extends Message<?>> publisher) {
this.publisherThreadLocal.set(Flux.from(publisher).collectList().cast(List.class).block());
}
@Override
public boolean send(Message<?> message, long l) {
throw new UnsupportedOperationException("This channel only supports a reactive 'subscribeTo()' ");
}
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2019 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.fn.splitter;
import javax.validation.constraints.AssertTrue;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* Configuration properties for the Splitter Processor app.
*
* @author Gary Russell
* @author Artem Bilan
*/
@ConfigurationProperties("splitter")
@Validated
public class SplitterFunctionProperties {
/**
* A SpEL expression for splitting payloads.
*/
private String expression;
/**
* When expression is null, delimiters to use when tokenizing
* {@link String} payloads.
*/
private String delimiters;
/**
* Set to true or false to use a {@code FileSplitter} (to split
* text-based files by line) that includes
* (or not) beginning/end of file markers.
*/
private Boolean fileMarkers;
/**
* When 'fileMarkers == true', specify if they should be produced
* as FileSplitter.FileMarker objects or JSON.
*/
private boolean markersJson = true;
/**
* The charset to use when converting bytes in text-based files
* to String.
*/
private String charset;
/**
* Add correlation/sequence information in headers to facilitate later
* aggregation.
*/
private boolean applySequence = true;
public void setExpression(String expression) {
this.expression = expression;
}
public String getExpression() {
return this.expression;
}
public String getDelimiters() {
return this.delimiters;
}
public void setDelimiters(String delimiters) {
this.delimiters = delimiters;
}
public Boolean getFileMarkers() {
return this.fileMarkers;
}
public void setFileMarkers(Boolean fileMarkers) {
this.fileMarkers = fileMarkers;
}
public boolean getMarkersJson() {
return this.markersJson;
}
public void setMarkersJson(boolean markersJson) {
this.markersJson = markersJson;
}
public String getCharset() {
return this.charset;
}
public void setCharset(String charset) {
this.charset = charset;
}
public boolean isApplySequence() {
return this.applySequence;
}
public void setApplySequence(boolean applySequence) {
this.applySequence = applySequence;
}
@AssertTrue(message = "'delimiters' is not allowed when an 'expression' is provided")
public boolean isDelimitersAllowed() {
return this.expression == null || this.delimiters == null;
}
@AssertTrue(message = "File properties are not allowed when an 'expression' or 'delimiters' property is provided")
public boolean isFilePropsAllowed() {
return !(this.expression != null || this.delimiters != null) || this.fileMarkers == null && this.charset == null;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved.
*
* 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
*
* https://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.fn.splitter;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.test.annotation.DirtiesContext;
@SpringBootTest(properties = "splitter.expression=payload.split(',')")
@DirtiesContext
public class SplitterFunctionApplicationTests {
@Autowired
Function<Message<?>, List<Message<?>>> splitter;
@Test
public void testExpressionSplitter() {
List<Message<?>> messageList = this.splitter.apply(new GenericMessage<>("hello,world"));
assertThat(messageList).extracting(m -> m.getPayload().toString()).contains("hello", "world");
}
@SpringBootApplication
static class TestApplication {}
}

205
pom.xml Normal file
View File

@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>java-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>java-functions-parent</name>
<description>Pivotal Java Functions Parent</description>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<maven-javadoc-plugin.version>3.1.1</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.2.1</maven-source-plugin.version>
<maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<modules>
<module>consumer/cassandra-consumer</module>
<module>consumer/counter-consumer</module>
<module>consumer/file-consumer</module>
<module>consumer/jdbc-consumer</module>
<module>consumer/log-consumer</module>
<module>consumer/mongodb-consumer</module>
<module>consumer/rabbit-consumer</module>
<module>function/filter-function</module>
<module>function/spel-function</module>
<module>function/payload-converter-function</module>
<module>function/splitter-function</module>
<module>supplier/file-supplier</module>
<module>supplier/http-supplier</module>
<module>supplier/jdbc-supplier</module>
<module>supplier/mongodb-supplier</module>
<module>supplier/time-supplier</module>
<module>spring-functions-parent</module>
</modules>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
<comments>Copyright 2014-2020 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.</comments>
</license>
</licenses>
<scm>
<connection>scm:git:git://github.com/pivotal/java-functions.git</connection>
<developerConnection>scm:git:ssh://git@github.com/pivotal/java-functions.git</developerConnection>
<url>https://github.com/pivotal/java-functions</url>
</scm>
<build>
<plugins>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
<executions>
<execution>
<id>javadoc</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<quiet>true</quiet>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>${maven-source-plugin.version}</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>**/*Tests.java</include>
<include>**/*Test.java</include>
</includes>
<excludes>
<exclude>**/Abstract*.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>repo.spring.io</id>
<name>Spring Release Repository</name>
<url>https://repo.spring.io/libs-release-local</url>
</repository>
<snapshotRepository>
<id>repo.spring.io</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
</snapshotRepository>
</distributionManagement>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>milestone</id>
<distributionManagement>
<repository>
<id>repo.spring.io</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/libs-milestone-local</url>
</repository>
</distributionManagement>
</profile>
<profile>
<id>central</id>
<build>
<plugins>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<repository>
<id>sonatype-nexus-staging</id>
<name>Nexus Release Repository</name>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
<snapshotRepository>
<id>sonatype-nexus-snapshots</id>
<name>Sonatype Nexus Snapshots</name>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</snapshotRepository>
</distributionManagement>
</profile>
</profiles>
</project>

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>java-functions-parent</artifactId>
<groupId>org.springframework.cloud.fn</groupId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<spring-boot.version>2.3.0.M4</spring-boot.version>
<spring-cloud-function.version>3.0.3.RELEASE</spring-cloud-function.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring-boot.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-dependencies</artifactId>
<version>${spring-cloud-function.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>file-supplier</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<name>file-supplier</name>
<description>file supplier</description>
<parent>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>spring-functions-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>../../spring-functions-parent</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-file</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* TODO: This will be used in other apps like (S)FTP and S3. Therefore, it might be moved to a common place.
*
* @author David Turanski
* @author Artem Bilan
*/
@ConfigurationProperties("file.consumer")
@Validated
public class FileConsumerProperties {
/**
* The FileReadingMode to use for file reading sources.
* Values are 'ref' - The File object,
* 'lines' - a message per line, or
* 'contents' - the contents as bytes.
*/
private FileReadingMode mode = FileReadingMode.contents;
/**
* Set to true to emit start of file/end of file marker messages before/after the data.
* Only valid with FileReadingMode 'lines'.
*/
private Boolean withMarkers = null;
/**
* When 'fileMarkers == true', specify if they should be produced
* as FileSplitter.FileMarker objects or JSON.
*/
private boolean markersJson = true;
@NotNull
public FileReadingMode getMode() {
return this.mode;
}
public void setMode(FileReadingMode mode) {
this.mode = mode;
}
public Boolean getWithMarkers() {
return this.withMarkers;
}
public void setWithMarkers(Boolean withMarkers) {
this.withMarkers = withMarkers;
}
public boolean getMarkersJson() {
return this.markersJson;
}
public void setMarkersJson(boolean markersJson) {
this.markersJson = markersJson;
}
@AssertTrue(message = "withMarkers can only be supplied when FileReadingMode is 'lines'")
public boolean isWithMarkersValid() {
return this.withMarkers == null || FileReadingMode.lines == this.mode;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
/**
* Defines the supported modes of reading and processing files.
*
* @author Gunnar Hillert
* @author David Turanski
*/
public enum FileReadingMode {
ref,
lines,
contents;
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.dsl.FileInboundChannelAdapterSpec;
import org.springframework.integration.file.dsl.Files;
import org.springframework.messaging.Message;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* @author Artem Bilan
* @author Soby Chacko
*/
@Configuration
@EnableConfigurationProperties({FileSupplierProperties.class, FileConsumerProperties.class})
public class FileSupplierConfiguration {
private final FileSupplierProperties fileSupplierProperties;
private final FileConsumerProperties fileConsumerProperties;
@Autowired
@Lazy
@Qualifier("fileMessageSource")
private FileReadingMessageSource fileMessageSource;
public FileSupplierConfiguration(FileSupplierProperties fileSupplierProperties,
FileConsumerProperties fileConsumerProperties) {
this.fileSupplierProperties = fileSupplierProperties;
this.fileConsumerProperties = fileConsumerProperties;
}
@Bean
public FileInboundChannelAdapterSpec fileMessageSource() {
final FileInboundChannelAdapterSpec fileInboundChannelAdapterSpec =
Files.inboundAdapter(this.fileSupplierProperties.getDirectory());
if (StringUtils.hasText(this.fileSupplierProperties.getFilenamePattern())) {
fileInboundChannelAdapterSpec.patternFilter(this.fileSupplierProperties.getFilenamePattern());
}
else if (this.fileSupplierProperties.getFilenameRegex() != null) {
fileInboundChannelAdapterSpec.regexFilter(this.fileSupplierProperties.getFilenameRegex().pattern());
}
fileInboundChannelAdapterSpec.preventDuplicates(this.fileSupplierProperties.isPreventDuplicates());
return fileInboundChannelAdapterSpec;
}
@Bean
public Flux<Message<?>> fileMessageFlux() {
return Mono.<Message<?>>create(monoSink ->
monoSink.onRequest(value ->
monoSink.success(this.fileMessageSource.receive())))
.subscribeOn(Schedulers.boundedElastic())
.repeatWhenEmpty(it -> it.delayElements(this.fileSupplierProperties.getDelayWhenEmpty()))
.repeat();
}
@Bean
@ConditionalOnExpression("environment['file.consumer.mode'] != 'ref'")
public Publisher<Message<Object>> fileReadingFlow() {
IntegrationFlowBuilder flowBuilder = IntegrationFlows.from(fileMessageFlux());
return FileUtils.enhanceFlowForReadingMode(flowBuilder, this.fileConsumerProperties)
.toReactivePublisher();
}
@Bean
public Supplier<Flux<Message<?>>> fileSupplier() {
if (this.fileConsumerProperties.getMode() == FileReadingMode.ref) {
return this::fileMessageFlux;
}
else {
return () -> Flux.from(fileReadingFlow());
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
import java.io.File;
import java.time.Duration;
import java.util.regex.Pattern;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
/**
* Properties for the file supplier.
*
* @author Gary Russell
* @author Artem Bilan
* @author Soby Chacko
*/
@ConfigurationProperties("file.supplier")
@Validated
public class FileSupplierProperties {
private static final String DEFAULT_DIR = System.getProperty("java.io.tmpdir") +
File.separator + "file-supplier";
/**
* The directory to poll for new files.
*/
private File directory = new File(DEFAULT_DIR);
/**
* Set to true to include an AcceptOnceFileListFilter which prevents duplicates.
*/
private boolean preventDuplicates = true;
/**
* A simple ant pattern to match files.
*/
private String filenamePattern;
/**
* A regex pattern to match files.
*/
private Pattern filenameRegex;
/**
* Duration of delay when no new files are detected.
*/
private Duration delayWhenEmpty = Duration.ofSeconds(1);
public File getDirectory() {
return this.directory;
}
public void setDirectory(File directory) {
this.directory = directory;
}
public boolean isPreventDuplicates() {
return this.preventDuplicates;
}
public void setPreventDuplicates(boolean preventDuplicates) {
this.preventDuplicates = preventDuplicates;
}
public String getFilenamePattern() {
return this.filenamePattern;
}
public void setFilenamePattern(String filenamePattern) {
this.filenamePattern = filenamePattern;
}
public Pattern getFilenameRegex() {
return this.filenameRegex;
}
public void setFilenameRegex(Pattern filenameRegex) {
this.filenameRegex = filenameRegex;
}
//@AssertTrue(message = "filenamePattern and filenameRegex are mutually exclusive")
public boolean isExclusivePatterns() {
return !(this.filenamePattern != null && this.filenameRegex != null);
}
public Duration getDelayWhenEmpty() {
return delayWhenEmpty;
}
public void setDelayWhenEmpty(Duration delayWhenEmpty) {
this.delayWhenEmpty = delayWhenEmpty;
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
import java.util.Collections;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.file.splitter.FileSplitter;
import org.springframework.integration.file.transformer.FileToByteArrayTransformer;
import org.springframework.integration.transformer.StreamTransformer;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.MimeTypeUtils;
/**
* @author Gary Russell
* @author Artem Bilan
* @author Christian Tzolov
*
*/
public class FileUtils {
/**
* Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on
* {@link FileConsumerProperties}.
* @param flowBuilder the flow builder.
* @param fileConsumerProperties the properties.
* @return the updated flow builder.
*/
public static IntegrationFlowBuilder enhanceFlowForReadingMode(IntegrationFlowBuilder flowBuilder,
FileConsumerProperties fileConsumerProperties) {
switch (fileConsumerProperties.getMode()) {
case contents:
flowBuilder.enrichHeaders(Collections.<String, Object>singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE))
.transform(new FileToByteArrayTransformer());
break;
case lines:
Boolean withMarkers = fileConsumerProperties.getWithMarkers();
if (withMarkers == null) {
withMarkers = false;
}
flowBuilder.enrichHeaders(Collections.<String, Object>singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.TEXT_PLAIN_VALUE))
.split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson()));
break;
case ref:
flowBuilder.enrichHeaders(Collections.<String, Object>singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.APPLICATION_JSON_VALUE));
break;
default:
throw new IllegalArgumentException(fileConsumerProperties.getMode().name() +
" is not a supported file reading mode.");
}
return flowBuilder;
}
/**
* Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on
* {@link FileConsumerProperties}; used for streaming sources.
* @param flowBuilder the flow builder.
* @param fileConsumerProperties the properties.
* @return the updated flow builder.
*/
public static IntegrationFlowBuilder enhanceStreamFlowForReadingMode(IntegrationFlowBuilder flowBuilder,
FileConsumerProperties fileConsumerProperties) {
switch (fileConsumerProperties.getMode()) {
case contents:
flowBuilder.enrichHeaders(Collections.<String, Object>singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE))
.transform(new StreamTransformer());
break;
case lines:
Boolean withMarkers = fileConsumerProperties.getWithMarkers();
if (withMarkers == null) {
withMarkers = false;
}
flowBuilder.enrichHeaders(Collections.<String, Object>singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.TEXT_PLAIN_VALUE))
.split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson()));
break;
case ref:
default:
throw new IllegalArgumentException(fileConsumerProperties.getMode().name() +
" is not a supported file reading mode when streaming.");
}
return flowBuilder;
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2020 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
*
* https://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.fn.supplier.file;
import java.nio.file.Path;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
import reactor.core.publisher.Flux;
/**
* @author Gary Russell
* @author Artem Bilan
* @author Soby Chacko
*/
@SpringBootTest
@DirtiesContext
public class AbstractFileSupplierTests {
@TempDir
static Path tempDir;
@Autowired
Supplier<Flux<Message<?>>> fileSupplier;
@BeforeAll
public static void beforeAll() {
System.setProperty("file.supplier.directory", tempDir.toAbsolutePath().toString());
}
@AfterAll
public static void afterAll() {
System.clearProperty("file.supplier.directory");
}
@SpringBootApplication
static class TestApplication {
}
}

Some files were not shown because too many files have changed in this diff Show More