GH-9507: Migrate Python support to GraalVM Polyglot
Fixes: #9507 Issue link: https://github.com/spring-projects/spring-integration/issues/9507 * Deprecate `PythonScriptExecutor` in favor of `PolyglotScriptExecutor` with a `python` as language * Add handling for `PolyglotWrapper` return type of the script evaluation * Rework `DeriveLanguageFromExtensionTests.testParseLanguage()` to the `@ParameterizedTest`
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 the original author or authors.
|
||||
* Copyright 2022-2024 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.integration.scripting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.graalvm.polyglot.Context;
|
||||
@@ -36,7 +38,7 @@ public class PolyglotScriptExecutor implements ScriptExecutor {
|
||||
|
||||
private final String language;
|
||||
|
||||
private Context.Builder contextBuilder;
|
||||
private final Context.Builder contextBuilder;
|
||||
|
||||
/**
|
||||
* Construct an executor based on the provided language id.
|
||||
@@ -67,11 +69,28 @@ public class PolyglotScriptExecutor implements ScriptExecutor {
|
||||
Value bindings = context.getBindings(this.language);
|
||||
variables.forEach(bindings::putMember);
|
||||
}
|
||||
return context.eval(this.language, scriptSource.getScriptAsString()).as(Object.class);
|
||||
String scriptAsString = scriptSource.getScriptAsString();
|
||||
Object result = context.eval(this.language, scriptAsString).as(Object.class);
|
||||
// We have to copy all the expected PolyglotWrapper instances before context is closed.
|
||||
if (result instanceof Map<?, ?> map) {
|
||||
String returnVariable = parseReturnVariable(scriptAsString);
|
||||
result = map.get(returnVariable);
|
||||
}
|
||||
if (result instanceof List<?> list) {
|
||||
result = new ArrayList<>(list);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new ScriptingException(ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String parseReturnVariable(String script) {
|
||||
String[] lines = script.trim().split("\n");
|
||||
String lastLine = lines[lines.length - 1];
|
||||
String[] tokens = lastLine.split("=");
|
||||
return tokens[0].trim();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -32,9 +32,14 @@ import javax.script.ScriptEngine;
|
||||
*
|
||||
* @author David Turanski
|
||||
* @author Gary Russell
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 2.1
|
||||
*
|
||||
* @deprecated in favor of {@link org.springframework.integration.scripting.PolyglotScriptExecutor}
|
||||
* with a {@code python} language argument.
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "6.4")
|
||||
public class PythonScriptExecutor extends AbstractScriptExecutor {
|
||||
|
||||
public PythonScriptExecutor() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -35,7 +35,7 @@ public final class ScriptExecutorFactory {
|
||||
|
||||
public static ScriptExecutor getScriptExecutor(String language) {
|
||||
if (language.equalsIgnoreCase("python") || language.equalsIgnoreCase("jython")) {
|
||||
return new PythonScriptExecutor();
|
||||
return new PolyglotScriptExecutor("python");
|
||||
}
|
||||
else if (language.equalsIgnoreCase("ruby") || language.equalsIgnoreCase("jruby")) {
|
||||
return new RubyScriptExecutor();
|
||||
@@ -56,11 +56,16 @@ public final class ScriptExecutorFactory {
|
||||
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";
|
||||
}
|
||||
else if (extension.equals("js")) {
|
||||
return "js";
|
||||
switch (extension) {
|
||||
case "kts" -> {
|
||||
return "kotlin";
|
||||
}
|
||||
case "js" -> {
|
||||
return "js";
|
||||
}
|
||||
case "py" -> {
|
||||
return "python";
|
||||
}
|
||||
}
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByExtension(extension);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/integration/scripting https://www.springframework.org/schema/integration/scripting/spring-integration-scripting.xsd">
|
||||
|
||||
<int-script:script location="foo.rb"/>
|
||||
<int-script:script location="foo.groovy"/>
|
||||
<int-script:script location="foo.py"/>
|
||||
<int-script:script location="foo.kts"/>
|
||||
<int-script:script location="script.rb"/>
|
||||
<int-script:script location="script.groovy"/>
|
||||
<int-script:script location="script.py"/>
|
||||
<int-script:script location="script.kts"/>
|
||||
<int-script:script location="script.js"/>
|
||||
</beans>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
@@ -16,14 +16,20 @@
|
||||
|
||||
package org.springframework.integration.scripting.jsr223;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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.scripting.PolyglotScriptExecutor;
|
||||
import org.springframework.integration.scripting.ScriptExecutor;
|
||||
import org.springframework.integration.test.util.TestUtils;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
@@ -41,30 +47,26 @@ public class DeriveLanguageFromExtensionTests {
|
||||
@Autowired
|
||||
private ApplicationContext ctx;
|
||||
|
||||
@Test
|
||||
public void testParseLanguage() {
|
||||
String[] langs = {"ruby", "Groovy", "python", "kotlin"};
|
||||
Class<?>[] executors = {
|
||||
RubyScriptExecutor.class,
|
||||
DefaultScriptExecutor.class,
|
||||
PythonScriptExecutor.class,
|
||||
DefaultScriptExecutor.class
|
||||
};
|
||||
@ParameterizedTest
|
||||
@MethodSource("languageExecutorSource")
|
||||
public void testParseLanguage(String language, Class<?> executorClass, ArgumentsAccessor argumentsAccessor) {
|
||||
assertThat(this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class)).hasSize(5);
|
||||
|
||||
Map<String, ScriptExecutingMessageProcessor> scriptProcessors =
|
||||
this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class);
|
||||
assertThat(scriptProcessors.size()).isEqualTo(4);
|
||||
var processor =
|
||||
ctx.getBean(
|
||||
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" +
|
||||
(argumentsAccessor.getInvocationIndex() - 1),
|
||||
ScriptExecutingMessageProcessor.class);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ScriptExecutingMessageProcessor processor = ctx.getBean(
|
||||
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" + i,
|
||||
ScriptExecutingMessageProcessor.class);
|
||||
|
||||
AbstractScriptExecutor executor =
|
||||
TestUtils.getPropertyValue(processor, "scriptExecutor", AbstractScriptExecutor.class);
|
||||
assertThat(executor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(langs[i]);
|
||||
assertThat(executor.getClass()).isEqualTo(executors[i]);
|
||||
ScriptExecutor executor = TestUtils.getPropertyValue(processor, "scriptExecutor", ScriptExecutor.class);
|
||||
if (executor instanceof PolyglotScriptExecutor) {
|
||||
assertThat(TestUtils.getPropertyValue(executor, "language")).isEqualTo(language);
|
||||
}
|
||||
else {
|
||||
AbstractScriptExecutor abstractScriptExecutor = (AbstractScriptExecutor) executor;
|
||||
assertThat(abstractScriptExecutor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(language);
|
||||
}
|
||||
assertThat(executor.getClass()).isEqualTo(executorClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,4 +87,13 @@ public class DeriveLanguageFromExtensionTests {
|
||||
.withStackTraceContaining("Unable to determine language for script 'foo'");
|
||||
}
|
||||
|
||||
private static Stream<Arguments> languageExecutorSource() {
|
||||
return Stream.of(
|
||||
Arguments.of("ruby", RubyScriptExecutor.class),
|
||||
Arguments.of("Groovy", DefaultScriptExecutor.class),
|
||||
Arguments.of("python", PolyglotScriptExecutor.class),
|
||||
Arguments.of("kotlin", DefaultScriptExecutor.class),
|
||||
Arguments.of("js", PolyglotScriptExecutor.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import java.util.Map;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.python.core.PyTuple;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.integration.scripting.PolyglotScriptExecutor;
|
||||
import org.springframework.integration.scripting.ScriptExecutor;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
@@ -44,7 +44,7 @@ public class PythonScriptExecutorTests {
|
||||
|
||||
@BeforeEach
|
||||
public void init() {
|
||||
this.executor = new PythonScriptExecutor();
|
||||
this.executor = new PolyglotScriptExecutor("python");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,11 +76,8 @@ public class PythonScriptExecutorTests {
|
||||
new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py"));
|
||||
Object obj = this.executor.executeScript(source);
|
||||
assertThat(obj)
|
||||
.isNotNull()
|
||||
.isInstanceOf(PyTuple.class)
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.element(0)
|
||||
.isEqualTo(1);
|
||||
.containsOnly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,11 +89,8 @@ public class PythonScriptExecutorTests {
|
||||
variables.put("foo", "bar");
|
||||
Object obj = this.executor.executeScript(source, variables);
|
||||
assertThat(obj)
|
||||
.isNotNull()
|
||||
.isInstanceOf(PyTuple.class)
|
||||
.asInstanceOf(InstanceOfAssertFactories.LIST)
|
||||
.element(0)
|
||||
.isEqualTo(1);
|
||||
.containsOnly(1, 2, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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 java.io.IOException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.scripting.ScriptSource;
|
||||
import org.springframework.scripting.support.ResourceScriptSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author David Turanski
|
||||
*
|
||||
*/
|
||||
public class PythonVariableParserTests {
|
||||
|
||||
@Test
|
||||
public void testBasic() throws IOException {
|
||||
String var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("x=2");
|
||||
assertThat(var).isEqualTo("x");
|
||||
|
||||
var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("\n\n\nx = 2\n\n\n");
|
||||
assertThat(var).isEqualTo("x");
|
||||
|
||||
var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable("\n\n\nx\n\n\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test2() throws IOException {
|
||||
ScriptSource source =
|
||||
new ResourceScriptSource(new ClassPathResource("/org/springframework/integration/scripting/jsr223/test2.py"));
|
||||
String var = PythonScriptExecutor.PythonVariableParser.parseReturnVariable(source.getScriptAsString());
|
||||
assertThat(var).isEqualTo("bar");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
def foo(y):
|
||||
x=y
|
||||
return x
|
||||
|
||||
bar = foo(7)
|
||||
Reference in New Issue
Block a user