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