diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java index 479fb94ae..53550687a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java @@ -23,6 +23,9 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; +import javax.batch.api.chunk.listener.RetryProcessListener; +import javax.batch.api.chunk.listener.RetryReadListener; +import javax.batch.api.chunk.listener.RetryWriteListener; import javax.batch.api.chunk.listener.SkipProcessListener; import javax.batch.api.chunk.listener.SkipReadListener; import javax.batch.api.chunk.listener.SkipWriteListener; @@ -41,6 +44,9 @@ import org.springframework.batch.core.jsr.ChunkListenerAdapter; import org.springframework.batch.core.jsr.ItemProcessListenerAdapter; import org.springframework.batch.core.jsr.ItemReadListenerAdapter; import org.springframework.batch.core.jsr.ItemWriteListenerAdapter; +import org.springframework.batch.core.jsr.RetryProcessListenerAdapter; +import org.springframework.batch.core.jsr.RetryReadListenerAdapter; +import org.springframework.batch.core.jsr.RetryWriteListenerAdapter; import org.springframework.batch.core.jsr.SkipListenerAdapter; import org.springframework.batch.core.jsr.StepListenerAdapter; import org.springframework.batch.core.launch.JobLauncher; @@ -96,6 +102,7 @@ import org.springframework.util.Assert; * @author Dan Garrette * @author Josh Long * @author Michael Minella + * @author Chris Schaefer * @see SimpleStepFactoryBean * @see FaultTolerantStepFactoryBean * @see TaskletStep @@ -222,6 +229,8 @@ public class StepParserStepFactoryBean implements FactoryBean, BeanNameAwa private Set> skipListeners = new LinkedHashSet>(); + private Set jsrRetryListeners = new LinkedHashSet(); + // // Additional // @@ -333,6 +342,10 @@ public class StepParserStepFactoryBean implements FactoryBean, BeanNameAwa builder.listener(listener); } + for (org.springframework.batch.core.jsr.RetryListener listener : jsrRetryListeners) { + builder.listener(listener); + } + registerItemListeners(builder); if (skipPolicy != null) { @@ -797,6 +810,15 @@ public class StepParserStepFactoryBean implements FactoryBean, BeanNameAwa ItemProcessListener itemListener = new ItemProcessListenerAdapter((javax.batch.api.chunk.listener.ItemProcessListener) listener); processListeners.add(itemListener); } + if(listener instanceof RetryReadListener) { + jsrRetryListeners.add(new RetryReadListenerAdapter((RetryReadListener) listener)); + } + if(listener instanceof RetryProcessListener) { + jsrRetryListeners.add(new RetryProcessListenerAdapter((RetryProcessListener) listener)); + } + if(listener instanceof RetryWriteListener) { + jsrRetryListeners.add(new RetryWriteListenerAdapter((RetryWriteListener) listener)); + } } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JsrStepListenerMetaData.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JsrStepListenerMetaData.java index 8c6a35fd4..cb0fddbef 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JsrStepListenerMetaData.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/JsrStepListenerMetaData.java @@ -24,6 +24,9 @@ import javax.batch.api.chunk.listener.ChunkListener; import javax.batch.api.chunk.listener.ItemProcessListener; import javax.batch.api.chunk.listener.ItemReadListener; import javax.batch.api.chunk.listener.ItemWriteListener; +import javax.batch.api.chunk.listener.RetryProcessListener; +import javax.batch.api.chunk.listener.RetryReadListener; +import javax.batch.api.chunk.listener.RetryWriteListener; import javax.batch.api.chunk.listener.SkipProcessListener; import javax.batch.api.chunk.listener.SkipReadListener; import javax.batch.api.chunk.listener.SkipWriteListener; @@ -37,11 +40,11 @@ import org.springframework.batch.core.listener.StepListenerFactoryBean; * ties together the names of methods, their interfaces, and expected arguments. * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 * @see StepListenerFactoryBean */ public enum JsrStepListenerMetaData implements ListenerMetaData { - BEFORE_STEP("beforeStep", "jsr-before-step", StepListener.class), AFTER_STEP("afterStep", "jsr-after-step", StepListener.class), BEFORE_CHUNK("beforeChunk", "jsr-before-chunk", ChunkListener.class), @@ -58,7 +61,10 @@ public enum JsrStepListenerMetaData implements ListenerMetaData { AFTER_WRITE_ERROR("onWriteError", "jsr-after-write-error", ItemWriteListener.class, List.class, Exception.class), SKIP_READ("onSkipReadItem", "jsr-skip-read", SkipReadListener.class, Exception.class), SKIP_PROCESS("onSkipProcessItem", "jsr-skip-process", SkipProcessListener.class, Object.class, Exception.class), - SKIP_WRITE("onSkipWriteItem", "jsr-skip-write", SkipWriteListener.class, List.class, Exception.class); + SKIP_WRITE("onSkipWriteItem", "jsr-skip-write", SkipWriteListener.class, List.class, Exception.class), + RETRY_READ("onRetryReadException", "jsr-retry-read", RetryReadListener.class, Exception.class), + RETRY_PROCESS("onRetryProcessException", "jsr-retry-process", RetryProcessListener.class, Object.class, Exception.class), + RETRY_WRITE("onRetryWriteException", "jsr-retry-write", RetryWriteListener.class, List.class, Exception.class); private final String methodName; private final String propertyName; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryListener.java new file mode 100644 index 000000000..ee52e4345 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryListener.java @@ -0,0 +1,30 @@ +/* + * 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 org.springframework.batch.core.StepListener; + +/** + *

