From 7dff1d5416400da422c94507eec1a9a31b501c07 Mon Sep 17 00:00:00 2001 From: Artem Bilan Date: Fri, 19 Apr 2019 12:29:08 -0400 Subject: [PATCH] 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 --- build.gradle | 3 ++ ...stractScriptExecutingMessageProcessor.java | 7 ++- .../RefreshableResourceScriptSource.java | 4 ++ .../integration/scripting/ScriptExecutor.java | 26 +++++---- .../scripting/config/jsr223/ScriptParser.java | 34 ++---------- .../DslScriptExecutingMessageProcessor.java | 19 +++---- .../jsr223/AbstractScriptExecutor.java | 35 ++++++------ .../jsr223/DefaultScriptExecutor.java | 8 ++- .../jsr223/KotlinScriptExecutor.java | 51 ++++++++++++++++++ .../scripting/jsr223/RubyScriptExecutor.java | 18 ++++--- .../ScriptExecutingMessageProcessor.java | 15 +++--- .../jsr223/ScriptExecutorFactory.java | 36 ++++++++++++- .../scripting/dsl/ScriptsTests.java | 34 ++++++++++-- ...riveLanguageFromExtensionTests-context.xml | 1 + .../DeriveLanguageFromExtensionTests.java | 54 +++++++++---------- .../jsr223/Jsr223ScriptExecutorTests.java | 13 ++--- .../jsr223/PythonScriptExecutorTests.java | 35 +++++++----- .../resources/scripts/TestFilterScript.groovy | 1 - .../resources/scripts/TestFilterScript.kts | 1 + ...Script.ruby => TestMessageSourceScript.rb} | 0 src/reference/asciidoc/scripting.adoc | 17 +++++- src/reference/asciidoc/whats-new.adoc | 5 ++ 22 files changed, 277 insertions(+), 140 deletions(-) create mode 100644 spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/KotlinScriptExecutor.java delete mode 100644 spring-integration-scripting/src/test/resources/scripts/TestFilterScript.groovy create mode 100644 spring-integration-scripting/src/test/resources/scripts/TestFilterScript.kts rename spring-integration-scripting/src/test/resources/scripts/{TestMessageSourceScript.ruby => TestMessageSourceScript.rb} (100%) diff --git a/build.gradle b/build.gradle index 624898b870..259f8439de 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 6c130eccf8..47a14d12d6 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.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 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 * 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 * @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 3e812fd59b..cdffa71ff2 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.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 { 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 b17ebda31c..fcaa3b9bb3 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 @@ 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 variables); + @Nullable + Object executeScript(ScriptSource scriptSource, @Nullable Map 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); + } } 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 581ae4cc1d..e7c1fa26ce 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.integration.scripting.jsr223.ScriptExecutorFactory; 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 @@ 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(); - } } 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 7f8ca0b62d..292899b0f7 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 @@ 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, 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 69c7be4856..2afd5eca2e 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.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 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 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; } /** 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 dd67345e6c..6eee376a36 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.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 { 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 0000000000..5bbe376129 --- /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 9e42a65ae0..3dabd880a7 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 93a3f3376d..94b1e5ee74 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.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 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 21b2799c53..06300796b8 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 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(); + } + } 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 d3d3313199..8b0e4d1910 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.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()); + } + + } } 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 3037c6b6c6..7e5c379ecb 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 ad22395d41..8eae6a8eaf 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 d18a33861b..ad299329fe 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 class Jsr223ScriptExecutorTests { 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 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")); } } 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 3139acd86e..8494e246ec 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 @@ 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 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 06a775dfb2..0000000000 --- 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 0000000000..35189725d4 --- /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 6815ff0ed0..79cae4db67 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 24ea2db9ec..8c605fef79 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