diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepInterceptor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepListener.java similarity index 91% rename from spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepInterceptor.java rename to spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepListener.java index 805fecc98..43cee0be8 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepInterceptor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/domain/StepListener.java @@ -20,15 +20,12 @@ import org.springframework.batch.repeat.ExitStatus; /** * @author Lucas Ward + * @author Dave Syer * */ -public interface StepInterceptor { +public interface StepListener { void open(ExecutionContext executionContext); - void beforeChunk(); - - void afterChunk(); - ExitStatus close(); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/CompositeStepListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/CompositeStepListener.java new file mode 100644 index 000000000..2c9722669 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/CompositeStepListener.java @@ -0,0 +1,90 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.interceptor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.core.domain.StepListener; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.repeat.ExitStatus; + +/** + * @author Dave Syer + * + */ +public class CompositeStepListener implements StepListener { + + private List listeners = new ArrayList(); + + /** + * Public setter for the listeners. + * + * @param listeners + */ + public void setListeners(StepListener[] listeners) { + this.listeners = Arrays.asList(listeners); + } + + /** + * Public setter for the listeners. The result will be as if + * {@link #setListeners(StepListener[])} was called with an array of length + * one. + * + * @param listener + */ + public void setListener(StepListener listener) { + setListeners(new StepListener[] {listener}); + } + + /** + * Register additional listener. + * + * @param stepListener + */ + public void register(StepListener stepListener) { + if (!listeners.contains(stepListener)) { + listeners.add(stepListener); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.domain.StepListener#close() + */ + public ExitStatus close() { + ExitStatus status = ExitStatus.CONTINUABLE; + for (Iterator iterator = listeners.iterator(); iterator.hasNext();) { + StepListener listener = (StepListener) iterator.next(); + status = status.and(listener.close()); + } + return status; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.domain.StepListener#open(org.springframework.batch.item.ExecutionContext) + */ + public void open(ExecutionContext executionContext) { + for (Iterator iterator = listeners.iterator(); iterator.hasNext();) { + StepListener listener = (StepListener) iterator.next(); + listener.open(executionContext); + } + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/StepListenerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/StepListenerSupport.java new file mode 100644 index 000000000..20a606d32 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/interceptor/StepListenerSupport.java @@ -0,0 +1,41 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.interceptor; + +import org.springframework.batch.core.domain.StepListener; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.repeat.ExitStatus; + +/** + * @author Dave Syer + * + */ +public class StepListenerSupport implements StepListener { + + /* (non-Javadoc) + * @see org.springframework.batch.core.domain.StepListener#close() + */ + public ExitStatus close() { + return ExitStatus.CONTINUABLE; + } + + /* (non-Javadoc) + * @see org.springframework.batch.core.domain.StepListener#open(org.springframework.batch.item.ExecutionContext) + */ + public void open(ExecutionContext executionContext) { + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/interceptor/CompositeStepListenerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/interceptor/CompositeStepListenerTests.java new file mode 100644 index 000000000..c7824226d --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/interceptor/CompositeStepListenerTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.interceptor; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.StepListener; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.repeat.ExitStatus; + +/** + * @author Dave Syer + * + */ +public class CompositeStepListenerTests extends TestCase { + + private CompositeStepListener listener = new CompositeStepListener(); + + private List list = new ArrayList(); + + /** + * Test method for + * {@link org.springframework.batch.core.interceptor.CompositeStepListener#setListeners(org.springframework.batch.core.domain.StepListener[])}. + */ + public void testSetListeners() { + listener.setListeners(new StepListener[] { new StepListenerSupport() { + public ExitStatus close() { + list.add("fail"); + return ExitStatus.FAILED; + } + }, new StepListenerSupport() { + public ExitStatus close() { + list.add("continue"); + return ExitStatus.CONTINUABLE; + } + } }); + assertFalse(listener.close().isContinuable()); + assertEquals(2, list.size()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.interceptor.CompositeStepListener#setListener(org.springframework.batch.core.domain.StepListener)}. + */ + public void testSetListener() { + listener.setListener(new StepListenerSupport() { + public ExitStatus close() { + list.add("fail"); + return ExitStatus.FAILED; + } + }); + assertFalse(listener.close().isContinuable()); + assertEquals(1, list.size()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.interceptor.CompositeStepListener#open(org.springframework.batch.item.ExecutionContext)}. + */ + public void testOpen() { + listener.setListener(new StepListenerSupport() { + public void open(ExecutionContext executionContext) { + list.add("foo"); + } + }); + listener.open(new ExecutionContext()); + assertEquals(1, list.size()); + } + +} diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/TaskletStep.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/TaskletStep.java index b28c24cfc..7f2483b52 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/TaskletStep.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/TaskletStep.java @@ -23,15 +23,12 @@ import org.springframework.batch.core.domain.BatchStatus; import org.springframework.batch.core.domain.JobInterruptedException; import org.springframework.batch.core.domain.Step; import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepListener; +import org.springframework.batch.core.interceptor.CompositeStepListener; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.tasklet.Tasklet; import org.springframework.batch.io.exception.BatchCriticalException; import org.springframework.batch.repeat.ExitStatus; -import org.springframework.batch.repeat.RepeatCallback; -import org.springframework.batch.repeat.RepeatContext; -import org.springframework.batch.repeat.RepeatListener; -import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; -import org.springframework.batch.repeat.support.RepeatTemplate; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; @@ -118,14 +115,16 @@ public class TaskletStep implements Step, InitializingBean, BeanNameAware { this.allowStartIfComplete = allowStartIfComplete; } - private RepeatListener[] listeners = new RepeatListener[] {}; + private CompositeStepListener listener = new CompositeStepListener(); - public void setListeners(RepeatListener[] listeners) { - this.listeners = listeners; + public void setListeners(StepListener[] listeners) { + for (int i = 0; i < listeners.length; i++) { + this.listener.register(listeners[i]); + } } - public void setListener(RepeatListener listener) { - listeners = new RepeatListener[] { listener }; + public void setListener(StepListener listener) { + this.listener.register(listener); } /** @@ -181,17 +180,8 @@ public class TaskletStep implements Step, InitializingBean, BeanNameAware { Exception fatalException = null; try { - // We are using the RepeatTemplate as a vehicle for the listener - // so it can be set up cheaply here with standard properties. - RepeatTemplate template = new RepeatTemplate(); - template.setCompletionPolicy(new SimpleCompletionPolicy(1)); - - template.setListeners(listeners); - exitStatus = template.iterate(new RepeatCallback() { - public ExitStatus doInIteration(RepeatContext context) throws Exception { - return tasklet.execute(); - } - }); + listener.open(stepExecution.getExecutionContext()); + exitStatus = tasklet.execute(); try { jobRepository.saveOrUpdateExecutionContext(stepExecution); @@ -216,6 +206,12 @@ public class TaskletStep implements Step, InitializingBean, BeanNameAware { finally { stepExecution.setExitStatus(exitStatus); stepExecution.setEndTime(new Date()); + try { + listener.close(); + } + catch (Exception e) { + logger.error("Encountered an error on listener close."); + } try { jobRepository.saveOrUpdate(stepExecution); } diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/TaskletStepTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/TaskletStepTests.java index d87120427..a31fa43fc 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/TaskletStepTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/TaskletStepTests.java @@ -11,14 +11,14 @@ import org.springframework.batch.core.domain.JobInstance; import org.springframework.batch.core.domain.JobInterruptedException; import org.springframework.batch.core.domain.JobParameters; import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.interceptor.StepListenerSupport; import org.springframework.batch.core.tasklet.Tasklet; import org.springframework.batch.execution.job.JobSupport; import org.springframework.batch.execution.scope.StepSynchronizationManager; import org.springframework.batch.execution.step.support.JobRepositorySupport; import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.repeat.ExitStatus; -import org.springframework.batch.repeat.RepeatContext; -import org.springframework.batch.repeat.interceptor.RepeatListenerSupport; public class TaskletStepTests extends TestCase { @@ -105,12 +105,13 @@ public class TaskletStepTests extends TestCase { public void testSuccessfulExecutionWithListener() throws Exception { TaskletStep step = new TaskletStep(new StubTasklet(false, false), new JobRepositorySupport()); - step.setListener(new RepeatListenerSupport() { - public void open(RepeatContext context) { + step.setListener(new StepListenerSupport() { + public void open(ExecutionContext context) { list.add("open"); } - public void close(RepeatContext context) { + public ExitStatus close() { list.add("close"); + return ExitStatus.CONTINUABLE; } }); step.execute(stepExecution); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java index 3f8c7badd..344ab85a4 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java @@ -17,6 +17,8 @@ package org.springframework.batch.repeat; import java.io.Serializable; +import org.springframework.util.StringUtils; + /** * Value object used to carry information about the status of a * {@link RepeatOperations}. @@ -41,8 +43,7 @@ public class ExitStatus implements Serializable { /** * Convenient constant value representing unfinished processing. */ - public static final ExitStatus CONTINUABLE = new ExitStatus(true, - "CONTINUABLE"); + public static final ExitStatus CONTINUABLE = new ExitStatus(true, "CONTINUABLE"); /** * Convenient constant value representing finished processing. @@ -74,8 +75,7 @@ public class ExitStatus implements Serializable { this(continuable, exitCode, ""); } - public ExitStatus(boolean continuable, String exitCode, - String exitDescription) { + public ExitStatus(boolean continuable, String exitCode, String exitDescription) { super(); this.continuable = continuable; this.exitCode = exitCode; @@ -116,14 +116,24 @@ public class ExitStatus implements Serializable { * Create a new {@link ExitStatus} with a logical combination of the * continuable flag. * - * @param continuable - * true if the caller thinks it is safe to continue. + * @param continuable true if the caller thinks it is safe to continue. * @return a new {@link ExitStatus} with {@link #isContinuable()} the - * logical and of the current value and the argument provided. + * logical and of the current value and the argument provided. */ public ExitStatus and(boolean continuable) { - return new ExitStatus(this.continuable && continuable, this.exitCode, - this.exitDescription); + return new ExitStatus(this.continuable && continuable, this.exitCode, this.exitDescription); + } + + /** + * Create a new {@link ExitStatus} with a logical combination of the + * continuable flag, and a con. + * + * @param status an {@link ExitStatus} to combine with this one. + * @return a new {@link ExitStatus} with {@link #isContinuable()} the + * logical and of the current value and the argument provided. + */ + public ExitStatus and(ExitStatus status) { + return and(status.continuable).addExitCode(status.exitCode).addExitDescription(status.exitDescription); } /* @@ -132,8 +142,7 @@ public class ExitStatus implements Serializable { * @see java.lang.Object#toString() */ public String toString() { - return "continuable=" + continuable + ";exitCode=" + exitCode - + ";exitDescription=" + exitDescription; + return "continuable=" + continuable + ";exitCode=" + exitCode + ";exitDescription=" + exitDescription; } /** @@ -158,36 +167,41 @@ public class ExitStatus implements Serializable { } /** - * Add an exit code to an existing {@link ExitStatus}. + * Add an exit code to an existing {@link ExitStatus}. If there is already + * a code present the two will be concatenated with a semicolon. * - * @param code - * the code to add + * @param code the code to add * @return a new {@link ExitStatus} with the same properties but a new exit - * code. + * code. */ public ExitStatus addExitCode(String code) { + if (StringUtils.hasText(exitCode) && StringUtils.hasLength(code) && !exitCode.equals(code)) { + code = exitCode + "; " + code; + } return new ExitStatus(continuable, code, exitDescription); } /** * Check if this status represents a running process. * - * @return tru eif the exit code is "RUNNING" or "UNKNOWN" + * @return true if the exit code is "RUNNING" or "UNKNOWN" */ public boolean isRunning() { - return "RUNNING".equals(this.exitCode) - || "UNKNOWN".equals(this.exitCode); + return "RUNNING".equals(this.exitCode) || "UNKNOWN".equals(this.exitCode); } /** - * Add an exit description to an existing {@link ExitStatus}. + * Add an exit description to an existing {@link ExitStatus}. If there is already + * a description present the two will be concatenated with a semicolon. * - * @param description - * the description to add + * @param description the description to add * @return a new {@link ExitStatus} with the same properties but a new exit - * description + * description */ public ExitStatus addExitDescription(String description) { + if (StringUtils.hasText(exitDescription) && StringUtils.hasText(description) &&!exitDescription.equals(description)) { + description = exitDescription + "; " + description; + } return new ExitStatus(continuable, exitCode, description); } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java index ae36454dd..e729efc74 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java @@ -104,7 +104,21 @@ public class ExitStatusTests extends TestCase { ExitStatus status = ExitStatus.CONTINUABLE.addExitCode("FOO"); assertTrue(ExitStatus.CONTINUABLE!=status); assertTrue(status.isContinuable()); - assertEquals("FOO", status.getExitCode()); + assertEquals("CONTINUABLE; FOO", status.getExitCode()); + } + + public void testAddExitCodeToExistingStatus() throws Exception { + ExitStatus status = ExitStatus.CONTINUABLE.addExitCode("FOO").addExitCode("BAR"); + assertTrue(ExitStatus.CONTINUABLE!=status); + assertTrue(status.isContinuable()); + assertEquals("CONTINUABLE; FOO; BAR", status.getExitCode()); + } + + public void testAddExitCodeToSameStatus() throws Exception { + ExitStatus status = ExitStatus.CONTINUABLE.addExitCode(ExitStatus.CONTINUABLE.getExitCode()); + assertTrue(ExitStatus.CONTINUABLE!=status); + assertTrue(status.isContinuable()); + assertEquals(ExitStatus.CONTINUABLE.getExitCode(), status.getExitCode()); } public void testAddExitDescription() throws Exception { @@ -114,9 +128,16 @@ public class ExitStatusTests extends TestCase { assertEquals("Foo", status.getExitDescription()); } + public void testAddExitDescriptionToSameStatus() throws Exception { + ExitStatus status = ExitStatus.CONTINUABLE.addExitDescription("Foo").addExitDescription("Foo"); + assertTrue(ExitStatus.CONTINUABLE!=status); + assertTrue(status.isContinuable()); + assertEquals("Foo", status.getExitDescription()); + } + public void testAddExitCodeWithDescription() throws Exception { ExitStatus status = new ExitStatus(true, "BAR", "Bar").addExitCode("FOO"); - assertEquals("FOO", status.getExitCode()); + assertEquals("BAR; FOO", status.getExitCode()); assertEquals("Bar", status.getExitDescription()); } @@ -136,6 +157,6 @@ public class ExitStatusTests extends TestCase { assertTrue(object instanceof ExitStatus); ExitStatus restored = (ExitStatus) object; assertTrue(restored.isContinuable()); - assertEquals("FOO", restored.getExitCode()); + assertEquals(status.getExitCode(), restored.getExitCode()); } }