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

@@ -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"