diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index ee9cfdc3f..d7c90ce02 100644 --- a/spring-batch-core/pom.xml +++ b/spring-batch-core/pom.xml @@ -130,6 +130,12 @@ mockito-all test + + javax.inject + javax.inject + 1 + test + diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JobContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JobContext.java index 94f2085a5..e1e016a60 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JobContext.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JobContext.java @@ -21,7 +21,6 @@ import javax.batch.runtime.BatchStatus; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.util.Assert; /** @@ -30,23 +29,23 @@ import org.springframework.util.Assert; * obtain the related contextual information. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class JobContext implements javax.batch.runtime.context.JobContext { - private JobExecution jobExecution; private Object transientUserData; - private JobParametersConverter jobParametersConverter; + private Properties properties; + private JobExecution jobExecution; - /** + /** * @param jobExecution for the related job */ - public JobContext(JobExecution jobExecution, JobParametersConverter jobParametersConverter) { + public JobContext(JobExecution jobExecution, Properties properties) { Assert.notNull(jobExecution, "A JobExecution is required"); - Assert.notNull(jobParametersConverter, "A ParametersConverter is required"); - this.jobExecution = jobExecution; - this.jobParametersConverter = jobParametersConverter; + this.properties = properties; + this.jobExecution = jobExecution; } /* (non-Javadoc) @@ -94,7 +93,7 @@ public class JobContext implements javax.batch.runtime.context.JobContext { */ @Override public Properties getProperties() { - return jobParametersConverter.getProperties(this.jobExecution.getJobParameters()); + return properties != null ? properties : new Properties(); } /* (non-Javadoc) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContext.java index f57a8ce22..f4f262d68 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContext.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContext.java @@ -24,7 +24,6 @@ import javax.batch.runtime.Metric; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; -import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.util.Assert; /** @@ -33,20 +32,20 @@ import org.springframework.util.Assert; * obtain the related contextual information. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class StepContext implements javax.batch.runtime.context.StepContext { private StepExecution stepExecution; private Object transientUserData; - private JobParametersConverter jobParametersConveter; + private Properties properties = new Properties(); - public StepContext(StepExecution stepExecution, JobParametersConverter jobParametersConveter) { + public StepContext(StepExecution stepExecution, Properties properties) { Assert.notNull(stepExecution, "A StepExecution is required"); - Assert.notNull(jobParametersConveter, "A ParametersConverter is required"); this.stepExecution = stepExecution; - this.jobParametersConveter = jobParametersConveter; + this.properties = properties; } /* (non-Javadoc) @@ -86,8 +85,7 @@ public class StepContext implements javax.batch.runtime.context.StepContext { */ @Override public Properties getProperties() { - //TODO: Fix this...this should be properties, not parameters. Waiting on BATCH-2001 - return jobParametersConveter.getProperties(this.stepExecution.getJobParameters()); + return properties != null ? properties : new Properties(); } /* (non-Javadoc) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContextFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContextFactoryBean.java index da05c3ecb..20ca37308 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContextFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/StepContextFactoryBean.java @@ -15,49 +15,39 @@ */ package org.springframework.batch.core.jsr; -import javax.sql.DataSource; +import java.util.Properties; -import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext; import org.springframework.batch.core.scope.context.StepSynchronizationManager; import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.Assert; /** * {@link FactoryBean} implementation used to create {@link javax.batch.runtime.context.StepContext} * instances within the step scope. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ -public class StepContextFactoryBean implements FactoryBean, InitializingBean { - +public class StepContextFactoryBean implements FactoryBean { @Autowired - public DataSource dataSource; - private JobParametersConverter jobParametersConveter; + private BatchPropertyContext batchPropertyContext; - /* (non-Javadoc) - * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() - */ - @Override - public void afterPropertiesSet() throws Exception { - Assert.notNull(dataSource, "A DataSource is required"); - - jobParametersConveter = new JsrJobParametersConverter(dataSource); - } - - /* (non-Javadoc) - * @see org.springframework.beans.factory.FactoryBean#getObject() - */ + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ @Override public StepContext getObject() throws Exception { - return new StepContext(StepSynchronizationManager.getContext().getStepExecution(), jobParametersConveter); + org.springframework.batch.core.StepExecution stepExecution = StepSynchronizationManager.getContext().getStepExecution(); + Properties properties = batchPropertyContext.getBatchProperties(stepExecution.getStepName()); + + return new StepContext(stepExecution, properties); } - /* (non-Javadoc) - * @see org.springframework.beans.factory.FactoryBean#getObjectType() - */ + /* (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ @Override public Class getObjectType() { return StepContext.class; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyBeanPostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyBeanPostProcessor.java new file mode 100644 index 000000000..2f0bd383e --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyBeanPostProcessor.java @@ -0,0 +1,160 @@ +/* + * Copyright 2013 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.jsr.configuration.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import javax.batch.api.BatchProperty; +import javax.batch.api.Batchlet; +import javax.batch.api.chunk.ItemProcessor; +import javax.batch.api.chunk.ItemReader; +import javax.batch.api.chunk.ItemWriter; +import javax.batch.api.chunk.listener.ItemProcessListener; +import javax.batch.api.chunk.listener.ItemReadListener; +import javax.batch.api.chunk.listener.ItemWriteListener; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.flow.JobExecutionDecider; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.util.ReflectionUtils; + +/** + *

+ * {@link BeanPostProcessor} implementation used to inject JSR-352 String properties into batch artifact fields + * that are marked with the {@link BatchProperty} annotation. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class BatchPropertyBeanPostProcessor implements BeanPostProcessor { + @Autowired + private BatchPropertyContext batchPropertyContext; + private Log logger = LogFactory.getLog(getClass()); + private Set> requiredAnnotations = new HashSet>(); + + public BatchPropertyBeanPostProcessor() { + setRequiredAnnotations(); + } + + @Override + public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException { + if (!isBatchArtifact(bean)) { + return bean; + } + + final Properties artifactProperties = batchPropertyContext.getBatchProperties(beanName); + + if (artifactProperties.isEmpty()) { + return bean; + } + + injectBatchProperties(bean, artifactProperties); + + return bean; + } + + private void setRequiredAnnotations() { + ClassLoader cl = BatchPropertyBeanPostProcessor.class.getClassLoader(); + + try { + this.requiredAnnotations.add((Class) cl.loadClass("javax.inject.Inject")); + } catch (ClassNotFoundException ex) { + logger.warn("javax.inject.Inject not found - @BatchProperty marked fields will not be processed."); + } + + this.requiredAnnotations.add(BatchProperty.class); + } + + private boolean isBatchArtifact(Object bean) { + return (bean instanceof ItemReader) || + (bean instanceof ItemProcessor) || + (bean instanceof ItemWriter) || + (bean instanceof CompletionPolicy) || + (bean instanceof Batchlet) || + (bean instanceof ItemReadListener) || + (bean instanceof ItemProcessListener) || + (bean instanceof ItemWriteListener) || + (bean instanceof JobExecutionDecider) || + (bean instanceof Step) || + (bean instanceof Job); + } + + private void injectBatchProperties(final Object bean, final Properties artifactProperties) { + ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() { + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + if (isValidFieldModifier(field) && isAnnotated(field)) { + boolean isAccessible = field.isAccessible(); + field.setAccessible(true); + + String batchProperty = getBatchPropertyFieldValue(field, artifactProperties); + + if (batchProperty != null) { + field.set(bean, batchProperty); + } + + field.setAccessible(isAccessible); + } + } + }); + } + + private String getBatchPropertyFieldValue(Field field, Properties batchArtifactProperties) { + BatchProperty batchProperty = field.getAnnotation(BatchProperty.class); + + if (!"".equals(batchProperty.name())) { + return getBatchProperty(batchProperty.name(), batchArtifactProperties); + } + + return getBatchProperty(field.getName(), batchArtifactProperties); + } + + private String getBatchProperty(String propertyKey, Properties batchArtifactProperties) { + if (batchArtifactProperties.containsKey(propertyKey)) { + return (String) batchArtifactProperties.get(propertyKey); + } + + return null; + } + + private boolean isAnnotated(Field field) { + for (Class annotation : requiredAnnotations) { + if(!field.isAnnotationPresent(annotation)) { + return false; + } + } + + return true; + } + + private boolean isValidFieldModifier(Field field) { + return !Modifier.isStatic(field.getModifiers()) && !Modifier.isFinal(field.getModifiers()); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyContext.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyContext.java new file mode 100644 index 000000000..4bcc4c12d --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/BatchPropertyContext.java @@ -0,0 +1,140 @@ +/* + * Copyright 2013 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.jsr.configuration.support; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; + +/** + *

+ * Simple context object to hold parsed JSR-352 batch properties, mapping properties + * to beans / "batch artifacts". Used internally when parsing property tags from a batch + * configuration file and to obtain corresponding values when injecting into batch artifacts. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class BatchPropertyContext { + private ConcurrentHashMap batchProperties = new ConcurrentHashMap(); + + /** + *

+ * Adds each of the provided {@link BatchPropertyContext} objects to the existing propery + * context. + *

+ * + * @param batchPropertyContextEntries the {@link BatchPropertyContextEntry} objects to add + */ + public void setBatchContextEntries(List batchPropertyContextEntries) { + for (BatchPropertyContextEntry batchPropertyContextEntry : batchPropertyContextEntries) { + setBatchContextEntry(batchPropertyContextEntry); + } + } + + private void setBatchContextEntry(BatchPropertyContextEntry batchPropertyContextEntry) { + String beanName = batchPropertyContextEntry.getBeanName(); + Properties properties = batchPropertyContextEntry.getProperties(); + + if (batchProperties.containsKey(beanName)) { + Properties existingProperties = batchProperties.get(beanName); + existingProperties.putAll(properties); + + batchProperties.put(beanName, existingProperties); + } else { + batchProperties.put(beanName, properties); + } + } + + /** + *

+ * Obtains the batch {@link Properties} for the provided bean name / batch artifact. The returned + * {@link Properties} will also contain any job level properties that have been set. + *

+ * + * @param beanName the bean name representing the batch artifact to obtain properties for + * @return the {@link Properties} for the provided batch artifact + */ + public Properties getBatchProperties(String beanName) { + Properties properties = new Properties(); + + if (batchProperties.containsKey(beanName)) { + properties.putAll(batchProperties.get(beanName)); + } + + for (String jobLevelProperty : batchProperties.keySet()) { + if (jobLevelProperty.startsWith("job-")) { + if (batchProperties.containsKey(jobLevelProperty)) { + properties.putAll(batchProperties.get(jobLevelProperty)); + } + + break; + } + } + + return properties; + } + + /** + *

+ * Simple object to encapsulate batch properties of a given bean / batch artifact. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ + public class BatchPropertyContextEntry { + private String beanName; + private Properties properties; + + /** + *

+ * Creates a new entry instance using the provided bean name representing batch artifact + * and its associated {@link Properties}. + *

+ * + * @param beanName the bean name representing the batch artifact + * @param properties the associated {@link Properties} + */ + public BatchPropertyContextEntry(String beanName, Properties properties) { + this.beanName = beanName; + this.properties = properties; + } + + /** + *

+ * Obtains the bean name of the batch artifact this entry is associated with. + *

+ * + * @return the bean name of the batch artifact + */ + public String getBeanName() { + return beanName; + } + + /** + *

+ * Obtains the batch {@link Properties} that are associated with this entry. + *

+ * + * @return the batch {@link Properties} + */ + public Properties getProperties() { + return properties; + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/JsrAutowiredAnnotationBeanPostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/JsrAutowiredAnnotationBeanPostProcessor.java new file mode 100644 index 000000000..27d6ba06e --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/JsrAutowiredAnnotationBeanPostProcessor.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013 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.jsr.configuration.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import javax.batch.api.BatchProperty; +import org.springframework.beans.factory.annotation.InjectionMetadata; + +/** + *

This class overrides methods in the copied {@link SpringAutowiredAnnotationBeanPostProcessor} class + * to check for the {@link @BatchProperty} annotation before processing injection annotations. If the annotation + * is found, further injection processing for the field is skipped.

+ */ +public class JsrAutowiredAnnotationBeanPostProcessor extends SpringAutowiredAnnotationBeanPostProcessor { + @Override + protected InjectionMetadata findAutowiringMetadata(Class clazz) { + return super.buildAutowiringMetadata(clazz); + } + + @Override + protected Annotation findAutowiredAnnotation(AccessibleObject ao) { + if (ao.getAnnotation(BatchProperty.class) != null) { + return null; + } + + return super.findAutowiredAnnotation(ao); + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/SpringAutowiredAnnotationBeanPostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/SpringAutowiredAnnotationBeanPostProcessor.java new file mode 100644 index 000000000..33eacccc6 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/support/SpringAutowiredAnnotationBeanPostProcessor.java @@ -0,0 +1,585 @@ +/* + * Copyright 2013 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.jsr.configuration.support; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + *

This is a copy of AutowiredAnnotationBeanPostProcessor with modifications allow a subclass to + * do additional checks on other field annotations before processing injection annotations.

+ * + *

This class is considered a quick work around and needs to be refactored / removed.

+ * + *

The in addition to making this class package private, the following methods were modified to be protected:

+ *
  • findAutowiringMetadata(Class clazz)
  • + *
  • buildAutowiringMetadata(Class clazz)
  • + *
  • findAutowiredAnnotation(AccessibleObject ao)
  • + */ +class SpringAutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter + implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Set> autowiredAnnotationTypes = + new LinkedHashSet>(); + + private String requiredParameterName = "required"; + + private boolean requiredParameterValue = true; + + private int order = Ordered.LOWEST_PRECEDENCE - 2; + + private ConfigurableListableBeanFactory beanFactory; + + private final Map, Constructor[]> candidateConstructorsCache = + new ConcurrentHashMap, Constructor[]>(64); + + private final Map, InjectionMetadata> injectionMetadataCache = + new ConcurrentHashMap, InjectionMetadata>(64); + + + /** + * Create a new AutowiredAnnotationBeanPostProcessor + * for Spring's standard {@link org.springframework.beans.factory.annotation.Autowired} annotation. + *

    Also supports JSR-330's {@link javax.inject.Inject} annotation, if available. + */ + @SuppressWarnings("unchecked") + public SpringAutowiredAnnotationBeanPostProcessor() { + this.autowiredAnnotationTypes.add(Autowired.class); + this.autowiredAnnotationTypes.add(Value.class); + ClassLoader cl = SpringAutowiredAnnotationBeanPostProcessor.class.getClassLoader(); + try { + this.autowiredAnnotationTypes.add((Class) cl.loadClass("javax.inject.Inject")); + logger.info("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring"); + } + catch (ClassNotFoundException ex) { + // JSR-330 API not available - simply skip. + } + } + + + /** + * Set the 'autowired' annotation type, to be used on constructors, fields, + * setter methods and arbitrary config methods. + *

    The default autowired annotation type is the Spring-provided + * {@link Autowired} annotation, as well as {@link Value}. + *

    This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation type to indicate that a member is + * supposed to be autowired. + */ + public void setAutowiredAnnotationType(Class autowiredAnnotationType) { + Assert.notNull(autowiredAnnotationType, "'autowiredAnnotationType' must not be null"); + this.autowiredAnnotationTypes.clear(); + this.autowiredAnnotationTypes.add(autowiredAnnotationType); + } + + /** + * Set the 'autowired' annotation types, to be used on constructors, fields, + * setter methods and arbitrary config methods. + *

    The default autowired annotation type is the Spring-provided + * {@link Autowired} annotation, as well as {@link Value}. + *

    This setter property exists so that developers can provide their own + * (non-Spring-specific) annotation types to indicate that a member is + * supposed to be autowired. + */ + public void setAutowiredAnnotationTypes(Set> autowiredAnnotationTypes) { + Assert.notEmpty(autowiredAnnotationTypes, "'autowiredAnnotationTypes' must not be empty"); + this.autowiredAnnotationTypes.clear(); + this.autowiredAnnotationTypes.addAll(autowiredAnnotationTypes); + } + + /** + * Set the name of a parameter of the annotation that specifies + * whether it is required. + * @see #setRequiredParameterValue(boolean) + */ + public void setRequiredParameterName(String requiredParameterName) { + this.requiredParameterName = requiredParameterName; + } + + /** + * Set the boolean value that marks a dependency as required + *

    For example if using 'required=true' (the default), + * this value should be true; but if using + * 'optional=false', this value should be false. + * @see #setRequiredParameterName(String) + */ + public void setRequiredParameterValue(boolean requiredParameterValue) { + this.requiredParameterValue = requiredParameterValue; + } + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + throw new IllegalArgumentException( + "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory"); + } + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (beanType != null) { + InjectionMetadata metadata = findAutowiringMetadata(beanType); + metadata.checkConfigMembers(beanDefinition); + } + } + + @Override + public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) throws BeansException { + // Quick check on the concurrent map first, with minimal locking. + Constructor[] candidateConstructors = this.candidateConstructorsCache.get(beanClass); + if (candidateConstructors == null) { + synchronized (this.candidateConstructorsCache) { + candidateConstructors = this.candidateConstructorsCache.get(beanClass); + if (candidateConstructors == null) { + Constructor[] rawCandidates = beanClass.getDeclaredConstructors(); + List> candidates = new ArrayList>(rawCandidates.length); + Constructor requiredConstructor = null; + Constructor defaultConstructor = null; + for (Constructor candidate : rawCandidates) { + Annotation annotation = findAutowiredAnnotation(candidate); + if (annotation != null) { + if (requiredConstructor != null) { + throw new BeanCreationException("Invalid autowire-marked constructor: " + candidate + + ". Found another constructor with 'required' Autowired annotation: " + + requiredConstructor); + } + if (candidate.getParameterTypes().length == 0) { + throw new IllegalStateException( + "Autowired annotation requires at least one argument: " + candidate); + } + boolean required = determineRequiredStatus(annotation); + if (required) { + if (!candidates.isEmpty()) { + throw new BeanCreationException( + "Invalid autowire-marked constructors: " + candidates + + ". Found another constructor with 'required' Autowired annotation: " + + requiredConstructor); + } + requiredConstructor = candidate; + } + candidates.add(candidate); + } + else if (candidate.getParameterTypes().length == 0) { + defaultConstructor = candidate; + } + } + if (!candidates.isEmpty()) { + // Add default constructor to list of optional constructors, as fallback. + if (requiredConstructor == null && defaultConstructor != null) { + candidates.add(defaultConstructor); + } + candidateConstructors = candidates.toArray(new Constructor[candidates.size()]); + } + else { + candidateConstructors = new Constructor[0]; + } + this.candidateConstructorsCache.put(beanClass, candidateConstructors); + } + } + } + return (candidateConstructors.length > 0 ? candidateConstructors : null); + } + + @Override + public PropertyValues postProcessPropertyValues( + PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { + + InjectionMetadata metadata = findAutowiringMetadata(bean.getClass()); + try { + metadata.inject(bean, beanName, pvs); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex); + } + return pvs; + } + + /** + * 'Native' processing method for direct calls with an arbitrary target instance, + * resolving all of its fields and methods which are annotated with @Autowired. + * @param bean the target instance to process + * @throws BeansException if autowiring failed + */ + public void processInjection(Object bean) throws BeansException { + Class clazz = bean.getClass(); + InjectionMetadata metadata = findAutowiringMetadata(clazz); + try { + metadata.inject(bean, null, null); + } + catch (Throwable ex) { + throw new BeanCreationException("Injection of autowired dependencies failed for class [" + clazz + "]", ex); + } + } + + + protected InjectionMetadata findAutowiringMetadata(Class clazz) { + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(clazz); + if (metadata == null) { + metadata = buildAutowiringMetadata(clazz); + this.injectionMetadataCache.put(clazz, metadata); + } + } + } + return metadata; + } + + protected InjectionMetadata buildAutowiringMetadata(Class clazz) { + LinkedList elements = new LinkedList(); + Class targetClass = clazz; + + do { + LinkedList currElements = new LinkedList(); + for (Field field : targetClass.getDeclaredFields()) { + Annotation annotation = findAutowiredAnnotation(field); + if (annotation != null) { + if (Modifier.isStatic(field.getModifiers())) { + if (logger.isWarnEnabled()) { + logger.warn("Autowired annotation is not supported on static fields: " + field); + } + continue; + } + boolean required = determineRequiredStatus(annotation); + currElements.add(new AutowiredFieldElement(field, required)); + } + } + for (Method method : targetClass.getDeclaredMethods()) { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + Annotation annotation = BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod) ? + findAutowiredAnnotation(bridgedMethod) : findAutowiredAnnotation(method); + if (annotation != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + if (logger.isWarnEnabled()) { + logger.warn("Autowired annotation is not supported on static methods: " + method); + } + continue; + } + if (method.getParameterTypes().length == 0) { + if (logger.isWarnEnabled()) { + logger.warn("Autowired annotation should be used on methods with actual parameters: " + method); + } + } + boolean required = determineRequiredStatus(annotation); + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method); + currElements.add(new AutowiredMethodElement(method, required, pd)); + } + } + elements.addAll(0, currElements); + targetClass = targetClass.getSuperclass(); + } + while (targetClass != null && targetClass != Object.class); + + return new InjectionMetadata(clazz, elements); + } + + protected Annotation findAutowiredAnnotation(AccessibleObject ao) { + for (Class type : this.autowiredAnnotationTypes) { + Annotation annotation = AnnotationUtils.getAnnotation(ao, type); + if (annotation != null) { + return annotation; + } + } + return null; + } + + /** + * Obtain all beans of the given type as autowire candidates. + * @param type the type of the bean + * @return the target beans, or an empty Collection if no bean of this type is found + * @throws BeansException if bean retrieval failed + */ + protected Map findAutowireCandidates(Class type) throws BeansException { + if (this.beanFactory == null) { + throw new IllegalStateException("No BeanFactory configured - " + + "override the getBeanOfType method or specify the 'beanFactory' property"); + } + return BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type); + } + + /** + * Determine if the annotated field or method requires its dependency. + *

    A 'required' dependency means that autowiring should fail when no beans + * are found. Otherwise, the autowiring process will simply bypass the field + * or method when no beans are found. + * @param annotation the Autowired annotation + * @return whether the annotation indicates that a dependency is required + */ + protected boolean determineRequiredStatus(Annotation annotation) { + try { + Method method = ReflectionUtils.findMethod(annotation.annotationType(), this.requiredParameterName); + if (method == null) { + // annotations like @Inject and @Value don't have a method (attribute) named "required" + // -> default to required status + return true; + } + return (this.requiredParameterValue == (Boolean) ReflectionUtils.invokeMethod(method, annotation)); + } + catch (Exception ex) { + // an exception was thrown during reflective invocation of the required attribute + // -> default to required status + return true; + } + } + + /** + * Register the specified bean as dependent on the autowired beans. + */ + private void registerDependentBeans(String beanName, Set autowiredBeanNames) { + if (beanName != null) { + for (String autowiredBeanName : autowiredBeanNames) { + if (this.beanFactory.containsBean(autowiredBeanName)) { + this.beanFactory.registerDependentBean(autowiredBeanName, beanName); + } + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + + "' to bean named '" + autowiredBeanName + "'"); + } + } + } + } + + /** + * Resolve the specified cached method argument or field value. + */ + private Object resolvedCachedArgument(String beanName, Object cachedArgument) { + if (cachedArgument instanceof DependencyDescriptor) { + DependencyDescriptor descriptor = (DependencyDescriptor) cachedArgument; + TypeConverter typeConverter = this.beanFactory.getTypeConverter(); + return this.beanFactory.resolveDependency(descriptor, beanName, null, typeConverter); + } + else if (cachedArgument instanceof RuntimeBeanReference) { + return this.beanFactory.getBean(((RuntimeBeanReference) cachedArgument).getBeanName()); + } + else { + return cachedArgument; + } + } + + + /** + * Class representing injection information about an annotated field. + */ + private class AutowiredFieldElement extends InjectionMetadata.InjectedElement { + + private final boolean required; + + private volatile boolean cached = false; + + private volatile Object cachedFieldValue; + + public AutowiredFieldElement(Field field, boolean required) { + super(field, null); + this.required = required; + } + + @Override + protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { + Field field = (Field) this.member; + try { + Object value; + if (this.cached) { + value = resolvedCachedArgument(beanName, this.cachedFieldValue); + } + else { + DependencyDescriptor descriptor = new DependencyDescriptor(field, this.required); + Set autowiredBeanNames = new LinkedHashSet(1); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + value = beanFactory.resolveDependency(descriptor, beanName, autowiredBeanNames, typeConverter); + synchronized (this) { + if (!this.cached) { + if (value != null || this.required) { + this.cachedFieldValue = descriptor; + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == 1) { + String autowiredBeanName = autowiredBeanNames.iterator().next(); + if (beanFactory.containsBean(autowiredBeanName)) { + if (beanFactory.isTypeMatch(autowiredBeanName, field.getType())) { + this.cachedFieldValue = new RuntimeBeanReference(autowiredBeanName); + } + } + } + } + else { + this.cachedFieldValue = null; + } + this.cached = true; + } + } + } + if (value != null) { + ReflectionUtils.makeAccessible(field); + field.set(bean, value); + } + } + catch (Throwable ex) { + throw new BeanCreationException("Could not autowire field: " + field, ex); + } + } + } + + + /** + * Class representing injection information about an annotated method. + */ + private class AutowiredMethodElement extends InjectionMetadata.InjectedElement { + + private final boolean required; + + private volatile boolean cached = false; + + private volatile Object[] cachedMethodArguments; + + public AutowiredMethodElement(Method method, boolean required, PropertyDescriptor pd) { + super(method, pd); + this.required = required; + } + + @Override + protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable { + if (checkPropertySkipping(pvs)) { + return; + } + Method method = (Method) this.member; + try { + Object[] arguments; + if (this.cached) { + // Shortcut for avoiding synchronization... + arguments = resolveCachedArguments(beanName); + } + else { + Class[] paramTypes = method.getParameterTypes(); + arguments = new Object[paramTypes.length]; + DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length]; + Set autowiredBeanNames = new LinkedHashSet(paramTypes.length); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + for (int i = 0; i < arguments.length; i++) { + MethodParameter methodParam = new MethodParameter(method, i); + GenericTypeResolver.resolveParameterType(methodParam, bean.getClass()); + descriptors[i] = new DependencyDescriptor(methodParam, this.required); + arguments[i] = beanFactory.resolveDependency( + descriptors[i], beanName, autowiredBeanNames, typeConverter); + if (arguments[i] == null && !this.required) { + arguments = null; + break; + } + } + synchronized (this) { + if (!this.cached) { + if (arguments != null) { + this.cachedMethodArguments = new Object[arguments.length]; + for (int i = 0; i < arguments.length; i++) { + this.cachedMethodArguments[i] = descriptors[i]; + } + registerDependentBeans(beanName, autowiredBeanNames); + if (autowiredBeanNames.size() == paramTypes.length) { + Iterator it = autowiredBeanNames.iterator(); + for (int i = 0; i < paramTypes.length; i++) { + String autowiredBeanName = it.next(); + if (beanFactory.containsBean(autowiredBeanName)) { + if (beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) { + this.cachedMethodArguments[i] = new RuntimeBeanReference(autowiredBeanName); + } + } + } + } + } + else { + this.cachedMethodArguments = null; + } + this.cached = true; + } + } + } + if (arguments != null) { + ReflectionUtils.makeAccessible(method); + method.invoke(bean, arguments); + } + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + catch (Throwable ex) { + throw new BeanCreationException("Could not autowire method: " + method, ex); + } + } + + private Object[] resolveCachedArguments(String beanName) { + if (this.cachedMethodArguments == null) { + return null; + } + Object[] arguments = new Object[this.cachedMethodArguments.length]; + for (int i = 0; i < arguments.length; i++) { + arguments[i] = resolvedCachedArgument(beanName, this.cachedMethodArguments[i]); + } + return arguments; + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java index a906a897f..ff32cb0dd 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/BatchletParser.java @@ -30,25 +30,25 @@ import org.w3c.dom.Element; * attribute is expected to point to an implementation of Tasklet). * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class BatchletParser extends AbstractSingleBeanDefinitionParser { - private static final String REF = "ref"; - public void parseBatchlet(Element stepElement, Element taskletElement, AbstractBeanDefinition bd, - ParserContext parserContext) { - + public void parseBatchlet(Element batchletElement, AbstractBeanDefinition bd, ParserContext parserContext) { bd.setBeanClass(StepFactoryBean.class); bd.setAttribute("isNamespaceStep", false); - String taskletRef = taskletElement.getAttribute(REF); + String taskletRef = batchletElement.getAttribute(REF); if (StringUtils.hasText(taskletRef)) { bd.getPropertyValues().addPropertyValue("tasklet", new RuntimeBeanReference(taskletRef)); } bd.setRole(BeanDefinition.ROLE_SUPPORT); - bd.setSource(parserContext.extractSource(taskletElement)); + bd.setSource(parserContext.extractSource(batchletElement)); + + new PropertyParser(taskletRef, parserContext).parseProperties(batchletElement); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java index 949fd557d..b3dd2c3f6 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ChunkParser.java @@ -41,11 +41,11 @@ import org.w3c.dom.NodeList; * {@link ItemProcessor}, and {@link ItemWriter}). * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 * */ public class ChunkParser { - private static final String TIME_LIMIT_ATTRIBUTE = "time-limit"; private static final String ITEM_COUNT_ATTRIBUTE = "item-count"; private static final String CHECKPOINT_ALGORITHM_ELEMENT = "checkpoint-algorithm"; @@ -77,8 +77,7 @@ public class ChunkParser { parseSimpleAttribute(element, propertyValues, ITEM_COUNT_ATTRIBUTE, "commitInterval"); parseSimpleAttribute(element, propertyValues, TIME_LIMIT_ATTRIBUTE, "timeout"); } else if(checkpointPolicy.equals(CUSTOM_CHECKPOINT_POLICY)) { - parseCustomCheckpointAlgorithm(element, parserContext, - propertyValues); + parseCustomCheckpointAlgorithm(element, parserContext, propertyValues); } } @@ -102,26 +101,31 @@ public class ChunkParser { } @SuppressWarnings({"unchecked", "rawtypes"}) - private void parseChildElement(Element element, - ParserContext parserContext, MutablePropertyValues propertyValues, - Node nd) { + private void parseChildElement(Element element, ParserContext parserContext, + MutablePropertyValues propertyValues, Node nd) { if (nd instanceof Element) { Element nestedElement = (Element) nd; String name = nestedElement.getLocalName(); - String artifactName = nestedElement.getAttribute(REF_ATTRIBUTE); + if(name.equals(READER_ELEMENT)) { if (StringUtils.hasText(artifactName)) { propertyValues.addPropertyValue("itemReader", new RuntimeBeanReference(artifactName)); } + + new PropertyParser(artifactName, parserContext).parseProperties(nestedElement); } else if(name.equals(PROCESSOR_ELEMENT)) { if (StringUtils.hasText(artifactName)) { propertyValues.addPropertyValue("itemProcessor", new RuntimeBeanReference(artifactName)); } + + new PropertyParser(artifactName, parserContext).parseProperties(nestedElement); } else if(name.equals(WRITER_ELEMENT)) { if (StringUtils.hasText(artifactName)) { propertyValues.addPropertyValue("itemWriter", new RuntimeBeanReference(artifactName)); } + + new PropertyParser(artifactName, parserContext).parseProperties(nestedElement); } else if(name.equals(SKIPPABLE_EXCEPTION_CLASSES_ELEMENT)) { ManagedMap exceptionClasses = new ExceptionElementParser().parse(element, parserContext, SKIPPABLE_EXCEPTION_CLASSES_ELEMENT); if(exceptionClasses != null) { @@ -146,8 +150,7 @@ public class ChunkParser { } } - private void parseCustomCheckpointAlgorithm(Element element, - ParserContext parserContext, MutablePropertyValues propertyValues) { + private void parseCustomCheckpointAlgorithm(Element element, ParserContext parserContext, MutablePropertyValues propertyValues) { List elements = DomUtils.getChildElementsByTagName(element, CHECKPOINT_ALGORITHM_ELEMENT); if(elements.size() == 1) { @@ -157,6 +160,8 @@ public class ChunkParser { if(StringUtils.hasText(name)) { propertyValues.addPropertyValue("chunkCompletionPolicy", new RuntimeBeanReference(name)); } + + new PropertyParser(name, parserContext).parseProperties(checkpointAlgorithmElement); } else if(elements.size() > 1){ parserContext.getReaderContext().error( "The element may not appear more than once in a single <" diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java index c54597bf1..bdee142e0 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/DecisionParser.java @@ -29,6 +29,7 @@ import org.w3c.dom.Element; * parses a decision element and assumes that it refers to a {@link JobExecutionDecider} * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class DecisionParser { @@ -37,7 +38,6 @@ public class DecisionParser { private static final String REF_ATTRIBUTE = "ref"; public Collection parse(Element element, ParserContext parserContext) { - String refAttribute = element.getAttribute(REF_ATTRIBUTE); String idAttribute = element.getAttribute(ID_ATTRIBUTE); @@ -45,6 +45,9 @@ public class DecisionParser { BeanDefinitionBuilder.genericBeanDefinition("org.springframework.batch.core.job.flow.support.state.DecisionState"); stateBuilder.addConstructorArgValue(new RuntimeBeanReference(refAttribute)); stateBuilder.addConstructorArgValue(idAttribute); + + new PropertyParser(refAttribute, parserContext).parseProperties(element); + return FlowParser.getNextElements(parserContext, stateBuilder.getBeanDefinition(), element); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java index 2d8829a37..d85a465a4 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/JobParser.java @@ -17,9 +17,11 @@ package org.springframework.batch.core.jsr.configuration.xml; import org.springframework.batch.core.configuration.xml.CoreNamespaceUtils; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -28,14 +30,17 @@ import org.w3c.dom.Element; * the standard Spring Batch artifacts. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class JobParser extends AbstractSingleBeanDefinitionParser { + private static final String ID_ATTRIBUTE = "id"; + private static final String RESTARTABLE_ATTRIBUTE = "restartable"; + private static final String BATCH_PROPERTY_POST_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.jsr.configuration.support.BatchPropertyBeanPostProcessor"; + private static final String BATCH_PROPERTY_POST_PROCESSOR_BEAN_NAME = "batchPropertyPostProcessor"; + private static final String JSR_AUTOWIRED_ANNOTATION_BEAN_POST_PROCESSOR_CLASS_NAME = "org.springframework.batch.core.jsr.configuration.support.JsrAutowiredAnnotationBeanPostProcessor"; - private static final String RESTARTABLE_ATTRIBUTE = "restartable"; - private static final String ID_ATTRIBUTE = "id"; - - @Override + @Override protected Class getBeanClass(Element element) { return JobFactoryBean.class; } @@ -43,6 +48,7 @@ public class JobParser extends AbstractSingleBeanDefinitionParser { @Override protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { CoreNamespaceUtils.autoregisterBeansForNamespace(parserContext, parserContext.extractSource(element)); + autoregisterJsrBeansForNamespace(parserContext); String jobName = element.getAttribute(ID_ATTRIBUTE); builder.addConstructorArgValue(jobName); @@ -56,5 +62,34 @@ public class JobParser extends AbstractSingleBeanDefinitionParser { builder.addPropertyValue("flow", flowDef); new ListnerParser(JobListenerFactoryBean.class, "jobExecutionListeners").parseListeners(element, parserContext, builder); + new PropertyParser("job-" + jobName, parserContext).parseProperties(element); } + + private void autoregisterJsrBeansForNamespace(ParserContext parserContext) { + autoRegisterBatchPostProcessor(parserContext); + autoRegisterJsrAutowiredAnnotationBeanPostProcessor(parserContext); + } + + private void autoRegisterBatchPostProcessor(ParserContext parserContext) { + BeanDefinitionBuilder batchPropertyBeanPostProcessor = + BeanDefinitionBuilder.genericBeanDefinition(BATCH_PROPERTY_POST_PROCESSOR_CLASS_NAME); + + AbstractBeanDefinition batchPropertyBeanPostProcessorDefinition = batchPropertyBeanPostProcessor.getBeanDefinition(); + batchPropertyBeanPostProcessorDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + parserContext.getRegistry().registerBeanDefinition(BATCH_PROPERTY_POST_PROCESSOR_BEAN_NAME, batchPropertyBeanPostProcessorDefinition); + } + + private void autoRegisterJsrAutowiredAnnotationBeanPostProcessor(ParserContext parserContext) { + BeanDefinitionBuilder jsrAutowiredAnnotationBeanPostProcessor = + BeanDefinitionBuilder.genericBeanDefinition(JSR_AUTOWIRED_ANNOTATION_BEAN_POST_PROCESSOR_CLASS_NAME); + + AbstractBeanDefinition jsrAutowiredAnnotationBeanPostProcessorDefinition = + jsrAutowiredAnnotationBeanPostProcessor.getBeanDefinition(); + + jsrAutowiredAnnotationBeanPostProcessorDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + parserContext.getRegistry().registerBeanDefinition(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME, + jsrAutowiredAnnotationBeanPostProcessorDefinition); + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java index 64c6bab3d..809b7ebaa 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java @@ -35,7 +35,6 @@ import org.w3c.dom.Element; * @since 3.0 */ public class ListnerParser { - private static final String REF_ATTRIBUTE = "ref"; private static final String LISTENER_ELEMENT = "listener"; private static final String LISTENERS_ELEMENT = "listeners"; @@ -43,11 +42,11 @@ public class ListnerParser { private Class listenerType; private String propertyKey; - @SuppressWarnings("rawtypes") + @SuppressWarnings("rawtypes") public ListnerParser(Class listenerType, String propertyKey) { - this.listenerType = listenerType; this.propertyKey = propertyKey; - } + this.listenerType = listenerType; + } public void parseListeners(Element element, ParserContext parserContext, AbstractBeanDefinition bd) { ManagedList listeners = parseListeners(element, parserContext); @@ -78,9 +77,14 @@ public class ListnerParser { listeners.setMergeEnabled(false); List listenerElements = DomUtils.getChildElementsByTagName(listenersElement, LISTENER_ELEMENT); for (Element listenerElement : listenerElements) { + String beanName = listenerElement.getAttribute(REF_ATTRIBUTE); + BeanDefinitionBuilder bd = BeanDefinitionBuilder.genericBeanDefinition(listenerType); - bd.addPropertyValue("delegate", new RuntimeBeanReference(listenerElement.getAttribute(REF_ATTRIBUTE))); + bd.addPropertyValue("delegate", new RuntimeBeanReference(beanName)); + listeners.add(bd.getBeanDefinition()); + + new PropertyParser(beanName, parserContext).parseProperties(listenerElement); } parserContext.popAndRegisterContainingComponent(); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/PropertyParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/PropertyParser.java new file mode 100644 index 000000000..6477220dd --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/PropertyParser.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.util.List; +import java.util.Properties; +import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + *

    + * Parser for the <properties /> element defined by JSR-352. + *

    + * + * @author Chris Schaefer + * @since 3.0 + */ +public class PropertyParser { + private static final String PROPERTY_ELEMENT = "property"; + private static final String PROPERTIES_ELEMENT = "properties"; + private static final String PROPERTY_NAME_ATTRIBUTE = "name"; + private static final String PROPERTY_VALUE_ATTRIBUTE = "value"; + private static final String BATCH_CONTEXT_ENTRIES_PROPERTY_NAME = "batchContextEntries"; + private static final String BATCH_PROPERTY_CONTEXT_BEAN_CLASS_NAME = "org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext"; + private static final String BATCH_PROPERTY_CONTEXT_BEAN_NAME = "batchPropertyContext"; + + private String beanName; + private ParserContext parserContext; + + public PropertyParser(String beanName, ParserContext parserContext) { + this.beanName = beanName; + this.parserContext = parserContext; + + registerBatchPropertyContext(); + } + + /** + *

    + * Parses <property> tag values from the provided {@link Element} if it contains a <properties /> element. + * Only one <properties /> element may be present. <property> elements have a name and value attribute + * which represent the property entries key and value. + *

    + * + * @param element + */ + public void parseProperties(Element element) { + List propertiesElements = DomUtils.getChildElementsByTagName(element, PROPERTIES_ELEMENT); + + Properties properties = new Properties(); + + if (propertiesElements.size() == 1) { + List propertyElements = DomUtils.getChildElementsByTagName(propertiesElements.get(0), PROPERTY_ELEMENT); + + for (Element propertyElement : propertyElements) { + properties.put(propertyElement.getAttribute(PROPERTY_NAME_ATTRIBUTE), propertyElement.getAttribute(PROPERTY_VALUE_ATTRIBUTE)); + } + + addProperties(properties); + } else if (propertiesElements.size() > 1) { + parserContext.getReaderContext().error("The element may not appear more than once in a single .", element); + } + } + + private void addProperties(Properties properties) { + BeanDefinition beanDefinition = parserContext.getRegistry().getBeanDefinition(BATCH_PROPERTY_CONTEXT_BEAN_NAME); + + BatchPropertyContext batchPropertyContext = new BatchPropertyContext(); + BatchPropertyContext.BatchPropertyContextEntry batchPropertyContextEntry = + batchPropertyContext.new BatchPropertyContextEntry(beanName, properties); + + ManagedList managedList = new ManagedList(); + managedList.setMergeEnabled(true); + managedList.add(batchPropertyContextEntry); + + beanDefinition.getPropertyValues().addPropertyValue(BATCH_CONTEXT_ENTRIES_PROPERTY_NAME, managedList); + } + + private void registerBatchPropertyContext() { + if (!parserContext.getRegistry().containsBeanDefinition(BATCH_PROPERTY_CONTEXT_BEAN_NAME)) { + BeanDefinitionBuilder batchPropertyContextBeanDefinitionBuilder = + BeanDefinitionBuilder.genericBeanDefinition(BATCH_PROPERTY_CONTEXT_BEAN_CLASS_NAME); + + AbstractBeanDefinition batchPropertyContextBeanDefinition = batchPropertyContextBeanDefinitionBuilder.getBeanDefinition(); + batchPropertyContextBeanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + + parserContext.getRegistry().registerBeanDefinition(BATCH_PROPERTY_CONTEXT_BEAN_NAME, batchPropertyContextBeanDefinition); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java index 5473b9dcd..169ef8f24 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/StepParser.java @@ -37,17 +37,17 @@ import org.springframework.util.StringUtils; * * @author Michael Minella * @author Glenn Renfro + * @author Chris Schaefer * @since 3.0 */ public class StepParser extends AbstractSingleBeanDefinitionParser { - private static final String CHUNK_ELEMENT = "chunk"; private static final String BATCHLET_ELEMENT = "batchlet"; private static final String ALLOW_START_IF_COMPLETE_ATTRIBUTE = "allow-start-if-complete"; private static final String START_LIMIT_ATTRIBUTE = "start-limit"; private static final String SPLIT_ID_ATTRIBUTE = "id"; - protected Collection parse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + protected Collection parse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { BeanDefinitionBuilder defBuilder = BeanDefinitionBuilder.genericBeanDefinition(); AbstractBeanDefinition bd = defBuilder.getRawBeanDefinition(); bd.setBeanClass(StepFactoryBean.class); @@ -71,6 +71,7 @@ public class StepParser extends AbstractSingleBeanDefinitionParser { } new ListnerParser(StepListenerFactoryBean.class, "listeners").parseListeners(element, parserContext, bd); + new PropertyParser(stepName, parserContext).parseProperties(element); // look at all nested elements NodeList children = element.getChildNodes(); @@ -83,7 +84,7 @@ public class StepParser extends AbstractSingleBeanDefinitionParser { String name = nestedElement.getLocalName(); if(name.equalsIgnoreCase(BATCHLET_ELEMENT)) { - new BatchletParser().parseBatchlet(element, nestedElement, bd, parserContext); + new BatchletParser().parseBatchlet(nestedElement, bd, parserContext); } else if(name.equals(CHUNK_ELEMENT)) { new ChunkParser().parse(nestedElement, bd, parserContext); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/launch/JsrJobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/launch/JsrJobOperator.java index 5f707e05d..bd41c04e8 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/launch/JsrJobOperator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/launch/JsrJobOperator.java @@ -45,16 +45,14 @@ import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.jsr.JobContext; +import org.springframework.batch.core.jsr.configuration.support.BatchPropertyContext; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobRepository; import org.springframework.beans.factory.access.BeanFactoryLocator; import org.springframework.beans.factory.access.BeanFactoryReference; -import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.ApplicationContext; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.access.ContextSingletonBeanFactoryLocator; import org.springframework.context.support.GenericXmlApplicationContext; import org.springframework.core.convert.converter.Converter; @@ -114,9 +112,11 @@ import org.springframework.util.Assert; * how job instances are identified differently. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ public class JsrJobOperator implements JobOperator { + private static final String BATCH_PROPERTY_CONTEXT_BEAN_NAME = "batchPropertyContext"; private org.springframework.batch.core.launch.JobOperator batchJobOperator; private JobExplorer jobExplorer; @@ -406,10 +406,8 @@ public class JsrJobOperator implements JobOperator { } batchContext.setParent(baseContext); - GenericBeanDefinition bd = new GenericBeanDefinition(); - bd.setBeanClass(AutowiredAnnotationBeanPostProcessor.class); - batchContext.registerBeanDefinition("postProcessor", bd); batchContext.refresh(); + final Job job = batchContext.getBean(Job.class); if(!job.isRestartable()) { @@ -426,8 +424,12 @@ public class JsrJobOperator implements JobOperator { } try { - ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext)batchContext).getBeanFactory(); - factory.registerSingleton(job.getName() + "_" + jobExecution.getId() + "_jobContext", new JobContext(jobExecution, jobParametersConverter)); + ConfigurableListableBeanFactory factory = batchContext.getBeanFactory(); + + BatchPropertyContext batchPropertyContext = factory.getBean(BATCH_PROPERTY_CONTEXT_BEAN_NAME, BatchPropertyContext.class); + Properties properties = batchPropertyContext.getBatchProperties("job-" + job.getName()); + + factory.registerSingleton(job.getName() + "_" + jobExecution.getId() + "_jobContext", new JobContext(jobExecution, properties)); taskExecutor.execute(new Runnable() { @@ -485,9 +487,6 @@ public class JsrJobOperator implements JobOperator { } batchContext.setParent(baseContext); - GenericBeanDefinition bd = new GenericBeanDefinition(); - bd.setBeanClass(AutowiredAnnotationBeanPostProcessor.class); - batchContext.registerBeanDefinition("postProcessor", bd); batchContext.refresh(); final Job job = batchContext.getBean(Job.class); @@ -504,8 +503,12 @@ public class JsrJobOperator implements JobOperator { } try { - ConfigurableListableBeanFactory factory = ((ConfigurableApplicationContext)batchContext).getBeanFactory(); - factory.registerSingleton(job.getName() + "_" + jobExecution.getId() + "_jobContext", new JobContext(jobExecution, jobParametersConverter)); + ConfigurableListableBeanFactory factory = batchContext.getBeanFactory(); + + BatchPropertyContext batchPropertyContext = factory.getBean(BATCH_PROPERTY_CONTEXT_BEAN_NAME, BatchPropertyContext.class); + Properties properties = batchPropertyContext.getBatchProperties("job-" + job.getName()); + + factory.registerSingleton(job.getName() + "_" + jobExecution.getId() + "_jobContext", new JobContext(jobExecution, properties)); taskExecutor.execute(new Runnable() { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/JobContextTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/JobContextTests.java index 51d7e0992..d01de801a 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/JobContextTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/JobContextTests.java @@ -1,3 +1,18 @@ +/* + * Copyright 2013 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.jsr; import static org.junit.Assert.assertEquals; @@ -16,7 +31,6 @@ 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.converter.JobParametersConverter; public class JobContextTests { @@ -25,13 +39,15 @@ public class JobContextTests { private JobExecution execution; @Mock private JobInstance instance; - @Mock - private JobParametersConverter converter; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - context = new JobContext(execution, converter); + + Properties properties = new Properties(); + properties.put("jobLevelProperty1", "jobLevelValue1"); + + context = new JobContext(execution, properties); when(execution.getJobInstance()).thenReturn(instance); } @@ -68,19 +84,19 @@ public class JobContextTests { } @Test - public void testGetProperties() { + public void testJobParameters() { JobParameters params = new JobParametersBuilder() .addString("key1", "value1") .toJobParameters(); - Properties results = new Properties(); - results.put("key1", "value1"); when(execution.getJobParameters()).thenReturn(params); - when(converter.getProperties(params)).thenReturn(results); - Properties props = context.getProperties(); + assertEquals("value1", execution.getJobParameters().getString("key1")); + } - assertEquals("value1", props.get("key1")); + @Test + public void testJobProperties() { + assertEquals("jobLevelValue1", context.getProperties().get("jobLevelProperty1")); } @Test diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/StepContextTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/StepContextTests.java index 217aab984..5873378b1 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/StepContextTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/StepContextTests.java @@ -1,3 +1,18 @@ +/* + * Copyright 2013 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.jsr; import static org.junit.Assert.assertEquals; @@ -16,7 +31,6 @@ import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.StepExecution; -import org.springframework.batch.core.converter.JobParametersConverterSupport; import org.springframework.batch.item.ExecutionContext; public class StepContextTests { @@ -44,7 +58,10 @@ public class StepContextTests { executionContext = new ExecutionContext(); stepExecution.setExecutionContext(executionContext); - stepContext = new StepContext(stepExecution, new JobParametersConverterSupport()); + Properties properties = new Properties(); + properties.put("key", "value"); + + stepContext = new StepContext(stepExecution, properties); stepContext.setTransientUserData("This is my transient data"); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests.java new file mode 100644 index 000000000..04493a01f --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests.java @@ -0,0 +1,558 @@ +/* + * Copyright 2013 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.jsr.configuration.xml; + +import java.io.Serializable; +import java.util.List; +import java.util.Properties; +import javax.batch.api.BatchProperty; +import javax.batch.api.Batchlet; +import javax.batch.api.chunk.ItemProcessor; +import javax.batch.api.chunk.ItemReader; +import javax.batch.api.chunk.ItemWriter; +import javax.inject.Inject; +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.aop.framework.Advised; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.job.flow.FlowExecutionStatus; +import org.springframework.batch.core.job.flow.JobExecutionDecider; +import org.springframework.batch.core.jsr.StepContext; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.scope.context.StepSynchronizationManager; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + *

    + * Test cases for parsing various <properties /> elements defined by JSR-352. + *

    + * + * @author Chris Schaefer + */ +@ContextConfiguration +@RunWith(SpringJUnit4ClassRunner.class) +public class JobPropertyTests { + @Autowired + private TestItemReader testItemReader; + + @Autowired + private Job job; + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + private TestItemProcessor testItemProcessor; + + @Autowired + private TestItemWriter testItemWriter; + + @Autowired + private TestCheckpointAlgorithm testCheckpointAlgorithm; + + @Autowired + private TestDecider testDecider; + + @Autowired + private TestStepListener testStepListener; + + @Autowired + private TestBatchlet testBatchlet; + + @Autowired + private ApplicationContext applicationContext; + + @Test + public void testJobLevelPropertiesInItemReader() throws Exception { + assertEquals("jobPropertyValue1", testItemReader.getJobPropertyName1()); + assertEquals("jobPropertyValue2", testItemReader.getJobPropertyName2()); + } + + @Test + public void testStepContextProperties() throws Exception { + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + Properties step1Properties = new Properties(); + Properties step2Properties = new Properties(); + + for (StepExecution stepExecution : jobExecution.getStepExecutions()) { + try { + StepSynchronizationManager.register(stepExecution); + String contextBeanName = stepExecution.getStepName() + "stepContext"; + + // fix me? StepSynchronizationManager.context returns org.springframework.batch.core.scope.context.StepContext + StepContext stepContext = (StepContext) ((Advised)applicationContext.getBean(contextBeanName)).getTargetSource().getTarget(); + + if(contextBeanName.startsWith("step1")) { + step1Properties.putAll(stepContext.getProperties()); + } else { + step2Properties.putAll(stepContext.getProperties()); + } + } finally { + StepSynchronizationManager.close(); + } + } + + assertEquals(4, step1Properties.size()); + assertEquals("step1PropertyValue1", step1Properties.getProperty("step1PropertyName1")); + assertEquals("step1PropertyValue2", step1Properties.getProperty("step1PropertyName2")); + assertEquals("jobPropertyValue1", step1Properties.getProperty("jobPropertyName1")); + assertEquals("jobPropertyValue2", step1Properties.getProperty("jobPropertyName2")); + + assertEquals(4, step2Properties.size()); + assertEquals("step2PropertyValue1", step2Properties.getProperty("step2PropertyName1")); + assertEquals("step2PropertyValue2", step2Properties.getProperty("step2PropertyName2")); + assertEquals("jobPropertyValue1", step2Properties.getProperty("jobPropertyName1")); + assertEquals("jobPropertyValue2", step2Properties.getProperty("jobPropertyName2")); + + assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + } + + @Test + public void testItemReaderProperties() throws Exception { + assertEquals("readerPropertyValue1", testItemReader.getReaderPropertyName1()); + assertEquals("readerPropertyValue2", testItemReader.getReaderPropertyName2()); + assertEquals("annotationNamedReaderPropertyValue", testItemReader.getAnnotationNamedProperty()); + assertNull(testItemReader.getNotDefinedProperty()); + assertNull(testItemReader.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testItemProcessorProperties() throws Exception { + Assert.assertEquals("processorPropertyValue1", testItemProcessor.getProcessorPropertyName1()); + Assert.assertEquals("processorPropertyValue2", testItemProcessor.getProcessorPropertyName2()); + assertEquals("annotationNamedProcessorPropertyValue", testItemProcessor.getAnnotationNamedProperty()); + assertNull(testItemProcessor.getNotDefinedProperty()); + assertNull(testItemProcessor.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testItemWriterProperties() throws Exception { + Assert.assertEquals("writerPropertyValue1", testItemWriter.getWriterPropertyName1()); + Assert.assertEquals("writerPropertyValue2", testItemWriter.getWriterPropertyName2()); + assertEquals("annotationNamedWriterPropertyValue", testItemWriter.getAnnotationNamedProperty()); + assertNull(testItemWriter.getNotDefinedProperty()); + assertNull(testItemWriter.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testCheckpointAlgorithmProperties() throws Exception { + Assert.assertEquals("algorithmPropertyValue1", testCheckpointAlgorithm.getAlgorithmPropertyName1()); + Assert.assertEquals("algorithmPropertyValue2", testCheckpointAlgorithm.getAlgorithmPropertyName2()); + assertEquals("annotationNamedAlgorithmPropertyValue", testCheckpointAlgorithm.getAnnotationNamedProperty()); + assertNull(testCheckpointAlgorithm.getNotDefinedProperty()); + assertNull(testCheckpointAlgorithm.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testDeciderProperties() throws Exception { + Assert.assertEquals("deciderPropertyValue1", testDecider.getDeciderPropertyName1()); + Assert.assertEquals("deciderPropertyValue2", testDecider.getDeciderPropertyName2()); + assertEquals("annotationNamedDeciderPropertyValue", testDecider.getAnnotationNamedProperty()); + assertNull(testDecider.getNotDefinedProperty()); + assertNull(testDecider.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testStepListenerProperties() throws Exception { + Assert.assertEquals("stepListenerPropertyValue1", testStepListener.getStepListenerPropertyName1()); + Assert.assertEquals("stepListenerPropertyValue2", testStepListener.getStepListenerPropertyName2()); + assertEquals("annotationNamedStepListenerPropertyValue", testStepListener.getAnnotationNamedProperty()); + assertNull(testStepListener.getNotDefinedProperty()); + assertNull(testStepListener.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testBatchletProperties() throws Exception { + Assert.assertEquals("batchletPropertyValue1", testBatchlet.getBatchletPropertyName1()); + Assert.assertEquals("batchletPropertyValue2", testBatchlet.getBatchletPropertyName2()); + assertEquals("annotationNamedBatchletPropertyValue", testBatchlet.getAnnotationNamedProperty()); + assertNull(testBatchlet.getNotDefinedProperty()); + assertNull(testBatchlet.getNotDefinedAnnotationNamedProperty()); + } + + @Test + public void testFieldWithInjectAnnotationOnlyInjects() throws Exception { + assertNotNull(testItemReader.getInjectAnnotatedOnlyField()); + assertEquals("Chris", testItemReader.getInjectAnnotatedOnlyField().getName()); + } + + @Test + public void testFieldWithBatchPropertyAnnotationOnlyNoInjection() throws Exception { + assertNull(testItemReader.getBatchAnnotatedOnlyField()); + } + + public static final class TestItemReader implements ItemReader { + private int cnt; + + @Inject @BatchProperty String readerPropertyName1; + @Inject @BatchProperty String readerPropertyName2; + @Inject @BatchProperty(name = "annotationNamedReaderPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + @Inject @BatchProperty String jobPropertyName1; + @Inject @BatchProperty String jobPropertyName2; + @Inject InjectTestObj injectAnnotatedOnlyField; + @BatchProperty String batchAnnotatedOnlyField; + + @Override + public void open(Serializable serializable) throws Exception { + } + + @Override + public void close() throws Exception { + } + + @Override + public Object readItem() throws Exception { + if (cnt == 0) { + cnt++; + return "blah"; + } + + return null; + } + + @Override + public Serializable checkpointInfo() throws Exception { + return null; + } + + String getReaderPropertyName1() { + return readerPropertyName1; + } + + String getReaderPropertyName2() { + return readerPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + + String getJobPropertyName1() { + return jobPropertyName1; + } + + String getJobPropertyName2() { + return jobPropertyName2; + } + + InjectTestObj getInjectAnnotatedOnlyField() { + return injectAnnotatedOnlyField; + } + + String getBatchAnnotatedOnlyField() { + return batchAnnotatedOnlyField; + } + } + + public static final class TestItemProcessor implements ItemProcessor { + @Inject @BatchProperty String processorPropertyName1; + @Inject @BatchProperty String processorPropertyName2; + @Inject @BatchProperty(name = "annotationNamedProcessorPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public Object processItem(Object o) throws Exception { + return o; + } + + String getProcessorPropertyName1() { + return processorPropertyName1; + } + + String getProcessorPropertyName2() { + return processorPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static final class TestItemWriter implements ItemWriter { + @Inject @BatchProperty String writerPropertyName1; + @Inject @BatchProperty String writerPropertyName2; + @Inject @BatchProperty(name = "annotationNamedWriterPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public void open(Serializable serializable) throws Exception { + } + + @Override + public void close() throws Exception { + } + + @Override + public void writeItems(List objects) throws Exception { + System.out.println(objects); + } + + @Override + public Serializable checkpointInfo() throws Exception { + return null; + } + + String getWriterPropertyName1() { + return writerPropertyName1; + } + + String getWriterPropertyName2() { + return writerPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static final class TestCheckpointAlgorithm implements CompletionPolicy { + @Inject @BatchProperty String algorithmPropertyName1; + @Inject @BatchProperty String algorithmPropertyName2; + @Inject @BatchProperty(name = "annotationNamedAlgorithmPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public boolean isComplete(RepeatContext context, RepeatStatus result) { + return true; + } + + @Override + public boolean isComplete(RepeatContext context) { + return true; + } + + @Override + public RepeatContext start(RepeatContext parent) { + return parent; + } + + @Override + public void update(RepeatContext context) { + } + + String getAlgorithmPropertyName1() { + return algorithmPropertyName1; + } + + String getAlgorithmPropertyName2() { + return algorithmPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static class TestDecider implements JobExecutionDecider { + @Inject @BatchProperty String deciderPropertyName1; + @Inject @BatchProperty String deciderPropertyName2; + @Inject @BatchProperty(name = "annotationNamedDeciderPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public FlowExecutionStatus decide(JobExecution jobExecution, + StepExecution stepExecution) { + return new FlowExecutionStatus("step2"); + } + + String getDeciderPropertyName1() { + return deciderPropertyName1; + } + + String getDeciderPropertyName2() { + return deciderPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static class TestStepListener implements javax.batch.api.chunk.listener.ItemReadListener, + javax.batch.api.chunk.listener.ItemProcessListener, javax.batch.api.chunk.listener.ItemWriteListener { + @Inject @BatchProperty String stepListenerPropertyName1; + @Inject @BatchProperty String stepListenerPropertyName2; + @Inject @BatchProperty(name = "annotationNamedStepListenerPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public void beforeProcess(Object o) throws Exception { + } + + @Override + public void afterProcess(Object o, Object o2) throws Exception { + } + + @Override + public void onProcessError(Object o, Exception e) throws Exception { + } + + @Override + public void beforeRead() throws Exception { + } + + @Override + public void afterRead(Object o) throws Exception { + } + + @Override + public void onReadError(Exception e) throws Exception { + } + + @Override + public void beforeWrite(List objects) throws Exception { + } + + @Override + public void afterWrite(List objects) throws Exception { + } + + @Override + public void onWriteError(List objects, Exception e) throws Exception { + } + + String getStepListenerPropertyName1() { + return stepListenerPropertyName1; + } + + String getStepListenerPropertyName2() { + return stepListenerPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static class TestBatchlet implements Batchlet { + @Inject @BatchProperty String batchletPropertyName1; + @Inject @BatchProperty String batchletPropertyName2; + @Inject @BatchProperty(name = "annotationNamedBatchletPropertyName") String annotationNamedProperty; + @Inject @BatchProperty String notDefinedProperty; + @Inject @BatchProperty(name = "notDefinedAnnotationNamedProperty") String notDefinedAnnotationNamedProperty; + + @Override + public String process() throws Exception { + return null; + } + + @Override + public void stop() throws Exception { + } + + String getBatchletPropertyName1() { + return batchletPropertyName1; + } + + String getBatchletPropertyName2() { + return batchletPropertyName2; + } + + String getAnnotationNamedProperty() { + return annotationNamedProperty; + } + + String getNotDefinedProperty() { + return notDefinedProperty; + } + + String getNotDefinedAnnotationNamedProperty() { + return notDefinedAnnotationNamedProperty; + } + } + + public static class InjectTestObj { + private String name; + + public InjectTestObj(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } +} diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests-context.xml new file mode 100644 index 000000000..c92f35684 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/JobPropertyTests-context.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +