Adding script-processor apps

https://github.com/spring-cloud/stream-applications/issues/34

* Addressing PR review comments
This commit is contained in:
Soby Chacko
2020-05-18 16:11:44 -04:00
committed by GitHub
parent 12fb2cfd46
commit c598674a98
10 changed files with 668 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2016-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.stream.app.processor.script;
import java.util.function.Function;
import java.util.regex.Matcher;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.integration.scripting.ScriptVariableGenerator;
import org.springframework.integration.scripting.dsl.Scripts;
import org.springframework.messaging.Message;
/**
* A Processor module that transforms messages using a supplied script. The script
* code is passed in directly via property. For more information on Spring script
* processing, see
* <a href=
* "https://spring.io/blog/2011/12/08/spring-integration-scripting-support-part-1">
* this blog article</a>.
*
* @author Andy Clement
* @author Gary Russell
* @author Chris Schaefer
* @author Artme Bilan
* @author Soby Chacko
*/
@Configuration
@EnableConfigurationProperties(ScriptProcessorProperties.class)
@Import(ScriptVariableGeneratorConfiguration.class)
public class ScriptProcessorConfiguration {
private static final String NEWLINE_ESCAPE = Matcher.quoteReplacement("\\n");
private static final String DOUBLE_DOUBLE_QUOTE = Matcher.quoteReplacement("\"\"");
private static final Log logger = LogFactory.getLog(ScriptProcessorConfiguration.class);
@Autowired
private ScriptProcessorProperties properties;
@Autowired
private ScriptVariableGenerator scriptVariableGenerator;
@Bean
public Function<Message<?>, Object> scriptProcessorFunction() {
return processor()::processMessage;
}
@Bean
public MessageProcessor<?> processor() {
String language = this.properties.getLanguage();
String script = this.properties.getScript();
logger.info(String.format("Input script is '%s', language is '%s'", script, language));
Resource scriptResource = new ByteArrayResource(decodeScript(script).getBytes());
return Scripts.processor(scriptResource)
.lang(language)
.variableGenerator(scriptVariableGenerator)
.get();
}
private static String decodeScript(String script) {
String toProcess = script;
// If it has both a leading and trailing double quote, remove them
if (toProcess.startsWith("\"") && toProcess.endsWith("\"")) {
toProcess = script.substring(1, script.length() - 1);
}
return toProcess.replaceAll(NEWLINE_ESCAPE, "\n").replaceAll(DOUBLE_DOUBLE_QUOTE, "\"");
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2016-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.stream.app.processor.script;
import java.util.Properties;
import javax.validation.constraints.NotNull;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.validation.annotation.Validated;
/**
* Configuration properties for the Scriptable Transform Processor module.
*
* @author Andy Clement
*/
@ConfigurationProperties("script-processor")
@Validated
public class ScriptProcessorProperties {
/**
* Language of the text in the script property. Supported: groovy, javascript, ruby, python.
*/
@NotNull
private String language;
/*
* Extra notes on the script parameter. The UI will typically look after encoding
* newlines and double quotes when packaging the value to pass to the script
* property. If not using the UI, attempting to define
* a script directly in the shell for example, it is important to note:
* - newlines should be escaped (\\n)
* - a single " should be expressed in a pair "" - the DSL parser recognizes this pattern
* - If the script starts and ends with a " then they will be stripped off before treating what is
* left as the script.
*
* Examples:
* ruby: --script="return ""#{payload.upcase}"""
* javascript: --script="function double(a) {\\n return a+"" + ""+a;\\n}\\ndouble(payload);"
*/
/**
* Text of the script.
*/
@NotNull
private String script;
/**
* Variable bindings as a new line delimited string of name-value pairs, e.g. 'foo=bar\n baz=car'.
*/
private Properties variables;
/**
* The location of a properties file containing custom script variable bindings.
*/
private Resource variablesLocation;
public String getLanguage() {
return this.language;
}
public void setLanguage(String language) {
this.language = language;
}
public String getScript() {
return this.script;
}
public void setScript(String script) {
this.script = script;
}
public Properties getVariables() {
return variables;
}
public void setVariables(Properties variables) {
this.variables = variables;
}
public Resource getVariablesLocation() {
return variablesLocation;
}
public void setVariablesLocation(Resource variablesLocation) {
this.variablesLocation = variablesLocation;
}
}

View File

@@ -0,0 +1,54 @@
/*
* 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.stream.app.processor.script;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.integration.scripting.DefaultScriptVariableGenerator;
import org.springframework.integration.scripting.ScriptVariableGenerator;
import org.springframework.util.CollectionUtils;
/**
* Configuration that provides a {@link ScriptVariableGenerator} to customize a script.
*
* @author David Turanski
* @author Eric Bottard
* @author Mark Fisher
*/
@Configuration
public class ScriptVariableGeneratorConfiguration {
@Autowired
private ScriptProcessorProperties properties;
@Bean(name = "variableGenerator")
public ScriptVariableGenerator scriptVariableGenerator() throws IOException {
Map<String, Object> variables = new HashMap<>();
CollectionUtils.mergePropertiesIntoMap(properties.getVariables(), variables);
if (properties.getVariablesLocation() != null) {
CollectionUtils.mergePropertiesIntoMap(
PropertiesLoaderUtils.loadProperties(properties.getVariablesLocation()), variables);
}
return new DefaultScriptVariableGenerator(variables);
}
}

View File

@@ -0,0 +1,4 @@
configuration-properties.classes=org.springframework.cloud.stream.app.processor.script.ScriptProcessorProperties

View File

@@ -0,0 +1,240 @@
/*
* 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.stream.app.processor.script;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration Tests for the Script Processor.
*
* @author Andy Clement
* @author Artem Bilan
* @author Gary Russell
* @author Chris Schaefer
* @author Soby Chacko
*/
public class ScriptProcessorIntegrationTests {
@Test
public void testJavascriptFunctions() throws IOException {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=function add(a,b) { return a+b;}; add(1,3)",
"--script-processor.language=js")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("hello world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).matches(s -> s.equals("4") || s.equals("4.0"));
ObjectMapper objectMapper = new ObjectMapper();
final Integer deserializedValue = objectMapper.readValue(sourceMessage.getPayload(), Integer.class);
assertThat(deserializedValue).matches(i -> i == 4 || i == 4.0);
}
}
@Test
public void testJavascriptVariableTake1() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.variables=foo=\\\40WORLD",
"--script-processor.script=payload+foo",
"--script-processor.language=js")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("hello world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("hello world WORLD");
}
}
@Test
public void testJavascriptVariableTake2() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.variables=limit=5",
"--script-processor.script=payload*limit",
"--script-processor.language=js")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>(9));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("45.0");
}
}
@Test
public void testGroovyBasic() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=payload+foo",
"--script-processor.language=groovy",
"--script-processor.variables=foo=\\\40WORLD")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("hello world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("hello world WORLD");
}
}
@Test
public void testGroovyComplex() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=payload.substring(0, limit as int) + foo",
"--script-processor.language=groovy",
"--script-processor.variables=limit=5 \n foo=\\\40WORLD")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("hello world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("hello WORLD");
}
}
@Test
public void testRubyScript() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=return \"\"#{payload.upcase}\"\"",
"--script-processor.language=ruby")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("hello world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("HELLO WORLD");
}
}
@Test
public void testRubyScriptComplex() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=\"def foo(x)\\n return x+5\\nend\\nfoo(payload)\\n\"",
"--script-processor.language=ruby")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>(9));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("14");
}
}
@Test
public void testPython() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=\"def multiply(x,y):\\n return x*y\\nanswer = multiply(payload,5)\\n\"",
"--script-processor.language=python")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>(6));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("30");
}
}
@Test
public void testPythonComplex() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=\"def concat(x,y):\\n return x+y\\nanswer = concat(\"\"hello \"\",payload)\\n\"",
"--script-processor.language=python")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("world"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("hello world");
}
}
@Test
public void testGroovyToJavascript() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ScriptProcessorTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=scriptProcessorFunction",
"--script-processor.script=\"@Grab('org.grooscript:grooscript:1.3.0')\\n import org.grooscript.GrooScript\\n GrooScript.convert(new String(payload))\\n\"",
"--script-processor.language=groovy")) {
InputDestination processorInput = context.getBean(InputDestination.class);
OutputDestination processorOutput = context.getBean(OutputDestination.class);
processorInput.send(new GenericMessage<>("def age=18"));
Message<byte[]> sourceMessage = processorOutput.receive(10000);
assertThat(new String(sourceMessage.getPayload())).isEqualTo("var age = 18;"
+ System.getProperty("line.separator"));
}
}
@EnableAutoConfiguration
@Import({ScriptProcessorConfiguration.class})
public static class ScriptProcessorTestConfiguration {
}
}

View File

@@ -0,0 +1,6 @@
@Grab('org.grooscript:grooscript:1.3.0')
import org.grooscript.GrooScript
@Grab('org.grooscript:grooscript:1.3.0')
import org.grooscript.GrooScript
GrooScript.convert(new String(payload))

View File

@@ -0,0 +1 @@
payload.substring(0, limit as int) + foo