Skip to content

Commit 7dff1d5

Browse files
artembilangaryrussell
authored andcommitted
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
1 parent 9cc0cbe commit 7dff1d5

22 files changed

+272
-135
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,11 +600,14 @@ project('spring-integration-scripting') {
600600
description = 'Spring Integration Scripting Support'
601601
dependencies {
602602
compile project(":spring-integration-core")
603+
compile ('org.jetbrains.kotlin:kotlin-script-util', optional)
604+
compile ('org.jetbrains.kotlin:kotlin-compiler-embeddable', optional)
603605

604606
testCompile("org.jruby:jruby-complete:$jrubyVersion")
605607
testCompile("org.codehaus.groovy:groovy:$groovyVersion")
606608
testCompile("org.codehaus.groovy:groovy-jsr223:$groovyVersion")
607609
testCompile("org.python:jython-standalone:$jythonVersion")
610+
testRuntime 'org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable'
608611
}
609612
}
610613

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/AbstractScriptExecutingMessageProcessor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.beans.factory.BeanFactoryAware;
2525
import org.springframework.integration.handler.MessageProcessor;
2626
import org.springframework.integration.support.utils.IntegrationUtils;
27+
import org.springframework.lang.Nullable;
2728
import org.springframework.messaging.Message;
2829
import org.springframework.scripting.ScriptSource;
2930
import org.springframework.util.Assert;
@@ -42,9 +43,9 @@ public abstract class AbstractScriptExecutingMessageProcessor<T>
4243

4344
private final ScriptVariableGenerator scriptVariableGenerator;
4445

45-
protected volatile ClassLoader beanClassLoader;
46+
protected ClassLoader beanClassLoader;
4647

47-
protected volatile BeanFactory beanFactory;
48+
protected BeanFactory beanFactory;
4849

