Skip to content

Commit ec31a5b

Browse files
committed
GH-9507: Migrate Python support to GraalVM Polyglot
Fixes: #9507 Issue link: #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`
1 parent 8082f3c commit ec31a5b

File tree

12 files changed

+101
-116
lines changed

12 files changed

+101
-116
lines changed

build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ ext {
8787
jsonpathVersion = '2.9.0'
8888
junit4Version = '4.13.2'
8989
junitJupiterVersion = '5.11.0'
90-
jythonVersion = '2.7.4'
9190
kotlinCoroutinesVersion = '1.8.1'
9291
kryoVersion = '5.6.0'
9392
lettuceVersion = '6.4.0.RELEASE'
@@ -866,17 +865,18 @@ project('spring-integration-scripting') {
866865
optionalApi 'org.jetbrains.kotlin:kotlin-scripting-jsr223'
867866
provided "org.graalvm.sdk:graal-sdk:$graalvmVersion"
868867
provided "org.graalvm.polyglot:js:$graalvmVersion"
868+
provided "org.graalvm.polyglot:python:$graalvmVersion"
869869

870870
testImplementation "org.jruby:jruby-complete:$jrubyVersion"
871871
testImplementation 'org.apache.groovy:groovy-jsr223'
872-
testImplementation "org.python:jython-standalone:$jythonVersion"
873872
}
874873

875874
tasks.withType(JavaForkOptions) {
876875
jvmArgs '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED',
877876
'--add-opens', 'java.base/java.io=ALL-UNNAMED',
878877
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
879-
'--add-opens', 'java.base/java.util=ALL-UNNAMED'
878+
'--add-opens', 'java.base/java.util=ALL-UNNAMED',
879+
'-Dpolyglot.engine.WarnInterpreterOnly=false'
880880
}
881881
}
882882

spring-integration-core/src/main/java/org/springframework/integration/handler/advice/CacheRequestHandlerAdvice.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.Arrays;
2222
import java.util.List;
2323
import java.util.function.Function;
24-
import java.util.stream.Collectors;
2524

2625
import org.springframework.beans.factory.BeanFactory;
2726
import org.springframework.beans.factory.SmartInitializingSingleton;

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 the original author or authors.
2+
* Copyright 2022-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.integration.scripting;
1818

19+
import java.util.ArrayList;
20+
import java.util.List;
1921
import java.util.Map;
2022

