Skip to content

Add support for Kotlin JSR223 scripts #2898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
Expand All @@ -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);
Expand All @@ -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<String, Object> variables);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> 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<String, Object> variables);
@Nullable
default Object executeScript(ScriptSource scriptSource) {
return executeScript(scriptSource, null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,8 +26,11 @@
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 {
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> variables) {
Object result;

try {
Object result;
String script = scriptSource.getScriptAsString();
Date start = new Date();
if (this.logger.isDebugEnabled()) {
Expand All @@ -85,13 +92,11 @@ public Object executeScript(ScriptSource scriptSource, Map<String, Object> 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

}
Loading