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:
Artem Bilan
2024-09-24 17:19:43 -04:00
parent 8082f3c3f9
commit ec31a5bed8
12 changed files with 101 additions and 116 deletions

View File

@@ -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();
}
}

View File

@@ -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() {

View File

@@ -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);

View File

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

View File

@@ -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));
}
}

View File

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

View File

@@ -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");
}
}

View File

@@ -1,5 +0,0 @@
def foo(y):
x=y
return x
bar = foo(7)