diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
index ac6243f1e..1f19b6f56 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
@@ -43,6 +43,10 @@ public class CoreNamespaceUtils {
private static final String STEP_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.StepScope";
+ private static final String JOB_SCOPE_PROCESSOR_BEAN_NAME = "org.springframework.batch.core.scope.internalJobScope";
+
+ private static final String JOB_SCOPE_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.scope.JobScope";
+
private static final String CUSTOM_EDITOR_CONFIGURER_CLASS_NAME = "org.springframework.beans.factory.config.CustomEditorConfigurer";
private static final String RANGE_ARRAY_CLASS_NAME = "org.springframework.batch.item.file.transform.Range[]";
@@ -53,28 +57,38 @@ public class CoreNamespaceUtils {
public static void autoregisterBeansForNamespace(ParserContext parserContext, Object source) {
checkForStepScope(parserContext, source);
+ checkForJobScope(parserContext, source);
addRangePropertyEditor(parserContext);
addCoreNamespacePostProcessor(parserContext);
addStateTransitionComparator(parserContext);
}
private static void checkForStepScope(ParserContext parserContext, Object source) {
- boolean foundStepScope = false;
+ checkForScope(parserContext, source, STEP_SCOPE_PROCESSOR_CLASS_NAME, STEP_SCOPE_PROCESSOR_BEAN_NAME);
+ }
+
+ private static void checkForJobScope(ParserContext parserContext, Object source) {
+ checkForScope(parserContext, source, JOB_SCOPE_PROCESSOR_CLASS_NAME, JOB_SCOPE_PROCESSOR_BEAN_NAME);
+ }
+
+ private static void checkForScope(ParserContext parserContext, Object source, String scopeClassName,
+ String scopeBeanName) {
+ boolean foundScope = false;
String[] beanNames = parserContext.getRegistry().getBeanDefinitionNames();
for (String beanName : beanNames) {
BeanDefinition bd = parserContext.getRegistry().getBeanDefinition(beanName);
- if (STEP_SCOPE_PROCESSOR_CLASS_NAME.equals(bd.getBeanClassName())) {
- foundStepScope = true;
+ if (scopeClassName.equals(bd.getBeanClassName())) {
+ foundScope = true;
break;
}
}
- if (!foundStepScope) {
+ if (!foundScope) {
BeanDefinitionBuilder stepScopeBuilder = BeanDefinitionBuilder
- .genericBeanDefinition(STEP_SCOPE_PROCESSOR_CLASS_NAME);
+ .genericBeanDefinition(scopeClassName);
AbstractBeanDefinition abd = stepScopeBuilder.getBeanDefinition();
abd.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
abd.setSource(source);
- parserContext.getRegistry().registerBeanDefinition(STEP_SCOPE_PROCESSOR_BEAN_NAME, abd);
+ parserContext.getRegistry().registerBeanDefinition(scopeBeanName, abd);
}
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
index 0373bece0..a8984d09c 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
@@ -38,6 +38,7 @@ import org.springframework.batch.core.launch.support.ExitCodeMapper;
import org.springframework.batch.core.listener.CompositeJobExecutionListener;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
import org.springframework.batch.core.step.StepLocator;
import org.springframework.batch.repeat.RepeatException;
import org.springframework.beans.factory.BeanNameAware;
@@ -286,6 +287,8 @@ InitializingBean {
logger.debug("Job execution starting: " + execution);
+ JobSynchronizationManager.register(execution);
+
try {
jobParametersValidator.validate(execution.getJobParameters());
@@ -328,24 +331,27 @@ InitializingBean {
execution.setStatus(BatchStatus.FAILED);
execution.addFailureException(t);
} finally {
-
- if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED)
- && execution.getStepExecutions().isEmpty()) {
- ExitStatus exitStatus = execution.getExitStatus();
- execution
- .setExitStatus(exitStatus.and(ExitStatus.NOOP
- .addExitDescription("All steps already completed or no steps configured for this job.")));
- }
-
- execution.setEndTime(new Date());
-
try {
- listener.afterJob(execution);
- } catch (Exception e) {
- logger.error("Exception encountered in afterStep callback", e);
- }
+ if (execution.getStatus().isLessThanOrEqualTo(BatchStatus.STOPPED)
+ && execution.getStepExecutions().isEmpty()) {
+ ExitStatus exitStatus = execution.getExitStatus();
+ execution
+ .setExitStatus(exitStatus.and(ExitStatus.NOOP
+ .addExitDescription("All steps already completed or no steps configured for this job.")));
+ }
- jobRepository.update(execution);
+ execution.setEndTime(new Date());
+
+ try {
+ listener.afterJob(execution);
+ } catch (Exception e) {
+ logger.error("Exception encountered in afterStep callback", e);
+ }
+
+ jobRepository.update(execution);
+ } finally {
+ JobSynchronizationManager.release();
+ }
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java
new file mode 100644
index 000000000..e8de653a8
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/JobScope.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2006-2007 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.scope;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.core.scope.util.ContextFactory;
+import org.springframework.batch.core.scope.util.JobContextFactory;
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.beans.factory.config.Scope;
+
+/**
+ * Scope for job context. Objects in this scope use the Spring container as an
+ * object factory, so there is only one instance of such a bean per executing
+ * job. All objects in this scope are <aop:scoped-proxy/> (no need to
+ * decorate the bean definitions).
+ *
+ *
+ * In addition, support is provided for late binding of references accessible
+ * from the {@link JobContext} using #{..} placeholders. Using this feature,
+ * bean properties can be pulled from the job or job execution context and the
+ * job parameters. E.g.
+ *
+ *
+ * <bean id="..." class="..." scope="job">
+ * <property name="name" value="#{jobParameters[input]}" />
+ * </bean>
+ *
+ * <bean id="..." class="..." scope="job">
+ * <property name="name" value="#{jobExecutionContext['input.stem']}.txt" />
+ * </bean>
+ *
+ *
+ * The {@link JobContext} is referenced using standard bean property paths (as
+ * per {@link BeanWrapper}). The examples above all show the use of the Map
+ * accessors provided as a convenience for job attributes.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet (create JobScope based on {@link StepScope})
+ * @since 2.0
+ */
+public class JobScope extends ScopeSupport {
+
+ private Log logger = LogFactory.getLog(getClass());
+
+ private final Object mutex = new Object();
+
+ /**
+ * Context key for clients to use for conversation identifier.
+ */
+ public static final String ID_KEY = "JOB_IDENTIFIER";
+
+ /**
+ * The ContextFactory.
+ */
+ private static final ContextFactory CONTEXT_FACTORY = new JobContextFactory();
+
+ public JobScope() {
+ super("job", CONTEXT_FACTORY);
+ }
+
+ /**
+ * If Spring 3.0 is available, this will be used to resolve expressions in
+ * job-scoped beans. This method is part of the Scope SPI in Spring 3.0,
+ * but should just be ignored by earlier versions of Spring.
+ */
+ public Object resolveContextualObject(String key) {
+ JobContext context = getContext();
+ // TODO: support for attributes as well maybe (setters not exposed yet
+ // so not urgent).
+ return new BeanWrapperImpl(context).getPropertyValue(key);
+ }
+
+ /**
+ * @see Scope#get(String, ObjectFactory)
+ */
+ public Object get(String name, ObjectFactory objectFactory) {
+
+ JobContext context = getContext();
+ Object scopedObject = context.getAttribute(name);
+
+ if (scopedObject == null) {
+
+ synchronized (mutex) {
+ scopedObject = context.getAttribute(name);
+ if (scopedObject == null) {
+
+ logger.debug(String.format("Creating object in scope=%s, name=%s", this.getName(), name));
+
+ scopedObject = objectFactory.getObject();
+ context.setAttribute(name, scopedObject);
+
+ }
+
+ }
+
+ }
+ return scopedObject;
+ }
+
+ /**
+ * @see Scope#getConversationId()
+ */
+ public String getConversationId() {
+ JobContext context = getContext();
+ return context.getId();
+ }
+
+ /**
+ * @see Scope#registerDestructionCallback(String, Runnable)
+ */
+ public void registerDestructionCallback(String name, Runnable callback) {
+ JobContext context = getContext();
+ logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.getName(), name));
+ context.registerDestructionCallback(name, callback);
+ }
+
+ /**
+ * @see Scope#remove(String)
+ */
+ public Object remove(String name) {
+ JobContext context = getContext();
+ logger.debug(String.format("Removing from scope=%s, name=%s", this.getName(), name));
+ return context.removeAttribute(name);
+ }
+
+ /**
+ * Get an attribute accessor in the form of a {@link JobContext} that can
+ * be used to store scoped bean instances.
+ *
+ * @return the current job context which we can use as a scope storage
+ * medium
+ */
+ private JobContext getContext() {
+ JobContext context = JobSynchronizationManager.getContext();
+ if (context == null) {
+ throw new IllegalStateException("No context holder available for job scope");
+ }
+ return context;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java
new file mode 100644
index 000000000..ded838484
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/ScopeSupport.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2006-2007 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.scope;
+
+import org.springframework.aop.scope.ScopedProxyUtils;
+import org.springframework.batch.core.scope.util.ContextFactory;
+import org.springframework.batch.core.scope.util.PlaceholderProxyFactoryBean;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanDefinitionHolder;
+import org.springframework.beans.factory.config.BeanDefinitionVisitor;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.config.Scope;
+import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.core.Ordered;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.StringValueResolver;
+
+/**
+ * ScopeSupport.
+ *
+ * @author Dave Syer
+ */
+public abstract class ScopeSupport implements Scope, BeanFactoryPostProcessor, Ordered {
+
+ private static boolean springThreeDetected;
+
+ private static boolean cachedSpringThreeResult;
+
+ private int order = Ordered.LOWEST_PRECEDENCE;
+
+ private String name;
+
+ private boolean proxyTargetClass = false;
+
+ private ContextFactory contextFactory;
+
+ /**
+ * ScopeSupport constructor.
+ *
+ * @param defaultScopeName the default scope name
+ * @param contextFactory the ContextFactory
+ */
+ protected ScopeSupport(String defaultScopeName, ContextFactory contextFactory) {
+ this.name = defaultScopeName;
+ this.contextFactory = contextFactory;
+ }
+
+ /**
+ * Flag to indicate that proxies should use dynamic subclassing. This allows
+ * classes with no interface to be proxied. Defaults to false.
+ *
+ * @param proxyTargetClass set to true to have proxies created using dynamic
+ * subclasses
+ */
+ public void setProxyTargetClass(boolean proxyTargetClass) {
+ this.proxyTargetClass = proxyTargetClass;
+ }
+
+ /**
+ * @param order the order value to set priority of callback execution for
+ * the {@link BeanFactoryPostProcessor} part of this scope bean.
+ */
+ public void setOrder(int order) {
+ this.order = order;
+ }
+
+ public int getOrder() {
+ return order;
+ }
+
+ /**
+ * Public setter for the name property. This can then be used as a bean
+ * definition attribute, e.g. scope="step".
+ *
+ * @param name the name to set for this scope.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return the scope name.
+ *
+ * @return the scope name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Register this scope with the enclosing BeanFactory.
+ *
+ * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
+ *
+ * @param beanFactory the BeanFactory to register with
+ * @throws BeansException if there is a problem.
+ */
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+
+ beanFactory.registerScope(name, this);
+
+ Assert.state(beanFactory instanceof BeanDefinitionRegistry,
+ "BeanFactory was not a BeanDefinitionRegistry, so scope cannot be used.");
+ BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
+
+ for (String beanName : beanFactory.getBeanDefinitionNames()) {
+ BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
+ // Replace this or any of its inner beans with scoped proxy if it
+ // has this scope
+ boolean scoped = name.equals(definition.getScope());
+ Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped, contextFactory);
+ scopifier.visitBeanDefinition(definition);
+ if (scoped) {
+ if (!isSpringThree()) {
+ new ExpressionHider(name, scoped).visitBeanDefinition(definition);
+ }
+ createScopedProxy(beanName, definition, registry, proxyTargetClass, contextFactory);
+ }
+ }
+
+ }
+
+ private static boolean isSpringThree() {
+ if (!cachedSpringThreeResult) {
+ springThreeDetected = ReflectionUtils.findMethod(Scope.class, "resolveContextualObject",
+ new Class>[] { String.class }) != null;
+ cachedSpringThreeResult = true;
+ }
+ return springThreeDetected;
+ }
+
+ /**
+ * Wrap a target bean definition in a proxy that defers initialization until
+ * after the context is available. Amounts to adding
+ * <aop-auto-proxy/> to a scoped bean. Also if Spring EL is not
+ * available will enable a weak version of late binding as described in the
+ * class-level docs.
+ *
+ * @param beanName the bean name to replace
+ * @param definition the bean definition to replace
+ * @param registry the enclosing {@link BeanDefinitionRegistry}
+ * @param proxyTargetClass true if we need to force use of dynamic
+ * subclasses
+ * @param contextFactory the ContextFactory
+ * @return a {@link BeanDefinitionHolder} for the new representation of the
+ * target. Caller should register it if needed to be visible at top level in
+ * bean factory.
+ */
+ private static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition,
+ BeanDefinitionRegistry registry, boolean proxyTargetClass, ContextFactory contextFactory) {
+
+ BeanDefinitionHolder proxyHolder;
+
+ if (isSpringThree()) {
+ proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry,
+ proxyTargetClass);
+ }
+ else {
+ // Create the scoped proxy...
+ proxyHolder = PlaceholderProxyFactoryBean.createScopedProxy(new BeanDefinitionHolder(definition, beanName),
+ registry, proxyTargetClass, contextFactory);
+ // ...and register it under the original target name
+ }
+ registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition());
+
+ return proxyHolder;
+
+ }
+
+ /**
+ * Helper class to scan a bean definition hierarchy and force the use of
+ * auto-proxy for scoped beans.
+ *
+ * @author Dave Syer
+ *
+ */
+ private static class Scopifier extends BeanDefinitionVisitor {
+
+ private final boolean proxyTargetClass;
+
+ private final BeanDefinitionRegistry registry;
+
+ private final String scope;
+
+ private final boolean scoped;
+
+ private final ContextFactory contextFactory;
+
+ public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped,
+ ContextFactory contextFactory) {
+ super(new StringValueResolver() {
+ public String resolveStringValue(String value) {
+ return value;
+ }
+ });
+ this.registry = registry;
+ this.proxyTargetClass = proxyTargetClass;
+ this.scope = scope;
+ this.scoped = scoped;
+ this.contextFactory = contextFactory;
+ }
+
+ @Override
+ protected Object resolveValue(Object value) {
+
+ BeanDefinition definition = null;
+ String beanName = null;
+ if (value instanceof BeanDefinition) {
+ definition = (BeanDefinition) value;
+ beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry);
+ }
+ else if (value instanceof BeanDefinitionHolder) {
+ BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
+ definition = holder.getBeanDefinition();
+ beanName = holder.getBeanName();
+ }
+
+ if (definition != null) {
+ boolean nestedScoped = scope.equals(definition.getScope());
+ boolean scopeChangeRequiresProxy = !scoped && nestedScoped;
+ if (!isSpringThree()) {
+ new ExpressionHider(scope, nestedScoped).visitBeanDefinition(definition);
+ }
+ if (scopeChangeRequiresProxy) {
+ // Exit here so that nested inner bean definitions are not
+ // analysed
+ return createScopedProxy(beanName, definition, registry, proxyTargetClass, contextFactory);
+ }
+ }
+
+ // Nested inner bean definitions are recursively analysed here
+ value = super.resolveValue(value);
+ return value;
+
+ }
+
+ }
+
+ /**
+ * Helper class to scan a bean definition hierarchy and hide placeholders
+ * from Spring EL.
+ *
+ * @author Dave Syer
+ *
+ */
+ private static class ExpressionHider extends BeanDefinitionVisitor {
+
+ private static final String PLACEHOLDER_PREFIX = "#{";
+
+ private static final String PLACEHOLDER_SUFFIX = "}";
+
+ private static final String REPLACEMENT_PREFIX = "%{";
+
+ private final String scope;
+
+ private final boolean scoped;
+
+ private ExpressionHider(String scope, final boolean scoped) {
+ super(new StringValueResolver() {
+ public String resolveStringValue(String value) {
+ if (scoped && value.contains(PLACEHOLDER_PREFIX) && value.contains(PLACEHOLDER_SUFFIX)) {
+ value = value.replace(PLACEHOLDER_PREFIX, REPLACEMENT_PREFIX);
+ }
+ return value;
+ }
+ });
+ this.scope = scope;
+ this.scoped = scoped;
+ }
+
+ @Override
+ protected Object resolveValue(Object value) {
+ BeanDefinition definition = null;
+ if (value instanceof BeanDefinition) {
+ definition = (BeanDefinition) value;
+ }
+ else if (value instanceof BeanDefinitionHolder) {
+ BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
+ definition = holder.getBeanDefinition();
+ }
+ if (definition != null) {
+ String otherScope = definition.getScope();
+ boolean scopeChange = !scope.equals(otherScope);
+ if (scopeChange) {
+ new ExpressionHider(otherScope == null ? scope : otherScope, !scoped)
+ .visitBeanDefinition(definition);
+ // Exit here so that nested inner bean definitions are not
+ // analysed by both visitors
+ return value;
+ }
+ }
+ // Nested inner bean definitions are recursively analysed here
+ value = super.resolveValue(value);
+ return value;
+ }
+
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java
index 718630a68..3be3bce90 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepScope.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2013 the original author or authors.
+ * Copyright 2006-2007 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.
@@ -17,24 +17,14 @@ package org.springframework.batch.core.scope;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.batch.core.scope.context.StepContext;
import org.springframework.batch.core.scope.context.StepSynchronizationManager;
+import org.springframework.batch.core.scope.util.ContextFactory;
+import org.springframework.batch.core.scope.util.StepContextFactory;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
-import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.config.BeanDefinitionHolder;
-import org.springframework.beans.factory.config.BeanDefinitionVisitor;
-import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
-import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
-import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
-import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.core.Ordered;
-import org.springframework.util.Assert;
-import org.springframework.util.StringValueResolver;
/**
* Scope for step context. Objects in this scope use the Spring container as an
@@ -42,97 +32,62 @@ import org.springframework.util.StringValueResolver;
* step. All objects in this scope are <aop:scoped-proxy/> (no need to
* decorate the bean definitions).
*
- *
+ *
* In addition, support is provided for late binding of references accessible
* from the {@link StepContext} using #{..} placeholders. Using this feature,
* bean properties can be pulled from the step or job execution context and the
* job parameters. E.g.
- *
+ *
*
* <bean id="..." class="..." scope="step">
* <property name="parent" ref="#{stepExecutionContext[helper]}" />
* </bean>
- *
+ *
* <bean id="..." class="..." scope="step">
* <property name="name" value="#{stepExecutionContext['input.name']}" />
* </bean>
- *
+ *
* <bean id="..." class="..." scope="step">
* <property name="name" value="#{jobParameters[input]}" />
* </bean>
- *
+ *
* <bean id="..." class="..." scope="step">
* <property name="name" value="#{jobExecutionContext['input.stem']}.txt" />
* </bean>
*
- *
+ *
* The {@link StepContext} is referenced using standard bean property paths (as
* per {@link BeanWrapper}). The examples above all show the use of the Map
* accessors provided as a convenience for step and job attributes.
- *
+ *
* @author Dave Syer
- * @author Michael Minella
* @since 2.0
*/
-public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
-
- private static final String TARGET_NAME_PREFIX = "scopedTarget.";
+public class StepScope extends ScopeSupport {
private Log logger = LogFactory.getLog(getClass());
- private int order = Ordered.LOWEST_PRECEDENCE;
-
- private boolean autoProxy = true;
-
private final Object mutex = new Object();
- /**
- * @param order the order value to set priority of callback execution for
- * the {@link BeanFactoryPostProcessor} part of this scope bean.
- */
- public void setOrder(int order) {
- this.order = order;
- }
-
- @Override
- public int getOrder() {
- return order;
- }
-
/**
* Context key for clients to use for conversation identifier.
*/
public static final String ID_KEY = "STEP_IDENTIFIER";
- private String name = "step";
-
- private boolean proxyTargetClass = false;
-
/**
- * Flag to indicate that proxies should use dynamic subclassing. This allows
- * classes with no interface to be proxied. Defaults to false.
- *
- * @param proxyTargetClass set to true to have proxies created using dynamic
- * subclasses
+ * The ContextFactory.
*/
- public void setProxyTargetClass(boolean proxyTargetClass) {
- this.proxyTargetClass = proxyTargetClass;
+ private static final ContextFactory CONTEXT_FACTORY = new StepContextFactory();
+
+ public StepScope() {
+ super("step", CONTEXT_FACTORY);
}
/**
- * Flag to indicate that bean definitions need not be auto proxied. This gives control back to the declarer of the
- * bean definition (e.g. in an @Configuration class).
- *
- * @param autoProxy the flag value to set (default true)
+ * If Spring 3.0 is available, this will be used to resolve expressions in
+ * step-scoped beans. This method is part of the Scope SPI in Spring 3.0,
+ * but should just be ignored by earlier versions of Spring.
*/
- public void setAutoProxy(boolean autoProxy) {
- this.autoProxy = autoProxy;
- }
-
- /**
- * This will be used to resolve expressions in step-scoped beans.
- */
- @Override
public Object resolveContextualObject(String key) {
StepContext context = getContext();
// TODO: support for attributes as well maybe (setters not exposed yet
@@ -143,9 +98,8 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
/**
* @see Scope#get(String, ObjectFactory)
*/
- @SuppressWarnings("rawtypes")
- @Override
public Object get(String name, ObjectFactory objectFactory) {
+
StepContext context = getContext();
Object scopedObject = context.getAttribute(name);
@@ -155,7 +109,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
scopedObject = context.getAttribute(name);
if (scopedObject == null) {
- logger.debug(String.format("Creating object in scope=%s, name=%s", this.name, name));
+ logger.debug(String.format("Creating object in scope=%s, name=%s", this.getName(), name));
scopedObject = objectFactory.getObject();
context.setAttribute(name, scopedObject);
@@ -171,7 +125,6 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
/**
* @see Scope#getConversationId()
*/
- @Override
public String getConversationId() {
StepContext context = getContext();
return context.getId();
@@ -180,27 +133,25 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
/**
* @see Scope#registerDestructionCallback(String, Runnable)
*/
- @Override
public void registerDestructionCallback(String name, Runnable callback) {
StepContext context = getContext();
- logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.name, name));
+ logger.debug(String.format("Registered destruction callback in scope=%s, name=%s", this.getName(), name));
context.registerDestructionCallback(name, callback);
}
/**
* @see Scope#remove(String)
*/
- @Override
public Object remove(String name) {
StepContext context = getContext();
- logger.debug(String.format("Removing from scope=%s, name=%s", this.name, name));
+ logger.debug(String.format("Removing from scope=%s, name=%s", this.getName(), name));
return context.removeAttribute(name);
}
/**
* Get an attribute accessor in the form of a {@link StepContext} that can
* be used to store scoped bean instances.
- *
+ *
* @return the current step context which we can use as a scope storage
* medium
*/
@@ -212,142 +163,4 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered {
return context;
}
- /**
- * Register this scope with the enclosing BeanFactory.
- *
- * @see BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
- *
- * @param beanFactory the BeanFactory to register with
- * @throws BeansException if there is a problem.
- */
- @Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
-
- beanFactory.registerScope(name, this);
-
- if(!autoProxy) {
- return;
- }
-
- Assert.state(beanFactory instanceof BeanDefinitionRegistry,
- "BeanFactory was not a BeanDefinitionRegistry, so StepScope cannot be used.");
- BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
-
- for (String beanName : beanFactory.getBeanDefinitionNames()) {
- if (!beanName.startsWith(TARGET_NAME_PREFIX)) {
- BeanDefinition definition = beanFactory.getBeanDefinition(beanName);
- // Replace this or any of its inner beans with scoped proxy if it
- // has this scope
- boolean scoped = name.equals(definition.getScope());
- Scopifier scopifier = new Scopifier(registry, name, proxyTargetClass, scoped);
- scopifier.visitBeanDefinition(definition);
-
- if (scoped && !definition.isAbstract()) {
- createScopedProxy(beanName, definition, registry, proxyTargetClass);
- }
- }
- }
-
- }
-
- /**
- * Public setter for the name property. This can then be used as a bean
- * definition attribute, e.g. scope="step". Defaults to "step".
- *
- * @param name the name to set for this scope.
- */
- public void setName(String name) {
- this.name = name;
- }
-
- /**
- * Wrap a target bean definition in a proxy that defers initialization until
- * after the {@link StepContext} is available. Amounts to adding
- * <aop-auto-proxy/> to a step scoped bean.
- *
- * @param beanName the bean name to replace
- * @param definition the bean definition to replace
- * @param registry the enclosing {@link BeanDefinitionRegistry}
- * @param proxyTargetClass true if we need to force use of dynamic
- * subclasses
- * @return a {@link BeanDefinitionHolder} for the new representation of the
- * target. Caller should register it if needed to be visible at top level in
- * bean factory.
- */
- private static BeanDefinitionHolder createScopedProxy(String beanName, BeanDefinition definition,
- BeanDefinitionRegistry registry, boolean proxyTargetClass) {
-
- BeanDefinitionHolder proxyHolder;
-
- proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry,
- proxyTargetClass);
-
- registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition());
-
- return proxyHolder;
-
- }
-
- /**
- * Helper class to scan a bean definition hierarchy and force the use of
- * auto-proxy for step scoped beans.
- *
- * @author Dave Syer
- *
- */
- private static class Scopifier extends BeanDefinitionVisitor {
-
- private final boolean proxyTargetClass;
-
- private final BeanDefinitionRegistry registry;
-
- private final String scope;
-
- private final boolean scoped;
-
- public Scopifier(BeanDefinitionRegistry registry, String scope, boolean proxyTargetClass, boolean scoped) {
- super(new StringValueResolver() {
- @Override
- public String resolveStringValue(String value) {
- return value;
- }
- });
- this.registry = registry;
- this.proxyTargetClass = proxyTargetClass;
- this.scope = scope;
- this.scoped = scoped;
- }
-
- @Override
- protected Object resolveValue(Object value) {
-
- BeanDefinition definition = null;
- String beanName = null;
- if (value instanceof BeanDefinition) {
- definition = (BeanDefinition) value;
- beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry);
- }
- else if (value instanceof BeanDefinitionHolder) {
- BeanDefinitionHolder holder = (BeanDefinitionHolder) value;
- definition = holder.getBeanDefinition();
- beanName = holder.getBeanName();
- }
-
- if (definition != null) {
- boolean nestedScoped = scope.equals(definition.getScope());
- boolean scopeChangeRequiresProxy = !scoped && nestedScoped;
- if (scopeChangeRequiresProxy) {
- // Exit here so that nested inner bean definitions are not
- // analysed
- return createScopedProxy(beanName, definition, registry, proxyTargetClass);
- }
- }
-
- // Nested inner bean definitions are recursively analysed here
- value = super.resolveValue(value);
- return value;
-
- }
-
- }
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java
new file mode 100644
index 000000000..acdd5e0b1
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobContext.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2006-2007 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.scope.context;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameter;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.UnexpectedJobExecutionException;
+import org.springframework.batch.core.scope.StepScope;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor;
+import org.springframework.util.Assert;
+
+/**
+ * A context object that can be used to interrogate the current {@link JobExecution} and some of its associated
+ * properties using expressions
+ * based on bean paths. Has public getters for the job execution and
+ * convenience methods for accessing commonly used properties like the {@link ExecutionContext} associated with the job
+ * execution.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet (create JobContext based on {@link StepContext})
+ */
+public class JobContext extends SynchronizedAttributeAccessor {
+
+ private JobExecution jobExecution;
+
+ private Map> callbacks = new HashMap>();
+
+ public JobContext(JobExecution jobExecution) {
+ super();
+ Assert.notNull(jobExecution, "A JobContext must have a non-null JobExecution");
+ this.jobExecution = jobExecution;
+ }
+
+ /**
+ * Convenient accessor for current job name identifier.
+ *
+ * @return the job name identifier of the enclosing {@link JobInstance} associated with the current
+ * {@link JobExecution}
+ */
+ public String getJobName() {
+ Assert.state(jobExecution.getJobInstance() != null, "StepExecution does not have a JobInstance");
+ return jobExecution.getJobInstance().getJobName();
+ }
+
+ /**
+ * Convenient accessor for System properties to make it easy to access them
+ * from placeholder expressions.
+ *
+ * @return the current System properties
+ */
+ public Properties getSystemProperties() {
+ return System.getProperties();
+ }
+
+ /**
+ * @return a map containing the items from the job {@link ExecutionContext}
+ */
+ public Map getJobExecutionContext() {
+ Map result = new HashMap();
+ for (Entry entry : jobExecution.getExecutionContext().entrySet()) {
+ result.put(entry.getKey(), entry.getValue());
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ /**
+ * @return a map containing the items from the {@link JobParameters}
+ */
+ public Map getJobParameters() {
+ Map result = new HashMap();
+ for (Entry entry : jobExecution.getJobInstance().getJobParameters().getParameters()
+ .entrySet()) {
+ result.put(entry.getKey(), entry.getValue().getValue());
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ /**
+ * Allow clients to register callbacks for clean up on close.
+ *
+ * @param name
+ * the callback id (unique attribute key in this context)
+ * @param callback
+ * a callback to execute on close
+ */
+ public void registerDestructionCallback(String name, Runnable callback) {
+ synchronized (callbacks) {
+ Set set = callbacks.get(name);
+ if (set == null) {
+ set = new HashSet();
+ callbacks.put(name, set);
+ }
+ set.add(callback);
+ }
+ }
+
+ private void unregisterDestructionCallbacks(String name) {
+ synchronized (callbacks) {
+ callbacks.remove(name);
+ }
+ }
+
+ /**
+ * Override base class behaviour to ensure destruction callbacks are
+ * unregistered as well as the default behaviour.
+ *
+ * @see SynchronizedAttributeAccessor#removeAttribute(String)
+ */
+ @Override
+ public Object removeAttribute(String name) {
+ unregisterDestructionCallbacks(name);
+ return super.removeAttribute(name);
+ }
+
+ /**
+ * Clean up the context at the end of a step execution. Must be called once
+ * at the end of a step execution to honour the destruction callback
+ * contract from the {@link StepScope}.
+ */
+ public void close() {
+
+ List errors = new ArrayList();
+
+ Map> copy = Collections.unmodifiableMap(callbacks);
+
+ for (Entry> entry : copy.entrySet()) {
+ Set set = entry.getValue();
+ for (Runnable callback : set) {
+ if (callback != null) {
+ /*
+ * The documentation of the interface says that these
+ * callbacks must not throw exceptions, but we don't trust
+ * them necessarily...
+ */
+ try {
+ callback.run();
+ } catch (RuntimeException t) {
+ errors.add(t);
+ }
+ }
+ }
+ }
+
+ if (errors.isEmpty()) {
+ return;
+ }
+
+ Exception error = errors.get(0);
+ if (error instanceof RuntimeException) {
+ throw (RuntimeException) error;
+ } else {
+ throw new UnexpectedJobExecutionException("Could not close step context, rethrowing first of "
+ + errors.size() + " exceptions.", error);
+ }
+ }
+
+ /**
+ * The current {@link JobExecution} that is active in this context.
+ *
+ * @return the current {@link JobExecution}
+ */
+ public JobExecution getJobExecution() {
+ return jobExecution;
+ }
+
+ /**
+ * @return unique identifier for this context based on the step execution
+ */
+ public String getId() {
+ Assert.state(jobExecution.getId() != null, "JobExecution has no id. "
+ + "It must be saved before it can be used in job scope.");
+ return "jobExecution#" + jobExecution.getId();
+ }
+
+ /**
+ * Extend the base class method to include the job execution itself as a key
+ * (i.e. two contexts are only equal if their job executions are the same).
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof JobContext))
+ return false;
+ if (other == this)
+ return true;
+ JobContext context = (JobContext) other;
+ if (context.jobExecution == jobExecution) {
+ return true;
+ }
+ return jobExecution.equals(context.jobExecution);
+ }
+
+ /**
+ * Overrides the default behaviour to provide a hash code based only on the
+ * job execution.
+ */
+ @Override
+ public int hashCode() {
+ return jobExecution.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ", jobExecutionContext=" + getJobExecutionContext() + ", jobParameters="
+ + getJobParameters();
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java
new file mode 100644
index 000000000..1007c18e5
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobScopeManager.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2006-2007 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.scope.context;
+
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+
+/**
+ * Convenient aspect to wrap a single threaded job execution, where the
+ * implementation of the {@link Job} is not job scope aware (i.e. not the ones
+ * provided by the framework).
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+@Aspect
+public class JobScopeManager {
+
+ @Around("execution(void org.springframework.batch.core.Job+.execute(*)) && target(job) && args(jobExecution)")
+ public void execute(Job job, JobExecution jobExecution) {
+ JobSynchronizationManager.register(jobExecution);
+ try {
+ job.execute(jobExecution);
+ }
+ finally {
+ JobSynchronizationManager.release();
+ }
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java
new file mode 100644
index 000000000..3d056aa08
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2006-2007 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.scope.context;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+
+/**
+ * Central convenience class for framework use in managing the job scope
+ * context. Generally only to be used by implementations of {@link Job}. N.B.
+ * it is the responsibility of every {@link Job} implementation to ensure that
+ * a {@link JobContext} is available on every thread that might be involved in
+ * a job execution, including worker threads from a pool.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobSynchronizationManager {
+
+ private static final SynchronizationManagerSupport synchronizationManager =
+ new SynchronizationManagerSupport() {
+
+ @Override
+ protected JobContext createNewContext(JobExecution execution) {
+ return new JobContext(execution);
+ }
+
+ @Override
+ protected void close(JobContext context) {
+ context.close();
+ }
+ };
+
+ /*
+ * We have to deal with single and multi-threaded execution, with a single
+ * and with multiple job execution instances. That's 2x2 = 4 scenarios.
+ */
+
+ /**
+ * Getter for the current context if there is one, otherwise returns null.
+ *
+ * @return the current {@link JobContext} or null if there is none (if one
+ * has not been registered for this thread).
+ */
+ public static JobContext getContext() {
+ return synchronizationManager.getContext();
+ }
+
+ /**
+ * Register a context with the current thread - always put a matching {@link #close()} call in a finally block to
+ * ensure that the correct context is available in the enclosing block.
+ *
+ * @param jobExecution the job context to register
+ * @return a new {@link JobContext} or the current one if it has the same {@link JobExecution}
+ */
+ public static JobContext register(JobExecution jobExecution) {
+ return synchronizationManager.register(jobExecution);
+ }
+
+ /**
+ * Method for de-registering the current context - should always and only be
+ * used by in conjunction with a matching {@link #register(JobExecution)} to ensure that {@link #getContext()}
+ * always returns the correct value.
+ * Does not call {@link JobContext#close()} - that is left up to the caller
+ * because he has a reference to the context (having registered it) and only
+ * he has knowledge of when the job actually ended.
+ */
+ public static void close() {
+ synchronizationManager.close();
+ }
+
+ /**
+ * A convenient "deep" close operation. Call this instead of {@link #close()} if the job execution for the current
+ * context is ending.
+ * Delegates to {@link JobContext#close()} and then ensures that {@link #close()} is also called in a finally block.
+ */
+ public static void release() {
+ synchronizationManager.release();
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
index 71a6c509f..11f25bf09 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
@@ -15,11 +15,6 @@
*/
package org.springframework.batch.core.scope.context;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Stack;
-import java.util.concurrent.atomic.AtomicInteger;
-
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext;
@@ -37,33 +32,25 @@ import org.springframework.batch.core.jsr.configuration.support.BatchPropertyCon
*/
public class StepSynchronizationManager {
+ private static final SynchronizationManagerSupport synchronizationManager =
+ new SynchronizationManagerSupport() {
+
+ @Override
+ protected StepContext createNewContext(StepExecution execution) {
+ return new StepContext(execution);
+ }
+
+ @Override
+ protected void close(StepContext context) {
+ context.close();
+ }
+ };
+
/*
* We have to deal with single and multi-threaded execution, with a single
* and with multiple step execution instances. That's 2x2 = 4 scenarios.
*/
- /**
- * Storage for the current step execution; has to be ThreadLocal because it
- * is needed to locate a StepContext in components that are not part of a
- * Step (like when re-hydrating a scoped proxy). Doesn't use
- * InheritableThreadLocal because there are side effects if a step is trying
- * to run multiple child steps (e.g. with partitioning). The Stack is used
- * to cover the single threaded case, so that the API is the same as
- * multi-threaded.
- */
- private static final ThreadLocal> executionHolder = new ThreadLocal>();
-
- /**
- * Reference counter for each step execution: how many threads are using the
- * same one?
- */
- private static final Map counts = new HashMap();
-
- /**
- * Simple map from a running step execution to the associated context.
- */
- private static final Map contexts = new HashMap();
-
/**
* Getter for the current context if there is one, otherwise returns null.
*
@@ -71,12 +58,7 @@ public class StepSynchronizationManager {
* has not been registered for this thread).
*/
public static StepContext getContext() {
- if (getCurrent().isEmpty()) {
- return null;
- }
- synchronized (contexts) {
- return contexts.get(getCurrent().peek());
- }
+ return synchronizationManager.getContext();
}
/**
@@ -89,20 +71,7 @@ public class StepSynchronizationManager {
* {@link StepExecution}
*/
public static StepContext register(StepExecution stepExecution) {
- if (stepExecution == null) {
- return null;
- }
- getCurrent().push(stepExecution);
- StepContext context;
- synchronized (contexts) {
- context = contexts.get(stepExecution);
- if (context == null) {
- context = new StepContext(stepExecution);
- contexts.put(stepExecution, context);
- }
- }
- increment();
- return context;
+ return synchronizationManager.register(stepExecution);
}
/**
@@ -140,46 +109,7 @@ public class StepSynchronizationManager {
* he has knowledge of when the step actually ended.
*/
public static void close() {
- StepContext oldSession = getContext();
- if (oldSession == null) {
- return;
- }
- decrement();
- }
-
- private static void decrement() {
- StepExecution current = getCurrent().pop();
- if (current != null) {
- int remaining = counts.get(current).decrementAndGet();
- if (remaining <= 0) {
- synchronized (contexts) {
- contexts.remove(current);
- counts.remove(current);
- }
- }
- }
- }
-
- private static void increment() {
- StepExecution current = getCurrent().peek();
- if (current != null) {
- AtomicInteger count;
- synchronized (counts) {
- count = counts.get(current);
- if (count == null) {
- count = new AtomicInteger();
- counts.put(current, count);
- }
- }
- count.incrementAndGet();
- }
- }
-
- private static Stack getCurrent() {
- if (executionHolder.get() == null) {
- executionHolder.set(new Stack());
- }
- return executionHolder.get();
+ synchronizationManager.close();
}
/**
@@ -189,15 +119,7 @@ public class StepSynchronizationManager {
* {@link #close()} is also called in a finally block.
*/
public static void release() {
- StepContext context = getContext();
- try {
- if (context != null) {
- context.close();
- }
- }
- finally {
- close();
- }
+ synchronizationManager.release();
}
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java
new file mode 100644
index 000000000..647c01e25
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2006-2007 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.scope.context;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Central convenience class for framework use in managing the scope
+ * context.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public abstract class SynchronizationManagerSupport {
+
+ /*
+ * We have to deal with single and multi-threaded execution, with a single
+ * and with multiple step execution instances. That's 2x2 = 4 scenarios.
+ */
+
+ /**
+ * Storage for the current execution; has to be ThreadLocal because it
+ * is needed to locate a context in components that are not part of a
+ * step/job (like when re-hydrating a scoped proxy). Doesn't use
+ * InheritableThreadLocal because there are side effects if a step is trying
+ * to run multiple child steps (e.g. with partitioning). The Stack is used
+ * to cover the single threaded case, so that the API is the same as
+ * multi-threaded.
+ */
+ private final ThreadLocal> executionHolder = new ThreadLocal>();
+
+ /**
+ * Reference counter for each execution: how many threads are using the
+ * same one?
+ */
+ private final Map counts = new HashMap();
+
+ /**
+ * Simple map from a running execution to the associated context.
+ */
+ private final Map contexts = new HashMap();
+
+ /**
+ * Getter for the current context if there is one, otherwise returns null.
+ *
+ * @return the current context or null if there is none (if one
+ * has not been registered for this thread).
+ */
+ public C getContext() {
+ if (getCurrent().isEmpty()) {
+ return null;
+ }
+ synchronized (contexts) {
+ return contexts.get(getCurrent().peek());
+ }
+ }
+
+ /**
+ * Register a context with the current thread - always put a matching {@link #close()} call in a finally block to
+ * ensure that the correct
+ * context is available in the enclosing block.
+ *
+ * @param execution the execution to register
+ * @return a new context or the current one if it has the same
+ * execution
+ */
+ public C register(E execution) {
+ if (execution == null) {
+ return null;
+ }
+ getCurrent().push(execution);
+ C context;
+ synchronized (contexts) {
+ context = contexts.get(execution);
+ if (context == null) {
+ context = createNewContext(execution);
+ contexts.put(execution, context);
+ }
+ }
+ increment();
+ return context;
+ }
+
+ /**
+ * Method for de-registering the current context - should always and only be
+ * used by in conjunction with a matching {@link #register(E)} to ensure that {@link #getContext()} always returns
+ * the correct value.
+ * Does not call close on the context - that is left up to the caller
+ * because he has a reference to the context (having registered it) and only
+ * he has knowledge of when the execution actually ended.
+ */
+ public void close() {
+ C oldSession = getContext();
+ if (oldSession == null) {
+ return;
+ }
+ decrement();
+ }
+
+ private void decrement() {
+ E current = getCurrent().pop();
+ if (current != null) {
+ int remaining = counts.get(current).decrementAndGet();
+ if (remaining <= 0) {
+ synchronized (contexts) {
+ contexts.remove(current);
+ counts.remove(current);
+ }
+ }
+ }
+ }
+
+ private void increment() {
+ E current = getCurrent().peek();
+ if (current != null) {
+ AtomicInteger count;
+ synchronized (counts) {
+ count = counts.get(current);
+ if (count == null) {
+ count = new AtomicInteger();
+ counts.put(current, count);
+ }
+ }
+ count.incrementAndGet();
+ }
+ }
+
+ private Stack getCurrent() {
+ if (executionHolder.get() == null) {
+ executionHolder.set(new Stack());
+ }
+ return executionHolder.get();
+ }
+
+ /**
+ * A convenient "deep" close operation. Call this instead of {@link #close()} if the execution for the current
+ * context is ending.
+ * Delegates to {@link context#close()} and then ensures that {@link #close()} is also called in a finally block.
+ */
+ public void release() {
+ C context = getContext();
+ try {
+ if (context != null) {
+ close(context);
+ }
+ } finally {
+ close();
+ }
+ }
+
+ protected abstract void close(C context);
+
+ protected abstract C createNewContext(E execution);
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java
new file mode 100644
index 000000000..7e5fe1fcc
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/JobContextFactory.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2006-2007 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.scope.util;
+
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+
+/**
+ * Implementation of {@link ContextFactory} that provides the current
+ * {@link JobContext} as a context object.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobContextFactory implements ContextFactory {
+
+ public Object getContext() {
+ return JobSynchronizationManager.getContext();
+ }
+
+ public String getContextId() {
+ JobContext context = JobSynchronizationManager.getContext();
+ return context!=null ? (String) context.getId() : "sysinit";
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java
new file mode 100644
index 000000000..7bd8fe1f1
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2006-2009 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.configuration.xml;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.junit.Test;
+import org.springframework.batch.core.scope.JobScope;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+
+/**
+ * @author Thomas Risberg
+ * @author Jimmy Praet
+ */
+public class AutoRegisteringJobScopeTests {
+
+ @Test
+ public void testJobElement() throws Exception {
+ ConfigurableApplicationContext ctx =
+ new ClassPathXmlApplicationContext(
+ "org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml");
+ @SuppressWarnings("unchecked")
+ Map beans = ctx.getBeansOfType(JobScope.class);
+ assertTrue("JobScope not defined properly", beans.size() == 1);
+ }
+
+ @Test
+ public void testStepElement() throws Exception {
+ ConfigurableApplicationContext ctx =
+ new ClassPathXmlApplicationContext(
+ "org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml");
+ @SuppressWarnings("unchecked")
+ Map beans = ctx.getBeansOfType(JobScope.class);
+ assertTrue("JobScope not defined properly", beans.size() == 1);
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java
new file mode 100644
index 000000000..7c56d4d88
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests.java
@@ -0,0 +1,150 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.core.task.TaskExecutor;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class AsyncJobScopeIntegrationTests implements BeanFactoryAware {
+
+ private Log logger = LogFactory.getLog(getClass());
+
+ @Autowired
+ @Qualifier("simple")
+ private Collaborator simple;
+
+ private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
+
+ private ListableBeanFactory beanFactory;
+
+ private int beanCount;
+
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = (ListableBeanFactory) beanFactory;
+ }
+
+ @Before
+ public void countBeans() {
+ JobSynchronizationManager.release();
+ beanCount = beanFactory.getBeanDefinitionCount();
+ }
+
+ @After
+ public void cleanUp() {
+ JobSynchronizationManager.close();
+ // Check that all temporary bean definitions are cleaned up
+ assertEquals(beanCount, beanFactory.getBeanDefinitionCount());
+ }
+
+ @Test
+ public void testSimpleProperty() throws Exception {
+ JobExecution jobExecution = new JobExecution(11L);
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ executionContext.put("foo", "bar");
+ JobSynchronizationManager.register(jobExecution);
+ assertEquals("bar", simple.getName());
+ }
+
+ @Test
+ public void testGetMultipleInMultipleThreads() throws Exception {
+
+ List> tasks = new ArrayList>();
+
+ for (int i = 0; i < 12; i++) {
+ final String value = "foo" + i;
+ final Long id = 123L + i;
+ FutureTask task = new FutureTask(new Callable() {
+ public String call() throws Exception {
+ JobExecution jobExecution = new JobExecution(id);
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ executionContext.put("foo", value);
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ logger.debug("Registered: " + context.getJobExecutionContext());
+ try {
+ return simple.getName();
+ }
+ finally {
+ JobSynchronizationManager.close();
+ }
+ }
+ });
+ tasks.add(task);
+ taskExecutor.execute(task);
+ }
+
+ int i = 0;
+ for (FutureTask task : tasks) {
+ assertEquals("foo" + i, task.get());
+ i++;
+ }
+
+ }
+
+ @Test
+ public void testGetSameInMultipleThreads() throws Exception {
+
+ List> tasks = new ArrayList>();
+ final JobExecution jobExecution = new JobExecution(11L);
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ executionContext.put("foo", "foo");
+ JobSynchronizationManager.register(jobExecution);
+ assertEquals("foo", simple.getName());
+
+ for (int i = 0; i < 12; i++) {
+ final String value = "foo" + i;
+ FutureTask task = new FutureTask(new Callable() {
+ public String call() throws Exception {
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ executionContext.put("foo", value);
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ logger.debug("Registered: " + context.getJobExecutionContext());
+ try {
+ return simple.getName();
+ }
+ finally {
+ JobSynchronizationManager.close();
+ }
+ }
+ });
+ tasks.add(task);
+ taskExecutor.execute(task);
+ }
+
+ int i = 0;
+ for (FutureTask task : tasks) {
+ assertEquals("foo", task.get());
+ i++;
+ }
+
+ // Don't close the outer scope until all tasks are finished. This should
+ // always be the case if using an AbstractJob
+ JobSynchronizationManager.close();
+
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java
new file mode 100644
index 000000000..79af85f9f
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests.java
@@ -0,0 +1,87 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.StringUtils;
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeDestructionCallbackIntegrationTests {
+
+ @Autowired
+ @Qualifier("proxied")
+ private Job proxied;
+
+ @Autowired
+ @Qualifier("nested")
+ private Job nested;
+
+ @Autowired
+ @Qualifier("ref")
+ private Job ref;
+
+ @Autowired
+ @Qualifier("foo")
+ private Collaborator foo;
+
+ @Before
+ @After
+ public void resetMessage() throws Exception {
+ TestDisposableCollaborator.message = "none";
+ TestAdvice.names.clear();
+ }
+
+ @Test
+ public void testDisposableScopedProxy() throws Exception {
+ assertNotNull(proxied);
+ proxied.execute(new JobExecution(1L));
+ assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed"));
+ }
+
+ @Test
+ public void testDisposableInnerScopedProxy() throws Exception {
+ assertNotNull(nested);
+ nested.execute(new JobExecution(1L));
+ assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed"));
+ }
+
+ @Test
+ public void testProxiedScopedProxy() throws Exception {
+ assertNotNull(nested);
+ nested.execute(new JobExecution(1L));
+ assertEquals(4, TestAdvice.names.size());
+ assertEquals("bar", TestAdvice.names.get(0));
+ assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed"));
+ }
+
+ @Test
+ public void testRefScopedProxy() throws Exception {
+ assertNotNull(ref);
+ ref.execute(new JobExecution(1L));
+ assertEquals(4, TestAdvice.names.size());
+ assertEquals("spam", TestAdvice.names.get(0));
+ assertEquals(2, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "destroyed"));
+ assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "bar:destroyed"));
+ assertEquals(1, StringUtils.countOccurrencesOf(TestDisposableCollaborator.message, "spam:destroyed"));
+ }
+
+ @Test
+ public void testProxiedNormalBean() throws Exception {
+ assertNotNull(nested);
+ String name = foo.getName();
+ assertEquals(1, TestAdvice.names.size());
+ assertEquals(name, TestAdvice.names.get(0));
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java
new file mode 100644
index 000000000..cffe4eafe
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeIntegrationTests.java
@@ -0,0 +1,115 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeIntegrationTests {
+
+ @Autowired
+ @Qualifier("vanilla")
+ private Job vanilla;
+
+ @Autowired
+ @Qualifier("proxied")
+ private Job proxied;
+
+ @Autowired
+ @Qualifier("nested")
+ private Job nested;
+
+ @Autowired
+ @Qualifier("enhanced")
+ private Job enhanced;
+
+ @Autowired
+ @Qualifier("double")
+ private Job doubleEnhanced;
+
+ @Before
+ @After
+ public void start() {
+ JobSynchronizationManager.close();
+ TestJob.reset();
+ }
+
+ @Test
+ public void testScopeCreation() throws Exception {
+ vanilla.execute(new JobExecution(11L));
+ assertNotNull(TestJob.getContext());
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ @Test
+ public void testScopedProxy() throws Exception {
+ proxied.execute(new JobExecution(11L));
+ assertTrue(TestJob.getContext().attributeNames().length > 0);
+ String collaborator = (String) TestJob.getContext().getAttribute("collaborator");
+ assertNotNull(collaborator);
+ assertEquals("bar", collaborator);
+ assertTrue("Scoped proxy not created", ((String) TestJob.getContext().getAttribute("collaborator.class"))
+ .startsWith("class $Proxy"));
+ }
+
+ @Test
+ public void testNestedScopedProxy() throws Exception {
+ nested.execute(new JobExecution(11L));
+ assertTrue(TestJob.getContext().attributeNames().length > 0);
+ String collaborator = (String) TestJob.getContext().getAttribute("collaborator");
+ assertNotNull(collaborator);
+ assertEquals("foo", collaborator);
+ String parent = (String) TestJob.getContext().getAttribute("parent");
+ assertNotNull(parent);
+ assertEquals("bar", parent);
+ assertTrue("Scoped proxy not created", ((String) TestJob.getContext().getAttribute("parent.class"))
+ .startsWith("class $Proxy"));
+ }
+
+ @Test
+ public void testExecutionContext() throws Exception {
+ JobExecution stepExecution = new JobExecution(11L);
+ ExecutionContext executionContext = new ExecutionContext();
+ executionContext.put("name", "spam");
+ stepExecution.setExecutionContext(executionContext);
+ proxied.execute(stepExecution);
+ assertTrue(TestJob.getContext().attributeNames().length > 0);
+ String collaborator = (String) TestJob.getContext().getAttribute("collaborator");
+ assertNotNull(collaborator);
+ assertEquals("bar", collaborator);
+ }
+
+ @Test
+ public void testScopedProxyForReference() throws Exception {
+ enhanced.execute(new JobExecution(11L));
+ assertTrue(TestJob.getContext().attributeNames().length > 0);
+ String collaborator = (String) TestJob.getContext().getAttribute("collaborator");
+ assertNotNull(collaborator);
+ assertEquals("bar", collaborator);
+ }
+
+ @Test
+ public void testScopedProxyForSecondReference() throws Exception {
+ doubleEnhanced.execute(new JobExecution(11L));
+ assertTrue(TestJob.getContext().attributeNames().length > 0);
+ String collaborator = (String) TestJob.getContext().getAttribute("collaborator");
+ assertNotNull(collaborator);
+ assertEquals("bar", collaborator);
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java
new file mode 100644
index 000000000..1ca9c8465
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests.java
@@ -0,0 +1,33 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.Job;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeNestedIntegrationTests {
+
+ @Autowired
+ @Qualifier("proxied")
+ private Job proxied;
+
+ @Autowired
+ @Qualifier("parent")
+ private Collaborator parent;
+
+ @Test
+ public void testNestedScopedProxy() throws Exception {
+ assertNotNull(proxied);
+ assertEquals("foo", parent.getName());
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java
new file mode 100644
index 000000000..0df49e8c4
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests.java
@@ -0,0 +1,155 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopePlaceholderIntegrationTests implements BeanFactoryAware {
+
+ @Autowired
+ @Qualifier("simple")
+ private Collaborator simple;
+
+ @Autowired
+ @Qualifier("compound")
+ private Collaborator compound;
+
+ @Autowired
+ @Qualifier("value")
+ private Collaborator value;
+
+ @Autowired
+ @Qualifier("ref")
+ private Collaborator ref;
+
+ @Autowired
+ @Qualifier("scopedRef")
+ private Collaborator scopedRef;
+
+ @Autowired
+ @Qualifier("list")
+ private Collaborator list;
+
+ @Autowired
+ @Qualifier("bar")
+ private Collaborator bar;
+
+ @Autowired
+ @Qualifier("nested")
+ private Collaborator nested;
+
+ private JobExecution jobExecution;
+
+ private ListableBeanFactory beanFactory;
+
+ private int beanCount;
+
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = (ListableBeanFactory) beanFactory;
+ }
+
+ @Before
+ public void start() {
+ start("bar");
+ }
+
+ private void start(String foo) {
+
+ JobSynchronizationManager.close();
+ jobExecution = new JobExecution(123L);
+
+ ExecutionContext executionContext = new ExecutionContext();
+ executionContext.put("foo", foo);
+ executionContext.put("parent", bar);
+
+ jobExecution.setExecutionContext(executionContext);
+ JobSynchronizationManager.register(jobExecution);
+
+ beanCount = beanFactory.getBeanDefinitionCount();
+
+ }
+
+ @After
+ public void stop() {
+ JobSynchronizationManager.close();
+ // Check that all temporary bean definitions are cleaned up
+ assertEquals(beanCount, beanFactory.getBeanDefinitionCount());
+ }
+
+ @Test
+ public void testSimpleProperty() throws Exception {
+ assertEquals("bar", simple.getName());
+ // Once the job context is set up it should be baked into the proxies
+ // so changing it now should have no effect
+ jobExecution.getExecutionContext().put("foo", "wrong!");
+ assertEquals("bar", simple.getName());
+ }
+
+ @Test
+ public void testCompoundProperty() throws Exception {
+ assertEquals("bar-bar", compound.getName());
+ }
+
+ @Test
+ public void testCompoundPropertyTwice() throws Exception {
+
+ assertEquals("bar-bar", compound.getName());
+
+ JobSynchronizationManager.close();
+ jobExecution = new JobExecution(123L);
+
+ ExecutionContext executionContext = new ExecutionContext();
+ executionContext.put("foo", "spam");
+
+ jobExecution.setExecutionContext(executionContext);
+ JobSynchronizationManager.register(jobExecution);
+
+ assertEquals("spam-bar", compound.getName());
+
+ }
+
+ @Test
+ public void testParentByRef() throws Exception {
+ assertEquals("bar", ref.getParent().getName());
+ }
+
+ @Test
+ public void testParentByValue() throws Exception {
+ assertEquals("bar", value.getParent().getName());
+ }
+
+ @Test
+ public void testList() throws Exception {
+ assertEquals("[bar]", list.getList().toString());
+ }
+
+ @Test
+ public void testNested() throws Exception {
+ assertEquals("bar", nested.getParent().getName());
+ }
+
+ @Test
+ public void testScopedRef() throws Exception {
+ assertEquals("bar", scopedRef.getParent().getName());
+ stop();
+ start("spam");
+ assertEquals("spam", scopedRef.getParent().getName());
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java
new file mode 100644
index 000000000..2f23d70d8
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests.java
@@ -0,0 +1,71 @@
+package org.springframework.batch.core.scope;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeProxyTargetClassIntegrationTests implements BeanFactoryAware {
+
+ @Autowired
+ @Qualifier("simple")
+ private TestCollaborator simple;
+
+ private JobExecution jobExecution;
+
+ private ListableBeanFactory beanFactory;
+
+ private int beanCount;
+
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = (ListableBeanFactory) beanFactory;
+ }
+
+ @Before
+ public void start() {
+
+ JobSynchronizationManager.close();
+ jobExecution = new JobExecution(123L);
+
+ ExecutionContext executionContext = new ExecutionContext();
+ executionContext.put("foo", "bar");
+
+ jobExecution.setExecutionContext(executionContext);
+ JobSynchronizationManager.register(jobExecution);
+
+ beanCount = beanFactory.getBeanDefinitionCount();
+
+ }
+
+ @After
+ public void cleanUp() {
+ JobSynchronizationManager.close();
+ // Check that all temporary bean definitions are cleaned up
+ assertEquals(beanCount, beanFactory.getBeanDefinitionCount());
+ }
+
+ @Test
+ public void testSimpleProperty() throws Exception {
+ assertEquals("bar", simple.getName());
+ // Once the job context is set up it should be baked into the proxies
+ // so changing it now should have no effect
+ jobExecution.getExecutionContext().put("foo", "wrong!");
+ assertEquals("bar", simple.getName());
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java
new file mode 100644
index 000000000..e096de058
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests.java
@@ -0,0 +1,17 @@
+package org.springframework.batch.core.scope;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+
+@ContextConfiguration
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeStartupIntegrationTests {
+
+ @Test
+ public void testScopedProxyDuringStartup() throws Exception {
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java
new file mode 100644
index 000000000..f62598154
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobScopeTests.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2006-2007 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.scope;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ObjectFactory;
+import org.springframework.context.support.StaticApplicationContext;
+
+/**
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobScopeTests {
+
+ private JobScope scope = new JobScope();
+
+ private JobExecution jobExecution = new JobExecution(0L);
+
+ private JobContext context;
+
+ @Before
+ public void setUp() throws Exception {
+ context = JobSynchronizationManager.register(jobExecution);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ JobSynchronizationManager.release();
+ }
+
+ @Test
+ public void testGetWithNoContext() throws Exception {
+ final String foo = "bar";
+ JobSynchronizationManager.release();
+ try {
+ scope.get("foo", new ObjectFactory() {
+ public Object getObject() throws BeansException {
+ return foo;
+ }
+ });
+ fail("Expected IllegalStateException");
+ }
+ catch (IllegalStateException e) {
+ // expected
+ }
+
+ }
+
+ @Test
+ public void testGetWithNothingAlreadyThere() {
+ final String foo = "bar";
+ Object value = scope.get("foo", new ObjectFactory() {
+ public Object getObject() throws BeansException {
+ return foo;
+ }
+ });
+ assertEquals(foo, value);
+ assertTrue(context.hasAttribute("foo"));
+ }
+
+ @Test
+ public void testGetWithSomethingAlreadyThere() {
+ context.setAttribute("foo", "bar");
+ Object value = scope.get("foo", new ObjectFactory() {
+ public Object getObject() throws BeansException {
+ return null;
+ }
+ });
+ assertEquals("bar", value);
+ assertTrue(context.hasAttribute("foo"));
+ }
+
+ @Test
+ public void testGetConversationId() {
+ String id = scope.getConversationId();
+ assertNotNull(id);
+ }
+
+ @Test
+ public void testRegisterDestructionCallback() {
+ final List list = new ArrayList();
+ context.setAttribute("foo", "bar");
+ scope.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("foo");
+ }
+ });
+ assertEquals(0, list.size());
+ // When the context is closed, provided the attribute exists the
+ // callback is called...
+ context.close();
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ public void testRegisterAnotherDestructionCallback() {
+ final List list = new ArrayList();
+ context.setAttribute("foo", "bar");
+ scope.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("foo");
+ }
+ });
+ scope.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("bar");
+ }
+ });
+ assertEquals(0, list.size());
+ // When the context is closed, provided the attribute exists the
+ // callback is called...
+ context.close();
+ assertEquals(2, list.size());
+ }
+
+ @Test
+ public void testRemove() {
+ context.setAttribute("foo", "bar");
+ scope.remove("foo");
+ assertFalse(context.hasAttribute("foo"));
+ }
+
+ @Test
+ public void testOrder() throws Exception {
+ assertEquals(Integer.MAX_VALUE, scope.getOrder());
+ scope.setOrder(11);
+ assertEquals(11, scope.getOrder());
+ }
+
+ @Test
+ public void testName() throws Exception {
+ scope.setName("foo");
+ StaticApplicationContext beanFactory = new StaticApplicationContext();
+ scope.postProcessBeanFactory(beanFactory.getDefaultListableBeanFactory());
+ String[] scopes = beanFactory.getDefaultListableBeanFactory().getRegisteredScopeNames();
+ assertEquals(1, scopes.length);
+ assertEquals("foo", scopes[0]);
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java
index d67b72a15..30fb1c6fd 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/JobStartupRunner.java
@@ -1,22 +1,21 @@
package org.springframework.batch.core.scope;
+import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.Step;
-import org.springframework.batch.core.StepExecution;
import org.springframework.beans.factory.InitializingBean;
public class JobStartupRunner implements InitializingBean {
- private Step step;
+ private Job job;
- public void setStep(Step step) {
- this.step = step;
+ public void setJob(Job job) {
+ this.job = job;
}
@Override
public void afterPropertiesSet() throws Exception {
- StepExecution stepExecution = new StepExecution("step", new JobExecution(1L), 0L);
- step.execute(stepExecution);
+ JobExecution jobExecution = new JobExecution(11L);
+ job.execute(jobExecution);
// expect no errors
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java
new file mode 100644
index 000000000..87876c6c7
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepStartupRunner.java
@@ -0,0 +1,22 @@
+package org.springframework.batch.core.scope;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.Step;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.beans.factory.InitializingBean;
+
+public class StepStartupRunner implements InitializingBean {
+
+ private Step step;
+
+ public void setStep(Step step) {
+ this.step = step;
+ }
+
+ public void afterPropertiesSet() throws Exception {
+ StepExecution stepExecution = new StepExecution("step", new JobExecution(1L), 0L);
+ step.execute(stepExecution);
+ // expect no errors
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java
new file mode 100644
index 000000000..f28b374bd
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestJob.java
@@ -0,0 +1,61 @@
+package org.springframework.batch.core.scope;
+
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParametersIncrementer;
+import org.springframework.batch.core.JobParametersValidator;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+
+public class TestJob implements Job {
+
+ private static JobContext context;
+
+ private Collaborator collaborator;
+
+ public void setCollaborator(Collaborator collaborator) {
+ this.collaborator = collaborator;
+ }
+
+ public static JobContext getContext() {
+ return context;
+ }
+
+ public static void reset() {
+ context = null;
+ }
+
+ public void execute(JobExecution stepExecution) {
+ context = JobSynchronizationManager.getContext();
+ setContextFromCollaborator();
+ stepExecution.getExecutionContext().put("foo", "changed but it shouldn't affect the collaborator");
+ setContextFromCollaborator();
+ }
+
+ private void setContextFromCollaborator() {
+ if (context != null) {
+ context.setAttribute("collaborator", collaborator.getName());
+ context.setAttribute("collaborator.class", collaborator.getClass().toString());
+ if (collaborator.getParent()!=null) {
+ context.setAttribute("parent", collaborator.getParent().getName());
+ context.setAttribute("parent.class", collaborator.getParent().getClass().toString());
+ }
+ }
+ }
+
+ public String getName() {
+ return "foo";
+ }
+
+ public boolean isRestartable() {
+ return false;
+ }
+
+ public JobParametersIncrementer getJobParametersIncrementer() {
+ return null;
+ }
+
+ public JobParametersValidator getJobParametersValidator() {
+ return null;
+ }
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java
new file mode 100644
index 000000000..ed50c3e0b
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobContextTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2006-2007 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.scope.context;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.item.ExecutionContext;
+
+/**
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobContextTests {
+
+ private List list = new ArrayList();
+
+ private JobExecution jobExecution = new JobExecution(new JobInstance(2L, null, "job"), 1L);
+
+ private JobContext context = new JobContext(jobExecution);
+
+ @Test
+ public void testGetJobExecution() {
+ context = new JobContext(jobExecution);
+ assertNotNull(context.getJobExecution());
+ }
+
+ @Test
+ public void testNullJobExecution() {
+ try {
+ context = new JobContext(null);
+ fail("Expected IllegalArgumentException");
+ }
+ catch (IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testEqualsSelf() {
+ assertEquals(context, context);
+ }
+
+ @Test
+ public void testNotEqualsNull() {
+ assertFalse(context.equals(null));
+ }
+
+ @Test
+ public void testEqualsContextWithSameJobExecution() {
+ assertEquals(new JobContext(jobExecution), context);
+ }
+
+ @Test
+ public void testDestructionCallbackSunnyDay() throws Exception {
+ context.setAttribute("foo", "FOO");
+ context.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("bar");
+ }
+ });
+ context.close();
+ assertEquals(1, list.size());
+ assertEquals("bar", list.get(0));
+ }
+
+ @Test
+ public void testDestructionCallbackMissingAttribute() throws Exception {
+ context.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("bar");
+ }
+ });
+ context.close();
+ // Yes the callback should be called even if the attribute is missing -
+ // for inner beans
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ public void testDestructionCallbackWithException() throws Exception {
+ context.setAttribute("foo", "FOO");
+ context.setAttribute("bar", "BAR");
+ context.registerDestructionCallback("bar", new Runnable() {
+ public void run() {
+ list.add("spam");
+ throw new RuntimeException("fail!");
+ }
+ });
+ context.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("bar");
+ throw new RuntimeException("fail!");
+ }
+ });
+ try {
+ context.close();
+ fail("Expected RuntimeException");
+ }
+ catch (RuntimeException e) {
+ // We don't care which one was thrown...
+ assertEquals("fail!", e.getMessage());
+ }
+ // ...but we do care that both were executed:
+ assertEquals(2, list.size());
+ assertTrue(list.contains("bar"));
+ assertTrue(list.contains("spam"));
+ }
+
+ @Test
+ public void testJobName() throws Exception {
+ assertEquals("job", context.getJobName());
+ }
+
+ @Test
+ public void testJobExecutionContext() throws Exception {
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ executionContext.put("foo", "bar");
+ assertEquals("bar", context.getJobExecutionContext().get("foo"));
+ }
+
+ @Test
+ public void testSystemProperties() throws Exception {
+ System.setProperty("foo", "bar");
+ assertEquals("bar", context.getSystemProperties().getProperty("foo"));
+ }
+
+ @Test
+ public void testJobParameters() throws Exception {
+ JobParameters jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters();
+ JobInstance jobInstance = new JobInstance(0L, jobParameters, "foo");
+ jobExecution.setJobInstance(jobInstance);
+ assertEquals("bar", context.getJobParameters().get("foo"));
+ }
+
+ @Test
+ public void testContextId() throws Exception {
+ assertEquals("jobExecution#1", context.getId());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testIllegalContextId() throws Exception {
+ JobParameters jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters();
+ JobInstance jobInstance = new JobInstance(0L, jobParameters, "foo");
+ context = new JobContext(new JobExecution(jobInstance));
+ context.getId();
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java
new file mode 100644
index 000000000..7ab4d6008
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/JobSynchronizationManagerTests.java
@@ -0,0 +1,133 @@
+package org.springframework.batch.core.scope.context;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * JobSynchronizationManagerTests.
+ *
+ * @author Jimmy Praet
+ */
+public class JobSynchronizationManagerTests {
+
+ private JobExecution jobExecution = new JobExecution(0L);
+
+ @Before
+ @After
+ public void start() {
+ while (JobSynchronizationManager.getContext() != null) {
+ JobSynchronizationManager.close();
+ }
+ }
+
+ @Test
+ public void testGetContext() {
+ assertNull(JobSynchronizationManager.getContext());
+ JobSynchronizationManager.register(jobExecution);
+ assertNotNull(JobSynchronizationManager.getContext());
+ }
+
+ @Test
+ public void testClose() throws Exception {
+ final List list = new ArrayList();
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ context.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("foo");
+ }
+ });
+ JobSynchronizationManager.close();
+ assertNull(JobSynchronizationManager.getContext());
+ assertEquals(0, list.size());
+ // check for possible memory leak
+ assertEquals(0, extractStaticMap("counts").size());
+ assertEquals(0, extractStaticMap("contexts").size());
+ }
+
+ private Map, ?> extractStaticMap(String name) throws IllegalAccessException {
+ Field field = ReflectionUtils.findField(JobSynchronizationManager.class, "synchronizationManager");
+ ReflectionUtils.makeAccessible(field);
+ SynchronizationManagerSupport, ?> synchronizationManager =
+ (SynchronizationManagerSupport, ?>) field.get(JobSynchronizationManager.class);
+ field = ReflectionUtils.findField(SynchronizationManagerSupport.class, name);
+ ReflectionUtils.makeAccessible(field);
+ Map, ?> map = (Map, ?>) field.get(synchronizationManager);
+ return map;
+ }
+ @Test
+ public void testMultithreaded() throws Exception {
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ ExecutorService executorService = Executors.newFixedThreadPool(2);
+ FutureTask task = new FutureTask(new Callable() {
+ public JobContext call() throws Exception {
+ try {
+ JobSynchronizationManager.register(jobExecution);
+ JobContext context = JobSynchronizationManager.getContext();
+ context.setAttribute("foo", "bar");
+ return context;
+ }
+ finally {
+ JobSynchronizationManager.close();
+ }
+ }
+ });
+ executorService.execute(task);
+ executorService.awaitTermination(1, TimeUnit.SECONDS);
+ assertEquals(context.attributeNames().length, task.get().attributeNames().length);
+ JobSynchronizationManager.close();
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ @Test
+ public void testRelease() {
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ final List list = new ArrayList();
+ context.registerDestructionCallback("foo", new Runnable() {
+ public void run() {
+ list.add("foo");
+ }
+ });
+ // On release we expect the destruction callbacks to be called
+ JobSynchronizationManager.release();
+ assertNull(JobSynchronizationManager.getContext());
+ assertEquals(1, list.size());
+ }
+
+ @Test
+ public void testRegisterNull() {
+ assertNull(JobSynchronizationManager.getContext());
+ JobSynchronizationManager.register(null);
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ @Test
+ public void testRegisterTwice() {
+ JobSynchronizationManager.register(jobExecution);
+ JobSynchronizationManager.register(jobExecution);
+ JobSynchronizationManager.close();
+ // if someone registers you have to assume they are going to close, so
+ // the last thing you want is for the close to remove another context
+ // that someone else has registered
+ assertNotNull(JobSynchronizationManager.getContext());
+ JobSynchronizationManager.close();
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+}
\ No newline at end of file
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java
index 1a4836373..5c277bf5d 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/context/StepSynchronizationManagerTests.java
@@ -86,9 +86,13 @@ public class StepSynchronizationManagerTests {
}
private Map, ?> extractStaticMap(String name) throws IllegalAccessException {
- Field field = ReflectionUtils.findField(StepSynchronizationManager.class, name);
+ Field field = ReflectionUtils.findField(StepSynchronizationManager.class, "synchronizationManager");
ReflectionUtils.makeAccessible(field);
- Map, ?> map = (Map, ?>) field.get(StepSynchronizationManager.class);
+ SynchronizationManagerSupport, ?> synchronizationManager =
+ (SynchronizationManagerSupport, ?>) field.get(StepSynchronizationManager.class);
+ field = ReflectionUtils.findField(SynchronizationManagerSupport.class, name);
+ ReflectionUtils.makeAccessible(field);
+ Map, ?> map = (Map, ?>) field.get(synchronizationManager);
return map;
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java
new file mode 100644
index 000000000..97622691d
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/JobContextFactoryTests.java
@@ -0,0 +1,38 @@
+package org.springframework.batch.core.scope.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+
+public class JobContextFactoryTests {
+
+ private JobContextFactory factory = new JobContextFactory();
+
+ @After
+ public void cleanUp() {
+ JobSynchronizationManager.close();
+ JobSynchronizationManager.close();
+ }
+
+ @Test
+ public void testGetContext() {
+ JobExecution jobExecution = new JobExecution(11L);
+ JobContext context = JobSynchronizationManager.register(jobExecution);
+ assertEquals(context, factory.getContext());
+ }
+
+ @Test
+ public void testGetContextId() {
+ JobSynchronizationManager.register(new JobExecution(11L));
+ Object id1 = factory.getContextId();
+ JobSynchronizationManager.register(new JobExecution(12L));
+ Object id2 = factory.getContextId();
+ assertFalse(id2.equals(id1));
+ }
+
+}
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml
new file mode 100644
index 000000000..093b8dfd0
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForJobElementTests-context.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml
new file mode 100644
index 000000000..1d906944c
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/AutoRegisteringJobScopeForStepElementTests-context.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml
new file mode 100644
index 000000000..9b88a4e42
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncJobScopeIntegrationTests-context.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml
new file mode 100644
index 000000000..e839d3c77
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeDestructionCallbackIntegrationTests-context.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml
new file mode 100644
index 000000000..a6e50fb3b
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeIntegrationTests-context.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml
new file mode 100644
index 000000000..438be3d1f
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeNestedIntegrationTests-context.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml
new file mode 100644
index 000000000..526fa6c15
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopePlaceholderIntegrationTests-context.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ #{jobExecutionContext[foo]}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml
new file mode 100644
index 000000000..ac9c2d66a
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeProxyTargetClassIntegrationTests-context.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml
new file mode 100644
index 000000000..c5a32b6de
--- /dev/null
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/JobScopeStartupIntegrationTests-context.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml
index 3162bba5b..b2b1614a0 100644
--- a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml
+++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopeStartupIntegrationTests-context.xml
@@ -11,7 +11,7 @@
-
+
diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java
new file mode 100644
index 000000000..685ff91c6
--- /dev/null
+++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestExecutionListener.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2006-2010 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.test;
+
+import java.lang.reflect.Method;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.batch.item.adapter.HippyMethodInvoker;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestExecutionListener;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.ReflectionUtils.MethodCallback;
+
+/**
+ * A {@link TestExecutionListener} that sets up job-scope context for
+ * dependency injection into unit tests. A {@link JobContext} will be created
+ * for the duration of a test method and made available to any dependencies that
+ * are injected. The default behaviour is just to create a {@link JobExecution} with fixed properties. Alternatively it
+ * can be provided by the test case as a
+ * factory methods returning the correct type. Example:
+ *
+ *
+ * @ContextConfiguration
+ * @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, JobScopeTestExecutionListener.class })
+ * @RunWith(SpringJUnit4ClassRunner.class)
+ * public class JobScopeTestExecutionListenerIntegrationTests {
+ *
+ * // A job-scoped dependency configured in the ApplicationContext
+ * @Autowired
+ * private ItemReader<String> reader;
+ *
+ * public JobExecution getJobExecution() {
+ * JobExecution execution = MetaDataInstanceFactory.createJobExecution();
+ * execution.getExecutionContext().putString("foo", "bar");
+ * return execution;
+ * }
+ *
+ * @Test
+ * public void testJobScopedReader() {
+ * // Job context is active here so the reader can be used,
+ * // and the job execution context will contain foo=bar...
+ * assertNotNull(reader.read());
+ * }
+ *
+ * }
+ *
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobScopeTestExecutionListener implements TestExecutionListener {
+
+ private static final String JOB_EXECUTION = JobScopeTestExecutionListener.class.getName() + ".JOB_EXECUTION";
+
+ /**
+ * Set up a {@link JobExecution} as a test context attribute.
+ *
+ * @param testContext the current test context
+ * @throws Exception if there is a problem
+ * @see TestExecutionListener#prepareTestInstance(TestContext)
+ */
+ public void prepareTestInstance(TestContext testContext) throws Exception {
+ JobExecution jobExecution = getJobExecution(testContext);
+ if (jobExecution != null) {
+ testContext.setAttribute(JOB_EXECUTION, jobExecution);
+ }
+ }
+
+ /**
+ * @param testContext the current test context
+ * @throws Exception if there is a problem
+ * @see TestExecutionListener#beforeTestMethod(TestContext)
+ */
+ public void beforeTestMethod(org.springframework.test.context.TestContext testContext) throws Exception {
+ if (testContext.hasAttribute(JOB_EXECUTION)) {
+ JobExecution jobExecution = (JobExecution) testContext.getAttribute(JOB_EXECUTION);
+ JobSynchronizationManager.register(jobExecution);
+ }
+
+ }
+
+ /**
+ * @param testContext the current test context
+ * @throws Exception if there is a problem
+ * @see TestExecutionListener#afterTestMethod(TestContext)
+ */
+ public void afterTestMethod(TestContext testContext) throws Exception {
+ if (testContext.hasAttribute(JOB_EXECUTION)) {
+ JobSynchronizationManager.close();
+ }
+ }
+
+ /*
+ * Support for Spring 3.0 (empty).
+ */
+ public void afterTestClass(TestContext testContext) throws Exception {
+ }
+
+ /*
+ * Support for Spring 3.0 (empty).
+ */
+ public void beforeTestClass(TestContext testContext) throws Exception {
+ }
+
+ /**
+ * Discover a {@link JobExecution} as a field in the test case or create
+ * one if none is available.
+ *
+ * @param testContext the current test context
+ * @return a {@link JobExecution}
+ */
+ protected JobExecution getJobExecution(TestContext testContext) {
+
+ Object target = testContext.getTestInstance();
+
+ ExtractorMethodCallback method = new ExtractorMethodCallback(JobExecution.class, "getJobExecution");
+ ReflectionUtils.doWithMethods(target.getClass(), method);
+ if (method.getName() != null) {
+ HippyMethodInvoker invoker = new HippyMethodInvoker();
+ invoker.setTargetObject(target);
+ invoker.setTargetMethod(method.getName());
+ try {
+ invoker.prepare();
+ return (JobExecution) invoker.invoke();
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException("Could not create job execution from method: " + method.getName(),
+ e);
+ }
+ }
+
+ return MetaDataInstanceFactory.createJobExecution();
+ }
+
+ /**
+ * Look for a method returning the type provided, preferring one with the
+ * name provided.
+ */
+ private final class ExtractorMethodCallback implements MethodCallback {
+ private String preferredName;
+
+ private final Class> preferredType;
+
+ private Method result;
+
+ public ExtractorMethodCallback(Class> preferredType, String preferredName) {
+ super();
+ this.preferredType = preferredType;
+ this.preferredName = preferredName;
+ }
+
+ public String getName() {
+ return result == null ? null : result.getName();
+ }
+
+ public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
+ Class> type = method.getReturnType();
+ if (preferredType.isAssignableFrom(type)) {
+ if (result == null || method.getName().equals(preferredName)) {
+ result = method;
+ }
+ }
+ }
+ }
+
+}
diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java
new file mode 100644
index 000000000..baa63f0fb
--- /dev/null
+++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2006-2010 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.test;
+
+import java.util.concurrent.Callable;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.scope.JobScope;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+
+/**
+ * Utility class for creating and manipulating {@link JobScope} in unit tests.
+ * This is useful when you want to use the Spring test support and inject
+ * dependencies into your test case that happen to be job scoped in the
+ * application context.
+ *
+ * @author Dave Syer
+ * @author Jimmy Praet
+ */
+public class JobScopeTestUtils {
+
+ public static T doInJobScope(JobExecution jobExecution, Callable callable) throws Exception {
+ try {
+ JobSynchronizationManager.register(jobExecution);
+ return callable.call();
+ }
+ finally {
+ JobSynchronizationManager.close();
+ }
+ }
+
+}
diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java
new file mode 100644
index 000000000..5678a82b9
--- /dev/null
+++ b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests.java
@@ -0,0 +1,48 @@
+package org.springframework.batch.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.ItemStream;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+
+/**
+ * @author Dave Syer
+ * @since 2.1
+ */
+@ContextConfiguration
+@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, JobScopeTestExecutionListener.class })
+@RunWith(SpringJUnit4ClassRunner.class)
+public class JobScopeTestExecutionListenerIntegrationTests {
+
+ @Autowired
+ private ItemReader reader;
+
+ @Autowired
+ private ItemStream stream;
+
+ public JobExecution getJobExection() {
+ // Assert that dependencies are already injected...
+ assertNotNull(reader);
+ // Then create the execution for the job scope...
+ JobExecution execution = MetaDataInstanceFactory.createJobExecution();
+ execution.getExecutionContext().putString("input.file", "classpath:/org/springframework/batch/test/simple.txt");
+ return execution;
+ }
+
+ @Test
+ public void testJob() throws Exception {
+ stream.open(new ExecutionContext());
+ assertEquals("foo", reader.read());
+ }
+
+}
diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java
new file mode 100644
index 000000000..3b38a5262
--- /dev/null
+++ b/spring-batch-test/src/test/java/org/springframework/batch/test/JobScopeTestExecutionListenerTests.java
@@ -0,0 +1,103 @@
+package org.springframework.batch.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.scope.context.JobContext;
+import org.springframework.batch.core.scope.context.JobSynchronizationManager;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestContext;
+import org.springframework.test.context.TestContextManager;
+
+/**
+ * @author Dave Syer
+ * @since 2.1
+ */
+@ContextConfiguration
+public class JobScopeTestExecutionListenerTests {
+
+ private JobScopeTestExecutionListener listener = new JobScopeTestExecutionListener();
+
+ @Test
+ public void testDefaultJobContext() throws Exception {
+ TestContext testContext = getTestContext(new Object());
+ listener.prepareTestInstance(testContext);
+ listener.beforeTestMethod(testContext);
+ JobContext context = JobSynchronizationManager.getContext();
+ assertNotNull(context);
+ listener.afterTestMethod(testContext);
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ @Test
+ public void testWithJobExecutionFactory() throws Exception {
+ testExecutionContext(new WithJobExecutionFactory());
+ }
+
+ @Test
+ public void testWithParameters() throws Exception {
+ testJobParameters(new WithJobExecutionFactory());
+ }
+
+ private void testExecutionContext(Object target) throws Exception {
+ TestContext testContext = getTestContext(target);
+ listener.prepareTestInstance(testContext);
+ try {
+ listener.beforeTestMethod(testContext);
+ JobContext context = JobSynchronizationManager.getContext();
+ assertNotNull(context);
+ assertEquals("bar", context.getJobExecutionContext().get("foo"));
+ }
+ finally {
+ listener.afterTestMethod(testContext);
+ }
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ private void testJobParameters(Object target) throws Exception {
+ TestContext testContext = getTestContext(target);
+ listener.prepareTestInstance(testContext);
+ try {
+ listener.beforeTestMethod(testContext);
+ JobContext context = JobSynchronizationManager.getContext();
+ assertNotNull(context);
+ assertEquals("spam", context.getJobParameters().get("foo"));
+ }
+ finally {
+ listener.afterTestMethod(testContext);
+ }
+ assertNull(JobSynchronizationManager.getContext());
+ }
+
+ @SuppressWarnings("unused")
+ private static class WithJobExecutionFactory {
+ public JobExecution getJobExecution() {
+ JobExecution jobExecution = MetaDataInstanceFactory.createJobExecution("job", 11L, 123L,
+ new JobParametersBuilder().addString("foo", "spam").toJobParameters());
+ jobExecution.getExecutionContext().putString("foo", "bar");
+ return jobExecution;
+ }
+ }
+
+ private TestContext getTestContext(Object target) throws Exception {
+ return new MockTestContextManager(target, getClass()).getContext();
+ }
+
+ private final class MockTestContextManager extends TestContextManager {
+
+ private MockTestContextManager(Object target, Class> testClass) throws Exception {
+ super(testClass);
+ prepareTestInstance(target);
+ }
+
+ public TestContext getContext() {
+ return getTestContext();
+ }
+
+ }
+
+}
diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml
new file mode 100644
index 000000000..3323c84a5
--- /dev/null
+++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerIntegrationTests-context.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml
new file mode 100644
index 000000000..cacc056e3
--- /dev/null
+++ b/spring-batch-test/src/test/resources/org/springframework/batch/test/JobScopeTestExecutionListenerTests-context.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file