XD-1027: Create script-based batch ItemProcessor

This commit is contained in:
Chris Schaefer
2014-07-02 17:13:55 -04:00
committed by Michael Minella
parent 8f8600c14d
commit 587680ba56
7 changed files with 453 additions and 0 deletions

View File

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

View File

@@ -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;
/**
* <p>
* Test job utilizing a {@link org.springframework.batch.item.support.ScriptItemProcessor}.
* </p>
*
* @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<String> {
@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);
}
}
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/batch"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/batch
http://www.springframework.org/schema/batch/spring-batch.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<job id="scriptProcessorJob">
<step id="step1">
<tasklet>
<chunk reader="reader" processor="processor" writer="writer" commit-interval="1"/>
</tasklet>
</step>
</job>
<beans:bean id="transactionManager"
class="org.springframework.batch.support.transaction.ResourcelessTransactionManager"/>
<beans:bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean"
p:transactionManager-ref="transactionManager"/>
<beans:bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher"
p:jobRepository-ref="jobRepository"/>
<beans:bean id="reader" class="org.springframework.batch.item.support.ListItemReader">
<beans:constructor-arg>
<beans:list>
<beans:value>blah</beans:value>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<beans:bean id="processor" class="org.springframework.batch.item.support.ScriptItemProcessor"
p:script="org/springframework/batch/core/step/item/processor-test-simple.js"/>
<beans:bean id="writer" class="org.springframework.batch.core.step.item.ScriptItemProcessorTest$TestItemWriter"/>
</beans:beans>

View File

@@ -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;
/**
* <p>
* {@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.
* </p>
*
*
* @author Chris Schaefer
* @since 3.0
*/
public class ScriptItemProcessor<I, O> implements ItemProcessor<I, O>, 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<String, Object> arguments = new HashMap<String, Object>();
arguments.put(itemBindingVariableName, item);
return (O) scriptEvaluator.evaluate(getScriptSource(), arguments);
}
/**
* <p>
* Sets the {@link org.springframework.core.io.Resource} location of the script to use.
* The script language will be deduced from the filename extension.
* </p>
*
* @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);
}
/**
* <p>
* Sets the provided {@link String} as the script source code to use.
* </p>
*
* @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);
}
/**
* <p>
* 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").
* </p>
*
* @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.");
}
}

View File

@@ -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;
/**
* <p>
* Test cases around {@link org.springframework.batch.item.support.ScriptItemProcessor}.
* </p>
*
* @author Chris Schaefer
* @since 3.1
*/
public class ScriptItemProcessorTests {
private static List<String> availableLanguages = new ArrayList<String>();
@BeforeClass
public static void populateAvailableEngines() {
List<ScriptEngineFactory> scriptEngineFactories = new ScriptEngineManager().getEngineFactories();
for (ScriptEngineFactory scriptEngineFactory : scriptEngineFactories) {
availableLanguages.addAll(scriptEngineFactory.getNames());
}
}
@Test
public void testJavascriptScriptSourceSimple() throws Exception {
assumeTrue(languageExists("javascript"));
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
scriptItemProcessor.setScript(resource);
scriptItemProcessor.afterPropertiesSet();
assertEquals("Incorrect transformed value", "SS", scriptItemProcessor.process("ss"));
}
@Test(expected = IllegalStateException.class)
public void testNoScriptSet() throws Exception {
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
scriptItemProcessor.afterPropertiesSet();
}
@Test(expected = IllegalStateException.class)
public void testScriptSourceAndScriptResourceSet() throws Exception {
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
scriptItemProcessor.setScriptSource("blah", "blah");
scriptItemProcessor.setScript(new ClassPathResource("blah"));
scriptItemProcessor.afterPropertiesSet();
}
@Test(expected = IllegalStateException.class)
public void testNoScriptSetWithoutInitBean() throws Exception {
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
scriptItemProcessor.process("blah");
}
@Test(expected = IllegalArgumentException.class)
public void testScriptSourceWithNoLanguage() throws Exception {
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
scriptItemProcessor.setScriptSource("function process(item) { return item.toUpperCase(); } process(item);", null);
scriptItemProcessor.afterPropertiesSet();
}
@Test
public void testItemBindingNameChange() throws Exception {
assumeTrue(languageExists("javascript"));
ScriptItemProcessor<String, Object> scriptItemProcessor = new ScriptItemProcessor<String, Object>();
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);
}
}