diff --git a/build.gradle b/build.gradle index 624898b8703..259f8439deb 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/AbstractScriptExecutingMessageProcessor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/AbstractScriptExecutingMessageProcessor.java index 6c130eccf8f..47a14d12d6d 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/AbstractScriptExecutingMessageProcessor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/AbstractScriptExecutingMessageProcessor.java @@ -24,6 +24,7 @@ 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 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 void setBeanFactory(BeanFactory beanFactory) throws BeansException { * 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 final T processMessage(Message message) { * @param variables The variables. * @return The result of the execution. */ + @Nullable protected abstract T executeScript(ScriptSource scriptSource, Map variables); } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/RefreshableResourceScriptSource.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/RefreshableResourceScriptSource.java index 3e812fd59bc..cdffa71ff22 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/RefreshableResourceScriptSource.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/RefreshableResourceScriptSource.java @@ -24,9 +24,13 @@ 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 { diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutor.java index b17ebda31c3..fcaa3b9bb30 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutor.java @@ -18,29 +18,37 @@ 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 { /** + * 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. */ - default Object executeScript(ScriptSource scriptSource) { - return executeScript(scriptSource, null); - } + @Nullable + Object executeScript(ScriptSource scriptSource, @Nullable Map variables); /** + * Execute a script from the provided {@link ScriptSource} * @param scriptSource The script source. - * @param variables The variables. * @return The result of the execution. */ - Object executeScript(ScriptSource scriptSource, Map variables); + @Nullable + default Object executeScript(ScriptSource scriptSource) { + return executeScript(scriptSource, null); + } } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java index 581ae4cc1da..e7c1fa26ce1 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java @@ -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.util.StringUtils; /** + * An {@link AbstractScriptParser} parser extension for the {@code } tag. + * * @author David Turanski * @author Artem Bilan + * * @since 2.1 */ public class ScriptParser extends AbstractScriptParser { @@ -50,38 +50,12 @@ protected void postProcess(BeanDefinitionBuilder builder, Element element, Parse 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(); - } } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java index 7f8ca0b62dc..292899b0f79 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java @@ -18,7 +18,6 @@ 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.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 @@ public void afterPropertiesSet() { 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, diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/AbstractScriptExecutor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/AbstractScriptExecutor.java index 69c7be48568..2afd5eca2ec 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/AbstractScriptExecutor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/AbstractScriptExecutor.java @@ -29,42 +29,49 @@ 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 ScriptEngine scriptEngine; + protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - final - 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()); } } + 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 variables) { - Object result; - try { + Object result; String script = scriptSource.getScriptAsString(); Date start = new Date(); if (this.logger.isDebugEnabled()) { @@ -85,13 +92,11 @@ public Object executeScript(ScriptSource scriptSource, Map varia 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; } /** diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/DefaultScriptExecutor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/DefaultScriptExecutor.java index dd67345e6cd..6eee376a36b 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/DefaultScriptExecutor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/DefaultScriptExecutor.java @@ -20,12 +20,16 @@ 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 { diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/KotlinScriptExecutor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/KotlinScriptExecutor.java new file mode 100644 index 00000000000..5bbe376129a --- /dev/null +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/KotlinScriptExecutor.java @@ -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; + } + +} diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/RubyScriptExecutor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/RubyScriptExecutor.java index 9e42a65ae05..3dabd880a75 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/RubyScriptExecutor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/RubyScriptExecutor.java @@ -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"); } + } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutingMessageProcessor.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutingMessageProcessor.java index 93a3f3376d9..94b1e5ee741 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutingMessageProcessor.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutingMessageProcessor.java @@ -27,6 +27,10 @@ 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 ScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptVariable 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 ScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptVariable public ScriptExecutingMessageProcessor(ScriptSource scriptSource, ScriptExecutor scriptExecutor, Map variables) { - super(new DefaultScriptVariableGenerator(variables)); - this.scriptSource = scriptSource; - this.scriptExecutor = scriptExecutor; + this(scriptSource, new DefaultScriptVariableGenerator(variables), scriptExecutor); } diff --git a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutorFactory.java b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutorFactory.java index 21b2799c531..06300796b80 100644 --- a/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutorFactory.java +++ b/spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/ScriptExecutorFactory.java @@ -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 static ScriptExecutor getScriptExecutor(String language) { 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(); + } + } diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java index d3d33131997..8b0e4d19108 100644 --- a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/dsl/ScriptsTests.java @@ -34,6 +34,7 @@ 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 void serviceWithRefreshCheckDelayTest() throws IOException { } @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 void routerTest() throws IOException { } @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 void messageSourceTest() throws IOException, InterruptedException { 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 static class ContextConfiguration { @Value("scripts/TesSplitterScript.groovy") private Resource splitterScript; - @Value("scripts/TestFilterScript.groovy") + @Value("scripts/TestFilterScript.kts") private Resource filterScript; @Bean @@ -244,12 +259,21 @@ public PollableChannel shortStrings() { @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()); + } + + } } diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml index 3037c6b6c68..7e5c379ecb3 100644 --- a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml @@ -9,4 +9,5 @@ + diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java index ad22395d415..8eae6a8eafc 100644 --- a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java @@ -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 scriptProcessors = ctx - .getBeansOfType(ScriptExecutingMessageProcessor.class); - assertThat(scriptProcessors.size()).isEqualTo(4); - - for (int i = 0; i < 4; i++) { + Map 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'"); } } diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/Jsr223ScriptExecutorTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/Jsr223ScriptExecutorTests.java index d18a33861b7..ad299329feb 100644 --- a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/Jsr223ScriptExecutorTests.java +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/Jsr223ScriptExecutorTests.java @@ -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 void test() { executor.executeScript(new StaticScriptSource("'hello, world'")); executor.executeScript(new StaticScriptSource("'hello, again'")); - Map variables = new HashMap(); + Map variables = new HashMap<>(); - Map headers = new HashMap(); + Map headers = new HashMap<>(); headers.put("one", 1); headers.put("two", "two"); headers.put("three", 3); @@ -54,14 +55,14 @@ public void test() { 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 void testPython() { assertThat(obj).isEqualTo(2); } - @Test(expected = IllegalArgumentException.class) + @Test public void testInvalidLanguageThrowsIllegalArgumentException() { - ScriptExecutorFactory.getScriptExecutor("foo"); + assertThatIllegalArgumentException().isThrownBy(() -> ScriptExecutorFactory.getScriptExecutor("foo")); } } diff --git a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonScriptExecutorTests.java b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonScriptExecutorTests.java index 3139acd86ea..8494e246eca 100644 --- a/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonScriptExecutorTests.java +++ b/spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonScriptExecutorTests.java @@ -39,32 +39,32 @@ */ 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 void test3() { 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 void test3WithVariables() { new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py")); HashMap 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 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"); } diff --git a/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy b/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy deleted file mode 100644 index 06a775dfb29..00000000000 --- a/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy +++ /dev/null @@ -1 +0,0 @@ -headers.type == 'good' diff --git a/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.kts b/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.kts new file mode 100644 index 00000000000..35189725d46 --- /dev/null +++ b/spring-integration-scripting/src/test/resources/scripts/TestFilterScript.kts @@ -0,0 +1 @@ +(bindings["headers"] as Map)["type"] == "good" diff --git a/spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.ruby b/spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.rb similarity index 100% rename from spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.ruby rename to spring-integration-scripting/src/test/resources/scripts/TestMessageSourceScript.rb diff --git a/src/reference/asciidoc/scripting.adoc b/src/reference/asciidoc/scripting.adoc index 6815ff0ed00..79cae4db673 100644 --- a/src/reference/asciidoc/scripting.adoc +++ b/src/reference/asciidoc/scripting.adoc @@ -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] ---- - + diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 24ea2db9ece..8c605fef795 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -19,6 +19,11 @@ See <> for more information. The `CacheRequestHandlerAdvice` is now available for caching request results on handlers. See <> for more information. +[[x5.2-kotlinScripts]] +=== Kotlin Scripts Support + +The JSR223 scripting module now includes a support for Kotlin scripts. +See <> for more information. [[x5.2-general]] === General Changes