diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepContext.java index c5cbd2111..9209627a3 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepContext.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/StepContext.java @@ -22,9 +22,13 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Map.Entry; +import org.springframework.batch.core.JobParameter; +import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.UnexpectedJobExecutionException; +import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; import org.springframework.util.Assert; @@ -52,6 +56,39 @@ public class StepContext extends SynchronizedAttributeAccessor { this.stepExecution = stepExecution; } + /** + * @return a map containing the items from the step {@link ExecutionContext} + */ + public Map getStepExecutionContext() { + Map result = new HashMap(); + for (Entry entry : stepExecution.getExecutionContext().entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } + return Collections.unmodifiableMap(result); + } + + /** + * @return a map containing the items from the job {@link ExecutionContext} + */ + public Map getJobExecutionContext() { + Map result = new HashMap(); + for (Entry entry : stepExecution.getJobExecution().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 : stepExecution.getJobParameters().getParameters().entrySet()) { + result.put(entry.getKey(), entry.getValue().getValue()); + } + return Collections.unmodifiableMap(result); + } + /** * Allow clients to register callbacks for clean up on close. * @@ -135,6 +172,15 @@ public class StepContext extends SynchronizedAttributeAccessor { return stepExecution; } + /** + * @return unique identifier for this context based on the step execution + */ + public String getId() { + Assert.state(stepExecution.getId() != null, "StepExecution has no id. " + + "It must be saved before it can be used in step scope."); + return "execution#" + stepExecution.getId(); + } + /** * Extend the base class method to include the step execution itself as a * key (i.e. two contexts are only equal if their step executions are the @@ -166,4 +212,10 @@ public class StepContext extends SynchronizedAttributeAccessor { return stepExecution.hashCode(); } + @Override + public String toString() { + return super.toString() + ", stepExecutionContext=" + getStepExecutionContext() + ", jobExecutionContext=" + + getJobExecutionContext() + ", jobParameters=" + getJobParameters(); + } + } 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 2ed217130..40d1868ee 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 @@ -17,7 +17,10 @@ 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.aop.framework.autoproxy.AutoProxyUtils; +import org.springframework.batch.core.scope.util.PlaceholderProxyFactoryBean; +import org.springframework.batch.core.scope.util.StepContextFactory; +import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.BeanDefinition; @@ -28,14 +31,44 @@ 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.beans.factory.support.RootBeanDefinition; import org.springframework.core.Ordered; import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; /** - * Scope for step context. Objects in this scope with <aop:scoped-proxy/> - * use the Spring container as an object factory, so there is only one instance - * of such a bean per executing step. + * Scope for step 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 + * 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 * @@ -106,8 +139,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { */ public String getConversationId() { StepContext context = getContext(); - Object id = context.getAttribute(ID_KEY); - return "" + id; + return context.getId(); } /** @@ -167,9 +199,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { // has this scope scopifier.visitBeanDefinition(definition); if (name.equals(definition.getScope())) { - BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder( - definition, beanName), registry, proxyTargetClass); - registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition()); + createScopedProxy(beanName, definition, registry, proxyTargetClass); } } @@ -186,9 +216,77 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { } /** - * Helper class to scan a bean definition hierarchy looking for scoped - * objects and modifying their properties. In particular it forces the use - * of auto-proxy for step scoped beans. + * 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. 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 + * @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) { + + // TODO: detect presence of Spring 3.0 and use ScopedPoxyUtils instead + + // Create the scoped proxy... + BeanDefinitionHolder proxyHolder = createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry, + proxyTargetClass); + // ...and register it under the original target name + registry.registerBeanDefinition(beanName, proxyHolder.getBeanDefinition()); + + return proxyHolder; + + } + + private static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, + BeanDefinitionRegistry registry, boolean proxyTargetClass) { + + String originalBeanName = definition.getBeanName(); + BeanDefinition targetDefinition = definition.getBeanDefinition(); + + // Create a proxy definition for the original bean name, + // "hiding" the target bean in an internal target definition. + RootBeanDefinition proxyDefinition = new RootBeanDefinition(PlaceholderProxyFactoryBean.class); + proxyDefinition.getConstructorArgumentValues().addGenericArgumentValue(new StepContextFactory()); + proxyDefinition.setOriginatingBeanDefinition(definition.getBeanDefinition()); + proxyDefinition.setSource(definition.getSource()); + proxyDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + String targetBeanName = "lazyBindingProxy." + originalBeanName; + proxyDefinition.getPropertyValues().addPropertyValue("targetBeanName", targetBeanName); + + if (proxyTargetClass) { + targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); + // ProxyFactoryBean's "proxyTargetClass" default is TRUE, so we + // don't need to set it explicitly here. + } + else { + proxyDefinition.getPropertyValues().addPropertyValue("proxyTargetClass", Boolean.FALSE); + } + + proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); + // The target bean should be ignored in favor of the proxy. + targetDefinition.setAutowireCandidate(false); + + // Register the target bean as separate bean in the factory. + registry.registerBeanDefinition(targetBeanName, targetDefinition); + + // Return the scoped proxy definition as primary bean definition + // (potentially an inner bean). + return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases()); + } + + /** + * Helper class to scan a bean definition hierarchy and force the use of + * auto-proxy for step scoped beans. * * @author Dave Syer * @@ -218,14 +316,14 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { BeanDefinition definition = (BeanDefinition) value; if (scope.equals(definition.getScope())) { String beanName = BeanDefinitionReaderUtils.generateBeanName(definition, registry); - return ScopedProxyUtils.createScopedProxy(new BeanDefinitionHolder(definition, beanName), registry, - proxyTargetClass); + return createScopedProxy(beanName, definition, registry, proxyTargetClass); } } else if (value instanceof BeanDefinitionHolder) { - BeanDefinitionHolder definition = (BeanDefinitionHolder) value; - if (scope.equals(definition.getBeanDefinition().getScope())) { - return ScopedProxyUtils.createScopedProxy(definition, registry, proxyTargetClass); + BeanDefinitionHolder holder = (BeanDefinitionHolder) value; + BeanDefinition definition = holder.getBeanDefinition(); + if (scope.equals(definition.getScope())) { + return createScopedProxy(holder.getBeanName(), definition, registry, proxyTargetClass); } } return value; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/ContextFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/ContextFactory.java new file mode 100644 index 000000000..d7ec94cdb --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/ContextFactory.java @@ -0,0 +1,22 @@ +package org.springframework.batch.core.scope.util; + +/** + * Interface to allow the context root for placeholder resolution to be switched + * at runtime. Useful for testing. + * + * @author Dave Syer + * + */ +public interface ContextFactory { + + /** + * @return a root object to which placeholders resolve relatively + */ + Object getContext(); + + /** + * @return a unique identifier for this context + */ + String getContextId(); + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderProxyFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderProxyFactoryBean.java new file mode 100644 index 000000000..e5f3693d0 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderProxyFactoryBean.java @@ -0,0 +1,110 @@ +package org.springframework.batch.core.scope.util; + +import java.lang.reflect.Modifier; + +import org.springframework.aop.framework.AopInfrastructureBean; +import org.springframework.aop.framework.ProxyConfig; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.scope.DefaultScopedObject; +import org.springframework.aop.scope.ScopedObject; +import org.springframework.aop.scope.ScopedProxyFactoryBean; +import org.springframework.aop.support.DelegatingIntroductionInterceptor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.FactoryBeanNotInitializedException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.util.ClassUtils; + +/** + * Factory bean for proxies that can replace placeholders in their target. Just + * a specialisation of {@link ScopedProxyFactoryBean}, with a different target + * source type. + * + * @author Dave Syer + * + */ +public class PlaceholderProxyFactoryBean extends ProxyConfig implements FactoryBean, BeanFactoryAware { + + /** The TargetSource that manages scoping */ + private final PlaceholderTargetSource scopedTargetSource = new PlaceholderTargetSource(); + + /** The name of the target bean */ + private String targetBeanName; + + /** The cached singleton proxy */ + private Object proxy; + + private final ContextFactory contextFactory; + + /** + * Create a new FactoryBean instance. + */ + public PlaceholderProxyFactoryBean(ContextFactory contextFactory) { + this.contextFactory = contextFactory; + setProxyTargetClass(true); + } + + /** + * Set the name of the bean that is to be scoped. + */ + public void setTargetBeanName(String targetBeanName) { + this.targetBeanName = targetBeanName; + this.scopedTargetSource.setTargetBeanName(targetBeanName); + } + + public void setBeanFactory(BeanFactory beanFactory) { + if (!(beanFactory instanceof ConfigurableBeanFactory)) { + throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory); + } + ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory; + + this.scopedTargetSource.setBeanFactory(beanFactory); + this.scopedTargetSource.setContextFactory(contextFactory); + + ProxyFactory pf = new ProxyFactory(); + pf.copyFrom(this); + pf.setTargetSource(this.scopedTargetSource); + + Class beanType = beanFactory.getType(this.targetBeanName); + if (beanType == null) { + throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName + + "': Target type could not be determined at the time of proxy creation."); + } + if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) { + pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader())); + } + + // Add an introduction that implements only the methods on ScopedObject. + ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName()); + pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject)); + + // Add the AopInfrastructureBean marker to indicate that the scoped + // proxy + // itself is not subject to auto-proxying! Only its target bean is. + pf.addInterface(AopInfrastructureBean.class); + + this.proxy = pf.getProxy(cbf.getBeanClassLoader()); + } + + public Object getObject() { + if (this.proxy == null) { + throw new FactoryBeanNotInitializedException(); + } + return this.proxy; + } + + public Class getObjectType() { + if (this.proxy != null) { + return this.proxy.getClass(); + } + if (this.scopedTargetSource != null) { + return this.scopedTargetSource.getTargetClass(); + } + return null; + } + + public boolean isSingleton() { + return true; + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderTargetSource.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderTargetSource.java new file mode 100644 index 000000000..faf968af3 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/PlaceholderTargetSource.java @@ -0,0 +1,247 @@ +/* + * 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.aop.TargetSource; +import org.springframework.aop.target.SimpleBeanTargetSource; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.TypeMismatchException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionVisitor; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.core.MethodParameter; +import org.springframework.util.Assert; +import org.springframework.util.StringValueResolver; + +/** + * A {@link TargetSource} that lazily initializes its target, replacing bean + * definition properties dynamically if they are marked as placeholders. String + * values with embedded #{key} patterns will be replaced with the + * corresponding value from the injected context (which must also be a String). + * This includes dynamically locating a bean reference (e.g. + * ref="#{foo}"), and partial replacement of patterns (e.g. + * value="#{foo}-bar-#{spam}"). These replacements work for context + * values that are primitive (String, Long, Integer). You can also replace + * non-primitive values directly by making the whole bean property value into a + * placeholder (e.g. value="#{foo}" where foo is a + * property in the context). + * + * @author Dave Syer + * + */ +public class PlaceholderTargetSource extends SimpleBeanTargetSource implements InitializingBean { + + /** + * Key for placeholders to be replaced from the properties provided. + */ + private static final String PLACEHOLDER_PREFIX = "#{"; + + private static final String PLACEHOLDER_SUFFIX = "}"; + + private volatile boolean active = false; + + private ContextFactory contextFactory; + + /** + * Public setter for the context factory. Used to construct the context root + * whenever placeholders are replaced in a bean definition. + * + * @param contextFactory the {@link ContextFactory} + */ + public void setContextFactory(ContextFactory contextFactory) { + this.contextFactory = contextFactory; + } + + /* + * (non-Javadoc) + * + * @see + * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(contextFactory, "The ContextFactory must be set."); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.aop.target.LazyInitTargetSource#getTarget() + */ + @Override + public synchronized Object getTarget() throws BeansException { + + DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) getBeanFactory(); + + final TypeConverter typeConverter = listableBeanFactory.getTypeConverter(); + + // Try to prevent other threads from using this TypeConverter + active = true; + + listableBeanFactory.setTypeConverter(new TypeConverter() { + @SuppressWarnings("unchecked") + public Object convertIfNecessary(Object value, Class requiredType, MethodParameter methodParam) + throws TypeMismatchException { + Object result = null; + if (value instanceof String) { + String key = (String) value; + if (key.startsWith(PLACEHOLDER_PREFIX) && key.endsWith(PLACEHOLDER_SUFFIX)) { + key = extractKey(key); + result = convertFromContext(key, requiredType); + } + } + return result != null ? result : typeConverter.convertIfNecessary(value, requiredType, methodParam); + } + + @SuppressWarnings("unchecked") + public Object convertIfNecessary(Object value, Class requiredType) throws TypeMismatchException { + return convertIfNecessary(value, requiredType, null); + } + }); + + String beanName = getTargetBeanName() + "#" + contextFactory.getContextId(); + + try { + + /* + * Need to use the merged bean definition here, otherwise it gets + * cached and "frozen" in and the "regular" bean definition does not + * come back when getBean() is called later on + */ + String targetBeanName = getTargetBeanName(); + BeanDefinition originalDefinition = listableBeanFactory.getMergedBeanDefinition(getTargetBeanName()); + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(originalDefinition); + logger.debug("Rehydrating scoped target: [" + targetBeanName + "]"); + + BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(new StringValueResolver() { + public String resolveStringValue(String strVal) { + if (!strVal.contains(PLACEHOLDER_PREFIX)) { + return strVal; + } + return replacePlaceholders(strVal, typeConverter); + } + }) { + protected Object resolveValue(Object value) { + if (value instanceof TypedStringValue) { + TypedStringValue typedStringValue = (TypedStringValue) value; + String stringValue = typedStringValue.getValue(); + if (stringValue != null) { + String visitedString = resolveStringValue(stringValue); + value = new TypedStringValue(visitedString); + } + } + else { + value = super.resolveValue(value); + } + return value; + } + + }; + + listableBeanFactory.registerBeanDefinition(beanName, beanDefinition); + // Make the replacements before the target is hydrated + visitor.visitBeanDefinition(beanDefinition); + return listableBeanFactory.getBean(beanName); + + } + finally { + listableBeanFactory.removeBeanDefinition(beanName); + // Replace the original type converter. TODO: does this cause + // problems if in fact it was null to start with? + listableBeanFactory.setTypeConverter(typeConverter); + active = false; + } + + } + + /** + * @param value + * @param requiredType + * @return + */ + private Object convertFromContext(String key, Class requiredType) { + Object result = null; + if (active) { + BeanWrapper wrapper = new BeanWrapperImpl(contextFactory.getContext()); + if (wrapper.isReadableProperty(key)) { + Object property = wrapper.getPropertyValue(key); + if (requiredType.isAssignableFrom(property.getClass())) { + result = property; + } + } + } + return result; + } + + private String extractKey(String value) { + if (value.startsWith(PLACEHOLDER_PREFIX)) { + value = value.substring(PLACEHOLDER_PREFIX.length()); + value = value.substring(0, value.length() - PLACEHOLDER_SUFFIX.length()); + } + return value; + } + + /** + * @param typeConverter + * @param strVal + * @return + */ + private String replacePlaceholders(String value, TypeConverter typeConverter) { + + StringBuilder result = new StringBuilder(value); + + int first = result.indexOf(PLACEHOLDER_PREFIX); + int next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1); + + while (first >= 0) { + + Assert.state(next > 0, String.format("Placeholder key incorrectly specified: use %skey%s (in %s)", + PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX, value)); + + String key = result.substring(first + PLACEHOLDER_PREFIX.length(), next); + + replaceIfTypeMatches(result, first, next, key, String.class, typeConverter); + replaceIfTypeMatches(result, first, next, key, Long.class, typeConverter); + replaceIfTypeMatches(result, first, next, key, Integer.class, typeConverter); + // Spring cannot convert from String to Date, so there is an error + // here. + // replaceIfTypeMatches(result, first, next, key, Date.class, + // typeConverter); + + first = result.indexOf(PLACEHOLDER_PREFIX, next + 1); + next = result.indexOf(PLACEHOLDER_SUFFIX, first + 1); + + } + + logger.debug(String.format("Replaced [%s] with [%s]", value, result)); + return result.toString(); + + } + + private void replaceIfTypeMatches(StringBuilder result, int first, int next, String key, Class requiredType, + TypeConverter typeConverter) { + Object property = convertFromContext(key, requiredType); + if (property != null) { + result.replace(first, next + 1, (String) typeConverter.convertIfNecessary(property, String.class)); + } + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/StepContextFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/StepContextFactory.java new file mode 100644 index 000000000..f3cf838d1 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/util/StepContextFactory.java @@ -0,0 +1,23 @@ +package org.springframework.batch.core.scope.util; + +import org.springframework.batch.core.scope.StepContext; +import org.springframework.batch.core.scope.StepSynchronizationManager; + +/** + * Implementation of {@link ContextFactory} that provides the current + * {@link StepContext} as a contxt object. + * + * @author Dave Syer + * + */ +public class StepContextFactory implements ContextFactory { + + public Object getContext() { + return StepSynchronizationManager.getContext(); + } + + public String getContextId() { + return (String) StepSynchronizationManager.getContext().getId(); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests.java new file mode 100644 index 000000000..1a0f77b61 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests.java @@ -0,0 +1,106 @@ +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.StepExecution; +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 AsyncStepScopeIntegrationTests 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() { + beanCount = beanFactory.getBeanDefinitionCount(); + } + + @After + public void cleanUp() { + StepSynchronizationManager.close(); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Test + public void testSimpleProperty() throws Exception { + StepExecution stepExecution = new StepExecution("step", new JobExecution(0L), 123L); + ExecutionContext executionContext = stepExecution.getExecutionContext(); + executionContext.put("foo", "bar"); + StepSynchronizationManager.register(stepExecution); + assertEquals("bar", simple.getName()); + } + + @Test + public void testGetMultiple() 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 { + StepExecution stepExecution = new StepExecution(value, new JobExecution(0L), id); + ExecutionContext executionContext = stepExecution.getExecutionContext(); + executionContext.put("foo", value); + StepContext context = StepSynchronizationManager.register(stepExecution); + logger.debug("Registered: "+context.getStepExecutionContext()); + try { + return simple.getName(); + } + finally { + StepSynchronizationManager.close(); + } + } + }); + tasks.add(task); + taskExecutor.execute(task); + } + + int i = 0; + for (FutureTask task : tasks) { + assertEquals("foo" + i, task.get()); + i++; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/Collaborator.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/Collaborator.java index 80ae0f0a9..2bfc6759c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/Collaborator.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/Collaborator.java @@ -2,6 +2,8 @@ package org.springframework.batch.core.scope; public interface Collaborator { - public abstract String getName(); + String getName(); + Collaborator getParent(); + } \ No newline at end of file diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepContextTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepContextTests.java index 3977e844a..7e0f70466 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepContextTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepContextTests.java @@ -26,7 +26,11 @@ 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.core.StepExecution; +import org.springframework.batch.item.ExecutionContext; /** * @author Dave Syer @@ -36,7 +40,7 @@ public class StepContextTests { private List list = new ArrayList(); - private StepExecution stepExecution = new StepExecution("step", new JobExecution(0L)); + private StepExecution stepExecution = new StepExecution("step", new JobExecution(0L), 1L); private StepContext context = new StepContext(stepExecution); @@ -128,4 +132,37 @@ public class StepContextTests { assertTrue(list.contains("spam")); } + @Test + public void testStepExecutionContext() throws Exception { + ExecutionContext executionContext = stepExecution.getExecutionContext(); + executionContext.put("foo", "bar"); + assertEquals("bar", context.getStepExecutionContext().get("foo")); + } + + @Test + public void testJobExecutionContext() throws Exception { + ExecutionContext executionContext = stepExecution.getJobExecution().getExecutionContext(); + executionContext.put("foo", "bar"); + assertEquals("bar", context.getJobExecutionContext().get("foo")); + } + + @Test + public void testJobParameters() throws Exception { + JobParameters jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters(); + JobInstance jobInstance = new JobInstance(0L, jobParameters, "foo"); + stepExecution.getJobExecution().setJobInstance(jobInstance); + assertEquals("bar", context.getJobParameters().get("foo")); + } + + @Test + public void testContextId() throws Exception { + assertEquals("execution#1", context.getId()); + } + + @Test(expected = IllegalStateException.class) + public void testIllegalContextId() throws Exception { + context = new StepContext(new StepExecution("foo", new JobExecution(0L))); + context.getId(); + } + } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeIntegrationTests.java index d4f302d0d..353c40bcc 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeIntegrationTests.java @@ -48,14 +48,14 @@ public class StepScopeIntegrationTests { @Test public void testScopeCreation() throws Exception { - vanilla.execute(new StepExecution("foo",new JobExecution(11L))); + vanilla.execute(new StepExecution("foo",new JobExecution(11L),12L)); assertNotNull(TestStep.getContext()); assertNull(StepSynchronizationManager.getContext()); } @Test public void testScopedProxy() throws Exception { - proxied.execute(new StepExecution("foo",new JobExecution(11L))); + proxied.execute(new StepExecution("foo",new JobExecution(11L),31L)); assertTrue(TestStep.getContext().attributeNames().length>0); String collaborator = (String) TestStep.getContext().getAttribute("collaborator"); assertNotNull(collaborator); @@ -64,7 +64,7 @@ public class StepScopeIntegrationTests { @Test public void testExecutionContext() throws Exception { - StepExecution stepExecution = new StepExecution("foo",new JobExecution(11L)); + StepExecution stepExecution = new StepExecution("foo",new JobExecution(11L), 1L); ExecutionContext executionContext = new ExecutionContext(); executionContext.put("name", "spam"); stepExecution.setExecutionContext(executionContext); @@ -77,7 +77,7 @@ public class StepScopeIntegrationTests { @Test public void testScopedProxyForReference() throws Exception { - enhanced.execute(new StepExecution("foo",new JobExecution(11L))); + enhanced.execute(new StepExecution("foo",new JobExecution(11L),123L)); assertTrue(TestStep.getContext().attributeNames().length>0); String collaborator = (String) TestStep.getContext().getAttribute("collaborator"); assertNotNull(collaborator); @@ -86,7 +86,7 @@ public class StepScopeIntegrationTests { @Test public void testScopedProxyForSecondReference() throws Exception { - doubleEnhanced.execute(new StepExecution("foo",new JobExecution(11L))); + doubleEnhanced.execute(new StepExecution("foo",new JobExecution(11L),321L)); assertTrue(TestStep.getContext().attributeNames().length>0); String collaborator = (String) TestStep.getContext().getAttribute("collaborator"); assertNotNull(collaborator); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopePlaceholderIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopePlaceholderIntegrationTests.java new file mode 100644 index 000000000..ee6ec6580 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopePlaceholderIntegrationTests.java @@ -0,0 +1,99 @@ +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.StepExecution; +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 StepScopePlaceholderIntegrationTests 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("bar") + private Collaborator bar; + + private StepExecution stepExecution; + + private ListableBeanFactory beanFactory; + + private int beanCount; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + @Before + public void start() { + + StepSynchronizationManager.close(); + stepExecution = new StepExecution("foo", new JobExecution(11L), 123L); + + ExecutionContext executionContext = new ExecutionContext(); + executionContext.put("foo", "bar"); + executionContext.put("parent", bar); + + stepExecution.setExecutionContext(executionContext); + StepSynchronizationManager.register(stepExecution); + + beanCount = beanFactory.getBeanDefinitionCount(); + + } + + @After + public void cleanUp() { + StepSynchronizationManager.close(); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Test + public void testSimpleProperty() throws Exception { + assertEquals("bar", simple.getName()); + } + + @Test + public void testCompoundProperty() throws Exception { + assertEquals("bar-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()); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeTests.java index 21478b1c8..13fc6288a 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/StepScopeTests.java @@ -42,7 +42,7 @@ public class StepScopeTests { private StepScope scope = new StepScope(); - private StepExecution stepExecution = new StepExecution("foo", new JobExecution(0L)); + private StepExecution stepExecution = new StepExecution("foo", new JobExecution(0L), 123L); private StepContext context; @@ -126,13 +126,6 @@ public class StepScopeTests { assertNotNull(id); } - @Test - public void testGetConversationIdFromAttribute() { - context.setAttribute(StepScope.ID_KEY, "foo"); - String id = scope.getConversationId(); - assertEquals("foo", id); - } - @Test public void testRegisterDestructionCallback() { final List list = new ArrayList(); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestCollaborator.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestCollaborator.java index fa4e97b37..74c177017 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestCollaborator.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/TestCollaborator.java @@ -1,9 +1,21 @@ package org.springframework.batch.core.scope; +import java.io.Serializable; -public class TestCollaborator implements Collaborator { + +public class TestCollaborator implements Collaborator, Serializable { private String name; + + private Collaborator parent; + + public Collaborator getParent() { + return parent; + } + + public void setParent(Collaborator parent) { + this.parent = parent; + } /* (non-Javadoc) * @see org.springframework.batch.core.scope.Collaborator#getName() diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests.java new file mode 100644 index 000000000..3d21c1a2d --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests.java @@ -0,0 +1,143 @@ +package org.springframework.batch.core.scope.util; + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +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.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.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 AsyncPlaceholderTargetSourceTests implements BeanFactoryAware { + + private ThreadLocal> attributes = new ThreadLocal>(); + + public Map getAttributes() { + return attributes.get(); + } + + @Autowired + private Node simple; + + @Autowired + private SimpleContextFactory contextFactory; + + private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + + private ListableBeanFactory beanFactory; + + private int beanCount; + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + @After + public void removeContext() { + contextFactory.clearContext(); + attributes.set(null); + // Check that all temporary bean definitions are cleaned up + assertEquals(beanCount, beanFactory.getBeanDefinitionCount()); + } + + @Before + public void setUpContext() { + contextFactory.setContext(this); + beanCount = beanFactory.getBeanDefinitionCount(); + } + + @Test + public void testGetSimple() { + attributes.set(Collections.singletonMap("foo", "bar")); + assertEquals("bar", simple.getName()); + } + + @Test + public void testGetMultiple() throws Exception { + + List> tasks = new ArrayList>(); + + for (int i = 0; i < 12; i++) { + final String value = "foo" + i; + FutureTask task = new FutureTask(new Callable() { + public String call() throws Exception { + attributes.set(Collections.singletonMap("foo", value)); + try { + return simple.getName(); + } + finally { + attributes.set(null); + } + } + }); + tasks.add(task); + taskExecutor.execute(task); + } + + int i = 0; + for (FutureTask task : tasks) { + assertEquals("foo" + i, task.get()); + i++; + } + + } + + public static class SimpleContextFactory extends ContextFactorySupport { + + private Object root; + + public Object getContext() { + return root; + } + + public void setContext(Object root) { + this.root = root; + } + + public void clearContext() { + root = null; + } + + } + + public static interface Node { + String getName(); + } + + public static class Foo implements Node { + + private String name; + + private Log logger = LogFactory.getLog(getClass()); + + public String getName() { + return name; + } + + public void setName(String name) { + logger.debug("Setting name: " + name); + this.name = name; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/ContextFactorySupport.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/ContextFactorySupport.java new file mode 100644 index 000000000..24c428c01 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/ContextFactorySupport.java @@ -0,0 +1,25 @@ +package org.springframework.batch.core.scope.util; + +public class ContextFactorySupport implements ContextFactory { + + private int count = 0; + + /** + * Returns this. Override for more sensible behaviour. + * + * @see org.springframework.batch.core.scope.util.ContextFactory#getContext() + */ + public Object getContext() { + return this; + } + + /** + * Returns the context plus a counter, so each call is unique. + * + * @see org.springframework.batch.core.scope.util.ContextFactory#getContextId() + */ + public String getContextId() { + return getContext()+"#"+(count ++); + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests.java new file mode 100644 index 000000000..81d661845 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests.java @@ -0,0 +1,148 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +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; + +/** + * @author Dave Syer + * + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class MultipleContextPlaceholderTargetSourceTests { + + private Map attributes; + + public Map getAttributes() { + return attributes; + } + + @Autowired + private SimpleContextFactory contextFactory; + + @Autowired + @Qualifier("simple") + private TestBean simple; + + @Autowired + @Qualifier("list") + private TestBean list; + + @After + public void removeContext() { + contextFactory.clearContext(); + } + + @Before + public void setUpContext() { + contextFactory.setContext(this); + } + + @Test + public void testValueFromProperties() throws Exception { + attributes = Collections.singletonMap("foo", "bar"); + assertEquals("bar", simple.getName()); + } + + @Test + public void testMultipleValueFromProperties() throws Exception { + + for (int i = 0; i < 4; i++) { + final String value = "foo" + i; + attributes = Collections.singletonMap("foo", value); + assertEquals("foo" + i, simple.getName()); + } + + } + + @Test + public void testMultipleValueInList() throws Exception { + + for (int i = 0; i < 4; i++) { + final String value = "foo" + i; + contextFactory.setContext(this); + attributes = Collections.singletonMap("foo", value); + try { + assertEquals("foo" + i, list.getNames().get(0)); + } + finally { + contextFactory.clearContext(); + } + } + + } + + @Override + public String toString() { + return "Test context: attributes=" + attributes; + } + + public static class TestBean { + private String name; + + private List names = new ArrayList(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getNames() { + return new ArrayList(names); + } + + public void setNames(List names) { + this.names.addAll(names); + } + } + + public static class SimpleContextFactory extends ContextFactorySupport { + + private Object root; + + public Object getContext() { + return root; + } + + public void setContext(Object root) { + this.root = root; + } + + public void clearContext() { + root = null; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests.java new file mode 100644 index 000000000..d04be4dcf --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests.java @@ -0,0 +1,183 @@ +package org.springframework.batch.core.scope.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +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 PlaceholderTargetSourceTests extends ContextFactorySupport { + + @Autowired + @Qualifier("vanilla") + private PlaceholderTargetSource vanilla; + + @Autowired + @Qualifier("simple") + private PlaceholderTargetSource simple; + + @Autowired + @Qualifier("withLong") + private PlaceholderTargetSource withLong; + + @Autowired + @Qualifier("withInteger") + private PlaceholderTargetSource withInteger; + + @Autowired + @Qualifier("withDate") + private PlaceholderTargetSource withDate; + + @Autowired + @Qualifier("compound") + private PlaceholderTargetSource compound; + + @Autowired + @Qualifier("ref") + private PlaceholderTargetSource ref; + + @Autowired + @Qualifier("value") + private PlaceholderTargetSource value; + + private Map map = Collections.singletonMap("foo.foo", (Object) "bar"); + + private Date date = new Date(); + + public Object getContext() { + return this; + } + + public String getFoo() { + return "bar"; + } + + public Map getMap() { + return map; + } + + public Node getParent() { + return new Foo("spam"); + } + + public Long getLong() { + return 12345678912345L; + } + + public Integer getInteger() { + return 4321; + } + + public Date getDate() { + return date; + } + + @Test + public void testAfterPropertiesSet() throws Exception { + PlaceholderTargetSource targetSource = new PlaceholderTargetSource(); + try { + targetSource.afterPropertiesSet(); + fail("Axpected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testGetVanilla() { + Node target = (Node) vanilla.getTarget(); + assertEquals("foo", target.getName()); + } + + @Test + public void testGetSimple() { + Node target = (Node) simple.getTarget(); + assertEquals("bar", target.getName()); + } + + @Test + public void testGetCompound() { + Node target = (Node) compound.getTarget(); + assertEquals("bar-bar", target.getName()); + } + + @Test + public void testGetRef() { + Node target = (Node) ref.getTarget(); + assertEquals("foo", target.getParent().getName()); + } + + @Test + public void testGetValue() { + Node target = (Node) value.getTarget(); + assertEquals("spam", target.getParent().getName()); + } + + @Test + public void testGetLong() { + Node target = (Node) withLong.getTarget(); + assertEquals("bar-12345678912345", target.getName()); + } + + @Test + public void testGetInteger() { + Node target = (Node) withInteger.getTarget(); + assertEquals("bar-4321", target.getName()); + } + + @Test + public void testGetDate() { + Node target = (Node) withDate.getTarget(); + // Remains unconverted because Spring cannot convert from Date to String + assertEquals("bar-#{date}", target.getName()); + } + + public static interface Node { + String getName(); + + Node getParent(); + } + + public static class Foo implements Node { + + private String name; + + private Node parent; + + public Foo() { + } + + public Foo(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Node getParent() { + return parent; + } + + public void setParent(Node parent) { + this.parent = parent; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests.java new file mode 100644 index 000000000..faa133a15 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests.java @@ -0,0 +1,70 @@ +package org.springframework.batch.core.scope.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.junit.runner.RunWith; +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 SimplePlaceholderTargetSourceTests { + + @Autowired + @Qualifier("simple") + private Node simple; + + @Autowired + private SimpleContextFactory contextFactory; + + @Test + public void testGetSimple() { + contextFactory.set("bar"); + assertEquals("bar", simple.getName()); + contextFactory.clear(); + } + + public static class SimpleContextFactory extends ContextFactorySupport { + + private ThreadLocal fooHolder = new ThreadLocal(); + + public Object getContext() { + return this; + } + + public void set(String value) { + fooHolder.set(value); + } + + public void clear() { + fooHolder.set(null); + } + + public String getFoo() { + return fooHolder.get(); + } + + } + + public static interface Node { + String getName(); + } + + public static class Foo implements Node { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/StepContextFactoryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/StepContextFactoryTests.java new file mode 100644 index 000000000..555baa7ca --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/scope/util/StepContextFactoryTests.java @@ -0,0 +1,39 @@ +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.StepExecution; +import org.springframework.batch.core.scope.StepContext; +import org.springframework.batch.core.scope.StepSynchronizationManager; + +public class StepContextFactoryTests { + + private StepContextFactory factory = new StepContextFactory(); + + @After + public void cleanUp() { + StepSynchronizationManager.close(); + StepSynchronizationManager.close(); + } + + @Test + public void testGetContext() { + StepExecution stepExecution = new StepExecution("foo", new JobExecution(11L)); + StepContext context = StepSynchronizationManager.register(stepExecution); + assertEquals(context, factory.getContext()); + } + + @Test + public void testGetContextId() { + StepSynchronizationManager.register(new StepExecution("foo", new JobExecution(11L), 0L)); + Object id1 = factory.getContextId(); + StepSynchronizationManager.register(new StepExecution("foo", new JobExecution(12L), 1L)); + Object id2 = factory.getContextId(); + assertFalse(id2.equals(id1)); + } + +} diff --git a/spring-batch-core/src/test/resources/log4j.properties b/spring-batch-core/src/test/resources/log4j.properties index aa887525f..0fa986064 100644 --- a/spring-batch-core/src/test/resources/log4j.properties +++ b/spring-batch-core/src/test/resources/log4j.properties @@ -2,7 +2,7 @@ log4j.rootCategory=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %t %c{2} - %m%n log4j.category.org.apache.activemq=ERROR log4j.category.org.springframework.batch=DEBUG diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests-context.xml new file mode 100644 index 000000000..723774353 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/AsyncStepScopeIntegrationTests-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/StepScopePlaceholderIntegrationTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopePlaceholderIntegrationTests-context.xml new file mode 100644 index 000000000..bb82623c7 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/StepScopePlaceholderIntegrationTests-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests-context.xml new file mode 100644 index 000000000..8416f6b4b --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/AsyncPlaceholderTargetSourceTests-context.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests-context.xml new file mode 100644 index 000000000..10005b457 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/MultipleContextPlaceholderTargetSourceTests-context.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests-context.xml new file mode 100644 index 000000000..79d0b987a --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/PlaceholderTargetSourceTests-context.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests-context.xml new file mode 100644 index 000000000..041ca0b77 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/util/SimplePlaceholderTargetSourceTests-context.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file