4950
protected AbstractScriptExecutingMessageProcessor() {
5051
this(new DefaultScriptVariableGenerator());
@@ -70,6 +71,7 @@ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
7071
* Executes the script and returns the result.
7172
*/
7273
@Override
74+
@Nullable
7375
public final T processMessage(Message<?> message) {
7476
try {
7577
ScriptSource source = getScriptSource(message);
@@ -96,6 +98,7 @@ public final T processMessage(Message<?> message) {
9698
* @param variables The variables.
9799
* @return The result of the execution.
98100
*/
101+
@Nullable
99102
protected abstract T executeScript(ScriptSource scriptSource, Map<String, Object> variables);
100103

101104
}

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/RefreshableResourceScriptSource.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@
2424
import org.springframework.scripting.support.ResourceScriptSource;
2525

2626
/**
27+
* A {@link ScriptSource} implementation, which caches a script string and refreshes it from the
28+
* target file (if modified) according the provided {@link #refreshDelay}.
29+
*
2730
* @author Dave Syer
2831
* @author Oleg Zhurakousky
2932
* @author Artem Bilan
33+
*
3034
* @since 2.0
3135
*/
3236
public class RefreshableResourceScriptSource implements ScriptSource {

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/ScriptExecutor.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,37 @@
1818

1919
import java.util.Map;
2020

21+
import org.springframework.lang.Nullable;
2122
import org.springframework.scripting.ScriptSource;
2223

2324
/**
25+
* A script evaluation abstraction against {@link ScriptSource} and optional binding {@code variables}.
26+
*
2427
* @author David Turanski
2528
* @author Artem Bilan
29+
*
2630
* @since 2.1
2731
*/
2832
@FunctionalInterface
2933
public interface ScriptExecutor {
3034

3135
/**
36+
* Execute a script from the provided {@link ScriptSource} with an optional binding {@code variables}.
3237
* @param scriptSource The script source.
38+
* @param variables The variables.
3339
* @return The result of the execution.
3440
*/
35-
default Object executeScript(ScriptSource scriptSource) {
36-
return executeScript(scriptSource, null);
37-
}
41+
@Nullable
42+
Object executeScript(ScriptSource scriptSource, @Nullable Map<String, Object> variables);
3843

3944
/**
45+
* Execute a script from the provided {@link ScriptSource}
4046
* @param scriptSource The script source.
41-
* @param variables The variables.
4247
* @return The result of the execution.
4348
*/
44-
Object executeScript(ScriptSource scriptSource, Map<String, Object> variables);
49+
@Nullable
50+
default Object executeScript(ScriptSource scriptSource) {
51+
return executeScript(scriptSource, null);
52+
}
4553

4654
}

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/config/jsr223/ScriptParser.java

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
package org.springframework.integration.scripting.config.jsr223;
1818

19-
import javax.script.ScriptEngine;
20-
import javax.script.ScriptEngineManager;
21-
2219
import org.w3c.dom.Element;
2320

2421
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -29,8 +26,11 @@
2926
import org.springframework.util.StringUtils;
3027

3128
/**
29+
* An {@link AbstractScriptParser} parser extension for the {@code <int-script:script>} tag.
30+
*
3231
* @author David Turanski
3332
* @author Artem Bilan
33+
*
3434
* @since 2.1
3535
*/
3636
public class ScriptParser extends AbstractScriptParser {
@@ -50,38 +50,12 @@ protected void postProcess(BeanDefinitionBuilder builder, Element element, Parse
5050
if (!StringUtils.hasText(scriptLocation)) {
5151
parserContext.getReaderContext().error(
5252
"An inline script requires the '" + LANGUAGE_ATTRIBUTE + "' attribute.", element);
53-
return;
5453
}
5554
else {
56-
language = getLanguageFromFileExtension(scriptLocation, parserContext, element);
57-
if (language == null) {
58-
parserContext.getReaderContext().error(
59-
"Unable to determine language for script '" + scriptLocation + "'", element);
60-
return;
61-
}
55+
language = ScriptExecutorFactory.deriveLanguageFromFileExtension(scriptLocation);
6256
}
6357
}
64-
6558
builder.addConstructorArgValue(ScriptExecutorFactory.getScriptExecutor(language));
6659
}
6760

68-
private String getLanguageFromFileExtension(String scriptLocation, ParserContext parserContext, Element element) {
69-
ScriptEngineManager engineManager = new ScriptEngineManager();
70-
ScriptEngine engine = null;
71-
72-
int index = scriptLocation.lastIndexOf(".") + 1;
73-
if (index < 1) {
74-
return null;
75-
}
76-
String extension = scriptLocation.substring(index);
77-
78-
engine = engineManager.getEngineByExtension(extension);
79-
80-
if (engine == null) {
81-
parserContext.getReaderContext().error(
82-
"No suitable scripting engine found for extension '" + extension + "'", element);
83-
}
84-
85-
return engine.getFactory().getLanguageName();
86-
}
8761
}

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/dsl/DslScriptExecutingMessageProcessor.java

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import org.springframework.beans.BeansException;
2020
import org.springframework.beans.factory.BeanClassLoaderAware;
21-
import org.springframework.beans.factory.BeanCreationException;
2221
import org.springframework.beans.factory.InitializingBean;
2322
import org.springframework.context.ApplicationContext;
2423
import org.springframework.context.ApplicationContextAware;
@@ -32,6 +31,7 @@
3231
import org.springframework.integration.scripting.jsr223.ScriptExecutorFactory;
3332
import org.springframework.messaging.Message;
3433
import org.springframework.scripting.ScriptSource;
34+
import org.springframework.util.Assert;
3535
import org.springframework.util.StringUtils;
3636

3737
/**
@@ -99,21 +99,16 @@ public void afterPropertiesSet() {
9999
this.script = this.applicationContext.getResource(this.location);
100100
}
101101

102-
ScriptSource scriptSource = new RefreshableResourceScriptSource(this.script, this.refreshCheckDelay);
103102

104103
if (!StringUtils.hasText(this.lang)) {
105-
String filename = this.script.getFilename();
106-
int index =
107-
filename != null
108-
? filename.lastIndexOf('.') + 1
109-
: -1;
110-
if (index < 1) {
111-
throw new BeanCreationException(
112-
"'lang' isn't provided and there is no 'file extension' for script resource: " + this.script);
113-
}
114-
this.lang = filename.substring(index);
104+
String scriptFilename = this.script.getFilename();
105+
Assert.hasText(scriptFilename,
106+
() -> "Either 'lang' or file extension must be provided for script: " + this.script);
107+
this.lang = ScriptExecutorFactory.deriveLanguageFromFileExtension(scriptFilename);
115108
}
116109

110+
ScriptSource scriptSource = new RefreshableResourceScriptSource(this.script, this.refreshCheckDelay);
111+
117112
if (this.applicationContext.containsBean(ScriptExecutingProcessorFactory.BEAN_NAME)) {
118113
ScriptExecutingProcessorFactory processorFactory =
119114
this.applicationContext.getBean(ScriptExecutingProcessorFactory.BEAN_NAME,

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/AbstractScriptExecutor.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,42 +29,49 @@
2929

3030
import org.springframework.integration.scripting.ScriptExecutor;
3131
import org.springframework.integration.scripting.ScriptingException;
32+
import org.springframework.lang.Nullable;
3233
import org.springframework.scripting.ScriptSource;
3334
import org.springframework.util.Assert;
3435

3536
/**
36-
* Base Class for {@link ScriptExecutor}
37+
* Base Class for {@link ScriptExecutor}.
3738
*
3839
* @author David Turanski
3940
* @author Mark Fisher
4041
* @author Artem Bilan
4142
* @author Gary Russell
43+
*
4244
* @since 2.1
4345
*/
4446
public abstract class AbstractScriptExecutor implements ScriptExecutor {
4547

46-
protected final Log logger = LogFactory.getLog(this.getClass());
47-
48-
protected final ScriptEngine scriptEngine;
48+
protected final Log logger = LogFactory.getLog(getClass()); // NOSONAR - final
4949

50-
protected final String language;
50+
private final ScriptEngine scriptEngine;
5151

5252
protected AbstractScriptExecutor(String language) {
5353
Assert.hasText(language, "language must not be empty");
54-
this.language = language;
55-
56-
this.scriptEngine = new ScriptEngineManager().getEngineByName(this.language);
57-
Assert.notNull(this.scriptEngine, invalidLanguageMessage(this.language));
54+
this.scriptEngine = new ScriptEngineManager().getEngineByName(language);
55+
Assert.notNull(this.scriptEngine, () -> invalidLanguageMessage(language));
5856
if (this.logger.isDebugEnabled()) {
5957
this.logger.debug("Using script engine : " + this.scriptEngine.getFactory().getEngineName());
6058
}
6159
}
6260

61+
protected AbstractScriptExecutor(ScriptEngine scriptEngine) {
62+
Assert.notNull(scriptEngine, "'scriptEngine' must not be null.");
63+
this.scriptEngine = scriptEngine;
64+
}
65+
66+
public ScriptEngine getScriptEngine() {
67+
return this.scriptEngine;
68+
}
69+
6370
@Override
71+
@Nullable
6472
public Object executeScript(ScriptSource scriptSource, Map<String, Object> variables) {
65-
Object result;
66-
6773
try {
74+
Object result;
6875
String script = scriptSource.getScriptAsString();
6976
Date start = new Date();
7077
if (this.logger.isDebugEnabled()) {
@@ -85,13 +92,11 @@ public Object executeScript(ScriptSource scriptSource, Map<String, Object> varia
8592
if (this.logger.isDebugEnabled()) {
8693
this.logger.debug("script executed in " + (new Date().getTime() - start.getTime()) + " ms");
8794
}
95+
return result;
8896
}
89-
9097
catch (Exception e) {
9198
throw new ScriptingException(e.getMessage(), e);
9299
}
93-
94-
return result;
95100
}
96101

97102
/**

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/DefaultScriptExecutor.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
import javax.script.ScriptEngine;
2121

2222
/**
23-
* Default implementation of the
24-
* {@link org.springframework.integration.scripting.ScriptExecutor}
23+
* Default implementation of the {@link AbstractScriptExecutor}.
24+
* Accepts a scripting language for resolving a target {@code ScriptEngine} for
25+
* evaluation and does nothing with the {@code result} in the
26+
* {@link #postProcess(Object, ScriptEngine, String, Bindings)} implementation.
2527
*
2628
* @author David Turanski
2729
* @author Mark Fisher
2830
* @author Gary Russell
31+
* @author Artem Bilan
32+
*
2933
* @since 2.1
3034
*/
3135
public class DefaultScriptExecutor extends AbstractScriptExecutor {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.scripting.jsr223;
18+
19+
import javax.script.Bindings;
20+
import javax.script.ScriptEngine;
21+
22+
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory;
23+
24+
25+
/**
26+
* An {@link AbstractScriptExecutor} for the Kotlin scripts support.
27+
* Uses {@link KotlinJsr223JvmLocalScriptEngineFactory} directly since there is
28+
* no {@code META-INF/services/javax.script.ScriptEngineFactory} file in CLASSPATH.
29+
* Also sets an {@code idea.use.native.fs.for.win} system property to {@code false}
30+
* to disable a native engine discovery for Windows: bay be resolved in the future Kotlin versions.
31+
*
32+
* @author Artem Bilan
33+
*
34+
* @since 5.2
35+
*/
36+
public class KotlinScriptExecutor extends AbstractScriptExecutor {
37+
38+
static {
39+
System.setProperty("idea.use.native.fs.for.win", "false");
40+
}
41+
42+
public KotlinScriptExecutor() {
43+
super(new KotlinJsr223JvmLocalScriptEngineFactory().getScriptEngine());
44+
}
45+
46+
@Override
47+
protected Object postProcess(Object result, ScriptEngine scriptEngine, String script, Bindings bindings) {
48+
return result;
49+
}
50+
51+
}

spring-integration-scripting/src/main/java/org/springframework/integration/scripting/jsr223/RubyScriptExecutor.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,28 @@
1616

1717
package org.springframework.integration.scripting.jsr223;
1818

19-
import org.springframework.util.ClassUtils;
20-
21-
2219
/**
20+
* A {@link DefaultScriptExecutor} extension for Ruby scripting support.
21+
* It is present here only for the reason to populate
22+
* {@code org.jruby.embed.localvariable.behavior} and
23+
* {@code org.jruby.embed.localcontext.scope} system properties.
24+
* May be revised in the future.
25+
*
2326
* @author David Turanski
27+
* @author Artem Bilan
28+
*
2429
* @since 2.1
2530
*
2631
*/
2732
public class RubyScriptExecutor extends DefaultScriptExecutor {
2833

2934
static {
30-
if (ClassUtils.isPresent("org.jruby.embed.jsr223.JRubyEngine", System.class.getClassLoader())) {
31-
System.setProperty("org.jruby.embed.localvariable.behavior", "transient");
32-
System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");
33-
}
35+
System.setProperty("org.jruby.embed.localvariable.behavior", "transient");
36+
System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");
3437
}
3538

3639
public RubyScriptExecutor() {
3740
super("ruby");
3841
}
42+
3943
}

0 commit comments

Comments
 (0)