Add support for Kotlin JSR223 scripts (#2898)

* Add support for Kotlin JSR223 scripts

* Add required Kotlin dependencies into the `spring-integration-scripting`
module
* Introduce `KotlinScriptExecutor` to interact with the
`KotlinJsr223JvmLocalScriptEngineFactory` directly since there is no
`META-INF/services/javax.script.ScriptEngineFactory` file in the Kotlin
* Also set an `idea.use.native.fs.for.win` system property to `false` in
this class to disable check for native support on Windows.
(Might be removed in future Kotlin versions)
* Move `ScriptParser.getLanguageFromFileExtension()` logic into the
`ScriptExecutorFactory.deriveLanguageFromFileExtension()` since the same
one must be applied in the `DslScriptExecutingMessageProcessor`, too.
* Modify tests to reflect Kotlin support
* Fix some test scripts to their official extensions

* * Add JavaDocs
* Polishing according Sonar objections
This commit is contained in:
Artem Bilan
2019-04-19 12:29:08 -04:00
committed by Gary Russell
parent 9cc0cbe3bd
commit 7dff1d5416
22 changed files with 277 additions and 140 deletions

View File

@@ -600,11 +600,14 @@ project('spring-integration-scripting') {
description = 'Spring Integration Scripting Support'
dependencies {
compile project(":spring-integration-core")
compile ('org.jetbrains.kotlin:kotlin-script-util', optional)
compile ('org.jetbrains.kotlin:kotlin-compiler-embeddable', optional)
testCompile("org.jruby:jruby-complete:$jrubyVersion")
testCompile("org.codehaus.groovy:groovy:$groovyVersion")
testCompile("org.codehaus.groovy:groovy-jsr223:$groovyVersion")
testCompile("org.python:jython-standalone:$jythonVersion")
testRuntime 'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable'
}
}

View File

@@ -24,6 +24,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.integration.support.utils.IntegrationUtils;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;
@@ -42,9 +43,9 @@ public abstract class AbstractScriptExecutingMessageProcessor<T>
private final ScriptVariableGenerator scriptVariableGenerator;
protected volatile ClassLoader beanClassLoader;
protected ClassLoader beanClassLoader;
protected volatile BeanFactory beanFactory;
protected BeanFactory beanFactory;
protected AbstractScriptExecutingMessageProcessor() {
this(new DefaultScriptVariableGenerator());
@@ -70,6 +71,7 @@ public abstract class AbstractScriptExecutingMessageProcessor<T>
* Executes the script and returns the result.
*/
@Override
@Nullable
public final T processMessage(Message<?> message) {
try {
ScriptSource source = getScriptSource(message);
@@ -96,6 +98,7 @@ public abstract class AbstractScriptExecutingMessageProcessor<T>
* @param variables The variables.
* @return The result of the execution.
*/
@Nullable
protected abstract T executeScript(ScriptSource scriptSource, Map<String, Object> variables);
}

View File

@@ -24,9 +24,13 @@ import org.springframework.scripting.ScriptSource;
import org.springframework.scripting.support.ResourceScriptSource;
/**
* A {@link ScriptSource} implementation, which caches a script string and refreshes it from the
* target file (if modified) according the provided {@link #refreshDelay}.
*
* @author Dave Syer
* @author Oleg Zhurakousky
* @author Artem Bilan
*
* @since 2.0
*/
public class RefreshableResourceScriptSource implements ScriptSource {

View File

@@ -18,29 +18,37 @@ package org.springframework.integration.scripting;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.scripting.ScriptSource;
/**
* A script evaluation abstraction against {@link ScriptSource} and optional binding {@code variables}.
*
* @author David Turanski
* @author Artem Bilan
*
* @since 2.1
*/
@FunctionalInterface
public interface ScriptExecutor {
/**
* @param scriptSource The script source.
* @return The result of the execution.
*/
default Object executeScript(ScriptSource scriptSource) {
return executeScript(scriptSource, null);
}
/**
* Execute a script from the provided {@link ScriptSource} with an optional binding {@code variables}.
* @param scriptSource The script source.
* @param variables The variables.
* @return The result of the execution.
*/
Object executeScript(ScriptSource scriptSource, Map<String, Object> variables);
@Nullable
Object executeScript(ScriptSource scriptSource, @Nullable Map<String, Object> variables);
/**
* Execute a script from the provided {@link ScriptSource}
* @param scriptSource The script source.
* @return The result of the execution.
*/
@Nullable
default Object executeScript(ScriptSource scriptSource) {
return executeScript(scriptSource, null);
}
}

View File

@@ -16,9 +16,6 @@
package org.springframework.integration.scripting.config.jsr223;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -29,8 +26,11 @@ import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory;
import org.springframework.util.StringUtils;
/**
* An {@link AbstractScriptParser} parser extension for the {@code <int-script:script>} tag.
*
* @author David Turanski
* @author Artem Bilan
*
* @since 2.1
*/
public class ScriptParser extends AbstractScriptParser {
@@ -50,38 +50,12 @@ public class ScriptParser extends AbstractScriptParser {
if (!StringUtils.hasText(scriptLocation)) {
parserContext.getReaderContext().error(
"An inline script requires the '" + LANGUAGE_ATTRIBUTE + "' attribute.", element);
return;
}
else {
language = getLanguageFromFileExtension(scriptLocation, parserContext, element);
if (language == null) {
parserContext.getReaderContext().error(
"Unable to determine language for script '" + scriptLocation + "'", element);
return;
}
language = ScriptExecutorFactory.deriveLanguageFromFileExtension(scriptLocation);
}
}
builder.addConstructorArgValue(ScriptExecutorFactory.getScriptExecutor(language));
}
private String getLanguageFromFileExtension(String scriptLocation, ParserContext parserContext, Element element) {
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = null;
int index = scriptLocation.lastIndexOf(".") + 1;
if (index < 1) {
return null;
}
String extension = scriptLocation.substring(index);
engine = engineManager.getEngineByExtension(extension);
if (engine == null) {
parserContext.getReaderContext().error(
"No suitable scripting engine found for extension '" + extension + "'", element);
}
return engine.getFactory().getLanguageName();
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.integration.scripting.dsl;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
@@ -32,6 +31,7 @@ import org.springframework.integration.scripting.jsr223.ScriptExecutingMessagePr
import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory;
import org.springframework.messaging.Message;
import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@@ -99,21 +99,16 @@ class DslScriptExecutingMessageProcessor
this.script = this.applicationContext.getResource(this.location);
}
ScriptSource scriptSource = new RefreshableResourceScriptSource(this.script, this.refreshCheckDelay);
if (!StringUtils.hasText(this.lang)) {
String filename = this.script.getFilename();
int index =
filename != null
? filename.lastIndexOf('.') + 1
: -1;
if (index < 1) {
throw new BeanCreationException(
"'lang' isn't provided and there is no 'file extension' for script resource: " + this.script);
}
this.lang = filename.substring(index);
String scriptFilename = this.script.getFilename();
Assert.hasText(scriptFilename,
() -> "Either 'lang' or file extension must be provided for script: " + this.script);
this.lang = ScriptExecutorFactory.deriveLanguageFromFileExtension(scriptFilename);
}
ScriptSource scriptSource = new RefreshableResourceScriptSource(this.script, this.refreshCheckDelay);
if (this.applicationContext.containsBean(ScriptExecutingProcessorFactory.BEAN_NAME)) {
ScriptExecutingProcessorFactory processorFactory =
this.applicationContext.getBean(ScriptExecutingProcessorFactory.BEAN_NAME,

View File

@@ -29,42 +29,49 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.integration.scripting.ScriptExecutor;
import org.springframework.integration.scripting.ScriptingException;
import org.springframework.lang.Nullable;
import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;
/**
* Base Class for {@link ScriptExecutor}
* Base Class for {@link ScriptExecutor}.
*
* @author David Turanski
* @author Mark Fisher
* @author Artem Bilan
* @author Gary Russell
*
* @since 2.1
*/
public abstract class AbstractScriptExecutor implements ScriptExecutor {
protected final Log logger = LogFactory.getLog(this.getClass());
protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - final
protected final ScriptEngine scriptEngine;
protected final String language;
private final ScriptEngine scriptEngine;
protected AbstractScriptExecutor(String language) {
Assert.hasText(language, "language must not be empty");
this.language = language;
this.scriptEngine = new ScriptEngineManager().getEngineByName(this.language);
Assert.notNull(this.scriptEngine, invalidLanguageMessage(this.language));
this.scriptEngine = new ScriptEngineManager().getEngineByName(language);
Assert.notNull(this.scriptEngine, () -> invalidLanguageMessage(language));
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using script engine : " + this.scriptEngine.getFactory().getEngineName());
}
}
@Override
public Object executeScript(ScriptSource scriptSource, Map<String, Object> variables) {
Object result;
protected AbstractScriptExecutor(ScriptEngine scriptEngine) {
Assert.notNull(scriptEngine, "'scriptEngine' must not be null.");
this.scriptEngine = scriptEngine;
}
public ScriptEngine getScriptEngine() {
return this.scriptEngine;
}
@Override
@Nullable
public Object executeScript(ScriptSource scriptSource, Map<String, Object> variables) {
try {
Object result;
String script = scriptSource.getScriptAsString();
Date start = new Date();
if (this.logger.isDebugEnabled()) {
@@ -85,13 +92,11 @@ public abstract class AbstractScriptExecutor implements ScriptExecutor {
if (this.logger.isDebugEnabled()) {
this.logger.debug("script executed in " + (new Date().getTime() - start.getTime()) + " ms");
}
return result;
}
catch (Exception e) {
throw new ScriptingException(e.getMessage(), e);
}
return result;
}
/**

View File

@@ -20,12 +20,16 @@ import javax.script.Bindings;
import javax.script.ScriptEngine;
/**
* Default implementation of the
* {@link org.springframework.integration.scripting.ScriptExecutor}
* Default implementation of the {@link AbstractScriptExecutor}.
* Accepts a scripting language for resolving a target {@code ScriptEngine} for
* evaluation and does nothing with the {@code result} in the
* {@link #postProcess(Object, ScriptEngine, String, Bindings)} implementation.
*
* @author David Turanski
* @author Mark Fisher
* @author Gary Russell
* @author Artem Bilan
*
* @since 2.1
*/
public class DefaultScriptExecutor extends AbstractScriptExecutor {

View File

@@ -0,0 +1,51 @@
/*
* 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
*
* 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.integration.scripting.jsr223;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory;
/**
* An {@link AbstractScriptExecutor} for the Kotlin scripts support.
* Uses {@link KotlinJsr223JvmLocalScriptEngineFactory} directly since there is
* no {@code META-INF/services/javax.script.ScriptEngineFactory} file in CLASSPATH.
* Also sets an {@code idea.use.native.fs.for.win} system property to {@code false}
* to disable a native engine discovery for Windows: bay be resolved in the future Kotlin versions.
*
* @author Artem Bilan
*
* @since 5.2
*/
public class KotlinScriptExecutor extends AbstractScriptExecutor {
static {
System.setProperty("idea.use.native.fs.for.win", "false");
}
public KotlinScriptExecutor() {
super(new KotlinJsr223JvmLocalScriptEngineFactory().getScriptEngine());
}
@Override
protected Object postProcess(Object result, ScriptEngine scriptEngine, String script, Bindings bindings) {
return result;
}
}

View File

@@ -16,24 +16,28 @@
package org.springframework.integration.scripting.jsr223;
import org.springframework.util.ClassUtils;
/**
* A {@link DefaultScriptExecutor} extension for Ruby scripting support.
* It is present here only for the reason to populate
* {@code org.jruby.embed.localvariable.behavior} and
* {@code org.jruby.embed.localcontext.scope} system properties.
* May be revised in the future.
*
* @author David Turanski
* @author Artem Bilan
*
* @since 2.1
*
*/
public class RubyScriptExecutor extends DefaultScriptExecutor {
static {
if (ClassUtils.isPresent("org.jruby.embed.jsr223.JRubyEngine", System.class.getClassLoader())) {
System.setProperty("org.jruby.embed.localvariable.behavior", "transient");
System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");
}
System.setProperty("org.jruby.embed.localvariable.behavior", "transient");
System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");
}
public RubyScriptExecutor() {
super("ruby");
}
}

View File

@@ -27,6 +27,10 @@ import org.springframework.scripting.ScriptSource;
import org.springframework.util.Assert;
/**
* An {@link AbstractScriptExecutingMessageProcessor} implementation for evaluating scripts
* from the provided {@link ScriptSource} in the provided {@link ScriptExecutor} against an optional
* binding {@code variables}.
*
* @author David Turanski
* @author Artem Bilan
*
@@ -36,7 +40,7 @@ public class ScriptExecutingMessageProcessor extends AbstractScriptExecutingMess
private final ScriptExecutor scriptExecutor;
private volatile ScriptSource scriptSource;
private final ScriptSource scriptSource;
/**
@@ -46,8 +50,7 @@ public class ScriptExecutingMessageProcessor extends AbstractScriptExecutingMess
* @param scriptExecutor The script executor.
*/
public ScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptExecutor scriptExecutor) {
this.scriptSource = scriptSource;
this.scriptExecutor = scriptExecutor;
this(scriptSource, new DefaultScriptVariableGenerator(), scriptExecutor);
}
/**
@@ -61,6 +64,8 @@ public class ScriptExecutingMessageProcessor extends AbstractScriptExecutingMess
ScriptExecutor scriptExecutor) {
super(scriptVariableGenerator);
Assert.notNull(scriptSource, "'scriptSource' must not be null");
Assert.notNull(scriptExecutor, "'scriptExecutor' must not be null");
this.scriptSource = scriptSource;
this.scriptExecutor = scriptExecutor;
}
@@ -75,9 +80,7 @@ public class ScriptExecutingMessageProcessor extends AbstractScriptExecutingMess
public ScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptExecutor scriptExecutor,
Map<String, Object> variables) {
super(new DefaultScriptVariableGenerator(variables));
this.scriptSource = scriptSource;
this.scriptExecutor = scriptExecutor;
this(scriptSource, new DefaultScriptVariableGenerator(variables), scriptExecutor);
}

View File

@@ -16,13 +16,21 @@
package org.springframework.integration.scripting.jsr223;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import org.springframework.integration.scripting.ScriptExecutor;
import org.springframework.util.Assert;
/**
* The scripting configuration utilities.
*
* @author David Turanski
* @author Artem Bilan
*
* @since 2.1
*/
public abstract class ScriptExecutorFactory {
public final class ScriptExecutorFactory {
public static ScriptExecutor getScriptExecutor(String language) {
if (language.equalsIgnoreCase("python") || language.equalsIgnoreCase("jython")) {
@@ -31,7 +39,33 @@ public abstract class ScriptExecutorFactory {
else if (language.equalsIgnoreCase("ruby") || language.equalsIgnoreCase("jruby")) {
return new RubyScriptExecutor();
}
else if (language.equalsIgnoreCase("kotlin")) {
return new KotlinScriptExecutor();
}
return new DefaultScriptExecutor(language);
}
/**
* Derive a scripting language from the provided script file name.
* @param scriptLocation the script file to consult for extension.
* @return the language name for the {@link ScriptExecutor}.
* @since 5.2
*/
public static String deriveLanguageFromFileExtension(String scriptLocation) {
int index = scriptLocation.lastIndexOf(".") + 1;
Assert.state(index > 0, () -> "Unable to determine language for script '" + scriptLocation + "'");
String extension = scriptLocation.substring(index);
if (extension.equals("kts")) {
return "kotlin";
}
ScriptEngineManager engineManager = new ScriptEngineManager();
ScriptEngine engine = engineManager.getEngineByExtension(extension);
Assert.state(engine != null, () -> "No suitable scripting engine found for extension '" + extension + "'");
return engine.getFactory().getLanguageName();
}
private ScriptExecutorFactory() {
super();
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.channel.QueueChannelOperations;
@@ -100,7 +101,7 @@ public class ScriptsTests {
@BeforeClass
public static void setup() throws IOException {
SCRIPT_FILE = FOLDER.newFile("script.jython");
SCRIPT_FILE = FOLDER.newFile("script.py");
FileCopyUtils.copy("1".getBytes(), SCRIPT_FILE);
}
@@ -152,7 +153,7 @@ public class ScriptsTests {
}
@Test
public void routerTest() throws IOException {
public void routerTest() {
this.scriptRouterInput.send(new GenericMessage<>("aardvark"));
this.scriptRouterInput.send(new GenericMessage<>("bear"));
this.scriptRouterInput.send(new GenericMessage<>("cat"));
@@ -166,7 +167,7 @@ public class ScriptsTests {
}
@Test
public void messageSourceTest() throws IOException, InterruptedException {
public void messageSourceTest() throws InterruptedException {
Message<?> message = this.messageSourceChannel.receive(20000);
assertThat(message).isNotNull();
Object payload = message.getPayload();
@@ -180,6 +181,20 @@ public class ScriptsTests {
assertThat(this.messageSourceChannel.receive(20000)).isNotNull();
}
@Autowired
@Qualifier("kotlinScriptFlow.input")
private MessageChannel kotlinScriptFlowInput;
@Test
public void testKotlinScript() {
this.kotlinScriptFlowInput.send(new GenericMessage<>(3));
Message<?> receive = this.results.receive(10_000);
assertThat(receive).isNotNull()
.extracting(Message::getPayload)
.isEqualTo(5);
}
@Configuration
@EnableIntegration
public static class ContextConfiguration {
@@ -187,7 +202,7 @@ public class ScriptsTests {
@Value("scripts/TesSplitterScript.groovy")
private Resource splitterScript;
@Value("scripts/TestFilterScript.groovy")
@Value("scripts/TestFilterScript.kts")
private Resource filterScript;
@Bean
@@ -244,12 +259,21 @@ public class ScriptsTests {
@Bean
public IntegrationFlow scriptPollingAdapter() {
return IntegrationFlows
.from(Scripts.messageSource("scripts/TestMessageSourceScript.ruby"),
.from(Scripts.messageSource("scripts/TestMessageSourceScript.rb"),
e -> e.poller(p -> p.fixedDelay(100)))
.channel(c -> c.queue("messageSourceChannel"))
.get();
}
@Bean
public IntegrationFlow kotlinScriptFlow() {
return f -> f
.handle(Scripts.processor(new ByteArrayResource("2 + bindings[\"payload\"] as Int".getBytes()))
.lang("kotlin"))
.channel(results());
}
}
}

View File

@@ -9,4 +9,5 @@
<int-script:script location="foo.groovy"/>
<int-script:script location="foo.js"/>
<int-script:script location="foo.py"/>
<int-script:script location="foo.kts"/>
</beans>

View File

@@ -17,76 +17,74 @@
package org.springframework.integration.scripting.jsr223;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @author David Turanski
* @author Artem Bilan
*
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@RunWith(SpringRunner.class)
public class DeriveLanguageFromExtensionTests {
@Autowired
private ApplicationContext ctx;
@Test
public void testParseLanguage() {
String[] langs = { "ruby", "Groovy", "ECMAScript", "python" };
String[] langs = { "ruby", "Groovy", "ECMAScript", "python", "kotlin" };
Class<?>[] executors = {
RubyScriptExecutor.class,
DefaultScriptExecutor.class,
DefaultScriptExecutor.class,
PythonScriptExecutor.class
PythonScriptExecutor.class,
KotlinScriptExecutor.class
};
Map<String, ScriptExecutingMessageProcessor> scriptProcessors = ctx
.getBeansOfType(ScriptExecutingMessageProcessor.class);
assertThat(scriptProcessors.size()).isEqualTo(4);
for (int i = 0; i < 4; i++) {
Map<String, ScriptExecutingMessageProcessor> scriptProcessors =
this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class);
assertThat(scriptProcessors.size()).isEqualTo(5);
for (int i = 0; i < 5; i++) {
ScriptExecutingMessageProcessor processor = ctx.getBean(
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" + i,
ScriptExecutingMessageProcessor.class);
AbstractScriptExecutor executor = (AbstractScriptExecutor) TestUtils.getPropertyValue(processor,
"scriptExecutor");
assertThat(executor.language).isEqualTo(langs[i]);
AbstractScriptExecutor executor =
TestUtils.getPropertyValue(processor, "scriptExecutor", AbstractScriptExecutor.class);
assertThat(executor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(langs[i]);
assertThat(executor.getClass()).isEqualTo(executors[i]);
}
}
@Test
public void testBadExtension() {
try {
new ClassPathXmlApplicationContext(this.getClass().getSimpleName() + "-fail1-context.xml", this.getClass())
.close();
}
catch (Exception e) {
assertThat(e.getMessage().contains("No suitable scripting engine found for extension 'xx'")).isTrue();
}
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() ->
new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-fail1-context.xml",
getClass()).close())
.withMessageContaining("No suitable scripting engine found for extension 'xx'");
}
@Test
public void testNoExtension() {
try {
new ClassPathXmlApplicationContext(this.getClass().getSimpleName() + "-fail2-context.xml", this.getClass())
.close();
}
catch (Exception e) {
assertThat(e.getMessage().contains("Unable to determine language for script 'foo'")).isTrue();
}
assertThatExceptionOfType(BeanDefinitionStoreException.class)
.isThrownBy(() ->
new ClassPathXmlApplicationContext(getClass().getSimpleName() + "-fail2-context.xml",
getClass()).close())
.withMessageContaining("Unable to determine language for script 'foo'");
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.scripting.jsr223;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import java.util.HashMap;
import java.util.Map;
@@ -41,9 +42,9 @@ public class Jsr223ScriptExecutorTests {
executor.executeScript(new StaticScriptSource("'hello, world'"));
executor.executeScript(new StaticScriptSource("'hello, again'"));
Map<String, Object> variables = new HashMap<String, Object>();
Map<String, Object> variables = new HashMap<>();
Map<String, Object> headers = new HashMap<String, Object>();
Map<String, Object> headers = new HashMap<>();
headers.put("one", 1);
headers.put("two", "two");
headers.put("three", 3);
@@ -54,14 +55,14 @@ public class Jsr223ScriptExecutorTests {
Resource resource = new ClassPathResource("/org/springframework/integration/scripting/jsr223/print_message.rb");
String result = (String) executor.executeScript(new ResourceScriptSource(resource), variables);
assertThat(result.substring(0, "payload modified".length())).isEqualTo("payload modified");
assertThat(result).isNotNull().contains("payload modified");
}
@Test
public void testJs() {
ScriptExecutor executor = ScriptExecutorFactory.getScriptExecutor("js");
Object obj = executor.executeScript(new StaticScriptSource("function js(){ return 'js';} js();"));
assertThat(obj.toString()).isEqualTo("js");
assertThat(obj).isNotNull().isEqualTo("js");
}
@Test
@@ -74,9 +75,9 @@ public class Jsr223ScriptExecutorTests {
assertThat(obj).isEqualTo(2);
}
@Test(expected = IllegalArgumentException.class)
@Test
public void testInvalidLanguageThrowsIllegalArgumentException() {
ScriptExecutorFactory.getScriptExecutor("foo");
assertThatIllegalArgumentException().isThrownBy(() -> ScriptExecutorFactory.getScriptExecutor("foo"));
}
}

View File

@@ -39,32 +39,32 @@ import org.springframework.scripting.support.StaticScriptSource;
*/
public class PythonScriptExecutorTests {
ScriptExecutor executor;
private ScriptExecutor executor;
@Before
public void init() {
executor = new PythonScriptExecutor();
this.executor = new PythonScriptExecutor();
}
@Test
public void testLiteral() {
Object obj = executor.executeScript(new StaticScriptSource("3+4"));
Object obj = this.executor.executeScript(new StaticScriptSource("3+4"));
assertThat(obj).isEqualTo(7);
obj = executor.executeScript(new StaticScriptSource("'hello,world'"));
obj = this.executor.executeScript(new StaticScriptSource("'hello,world'"));
assertThat(obj).isEqualTo("hello,world");
}
@Test
public void test1() {
Object obj = executor.executeScript(new StaticScriptSource("x=2"));
Object obj = this.executor.executeScript(new StaticScriptSource("x=2"));
assertThat(obj).isEqualTo(2);
}
@Test
public void test2() {
Object obj = executor.executeScript(new StaticScriptSource("def foo(y):\n\tx=y\n\treturn y\nz=foo(2)"));
Object obj = this.executor.executeScript(new StaticScriptSource("def foo(y):\n\tx=y\n\treturn y\nz=foo(2)"));
assertThat(obj).isEqualTo(2);
}
@@ -73,9 +73,13 @@ public class PythonScriptExecutorTests {
ScriptSource source =
new ResourceScriptSource(
new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py"));
Object obj = executor.executeScript(source);
PyTuple tuple = (PyTuple) obj;
assertThat(tuple.get(0)).isEqualTo(1);
Object obj = this.executor.executeScript(source);
assertThat(obj)
.isNotNull()
.isInstanceOf(PyTuple.class)
.asList()
.element(0)
.isEqualTo(1);
}
@Test
@@ -85,17 +89,20 @@ public class PythonScriptExecutorTests {
new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py"));
HashMap<String, Object> variables = new HashMap<>();
variables.put("foo", "bar");
Object obj = executor.executeScript(source, variables);
assertThat(obj).isNotNull();
PyTuple tuple = (PyTuple) obj;
assertThat(tuple.get(0)).isEqualTo(1);
Object obj = this.executor.executeScript(source, variables);
assertThat(obj)
.isNotNull()
.isInstanceOf(PyTuple.class)
.asList()
.element(0)
.isEqualTo(1);
}
@Test
public void testEmbeddedVariable() {
Map<String, Object> variables = new HashMap<>();
variables.put("scope", "world");
Object obj = executor.executeScript(new StaticScriptSource("\"hello, %s\"% scope"), variables);
Object obj = this.executor.executeScript(new StaticScriptSource("\"hello, %s\"% scope"), variables);
assertThat(obj).isEqualTo("hello, world");
}

View File

@@ -0,0 +1 @@
(bindings["headers"] as Map<String, *>)["type"] == "good"

View File

@@ -2,7 +2,7 @@
=== Scripting Support
Spring Integration 2.1 added support for the https://www.jcp.org/en/jsr/detail?id=223[JSR223 Scripting for Java specification], introduced in Java version 6.
It lets you use scripts written in any supported language (including Ruby, JRuby, Javascript, and Groovy) to provide the logic for various integration components, similar to the way the Spring Expression Language (SpEL) is used in Spring Integration.
It lets you use scripts written in any supported language (including Ruby, JRuby, Javascript, Groovy and Kotlin) to provide the logic for various integration components, similar to the way the Spring Expression Language (SpEL) is used in Spring Integration.
For more information about JSR223, see the https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/api.html[documentation].
You need to include this dependency into your project:
@@ -27,6 +27,19 @@ compile "org.springframework.integration:spring-integration-scripting:{project-v
In addition you need to add a script engine implementation, e.g. JRuby, Jython.
Starting with version 5.2, Spring Integration provides a Kotlin Jsr223 support.
You need to add these dependencies into your project to make it working:
[source, groovy]
----
runtime 'org.jetbrains.kotlin:kotlin-script-util'
runtime 'org.jetbrains.kotlin:kotlin-compiler-embeddable'
runtime 'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable'
----
====
The `KotlinScriptExecutor` is selected by the provided `kotlin` language indicator or script file comes with the `.kts` extension.
IMPORTANT: Note that this feature requires Java 6 or higher.
In order to use a JVM scripting language, a JSR223 implementation for that language must be included in your class path.
@@ -52,7 +65,7 @@ The following pair of examples show sample configurations that create filters:
[source,xml]
----
<int:filter input-channel="referencedScriptInput">
<int-script:script lang="ruby" location="some/path/to/ruby/script/RubyFilterTests.rb"/>
<int-script:script location="some/path/to/ruby/script/RubyFilterTests.rb"/>
</int:filter>
<int:filter input-channel="inlineScriptInput">

View File

@@ -19,6 +19,11 @@ See <<rate-limiter-advice>> for more information.
The `CacheRequestHandlerAdvice` is now available for caching request results on handlers.
See <<cache-advice>> for more information.
[[x5.2-kotlinScripts]]
=== Kotlin Scripts Support
The JSR223 scripting module now includes a support for Kotlin scripts.
See <<scripting>> for more information.
[[x5.2-general]]
=== General Changes