+ * Marker interface to be implemented by JSR-352 retry listeners. Extends {@link StepListener} + * to allow registration with existing listener methods. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public interface RetryListener extends StepListener { +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryProcessListenerAdapter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryProcessListenerAdapter.java new file mode 100644 index 000000000..2ae8c72c8 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryProcessListenerAdapter.java @@ -0,0 +1,44 @@ +/* + * 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 javax.batch.api.chunk.listener.RetryProcessListener; +import javax.batch.operations.BatchRuntimeException; + +/** + *

+ * Wrapper class to adapt a {@link RetryProcessListener} to a {@link RetryListener}. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class RetryProcessListenerAdapter implements RetryListener, RetryProcessListener { + private RetryProcessListener retryProcessListener; + + public RetryProcessListenerAdapter(RetryProcessListener retryProcessListener) { + this.retryProcessListener = retryProcessListener; + } + + @Override + public void onRetryProcessException(Object item, Exception ex) throws Exception { + try { + retryProcessListener.onRetryProcessException(item, ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryReadListenerAdapter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryReadListenerAdapter.java new file mode 100644 index 000000000..ea936bea6 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryReadListenerAdapter.java @@ -0,0 +1,44 @@ +/* + * 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 javax.batch.api.chunk.listener.RetryReadListener; +import javax.batch.operations.BatchRuntimeException; + +/** + *

+ * Wrapper class to adapt a {@link RetryReadListener} to a {@link RetryListener}. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class RetryReadListenerAdapter implements RetryListener, RetryReadListener { + private RetryReadListener retryReadListener; + + public RetryReadListenerAdapter(RetryReadListener retryReadListener) { + this.retryReadListener = retryReadListener; + } + + @Override + public void onRetryReadException(Exception ex) throws Exception { + try { + retryReadListener.onRetryReadException(ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryWriteListenerAdapter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryWriteListenerAdapter.java new file mode 100644 index 000000000..429cff2ed --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/RetryWriteListenerAdapter.java @@ -0,0 +1,45 @@ +/* + * 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 java.util.List; +import javax.batch.api.chunk.listener.RetryWriteListener; +import javax.batch.operations.BatchRuntimeException; + +/** + *

+ * Wrapper class to adapt a {@link RetryWriteListener} to a {@link RetryListener}. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class RetryWriteListenerAdapter implements RetryListener, RetryWriteListener { + private RetryWriteListener retryWriteListener; + + public RetryWriteListenerAdapter(RetryWriteListener retryWriteListener) { + this.retryWriteListener = retryWriteListener; + } + + @Override + public void onRetryWriteException(List items, Exception ex) throws Exception { + try { + retryWriteListener.onRetryWriteException(items, ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } +} 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 34e275ef6..634d8c141 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 @@ -69,6 +69,6 @@ public class JobParser extends AbstractSingleBeanDefinitionParser { parserContext.getRegistry().registerBeanDefinition("stepContextFactory", stepContextBeanDefinition); - new ListnerParser(JobListenerFactoryBean.class, "jobExecutionListeners").parseListeners(element, parserContext, builder); + new ListenerParser(JobListenerFactoryBean.class, "jobExecutionListeners").parseListeners(element, parserContext, builder); } } 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/ListenerParser.java similarity index 97% rename from spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListnerParser.java rename to spring-batch-core/src/main/java/org/springframework/batch/core/jsr/configuration/xml/ListenerParser.java index c9bfc2527..c57c817b4 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/ListenerParser.java @@ -33,9 +33,10 @@ import org.w3c.dom.Element; * and not JSR interfaces * * @author Michael Minella + * @author Chris Schaefer * @since 3.0 */ -public class ListnerParser { +public class ListenerParser { private static final String REF_ATTRIBUTE = "ref"; private static final String LISTENER_ELEMENT = "listener"; private static final String LISTENERS_ELEMENT = "listeners"; @@ -44,7 +45,7 @@ public class ListnerParser { private String propertyKey; @SuppressWarnings("rawtypes") - public ListnerParser(Class listenerType, String propertyKey) { + public ListenerParser(Class listenerType, String propertyKey) { this.propertyKey = propertyKey; this.listenerType = listenerType; } 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 f578ce82f..9bc4db1d3 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 @@ -70,7 +70,7 @@ public class StepParser extends AbstractSingleBeanDefinitionParser { allowStartIfComplete); } - new ListnerParser(StepListenerFactoryBean.class, "listeners").parseListeners(element, parserContext, bd, stepName); + new ListenerParser(StepListenerFactoryBean.class, "listeners").parseListeners(element, parserContext, bd, stepName); new PropertyParser(stepName, parserContext, BatchArtifact.BatchArtifactType.STEP).parseProperties(element); // look at all nested elements @@ -91,8 +91,6 @@ public class StepParser extends AbstractSingleBeanDefinitionParser { } } - Collection nextElements = FlowParser.getNextElements(parserContext, stepName, stateBuilder.getBeanDefinition(), element); - - return nextElements; + return FlowParser.getNextElements(parserContext, stepName, stateBuilder.getBeanDefinition(), element); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/job/flow/JsrFlowExecutor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/job/flow/JsrFlowExecutor.java index ebbd9b122..c2c2e2435 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/job/flow/JsrFlowExecutor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/job/flow/JsrFlowExecutor.java @@ -42,7 +42,7 @@ public class JsrFlowExecutor extends JobFlowExecutor { */ @Override public void addExitStatus(String code) { - if((exitStatus != null && isNonDefaultExitStauts(exitStatus.getExitCode())) && !isNonDefaultExitStauts(code)) { + if((exitStatus != null && isNonDefaultExitStatus(exitStatus.getExitCode())) && !isNonDefaultExitStatus(code)) { exitStatus = exitStatus.and(new ExitStatus(code)); } } @@ -57,7 +57,7 @@ public class JsrFlowExecutor extends JobFlowExecutor { execution.setStatus(findBatchStatus(status)); ExitStatus curStatus = execution.getExitStatus(); - if(isNonDefaultExitStauts(curStatus.getExitCode())) { + if(isNonDefaultExitStatus(curStatus.getExitCode())) { exitStatus = exitStatus.and(new ExitStatus(status.getName())); execution.setExitStatus(exitStatus); } @@ -67,7 +67,7 @@ public class JsrFlowExecutor extends JobFlowExecutor { * @param curStatus the exit code to be evaluated * @return true if the value matches a known exit code */ - protected boolean isNonDefaultExitStauts(String curStatus) { + protected boolean isNonDefaultExitStatus(String curStatus) { return curStatus == null || curStatus.equals(ExitStatus.COMPLETED.getExitCode()) || curStatus.equals(ExitStatus.EXECUTING.getExitCode()) || diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/builder/JsrFaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/builder/JsrFaultTolerantStepBuilder.java index 1695cfc00..e0c64960f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/builder/JsrFaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/builder/JsrFaultTolerantStepBuilder.java @@ -17,6 +17,7 @@ package org.springframework.batch.core.jsr.step.builder; import java.util.ArrayList; +import java.util.List; import org.springframework.batch.core.StepListener; import org.springframework.batch.core.jsr.step.item.JsrChunkProvider; import org.springframework.batch.core.jsr.step.item.JsrFaultTolerantChunkProcessor; @@ -33,6 +34,7 @@ import org.springframework.batch.core.step.skip.SkipPolicy; * pattern defined by the spec as well as skip/retry logic. * * @author Michael Minella + * @author Chris Schaefer * * @param The input type for the step * @param The output type for the step @@ -70,10 +72,17 @@ public class JsrFaultTolerantStepBuilder extends FaultTolerantStepBuilder< chunkProcessor.setRollbackClassifier(getRollbackClassifier()); detectStreamInReader(); chunkProcessor.setChunkMonitor(getChunkMonitor()); - ArrayList listeners = new ArrayList(getItemListeners()); - listeners.addAll(getSkipListeners()); - chunkProcessor.setListeners(listeners); + chunkProcessor.setListeners(getChunkListeners()); return chunkProcessor; } + + private List getChunkListeners() { + List listeners = new ArrayList(); + listeners.addAll(getItemListeners()); + listeners.addAll(getSkipListeners()); + listeners.addAll(getJsrRetryListeners()); + + return listeners; + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/item/JsrFaultTolerantChunkProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/item/JsrFaultTolerantChunkProcessor.java index d3352d923..2f45c5270 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/item/JsrFaultTolerantChunkProcessor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/jsr/step/item/JsrFaultTolerantChunkProcessor.java @@ -17,6 +17,7 @@ package org.springframework.batch.core.jsr.step.item; import java.util.List; +import javax.batch.operations.BatchRuntimeException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.batch.core.StepContribution; @@ -45,12 +46,12 @@ import org.springframework.util.Assert; * Extension of the {@link JsrChunkProcessor} that adds skip and retry functionality. * * @author Michael Minella + * @author Chris Schaefer * * @param input type for the step * @param output type for the step */ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor { - protected final Log logger = LogFactory.getLog(getClass()); private SkipPolicy skipPolicy = new LimitCheckingItemSkipPolicy(); private Classifier rollbackClassifier = new BinaryExceptionClassifier(true); @@ -58,10 +59,6 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor private ChunkMonitor chunkMonitor = new ChunkMonitor(); private boolean hasProcessor = false; - public JsrFaultTolerantChunkProcessor() { - this(null, null, null, null, null); - } - public JsrFaultTolerantChunkProcessor(ItemReader reader, ItemProcessor processor, ItemWriter writer, RepeatOperations repeatTemplate, BatchRetryTemplate batchRetryTemplate) { super(reader, processor, writer, repeatTemplate); hasProcessor = processor != null; @@ -148,10 +145,14 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor logger.debug("Skipping failed input", e); } else { - if (rollbackClassifier.classify(e)) { + getListener().onRetryReadException(e); + + if(rollbackClassifier.classify(e)) { + throw e; + } + else { throw e; } - logger.debug("No-rollback for non-skippable exception (ignored)", e); } } } @@ -174,7 +175,8 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor // allows us to continue throw new RetryException("Non-skippable exception in recoverer while reading", e); } - return null; + + throw new BatchRuntimeException(e); } } @@ -234,13 +236,17 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor // If not re-throwing then the listener will not be // called in next chunk. getListener().onSkipInProcess(item, e); - } else if (rollbackClassifier.classify(e)) { - // Default is to rollback unless the classifier - // allows us to continue - throw e; - } - else { - throw e; + } else { + getListener().onRetryProcessException(item, e); + + if (rollbackClassifier.classify(e)) { + // Default is to rollback unless the classifier + // allows us to continue + throw e; + } + else { + throw e; + } } } return null; @@ -249,7 +255,6 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor }; RecoveryCallback recoveryCallback = new RecoveryCallback() { - @Override public O recover(RetryContext context) throws Exception { Throwable e = context.getLastThrowable(); @@ -264,10 +269,10 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor // allows us to continue throw new RetryException("Non-skippable exception in recoverer while processing", e); } - return null; + + throw new BatchRuntimeException(e); } } - }; return batchRetryTemplate.execute(retryCallback, recoveryCallback); @@ -295,8 +300,12 @@ public class JsrFaultTolerantChunkProcessor extends JsrChunkProcessor catch (Exception e) { if(shouldSkip(skipPolicy, e, contribution.getStepSkipCount())) { getListener().onSkipInWrite(chunk.getItems(), e); - } else if (rollbackClassifier.classify(e)) { - throw e; + } else { + getListener().onRetryWriteException(chunk.getItems(), e); + + if (rollbackClassifier.classify(e)) { + throw e; + } } /* * If the exception is marked as no-rollback, we need to diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryProcessListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryProcessListener.java new file mode 100644 index 000000000..9a7ae5b14 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryProcessListener.java @@ -0,0 +1,62 @@ +/* + * 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.listener; + +import java.util.Iterator; +import java.util.List; +import javax.batch.api.chunk.listener.RetryProcessListener; + +/** + *

+ * Composite class holding {@link RetryProcessListener}'s. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class CompositeRetryProcessListener implements RetryProcessListener { + private OrderedComposite listeners = new OrderedComposite(); + + /** + *

+ * Public setter for the {@link RetryProcessListener}'s. + *

+ * + * @param listeners the {@link RetryProcessListener}'s to set + */ + public void setListeners(List listeners) { + this.listeners.setItems(listeners); + } + + /** + *

+ * Register an additional {@link RetryProcessListener}. + *

+ * + * @param listener the {@link RetryProcessListener} to register + */ + public void register(RetryProcessListener listener) { + listeners.add(listener); + } + + @Override + public void onRetryProcessException(Object item, Exception ex) throws Exception { + for (Iterator iterator = listeners.reverse(); iterator.hasNext();) { + RetryProcessListener listener = iterator.next(); + listener.onRetryProcessException(item, ex); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryReadListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryReadListener.java new file mode 100644 index 000000000..a3389e51c --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryReadListener.java @@ -0,0 +1,62 @@ +/* + * 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.listener; + +import java.util.Iterator; +import java.util.List; +import javax.batch.api.chunk.listener.RetryReadListener; + +/** + *

+ * Composite class holding {@link RetryReadListener}'s. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class CompositeRetryReadListener implements RetryReadListener { + private OrderedComposite listeners = new OrderedComposite(); + + /** + *

+ * Public setter for the {@link RetryReadListener}'s. + *

+ * + * @param listeners the {@link RetryReadListener}'s to set + */ + public void setListeners(List listeners) { + this.listeners.setItems(listeners); + } + + /** + *

+ * Register an additional {@link RetryReadListener}. + *

+ * + * @param listener the {@link RetryReadListener} to register + */ + public void register(RetryReadListener listener) { + listeners.add(listener); + } + + @Override + public void onRetryReadException(Exception ex) throws Exception { + for (Iterator iterator = listeners.reverse(); iterator.hasNext();) { + RetryReadListener listener = iterator.next(); + listener.onRetryReadException(ex); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryWriteListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryWriteListener.java new file mode 100644 index 000000000..af708b0f8 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/CompositeRetryWriteListener.java @@ -0,0 +1,62 @@ +/* + * 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.listener; + +import java.util.Iterator; +import java.util.List; +import javax.batch.api.chunk.listener.RetryWriteListener; + +/** + *

+ * Composite class holding {@link RetryWriteListener}'s. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class CompositeRetryWriteListener implements RetryWriteListener { + private OrderedComposite listeners = new OrderedComposite(); + + /** + *

+ * Public setter for the {@link RetryWriteListener}'s. + *

+ * + * @param listeners the {@link RetryWriteListener}'s to set + */ + public void setListeners(List listeners) { + this.listeners.setItems(listeners); + } + + /** + *

+ * Register an additional {@link RetryWriteListener}. + *

+ * + * @param listener the {@link RetryWriteListener} to register + */ + public void register(RetryWriteListener listener) { + listeners.add(listener); + } + + @Override + public void onRetryWriteException(List items, Exception ex) throws Exception { + for (Iterator iterator = listeners.reverse(); iterator.hasNext();) { + RetryWriteListener listener = iterator.next(); + listener.onRetryWriteException(items, ex); + } + } +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java index 9884a7549..2e15b2d95 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/MulticasterBatchListener.java @@ -17,6 +17,10 @@ package org.springframework.batch.core.listener; import java.util.List; +import javax.batch.api.chunk.listener.RetryProcessListener; +import javax.batch.api.chunk.listener.RetryReadListener; +import javax.batch.api.chunk.listener.RetryWriteListener; +import javax.batch.operations.BatchRuntimeException; import org.springframework.batch.core.ChunkListener; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.ItemProcessListener; @@ -32,9 +36,10 @@ import org.springframework.batch.item.ItemStream; /** * @author Dave Syer * @author Michael Minella + * @author Chris Schaefer */ public class MulticasterBatchListener implements StepExecutionListener, ChunkListener, ItemReadListener, -ItemProcessListener, ItemWriteListener, SkipListener { +ItemProcessListener, ItemWriteListener, SkipListener, RetryReadListener, RetryProcessListener, RetryWriteListener { private CompositeStepExecutionListener stepListener = new CompositeStepExecutionListener(); @@ -48,6 +53,12 @@ ItemProcessListener, ItemWriteListener, SkipListener { private CompositeSkipListener skipListener = new CompositeSkipListener(); + private CompositeRetryReadListener retryReadListener = new CompositeRetryReadListener(); + + private CompositeRetryProcessListener retryProcessListener = new CompositeRetryProcessListener(); + + private CompositeRetryWriteListener retryWriteListener = new CompositeRetryWriteListener(); + /** * Initialize the listener instance. */ @@ -99,6 +110,15 @@ ItemProcessListener, ItemWriteListener, SkipListener { SkipListener skipListener = (SkipListener) listener; this.skipListener.register(skipListener); } + if(listener instanceof RetryReadListener) { + this.retryReadListener.register((RetryReadListener) listener); + } + if(listener instanceof RetryProcessListener) { + this.retryProcessListener.register((RetryProcessListener) listener); + } + if(listener instanceof RetryWriteListener) { + this.retryWriteListener.register((RetryWriteListener) listener); + } } /** @@ -327,4 +347,31 @@ ItemProcessListener, ItemWriteListener, SkipListener { throw new StepListenerFailedException("Error in afterFailedChunk.", e); } } + + @Override + public void onRetryReadException(Exception ex) throws Exception { + try { + retryReadListener.onRetryReadException(ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } + + @Override + public void onRetryProcessException(Object item, Exception ex) throws Exception { + try { + retryProcessListener.onRetryProcessException(item, ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } + + @Override + public void onRetryWriteException(List items, Exception ex) throws Exception { + try { + retryWriteListener.onRetryWriteException(items, ex); + } catch (Exception e) { + throw new BatchRuntimeException(e); + } + } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java index a5c29530c..16b709b77 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.batch.operations.BatchRuntimeException; import org.springframework.batch.core.ChunkListener; import org.springframework.batch.core.JobInterruptedException; import org.springframework.batch.core.SkipListener; @@ -80,6 +81,7 @@ import org.springframework.util.Assert; * additional properties for retry and skip of failed items. * * @author Dave Syer + * @author Chris Schaefer * * @since 2.2 */ @@ -113,6 +115,8 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { private Set> skipListeners = new LinkedHashSet>(); + private Set jsrRetryListeners = new LinkedHashSet(); + private int skipLimit = 0; private SkipPolicy skipPolicy; @@ -186,6 +190,11 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { return this; } + public FaultTolerantStepBuilder listener(org.springframework.batch.core.jsr.RetryListener listener) { + jsrRetryListeners.add(listener); + return this; + } + @Override public FaultTolerantStepBuilder listener(ChunkListener listener) { super.listener(new TerminateOnExceptionChunkListenerDelegate(listener)); @@ -434,7 +443,8 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { JobInterruptedException.class, Error.class); addNonRetryableExceptionIfMissing(SkipLimitExceededException.class, NonSkippableReadException.class, TransactionException.class, FatalStepExecutionException.class, SkipListenerFailedException.class, - SkipPolicyFailedException.class, RetryException.class, JobInterruptedException.class, Error.class); + SkipPolicyFailedException.class, RetryException.class, JobInterruptedException.class, Error.class, + BatchRuntimeException.class); } protected void detectStreamInReader() { @@ -592,6 +602,10 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder { return skipListeners; } + protected Set getJsrRetryListeners() { + return jsrRetryListeners; + } + /** * Wrap the provided {@link #setRetryPolicy(RetryPolicy)} so that it never retries explicitly non-retryable * exceptions. diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTests.java new file mode 100644 index 000000000..4d8c1dd4a --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTests.java @@ -0,0 +1,244 @@ +/* + * 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.Collection; +import java.util.List; +import javax.batch.api.chunk.ItemProcessor; +import javax.batch.api.chunk.ItemReader; +import javax.batch.api.chunk.ItemWriter; +import javax.batch.api.chunk.listener.RetryProcessListener; +import javax.batch.api.chunk.listener.RetryReadListener; +import javax.batch.api.chunk.listener.RetryWriteListener; +import javax.batch.operations.BatchRuntimeException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.Test; +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.launch.JobLauncher; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.retry.RetryException; +import org.springframework.util.Assert; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + *

+ * Test cases around JSR-352 retry listeners. + *

+ * + * @author Chris Schaefer + * @since 3.0 + */ +public class RetryListenerTests { + private static final Log LOG = LogFactory.getLog(RetryListenerTests.class); + + @Test + public void testReadRetryExhausted() throws Exception { + ApplicationContext context = new ClassPathXmlApplicationContext("org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerExhausted.xml"); + + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + JobExecution jobExecution = jobLauncher.run(context.getBean(Job.class), new JobParameters()); + + List failureExceptions = jobExecution.getAllFailureExceptions(); + assertTrue("Expected 1 failure exceptions", failureExceptions.size() == 1); + assertTrue("Failure exception must be of type RetryException", (failureExceptions.get(0) instanceof RetryException)); + assertTrue("Exception cause must be of type IllegalArgumentException", (failureExceptions.get(0).getCause() instanceof IllegalArgumentException)); + + assertEquals(ExitStatus.FAILED, jobExecution.getExitStatus()); + } + + @Test + public void testReadRetryOnce() throws Exception { + ApplicationContext context = new ClassPathXmlApplicationContext("org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerRetryOnce.xml"); + + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + JobExecution jobExecution = jobLauncher.run(context.getBean(Job.class), new JobParameters()); + + Collection stepExecutions = jobExecution.getStepExecutions(); + assertEquals(1, stepExecutions.size()); + + StepExecution stepExecution = stepExecutions.iterator().next(); + assertEquals(1, stepExecution.getCommitCount()); + assertEquals(2, stepExecution.getReadCount()); + + assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + } + + @Test + public void testReadRetryExceptionInListener() throws Exception { + ApplicationContext context = new ClassPathXmlApplicationContext("org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerListenerException.xml"); + + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + JobExecution jobExecution = jobLauncher.run(context.getBean(Job.class), new JobParameters()); + + List failureExceptions = jobExecution.getAllFailureExceptions(); + assertTrue("Failure exceptions must equal one", failureExceptions.size() == 1); + assertTrue("Failure exception must be of type RetryException", (failureExceptions.get(0) instanceof RetryException)); + assertTrue("Exception cause must be of type BatchRuntimeException", (failureExceptions.get(0).getCause() instanceof BatchRuntimeException)); + + assertEquals(ExitStatus.FAILED, jobExecution.getExitStatus()); + } + + public static class ExceptionThrowingRetryReadListener implements RetryReadListener { + @Override + public void onRetryReadException(Exception ex) throws Exception { + Assert.isInstanceOf(IllegalArgumentException.class, ex); + throw new IllegalStateException(); + } + } + + public static class TestRetryReadListener implements RetryReadListener { + @Override + public void onRetryReadException(Exception ex) throws Exception { + Assert.isInstanceOf(IllegalArgumentException.class, ex); + } + } + + public static class TestRetryProcessListener implements RetryProcessListener { + @Override + public void onRetryProcessException(Object item, Exception ex) throws Exception { + Assert.isInstanceOf(String.class, item); + + String currentItem = (String) item; + + Assert.isTrue("three".equals(currentItem)); + Assert.isInstanceOf(IllegalArgumentException.class, ex); + } + } + + public static class TestRetryWriteListener implements RetryWriteListener { + @Override + public void onRetryWriteException(List items, Exception ex) throws Exception { + Assert.isTrue(items.size() == 2, "Must be two items to write"); + Assert.isTrue(items.contains("three"), "Items must contain the string 'three'"); + Assert.isTrue(items.contains("one"), "Items must contain the string 'one'"); + Assert.isInstanceOf(IllegalArgumentException.class, ex); + } + } + + public static class AlwaysFailItemReader implements ItemReader { + @Override + public void open(Serializable checkpoint) throws Exception { + } + + @Override + public void close() throws Exception { + } + + @Override + public Object readItem() throws Exception { + throw new IllegalArgumentException(); + } + + @Override + public Serializable checkpointInfo() throws Exception { + return null; + } + } + + public static class FailOnceItemReader implements ItemReader { + private int cnt; + + @Override + public void open(Serializable checkpoint) throws Exception { + } + + @Override + public void close() throws Exception { + } + + @Override + public Object readItem() throws Exception { + if(cnt == 0) { + cnt++; + return "one"; + } else if (cnt == 1) { + cnt++; + throw new IllegalArgumentException(); + } else if (cnt == 2) { + cnt++; + return "three"; + } + + return null; + } + + @Override + public Serializable checkpointInfo() throws Exception { + return null; + } + } + + public static class FailOnceItemProcessor implements ItemProcessor { + private int cnt; + + @Override + public Object processItem(Object item) throws Exception { + if(cnt == 0) { + cnt++; + return "one"; + } else if (cnt == 1) { + cnt++; + throw new IllegalArgumentException(); + } else if (cnt == 2) { + cnt++; + return "three"; + } + + return null; + } + } + + public static class FailOnceItemWriter implements ItemWriter { + private int cnt; + + @Override + public void open(Serializable checkpoint) throws Exception { + } + + @Override + public void close() throws Exception { + } + + @Override + public void writeItems(List items) throws Exception { + for(Object item : items) { + if(cnt == 0) { + cnt++; + LOG.info("one"); + } else if (cnt == 1) { + cnt++; + throw new IllegalArgumentException(); + } else if (cnt == 2) { + cnt++; + LOG.info("three"); + } + } + } + + @Override + public Serializable checkpointInfo() throws Exception { + return null; + } + } +} diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTestBase-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTestBase-context.xml new file mode 100644 index 000000000..d089a61e7 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryListenerTestBase-context.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerExhausted.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerExhausted.xml new file mode 100644 index 000000000..72425d840 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerExhausted.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerListenerException.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerListenerException.xml new file mode 100644 index 000000000..1e0d96615 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerListenerException.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerRetryOnce.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerRetryOnce.xml new file mode 100644 index 000000000..5bd61ae29 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/jsr/configuration/xml/RetryReadListenerRetryOnce.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + +