diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java index 01ae1ad2b..1dd8c3bf2 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParser.java @@ -24,6 +24,9 @@ import org.springframework.batch.core.Step; import org.springframework.batch.core.job.flow.support.StateTransition; import org.springframework.batch.core.job.flow.support.state.EndState; import org.springframework.batch.core.job.flow.support.state.StepState; +import org.springframework.batch.core.listener.JobExecutionListenerFactoryBean; +import org.springframework.batch.core.listener.StepListenerFactoryBean; +import org.springframework.batch.core.listener.StepListenerMetaData; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; @@ -32,6 +35,7 @@ import org.springframework.beans.factory.parsing.BeanComponentDefinition; 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.support.ManagedMap; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.StringUtils; @@ -388,7 +392,7 @@ public class StepParser { DomUtils.getChildElementByTagName(element, "listeners"); if (listenersElement != null) { List listenerBeans = new ArrayList(); - handleListenerElements(parserContext, listenersElement, + handleStepListenerElements(parserContext, listenersElement, listenerBeans); ManagedList arguments = new ManagedList(); arguments.addAll(listenerBeans); @@ -452,6 +456,63 @@ public class StepParser { } } } + + @SuppressWarnings("unchecked") + private void handleStepListenerElements(ParserContext parserContext, + Element element, List beans) { + List listenerElements = + DomUtils.getChildElementsByTagName(element, "listener"); + if (listenerElements != null) { + for (Element listenerElement : listenerElements) { + BeanDefinitionBuilder listenerBuilder = BeanDefinitionBuilder.genericBeanDefinition(StepListenerFactoryBean.class); + String id = listenerElement.getAttribute("id"); + String listenerRef = listenerElement.getAttribute("ref"); + String className = listenerElement.getAttribute("class"); + if ((StringUtils.hasText(id) || StringUtils.hasText(className)) + && StringUtils.hasText(listenerRef)) { + NamedNodeMap attributeNodes = listenerElement.getAttributes(); + StringBuilder attributes = new StringBuilder(); + for (int i = 0; i < attributeNodes.getLength(); i++) { + if (i > 0) { + attributes.append(" "); + } + attributes.append(attributeNodes.item(i)); + } + throw new BeanCreationException("Both 'id' or 'ref' plus 'class' specified; use 'class' with an optional 'id' or just 'ref' for <" + + listenerElement.getTagName() + "> element with attributes: " + attributes); + } + if (StringUtils.hasText(listenerRef)) { + listenerBuilder.addPropertyReference("delegate", listenerRef); + } + else if (StringUtils.hasText(className)) { + RootBeanDefinition beanDef = new RootBeanDefinition(className, null, null); + String delegateId = parserContext.getReaderContext().generateBeanName(beanDef); + parserContext.getRegistry().registerBeanDefinition(delegateId, beanDef); + listenerBuilder.addPropertyReference("delegate", delegateId); + } + else { + throw new BeanCreationException("Neither 'ref' or 'class' specified for <" + listenerElement.getTagName() + "> element"); + } + + ManagedMap metaDataMap = new ManagedMap(); + for(StepListenerMetaData metaData: StepListenerMetaData.values()){ + String listenerMethod = listenerElement.getAttribute(metaData.getPropertyName()); + if(StringUtils.hasText(listenerMethod)){ + metaDataMap.put(metaData.getPropertyName(), listenerMethod); + } + } + listenerBuilder.addPropertyValue("metaDataMap", metaDataMap); + + AbstractBeanDefinition beanDef = listenerBuilder.getBeanDefinition(); + if (!StringUtils.hasText(id)) { + id = parserContext.getReaderContext().generateBeanName(beanDef); + } + parserContext.getRegistry().registerBeanDefinition(id, beanDef); + BeanReference bean = new RuntimeBeanReference(id); + beans.add(bean); + } + } + } @SuppressWarnings("unchecked") private void handleStreamsElement(Element element, RootBeanDefinition bd, ParserContext parserContext) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerFactoryBean.java index c6e1dbcf2..5141d58f5 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerFactoryBean.java @@ -30,6 +30,8 @@ import org.springframework.batch.core.StepListener; import org.springframework.batch.core.configuration.util.MethodInvoker; import org.springframework.batch.core.configuration.util.MethodInvokerUtils; import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; /** * {@link FactoryBean} implementation that builds a {@link StepListener} based on the @@ -54,31 +56,31 @@ import org.springframework.beans.factory.FactoryBean; * @since 2.0 * @see StepListenerMetaData */ -public class StepListenerFactoryBean implements FactoryBean{ +public class StepListenerFactoryBean implements FactoryBean, InitializingBean{ private Object delegate; - private Map metaDataMap; + private Map metaDataMap; public Object getObject() throws Exception { Map> invokerMap = new HashMap>(); if(metaDataMap == null){ - metaDataMap = new HashMap(); + metaDataMap = new HashMap(); } //Because all annotations and interfaces should be checked for, make sure that each meta data //entry is represented. for(StepListenerMetaData metaData : StepListenerMetaData.values()){ - if(!metaDataMap.containsKey(metaData)){ + if(!metaDataMap.containsKey(metaData.getPropertyName())){ //put null so that the annotation and interface is checked - metaDataMap.put(metaData, null); + metaDataMap.put(metaData.getPropertyName(), null); } } Set> listenerInterfaces = new HashSet>(); //For every entry in th emap, try and find a method by interface, name, or annotation. If the same - for(Entry entry : metaDataMap.entrySet()){ - StepListenerMetaData metaData = entry.getKey(); + for(Entry entry : metaDataMap.entrySet()){ + StepListenerMetaData metaData = StepListenerMetaData.fromPropertyName(entry.getKey()); Set invokers = new NullIgnoringSet(); invokers.add(getMethodInvokerByName(entry.getValue(), delegate, metaData.getParamTypes())); invokers.add(getMethodInvokerForInterface(metaData.getListenerInterface(), metaData.getMethodName(), @@ -119,7 +121,7 @@ public class StepListenerFactoryBean implements FactoryBean{ this.delegate = delegate; } - public void setMetaDataMap(Map metaDataMap) { + public void setMetaDataMap(Map metaDataMap) { this.metaDataMap = metaDataMap; } @@ -139,4 +141,8 @@ public class StepListenerFactoryBean implements FactoryBean{ } }; } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(delegate, "Delegate must not be null"); + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java index 9b784c32e..05c9e7575 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java @@ -16,6 +16,8 @@ package org.springframework.batch.core.listener; import java.lang.annotation.Annotation; +import java.util.HashMap; +import java.util.Map; import org.springframework.batch.core.ChunkListener; import org.springframework.batch.core.ItemProcessListener; @@ -52,34 +54,45 @@ import org.springframework.batch.core.annotation.OnWriteError; */ public enum StepListenerMetaData { - BEFORE_STEP("beforeStep", BeforeStep.class, StepExecutionListener.class, StepExecution.class), - AFTER_STEP("afterStep", AfterStep.class, StepExecutionListener.class, StepExecution.class), - BEFORE_CHUNK("beforeChunk", BeforeChunk.class, ChunkListener.class), - AFTER_CHUNK("afterChunk", AfterChunk.class, ChunkListener.class), - BEFORE_READ("beforeRead", BeforeRead.class, ItemReadListener.class), - AFTER_READ("afterRead", AfterRead.class, ItemReadListener.class, Object.class), - ON_READ_ERROR("onReadError", OnReadError.class, ItemReadListener.class, Exception.class), - BEFORE_PROCESS("beforeProcess", BeforeProcess.class, ItemProcessListener.class, Object.class), - AFTER_PROCESS("afterProcess", AfterProcess.class, ItemProcessListener.class, Object.class), - ON_PROCESS_ERROR("onProcessError", OnProcessError.class, ItemProcessListener.class, Object.class, Exception.class), - BEFORE_WRITE("beforeWrite", BeforeWrite.class, ItemWriteListener.class, Object.class), - AFTER_WRITE("afterWrite", AfterWrite.class, ItemWriteListener.class, Object.class), - ON_WRITE_ERROR("onWriteError", OnWriteError.class, ItemWriteListener.class, Object.class, Exception.class), - ON_SKIP_IN_READ("onSkipInRead", OnSkipInRead.class, SkipListener.class, Throwable.class), - ON_SKIP_IN_PROCESS("onSkipInProcess", OnSkipInProcess.class, SkipListener.class, Object.class, Throwable.class), - ON_SKIP_IN_WRITE("onSkipInWrite", OnSkipInWrite.class, SkipListener.class, Object.class, Throwable.class); + BEFORE_STEP("beforeStep", "before-step-method", BeforeStep.class, StepExecutionListener.class, StepExecution.class), + AFTER_STEP("afterStep", "after-step-method", AfterStep.class, StepExecutionListener.class, StepExecution.class), + BEFORE_CHUNK("beforeChunk", "before-chunk-method", BeforeChunk.class, ChunkListener.class), + AFTER_CHUNK("afterChunk", "after-chunk-method", AfterChunk.class, ChunkListener.class), + BEFORE_READ("beforeRead", "before-read-method", BeforeRead.class, ItemReadListener.class), + AFTER_READ("afterRead", "after-read-method", AfterRead.class, ItemReadListener.class, Object.class), + ON_READ_ERROR("onReadError", "on-read-error-method", OnReadError.class, ItemReadListener.class, Exception.class), + BEFORE_PROCESS("beforeProcess", "before-process-method", BeforeProcess.class, ItemProcessListener.class, Object.class), + AFTER_PROCESS("afterProcess", "after-process-method", AfterProcess.class, ItemProcessListener.class, Object.class), + ON_PROCESS_ERROR("onProcessError", "on-process-error-method", OnProcessError.class, ItemProcessListener.class, Object.class, Exception.class), + BEFORE_WRITE("beforeWrite", "before-write-method", BeforeWrite.class, ItemWriteListener.class, Object.class), + AFTER_WRITE("afterWrite", "after-write-method", AfterWrite.class, ItemWriteListener.class, Object.class), + ON_WRITE_ERROR("onWriteError", "on-write-error-method", OnWriteError.class, ItemWriteListener.class, Object.class, Exception.class), + ON_SKIP_IN_READ("onSkipInRead", "on-skip-in-read-method", OnSkipInRead.class, SkipListener.class, Throwable.class), + ON_SKIP_IN_PROCESS("onSkipInProcess", "on-skip-in-process-method", OnSkipInProcess.class, SkipListener.class, Object.class, Throwable.class), + ON_SKIP_IN_WRITE("onSkipInWrite", "on-skip-in-write-method", OnSkipInWrite.class, SkipListener.class, Object.class, Throwable.class); private final String methodName; + private final String propertyName; private final Class annotation; private final Class listenerInterface; private final Class[] paramTypes; + private static final Map propertyMap; - StepListenerMetaData(String methodName, Class annotation, Class listenerInterface, Class... paramTypes) { + StepListenerMetaData(String methodName, String propertyName, Class annotation, + Class listenerInterface, Class... paramTypes) { this.methodName = methodName; + this.propertyName = propertyName; this.annotation = annotation; this.listenerInterface = listenerInterface; this.paramTypes = paramTypes; } + + static{ + propertyMap = new HashMap(); + for(StepListenerMetaData metaData : values()){ + propertyMap.put(metaData.getPropertyName(), metaData); + } + } public String getMethodName() { return methodName; @@ -96,4 +109,18 @@ public enum StepListenerMetaData { public Class[] getParamTypes() { return paramTypes; } + + public String getPropertyName() { + return propertyName; + } + + /** + * Return the relevant meta data for the provided property name. + * + * @param propertyName + * @return meta data with supplied property name, null if none exists. + */ + public static StepListenerMetaData fromPropertyName(String propertyName){ + return propertyMap.get(propertyName); + } } diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd index 193d5ac2b..dbfde2269 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd @@ -271,7 +271,7 @@ - + @@ -450,6 +450,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TestListener.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TestListener.java index 8c6a2498b..0b34198f9 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TestListener.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TestListener.java @@ -3,6 +3,7 @@ package org.springframework.batch.core.configuration.xml; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; +import org.springframework.batch.core.annotation.AfterRead; public class TestListener extends AbstractTestComponent implements StepExecutionListener { @@ -13,5 +14,14 @@ public class TestListener extends AbstractTestComponent implements StepExecution public void beforeStep(StepExecution stepExecution) { executed = true; } + + public void destroy(){ + + } + + @AfterRead + public void logItem(){ + + } } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java index 43ae3998d..0889bd571 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/StepListenerFactoryBeanTests.java @@ -73,9 +73,9 @@ public class StepListenerFactoryBeanTests { public void testStepAndChunk() throws Exception{ factoryBean.setDelegate(testClass); - Map metaDataMap = new HashMap();; - metaDataMap.put(AFTER_STEP, "destroy"); - metaDataMap.put(AFTER_CHUNK, "afterChunk"); + Map metaDataMap = new HashMap();; + metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy"); + metaDataMap.put(AFTER_CHUNK.getPropertyName(), "afterChunk"); factoryBean.setMetaDataMap(metaDataMap); Object item = new Object(); List items = new ArrayList(); @@ -120,8 +120,8 @@ public class StepListenerFactoryBeanTests { //method name, that all three will be called ThreeStepExecutionListener delegate = new ThreeStepExecutionListener(); factoryBean.setDelegate(delegate); - Map metaDataMap = new HashMap();; - metaDataMap.put(AFTER_STEP, "destroy"); + Map metaDataMap = new HashMap();; + metaDataMap.put(AFTER_STEP.getPropertyName(), "destroy"); factoryBean.setMetaDataMap(metaDataMap); StepListener listener = (StepListener) factoryBean.getObject(); ((StepExecutionListener)listener).afterStep(stepExecution); @@ -132,8 +132,8 @@ public class StepListenerFactoryBeanTests { public void testAnnotatingInterfaceResultsInOneCall() throws Exception{ MultipleAfterStep delegate = new MultipleAfterStep(); factoryBean.setDelegate(delegate); - Map metaDataMap = new HashMap();; - metaDataMap.put(AFTER_STEP, "afterStep"); + Map metaDataMap = new HashMap();; + metaDataMap.put(AFTER_STEP.getPropertyName(), "afterStep"); factoryBean.setMetaDataMap(metaDataMap); StepListener listener = (StepListener) factoryBean.getObject(); ((StepExecutionListener)listener).afterStep(stepExecution); diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepWithBasicProcessTaskJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepWithBasicProcessTaskJobParserTests-context.xml index 200e78357..b766c4b33 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepWithBasicProcessTaskJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StepWithBasicProcessTaskJobParserTests-context.xml @@ -11,7 +11,8 @@ - + @@ -28,5 +29,7 @@ + + \ No newline at end of file