diff --git a/build.gradle b/build.gradle
index 70b1ae6b5..3ea43621b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -89,6 +89,8 @@ allprojects {
xercesVersion = '2.8.1'
xmlunitVersion = '1.5'
xstreamVersion = '1.4.7'
+ jrubyVersion = '1.1.7'
+ beanshellVersion = '2.0b5'
}
}
@@ -261,6 +263,16 @@ project('spring-batch-core') {
project('spring-batch-infrastructure') {
description = 'Spring Batch Infrastructure'
+ test {
+ // permsize settings not passed down from GRADLE_OPTS nor JAVA_OPTS
+ // when running certain tests causing permgen OOM when using JDK7
+ // compilation is not performed with JDK6 and permgen is removed
+ // starting with JDK8..
+ if (JavaVersion.current().isJava7Compatible()) {
+ jvmArgs '-XX:MaxPermSize=256m'
+ }
+ }
+
dependencies {
compile "org.springframework:spring-core:$springVersion"
@@ -285,6 +297,9 @@ project('spring-batch-infrastructure') {
testCompile "org.xerial:sqlite-jdbc:$sqliteVersion"
testRuntime "com.sun.mail:javax.mail:$javaMailVersion"
+ testRuntime "org.codehaus.groovy:groovy-jsr223:$groovyVersion"
+ testRuntime "com.sun.script.jruby:jruby-engine:$jrubyVersion"
+ testRuntime "org.beanshell:bsh:$beanshellVersion"
optional "javax.jms:jms-api:1.1-rev-1"
optional "org.slf4j:slf4j-log4j12:$slf4jVersion"
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ScriptItemProcessorTest.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ScriptItemProcessorTest.java
new file mode 100644
index 000000000..41281f732
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ScriptItemProcessorTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2014 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
+ *
+ * http://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.batch.core.step.item;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.item.ItemWriter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.Assert;
+
+import java.util.List;
+
+/**
+ *
+ * Test job utilizing a {@link org.springframework.batch.item.support.ScriptItemProcessor}.
+ *
+ *
+ * @author Chris Schaefer
+ * @since 3.1
+ */
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class ScriptItemProcessorTest {
+ @Autowired
+ private Job job;
+
+ @Autowired
+ private JobLauncher jobLauncher;
+
+ @Test
+ public void testScriptProcessorJob() throws Exception {
+ jobLauncher.run(job, new JobParameters());
+ }
+
+ public static class TestItemWriter implements ItemWriter {
+ @Override
+ public void write(List extends String> items) throws Exception {
+ Assert.notNull(items, "Items cannot be null");
+ Assert.isTrue(!items.isEmpty(), "Items cannot be empty");
+ Assert.isTrue(items.size() == 1, "Items should only contain one entry");
+
+ String item = items.get(0);
+ Assert.isTrue("BLAH".equals(item), "Transformed item to write should have been: BLAH but got: " + item);
+ }
+ }
+}
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTest-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTest-context.xml
new file mode 100644
index 000000000..6a811daf7
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTest-context.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ blah
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/processor-test-simple.js b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/processor-test-simple.js
new file mode 100644
index 000000000..3a9b9db1d
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/processor-test-simple.js
@@ -0,0 +1 @@
+item.toUpperCase();
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ScriptItemProcessor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ScriptItemProcessor.java
new file mode 100644
index 000000000..9baba4296
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ScriptItemProcessor.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2014 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
+ *
+ * http://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.batch.item.support;
+
+import org.springframework.scripting.support.StaticScriptSource;
+import org.springframework.util.StringUtils;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.io.Resource;
+import org.springframework.scripting.ScriptEvaluator;
+import org.springframework.scripting.ScriptSource;
+import org.springframework.scripting.support.ResourceScriptSource;
+import org.springframework.scripting.support.StandardScriptEvaluator;
+import org.springframework.util.Assert;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * {@link org.springframework.batch.item.ItemProcessor} implementation that passes the current
+ * item to process to the provided script. Exposes the current item for processing via the
+ * {@link org.springframework.batch.item.support.ScriptItemProcessor#ITEM_BINDING_VARIABLE_NAME}
+ * key name ("item"). A custom key name can be set by invoking:
+ * {@link org.springframework.batch.item.support.ScriptItemProcessor#setItemBindingVariableName}
+ * with the desired key name. The thread safety of this {@link org.springframework.batch.item.ItemProcessor}
+ * depends on the implementation of the {@link org.springframework.scripting.ScriptEvaluator} used.
+ *
+ *
+ *
+ * @author Chris Schaefer
+ * @since 3.0
+ */
+public class ScriptItemProcessor implements ItemProcessor, InitializingBean {
+ private static final String ITEM_BINDING_VARIABLE_NAME = "item";
+
+ private String language;
+ private ScriptSource script;
+ private ScriptSource scriptSource;
+ private ScriptEvaluator scriptEvaluator;
+ private String itemBindingVariableName = ITEM_BINDING_VARIABLE_NAME;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public O process(I item) throws Exception {
+ Map arguments = new HashMap();
+ arguments.put(itemBindingVariableName, item);
+
+ return (O) scriptEvaluator.evaluate(getScriptSource(), arguments);
+ }
+
+ /**
+ *
+ * Sets the {@link org.springframework.core.io.Resource} location of the script to use.
+ * The script language will be deduced from the filename extension.
+ *
+ *
+ * @param resource the {@link org.springframework.core.io.Resource} location of the script to use.
+ */
+ public void setScript(Resource resource) {
+ Assert.notNull(resource, "The script resource cannot be null");
+
+ this.script = new ResourceScriptSource(resource);
+ }
+
+ /**
+ *
+ * Sets the provided {@link String} as the script source code to use.
+ *
+ *
+ * @param scriptSource the {@link String} form of the script source code to use.
+ * @param language the language of the script as returned by the {@link javax.script.ScriptEngineFactory}
+ */
+ public void setScriptSource(String scriptSource, String language) {
+ Assert.hasText(language, "Language must contain the script language");
+ Assert.hasText(scriptSource, "Script source must contain the script source to evaluate");
+
+ this.language = language;
+ this.scriptSource = new StaticScriptSource(scriptSource);
+ }
+
+ /**
+ *
+ * Provides the ability to change the key name that scripts use to obtain the current
+ * item to process if the variable represented by:
+ * {@link org.springframework.batch.item.support.ScriptItemProcessor#ITEM_BINDING_VARIABLE_NAME}
+ * is not suitable ("item").
+ *
+ *
+ * @param itemBindingVariableName the desired binding variable name
+ */
+ public void setItemBindingVariableName(String itemBindingVariableName) {
+ this.itemBindingVariableName = itemBindingVariableName;
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ scriptEvaluator = new StandardScriptEvaluator();
+
+ Assert.state(scriptSource != null || script != null,
+ "Either the script source or script file must be provided");
+
+ Assert.state(scriptSource == null || script == null,
+ "Either a script source or script file must be provided, not both");
+
+ if (scriptSource != null) {
+ Assert.isTrue(!StringUtils.isEmpty(language),
+ "Language must be provided when using script source");
+
+ ((StandardScriptEvaluator) scriptEvaluator).setLanguage(language);
+ }
+ }
+
+ private ScriptSource getScriptSource() {
+ if (script != null) {
+ return script;
+ }
+
+ if (scriptSource != null) {
+ return scriptSource;
+ }
+
+ throw new IllegalStateException("Either a script source or script needs to be provided.");
+ }
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java
new file mode 100644
index 000000000..5279827ac
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2014 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
+ *
+ * http://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.batch.item.support;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+import javax.script.ScriptEngineFactory;
+import javax.script.ScriptEngineManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+/**
+ *
+ * Test cases around {@link org.springframework.batch.item.support.ScriptItemProcessor}.
+ *
+ *
+ * @author Chris Schaefer
+ * @since 3.1
+ */
+public class ScriptItemProcessorTests {
+ private static List availableLanguages = new ArrayList();
+
+ @BeforeClass
+ public static void populateAvailableEngines() {
+ List scriptEngineFactories = new ScriptEngineManager().getEngineFactories();
+
+ for (ScriptEngineFactory scriptEngineFactory : scriptEngineFactories) {
+ availableLanguages.addAll(scriptEngineFactory.getNames());
+ }
+ }
+
+ @Test
+ public void testJavascriptScriptSourceSimple() throws Exception {
+ assumeTrue(languageExists("javascript"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("item.toUpperCase();", "javascript");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testJavascriptScriptSourceFunction() throws Exception {
+ assumeTrue(languageExists("javascript"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("function process(item) { return item.toUpperCase(); } process(item);", "javascript");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testJRubyScriptSourceSimple() throws Exception {
+ assumeTrue(languageExists("jruby"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("$item.upcase", "jruby");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testJRubyScriptSourceMethod() throws Exception {
+ assumeTrue(languageExists("jruby"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("def process(item) $item.upcase end \n process($item)", "jruby");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testBeanShellScriptSourceSimple() throws Exception {
+ assumeTrue(languageExists("bsh"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("item.toUpperCase();", "bsh");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testBeanShellScriptSourceFunction() throws Exception {
+ assumeTrue(languageExists("bsh"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("String process(String item) { return item.toUpperCase(); } process(item);", "bsh");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testGroovyScriptSourceSimple() throws Exception {
+ assumeTrue(languageExists("groovy"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("item.toUpperCase();", "groovy");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testGroovyScriptSourceMethod() throws Exception {
+ assumeTrue(languageExists("groovy"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("def process(item) { return item.toUpperCase() } \n process(item)", "groovy");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test
+ public void testJavascriptScriptSimple() throws Exception {
+ assumeTrue(languageExists("javascript"));
+
+ Resource resource = new ClassPathResource("org/springframework/batch/item/support/processor-test-simple.js");
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScript(resource);
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNoScriptSet() throws Exception {
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.afterPropertiesSet();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testScriptSourceAndScriptResourceSet() throws Exception {
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("blah", "blah");
+ scriptItemProcessor.setScript(new ClassPathResource("blah"));
+ scriptItemProcessor.afterPropertiesSet();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNoScriptSetWithoutInitBean() throws Exception {
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.process("blah");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testScriptSourceWithNoLanguage() throws Exception {
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setScriptSource("function process(item) { return item.toUpperCase(); } process(item);", null);
+ scriptItemProcessor.afterPropertiesSet();
+ }
+
+ @Test
+ public void testItemBindingNameChange() throws Exception {
+ assumeTrue(languageExists("javascript"));
+
+ ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor();
+ scriptItemProcessor.setItemBindingVariableName("someOtherVarName");
+ scriptItemProcessor.setScriptSource("function process(param) { return param.toUpperCase(); } process(someOtherVarName);", "javascript");
+ scriptItemProcessor.afterPropertiesSet();
+
+ assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
+ }
+
+ private boolean languageExists(String engineName) {
+ return availableLanguages.contains(engineName);
+ }
+}
diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/support/processor-test-simple.js b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/support/processor-test-simple.js
new file mode 100644
index 000000000..3a9b9db1d
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/support/processor-test-simple.js
@@ -0,0 +1 @@
+item.toUpperCase();