2123
import org.graalvm.polyglot.Context;
@@ -36,7 +38,7 @@ public class PolyglotScriptExecutor implements ScriptExecutor {
3638

3739
private final String language;
3840

39-
private Context.Builder contextBuilder;
41+
private final Context.Builder contextBuilder;
4042

4143
/**
4244
* Construct an executor based on the provided language id.
@@ -67,11 +69,28 @@ public Object executeScript(ScriptSource scriptSource, @Nullable Map<String, Obj
6769
Value bindings = context.getBindings(this.language);
6870
variables.forEach(bindings::putMember);
6971
}
70-
return context.eval(this.language, scriptSource.getScriptAsString()).as(Object.class);
72+
String scriptAsString = scriptSource.getScriptAsString();
73+
Object result = context.eval(this.language, scriptAsString).as(Object.class);
74+
// We have to copy all the expected PolyglotWrapper instances before context is closed.
75+
if (result instanceof Map<?, ?> map) {
76+
String returnVariable = parseReturnVariable(scriptAsString);
77+
result = map.get(returnVariable);
78+
}
79+
if (result instanceof List<?> list) {
80+
result = new ArrayList<>(list);
81+
}
82+
return result;
7183
}
7284
catch (Exception ex) {
7385
throw new ScriptingException(ex.getMessage(), ex);
7486
}
7587
}
7688

89+
private static String parseReturnVariable(String script) {
90+
String[] lines = script.trim().split("\n");
91+
String lastLine = lines[lines.length - 1];
92+
String[] tokens = lastLine.split("=");
93+
return tokens[0].trim();
94+
}
95+
7796
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,14 @@
3232
*
3333
* @author David Turanski
3434
* @author Gary Russell
35+
* @author Artem Bilan
36+
*
3537
* @since 2.1
3638
*
39+
* @deprecated in favor of {@link org.springframework.integration.scripting.PolyglotScriptExecutor}
40+
* with a {@code python} language argument.
3741
*/
42+
@Deprecated(forRemoval = true, since = "6.4")
3843
public class PythonScriptExecutor extends AbstractScriptExecutor {
3944

4045
public PythonScriptExecutor() {

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

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ public final class ScriptExecutorFactory {
3535

3636
public static ScriptExecutor getScriptExecutor(String language) {
3737
if (language.equalsIgnoreCase("python") || language.equalsIgnoreCase("jython")) {
38-
return new PythonScriptExecutor();
38+
return new PolyglotScriptExecutor("python");
3939
}
4040
else if (language.equalsIgnoreCase("ruby") || language.equalsIgnoreCase("jruby")) {
4141
return new RubyScriptExecutor();
@@ -56,11 +56,16 @@ public static String deriveLanguageFromFileExtension(String scriptLocation) {
5656
int index = scriptLocation.lastIndexOf('.') + 1;
5757
Assert.state(index > 0, () -> "Unable to determine language for script '" + scriptLocation + "'");
5858
String extension = scriptLocation.substring(index);
59-
if (extension.equals("kts")) {
60-
return "kotlin";
61-
}
62-
else if (extension.equals("js")) {
63-
return "js";
59+
switch (extension) {
60+
case "kts" -> {
61+
return "kotlin";
62+
}
63+
case "js" -> {
64+
return "js";
65+
}
66+
case "py" -> {
67+
return "python";
68+
}
6469
}
6570
ScriptEngineManager engineManager = new ScriptEngineManager();
6671
ScriptEngine engine = engineManager.getEngineByExtension(extension);

spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests-context.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
66
http://www.springframework.org/schema/integration/scripting https://www.springframework.org/schema/integration/scripting/spring-integration-scripting.xsd">
77

8-
<int-script:script location="foo.rb"/>
9-
<int-script:script location="foo.groovy"/>
10-
<int-script:script location="foo.py"/>
11-
<int-script:script location="foo.kts"/>
8+
<int-script:script location="script.rb"/>
9+
<int-script:script location="script.groovy"/>
10+
<int-script:script location="script.py"/>
11+
<int-script:script location="script.kts"/>
12+
<int-script:script location="script.js"/>
1213
</beans>

spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/DeriveLanguageFromExtensionTests.java

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,14 +16,20 @@
1616

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

19-
import java.util.Map;
19+
import java.util.stream.Stream;
2020

2121
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.aggregator.ArgumentsAccessor;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
import org.junit.jupiter.params.provider.MethodSource;
2226

2327
import org.springframework.beans.factory.BeanDefinitionStoreException;
2428
import org.springframework.beans.factory.annotation.Autowired;
2529
import org.springframework.context.ApplicationContext;
2630
import org.springframework.context.support.ClassPathXmlApplicationContext;
31+
import org.springframework.integration.scripting.PolyglotScriptExecutor;
32+
import org.springframework.integration.scripting.ScriptExecutor;
2733
import org.springframework.integration.test.util.TestUtils;
2834
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
2935

@@ -41,30 +47,26 @@ public class DeriveLanguageFromExtensionTests {
4147
@Autowired
4248
private ApplicationContext ctx;
4349

44-
@Test
45-
public void testParseLanguage() {
46-
String[] langs = {"ruby", "Groovy", "python", "kotlin"};
47-
Class<?>[] executors = {
48-
RubyScriptExecutor.class,
49-
DefaultScriptExecutor.class,
50-
PythonScriptExecutor.class,
51-
DefaultScriptExecutor.class
52-
};
53-
54-
Map<String, ScriptExecutingMessageProcessor> scriptProcessors =
55-
this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class);
56-
assertThat(scriptProcessors.size()).isEqualTo(4);
50+
@ParameterizedTest
51+
@MethodSource("languageExecutorSource")
52+
public void testParseLanguage(String language, Class<?> executorClass, ArgumentsAccessor argumentsAccessor) {
53+
assertThat(this.ctx.getBeansOfType(ScriptExecutingMessageProcessor.class)).hasSize(5);
5754

58-
for (int i = 0; i < 4; i++) {
59-
ScriptExecutingMessageProcessor processor = ctx.getBean(
60-
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" + i,
61-
ScriptExecutingMessageProcessor.class);
55+
var processor =
56+
ctx.getBean(
57+
"org.springframework.integration.scripting.jsr223.ScriptExecutingMessageProcessor#" +
58+
(argumentsAccessor.getInvocationIndex() - 1),
59+
ScriptExecutingMessageProcessor.class);
6260

63-
AbstractScriptExecutor executor =
64-
TestUtils.getPropertyValue(processor, "scriptExecutor", AbstractScriptExecutor.class);
65-
assertThat(executor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(langs[i]);
66-
assertThat(executor.getClass()).isEqualTo(executors[i]);
61+
ScriptExecutor executor = TestUtils.getPropertyValue(processor, "scriptExecutor", ScriptExecutor.class);
62+
if (executor instanceof PolyglotScriptExecutor) {
63+
assertThat(TestUtils.getPropertyValue(executor, "language")).isEqualTo(language);
64+
}
65+
else {
66+
AbstractScriptExecutor abstractScriptExecutor = (AbstractScriptExecutor) executor;
67+
assertThat(abstractScriptExecutor.getScriptEngine().getFactory().getLanguageName()).isEqualTo(language);
6768
}
69+
assertThat(executor.getClass()).isEqualTo(executorClass);
6870
}
6971

7072
@Test
@@ -85,4 +87,13 @@ public void testNoExtension() {
8587
.withStackTraceContaining("Unable to determine language for script 'foo'");
8688
}
8789

90+
private static Stream<Arguments> languageExecutorSource() {
91+
return Stream.of(
92+
Arguments.of("ruby", RubyScriptExecutor.class),
93+
Arguments.of("Groovy", DefaultScriptExecutor.class),
94+
Arguments.of("python", PolyglotScriptExecutor.class),
95+
Arguments.of("kotlin", DefaultScriptExecutor.class),
96+
Arguments.of("js", PolyglotScriptExecutor.class));
97+
}
98+
8899
}

spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonScriptExecutorTests.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
import org.assertj.core.api.InstanceOfAssertFactories;
2323
import org.junit.jupiter.api.BeforeEach;
2424
import org.junit.jupiter.api.Test;
25-
import org.python.core.PyTuple;
2625

2726
import org.springframework.core.io.ClassPathResource;
27+
import org.springframework.integration.scripting.PolyglotScriptExecutor;
2828
import org.springframework.integration.scripting.ScriptExecutor;
2929
import org.springframework.scripting.ScriptSource;
3030
import org.springframework.scripting.support.ResourceScriptSource;
@@ -44,7 +44,7 @@ public class PythonScriptExecutorTests {
4444

4545
@BeforeEach
4646
public void init() {
47-
this.executor = new PythonScriptExecutor();
47+
this.executor = new PolyglotScriptExecutor("python");
4848
}
4949

5050
@Test
@@ -76,11 +76,8 @@ public void test3() {
7676
new ClassPathResource("/org/springframework/integration/scripting/jsr223/test3.py"));
7777
Object obj = this.executor.executeScript(source);
7878
assertThat(obj)
79-
.isNotNull()
80-
.isInstanceOf(PyTuple.class)
8179
.asInstanceOf(InstanceOfAssertFactories.LIST)
82-
.element(0)
83-
.isEqualTo(1);
80+
.containsOnly(1, 2, 3);
8481
}
8582

8683
@Test
@@ -92,11 +89,8 @@ public void test3WithVariables() {
9289
variables.put("foo", "bar");
9390
Object obj = this.executor.executeScript(source, variables);
9491
assertThat(obj)
95-
.isNotNull()
96-
.isInstanceOf(PyTuple.class)
9792
.asInstanceOf(InstanceOfAssertFactories.LIST)
98-
.element(0)
99-
.isEqualTo(1);
93+
.containsOnly(1, 2, 3);
10094
}
10195

10296
@Test

spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/PythonVariableParserTests.java

Lines changed: 0 additions & 54 deletions
This file was deleted.

spring-integration-scripting/src/test/java/org/springframework/integration/scripting/jsr223/test2.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)