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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.scripting.jsr223;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory;
/**
* An {@link AbstractScriptExecutor} for the Kotlin scripts support.
* Uses {@link KotlinJsr223JvmLocalScriptEngineFactory} directly since there is
* no {@code META-INF/services/javax.script.ScriptEngineFactory} file in CLASSPATH.
* Also sets an {@code idea.use.native.fs.for.win} system property to {@code false}
* to disable a native engine discovery for Windows: bay be resolved in the future Kotlin versions.
*
* @author Artem Bilan
*
* @since 5.2
*/
public class KotlinScriptExecutor extends AbstractScriptExecutor {
static {
System.setProperty("idea.use.native.fs.for.win", "false");
}
public KotlinScriptExecutor() {
super(new KotlinJsr223JvmLocalScriptEngineFactory().getScriptEngine());
}
@Override
protected Object postProcess(Object result, ScriptEngine scriptEngine, String script, Bindings bindings) {
return result;
}
}

View File

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

View File

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

View File

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