From 00ca5ce850d7980da52014b856939ed17064e40b Mon Sep 17 00:00:00 2001 From: dsyer Date: Thu, 24 Jan 2008 16:22:56 +0000 Subject: [PATCH] RESOLVED - issue BATCH-88: StatisticsProvider is a leaky abstraction http://jira.springframework.org/browse/BATCH-88 --- .../support/CommandLineJobRunner.java | 263 ------------------ .../support/DefaultJobParametersFactory.java | 1 - .../ScheduledJobParametersFactory.java | 1 - .../execution/scope/SimpleStepContext.java | 54 +++- .../batch/execution/scope/StepContext.java | 8 +- .../batch/execution/scope/StepScope.java | 39 +-- .../scope/StepSynchronizationManager.java | 80 ------ .../step/simple/SimpleStepExecutor.java | 60 ++-- .../tasklet/ItemOrientedTasklet.java | 102 +------ .../support/CommandLineJobRunnerTests.java | 154 ---------- .../BatchResourceFactoryBeanTests.java | 3 +- .../scope/SimpleStepContextTests.java | 93 ++++++- .../scope/StepContextAwareStepScopeTests.java | 16 +- .../batch/execution/scope/StepScopeTests.java | 6 +- ...ests.java => SimpleStepExecutorTests.java} | 45 ++- .../tasklet/ItemOrientedTaskletTests.java | 35 --- .../processor/CompositeItemProcessor.java | 20 +- .../processor/ItemWriterItemProcessor.java | 41 +-- .../item/reader/DelegatingItemReader.java | 18 +- .../statistics/SimpleStatisticsService.java | 89 ++++++ .../batch/statistics/StatisticsProvider.java | 2 +- .../batch/statistics/StatisticsService.java | 50 ++++ .../CompositeItemProcessorTests.java | 18 -- .../ItemWriterItemProcessorTests.java | 18 -- .../reader/DelegatingItemReaderTests.java | 8 - .../SimpleStatisticsServiceTests.java | 72 +++++ .../sample/tasklet/SimpleTradeTasklet.java | 2 +- .../processor/StagingItemProcessorTests.java | 21 +- .../item/reader/StagingItemReaderTests.java | 69 +++-- 29 files changed, 522 insertions(+), 866 deletions(-) delete mode 100644 spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java delete mode 100644 spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java delete mode 100644 spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java rename spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/{DefaultStepExecutorTests.java => SimpleStepExecutorTests.java} (90%) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/SimpleStatisticsService.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsService.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/statistics/SimpleStatisticsServiceTests.java diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java deleted file mode 100644 index a0cdde6ea..000000000 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunner.java +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright 2006-2008 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.execution.bootstrap.support; - -import java.util.Properties; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.batch.core.domain.Job; -import org.springframework.batch.core.domain.JobExecution; -import org.springframework.batch.core.domain.JobLocator; -import org.springframework.batch.core.domain.JobParameters; -import org.springframework.batch.core.executor.ExitCodeExceptionClassifier; -import org.springframework.batch.core.runtime.JobParametersFactory; -import org.springframework.batch.execution.launch.JobLauncher; -import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier; -import org.springframework.beans.factory.BeanDefinitionStoreException; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.util.StringUtils; - -/** - *

- * Basic launcher for starting jobs from the command line. In general, it is - * assumed that this launcher will primarily be used to start a job via a script - * from an Enterprise Scheduler. Therefore, exit codes are mapped to integers so - * that schedulers can use the returned values to determine the next course of - * action. The returned values can also be useful to operations teams in - * determining what should happen upon failure. For example, a returned code of - * 5 might mean that some resource wasn't available and the job should be - * restarted. However, a code of 10 might mean that something critical has - * happened and the issue should be escalated. - *

- * - *

- * With any launch of a batch job within Spring Batch, a Spring context - * containing the Job and the 'Execution Environment' has to be created. This - * command line launcher can be used to load that context from a single - * location. It can also load the job as well All dependencies of the launcher - * will then be satisfied by autowiring by type from the combined application - * context. Default values are provided for all fields except the - * {@link JobLauncher} and {@link JobLocator}. Therefore, if autowiring fails - * to set it (it should be noted that dependency checking is disabled because - * most of the fields have default values and thus don't require dependencies to - * be fulfilled via autowiring) then an exception will be thrown. It should also - * be noted that even if an exception is thrown by this class, it will be mapped - * to an integer and returned. - *

- * - *

- * Notice a property is available to set the {@link SystemExiter}. This class - * is used to exit from the main method, rather than calling System.exit() - * directly. This is because unit testing a class the calls System.exit() is - * impossible without kicking off the test within a new Jvm, which it is - * possible to do, however it is a complex solution, much more so than - * strategizing the exiter. - *

- * - *

- * The arguments to this class are roughly as follows: - *

- * - * - * java jobPath jobName jobLauncherPath jobParameters... - * - * - *

- *

- *

- * - *

- * The combined application context must only contain one instance of a - * {@link JobLauncher}. The job parameters passed in to the command line will - * be converted to {@link Properties} by assuming that each individual element - * is one parameter that is separated by an equals sign. For example, - * "vendor.id=290232". Below is an example arguments list: " - * - *

- * - * java org.springframework.batch.execution.bootstrap.support.CommandLineJobRunner testJob.xml - * testJob standard-job-launcher.xml schedule.date=2008/01/24 vendor.id=3902483920 - *

- * - *

Once arguments have been successfully parsed, autowiring will be used to set - * various dependencies. The {@JobLauncher} for example, will be loaded this way. If - * none is contained in the bean factory (it searches by type) then a - * {@link BeanDefinitionStoreException} will be thrown. The same exception will also - * be thrown if there is more than one present. Assuming the JobLauncher has been - * set correctly, the jobName argument will be used to obtain an actual {@link Job}. - * If a {@link JobLocator} has been set, then it will be used, if not the beanFactory - * will be asked, using the jobName as the bean id.

- * - * @author Dave Syer - * @author Lucas Ward - * @since 1.0 - */ -public class CommandLineJobRunner { - - protected static final Log logger = LogFactory - .getLog(CommandLineJobRunner.class); - - private ExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper(); - - private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier(); - - private JobLauncher launcher; - - private JobLocator jobLocator; - - private SystemExiter systemExiter = new JvmSystemExiter(); - - private JobParametersFactory jobParametersFactory = new DefaultJobParametersFactory(); - - /** - * Injection setter for the {@link JobLauncher}. - * - * @param launcher - * the launcher to set - */ - public void setLauncher(JobLauncher launcher) { - this.launcher = launcher; - } - - /** - * Injection setter for the {@link ExitCodeExceptionClassifier} - * - * @param exceptionClassifier - */ - public void setExceptionClassifier( - ExitCodeExceptionClassifier exceptionClassifier) { - this.exceptionClassifier = exceptionClassifier; - } - - /** - * Injection setter for the {@link JvmExitCodeMapper}. - * - * @param exitCodeMapper - * the exitCodeMapper to set - */ - public void setExitCodeMapper(ExitCodeMapper exitCodeMapper) { - this.exitCodeMapper = exitCodeMapper; - } - - /** - * Injection setter for the {@link SystemExiter}. - * - * @param systemExitor - */ - public void setSystemExiter(SystemExiter systemExitor) { - this.systemExiter = systemExitor; - } - - /** - * Delegate to the exiter to (possibly) exit the VM gracefully. - * - * @param status - */ - public void exit(int status) { - systemExiter.exit(status); - } - - public void setJobLocator(JobLocator jobLocator) { - this.jobLocator = jobLocator; - } - - /* - * Start a job by obtaining a combined classpath using the job launcher and - * job paths. If a JobLocator has been set, then use it to obtain an actual - * job, if not ask the context for it. - */ - int start(String jobPath, String jobLauncherPath, String jobName, - String[] parameters) { - - try { - ApplicationContext context = new ClassPathXmlApplicationContext( - new String[] { jobPath, jobLauncherPath }); - context.getAutowireCapableBeanFactory().autowireBeanProperties( - this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false); - - Job job; - if (jobLocator != null) { - job = jobLocator.getJob(jobName); - } else { - job = (Job) context.getBean(jobName); - } - - JobParameters jobParameters = jobParametersFactory - .getJobParameters(StringUtils - .splitArrayElementsIntoProperties(parameters, "=")); - - JobExecution jobExecution = launcher.run(job, jobParameters); - return exitCodeMapper.getExitCode(jobExecution.getExitStatus() - .getExitCode()); - } catch (Throwable e) { - logger.error("Job Terminated in error:", e); - return exitCodeMapper.getExitCode(exceptionClassifier - .classifyForExitCode(e).getExitCode()); - } - } - - /** - * Launch a batch job using a {@link CommandLineJobRunner}. Creates a - * new Spring context for the job execution, and uses a common parent for - * all such contexts. No exception are thrown from this method, rather - * exceptions are logged and an integer returned through the exit status in - * a {@link JvmSystemExiter} (which can be overridden by defining one in the - * Spring context). - * - * @param args - *

- *

    - *
  • jobPath: the xml application context containing a - * {@link Job} - *
  • jobName: the bean id of the job. - *
  • jobLauncherPath: the xml application context containing a - * {@link JobLauncher} - *
  • jobParameters: 0 to many parameters that will be used to - * launch a job. - *
- *

- */ - public static void main(String[] args) { - - CommandLineJobRunner command = new CommandLineJobRunner(); - - if (args.length < 3) { - logger - .error("At least 3 arguments are required: JobPath, JobName, and ExecutionPath."); - command.exit(1); - } - - String jobPath = args[0]; - String jobName = args[1]; - String jobLauncherPath = args[2]; - String[] parameters = new String[args.length - 3]; - System.arraycopy(args, 2, parameters, 0, args.length - 3); - - int result = command.start(jobPath, jobLauncherPath, jobName, - parameters); - command.exit(result); - } - -} diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java index 6b76919cd..c866803ef 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/DefaultJobParametersFactory.java @@ -29,7 +29,6 @@ import java.util.Map.Entry; import org.springframework.batch.core.domain.JobParameters; import org.springframework.batch.core.domain.JobParametersBuilder; import org.springframework.batch.core.runtime.JobParametersFactory; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java index 04748ac17..4711ae0f5 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ScheduledJobParametersFactory.java @@ -27,7 +27,6 @@ import java.util.Map.Entry; import org.springframework.batch.core.domain.JobParameters; import org.springframework.batch.core.domain.JobParametersBuilder; import org.springframework.batch.core.runtime.JobParametersFactory; -import org.springframework.util.Assert; /** * @author Lucas Ward diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java index b476f839d..ba8b4b16e 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java @@ -21,10 +21,13 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import org.springframework.batch.core.domain.StepExecution; import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.statistics.StatisticsService; /** * Simple implementation of {@link StepContext}. @@ -33,25 +36,55 @@ import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; * */ public class SimpleStepContext extends SynchronizedAttributeAccessor implements - StepContext { + StepContext, StatisticsProvider { private Map callbacks = new HashMap(); private StepContext parent; private StepExecution stepExecution; + private StatisticsService statisticsService; /** * Default constructor. */ - public SimpleStepContext() { - this(null); + public SimpleStepContext(StepExecution stepExecution) { + this(stepExecution, null, null); + } + + /** + * Default constructor. + */ + public SimpleStepContext(StepExecution stepExecution, StepContext parent) { + this(stepExecution, parent, null); } /** * @param object */ - public SimpleStepContext(StepContext parent) { + public SimpleStepContext(StepExecution stepExecution, StepContext parent, StatisticsService statisticsService) { super(); this.parent = parent; + this.statisticsService = statisticsService; + this.stepExecution = stepExecution; + } + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.context.SynchronizedAttributeAccessor#setAttribute(java.lang.String, java.lang.Object) + */ + public void setAttribute(String name, Object value) { + super.setAttribute(name, value); + if (statisticsService!=null && (value instanceof StatisticsProvider)) { + statisticsService.register(this, (StatisticsProvider) value); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.statistics.StatisticsProvider#getStatistics() + */ + public Properties getStatistics() { + if (statisticsService==null) { + return new Properties(); + } + return statisticsService.getStatistics(this); } /* @@ -86,10 +119,10 @@ public class SimpleStepContext extends SynchronizedAttributeAccessor implements } } - /* - * Package access because only needed internally. + /* (non-Javadoc) + * @see org.springframework.batch.execution.scope.StepContext#close() */ - void close() { + public void close() { List errors = new ArrayList(); @@ -133,13 +166,6 @@ public class SimpleStepContext extends SynchronizedAttributeAccessor implements throw (RuntimeException) errors.get(0); } - /** - * @param stepExecution - */ - public void setStepExecution(StepExecution stepExecution) { - this.stepExecution = stepExecution; - } - /* * (non-Javadoc) * diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java index 61cafb629..0df4f248f 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java @@ -16,6 +16,7 @@ package org.springframework.batch.execution.scope; import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.statistics.StatisticsProvider; import org.springframework.core.AttributeAccessor; /** @@ -24,7 +25,7 @@ import org.springframework.core.AttributeAccessor; * @author Dave Syer * */ -public interface StepContext extends AttributeAccessor { +public interface StepContext extends AttributeAccessor, StatisticsProvider { /** * Accessor for the {@link StepExecution} associated with the currently @@ -45,4 +46,9 @@ public interface StepContext extends AttributeAccessor { * Register a destruction callback for the end of life of the scope. */ void registerDestructionCallback(String name, Runnable callback); + + /** + * Clean up any resources held during the context of the step. + */ + void close(); } \ No newline at end of file diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java index 75781f063..69cb6d976 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java @@ -36,10 +36,17 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { private Object mutex = new Object(); + /** + * @param order the order value to set priority of callback execution for + * the {@link BeanFactoryPostProcessor} part of this scope bean. + */ public void setOrder(int order) { this.order = order; } + /* (non-Javadoc) + * @see org.springframework.core.Ordered#getOrder() + */ public int getOrder() { return order; } @@ -48,16 +55,17 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * Context key for clients to use for conversation identifier. */ public static final String ID_KEY = "JOB_IDENTIFIER"; + private String name = "step"; /* * (non-Javadoc) * * @see org.springframework.beans.factory.config.Scope#get(java.lang.String, - * org.springframework.beans.factory.ObjectFactory) + * org.springframework.beans.factory.ObjectFactory) */ public Object get(String name, ObjectFactory objectFactory) { - SimpleStepContext context = getContext(); + StepContext context = getContext(); Object scopedObject = context.getAttribute(name); if (scopedObject == null) { synchronized (mutex) { @@ -80,7 +88,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * @see org.springframework.beans.factory.config.Scope#getConversationId() */ public String getConversationId() { - SimpleStepContext context = getContext(); + StepContext context = getContext(); Object id = context.getAttribute(ID_KEY); return "" + id; } @@ -89,7 +97,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * (non-Javadoc) * * @see org.springframework.beans.factory.config.Scope#registerDestructionCallback(java.lang.String, - * java.lang.Runnable) + * java.lang.Runnable) */ public void registerDestructionCallback(String name, Runnable callback) { StepContext context = getContext(); @@ -102,7 +110,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * @see org.springframework.beans.factory.config.Scope#remove(java.lang.String) */ public Object remove(String name) { - SimpleStepContext context = getContext(); + StepContext context = getContext(); return context.removeAttribute(name); } @@ -111,13 +119,12 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * can be used to store scoped bean instances. * * @return the current step context which we can use as a scope storage - * medium + * medium */ - private SimpleStepContext getContext() { - SimpleStepContext context = StepSynchronizationManager.getContext(); + private StepContext getContext() { + StepContext context = StepSynchronizationManager.getContext(); if (context == null) { - throw new IllegalStateException( - "No context holder available for step scope"); + throw new IllegalStateException("No context holder available for step scope"); } return context; } @@ -125,13 +132,10 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { /** * Register this scope with the enclosing BeanFactory. * - * @param beanFactory - * the BeanFactory to register with - * @throws BeansException - * if there is a problem. + * @param beanFactory the BeanFactory to register with + * @throws BeansException if there is a problem. */ - public void postProcessBeanFactory( - ConfigurableListableBeanFactory beanFactory) throws BeansException { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { beanFactory.registerScope(name, this); } @@ -139,8 +143,7 @@ public class StepScope implements Scope, BeanFactoryPostProcessor, Ordered { * Public setter for the name property. This can then be used as a bean * definition attribute, e.g. scope="step". Defaults to "step". * - * @param name - * the name to set for this scope. + * @param name the name to set for this scope. */ public void setName(String name) { this.name = name; diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java deleted file mode 100644 index b7b53380f..000000000 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * 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.execution.scope; - -/** - * @author Dave Syer - * - */ -public class StepSynchronizationManager { - - private static final ThreadLocal contextHolder = new InheritableThreadLocal(); - - /** - * Getter for the current context.. - * - * @return the current {@link SimpleStepContext} or null if there is none - * (if we are not in a step). - */ - public static SimpleStepContext getContext() { - return (SimpleStepContext) contextHolder.get(); - } - - /** - * Method for registering a context - should only be used by - * {@link StepExecutor} implementations to ensure that {@link #getContext()} - * always returns the correct value. - * - * @return a new context at the start of a batch. - */ - public static SimpleStepContext open() { - StepContext oldSession = getContext(); - SimpleStepContext context = new SimpleStepContext(oldSession); - StepSynchronizationManager.contextHolder.set(context); - return context; - } - - /** - * Method for de-registering the current context - should only be used by - * {@link StepExecutor} implementations to ensure that {@link #getContext()} - * always returns the correct value. - * - * @return the old value if there was one. - */ - public static StepContext close() { - SimpleStepContext oldSession = getContext(); - if (oldSession == null) { - return null; - } - oldSession.close(); - StepContext context = oldSession.getParent(); - StepSynchronizationManager.contextHolder.set(context); - return context; - } - - /** - * Used internally by {@link StepExecutor} implementations to clear the - * current context at the end of a batch. - * - * @return the old value if there was one. - */ - public static StepContext clear() { - StepContext context = getContext(); - StepSynchronizationManager.contextHolder.set(null); - return context; - } - -} diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java index 3c3252b0e..6b17c3751 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java @@ -30,6 +30,7 @@ import org.springframework.batch.core.executor.StepInterruptedException; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.tasklet.Tasklet; import org.springframework.batch.execution.scope.SimpleStepContext; +import org.springframework.batch.execution.scope.StepContext; import org.springframework.batch.execution.scope.StepScope; import org.springframework.batch.execution.scope.StepSynchronizationManager; import org.springframework.batch.execution.step.RepeatOperationsHolder; @@ -47,7 +48,9 @@ import org.springframework.batch.repeat.support.RepeatTemplate; import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; +import org.springframework.batch.statistics.SimpleStatisticsService; import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.statistics.StatisticsService; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; @@ -91,6 +94,26 @@ public class SimpleStepExecutor implements StepExecutor { // Not for production use... protected PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); + private StatisticsService statisticsService = new SimpleStatisticsService(); + + /** + * Public setter for the {@link StatisticsService}. This will be used to + * create the {@link StepContext}, and hence any component that is a + * {@link StatisticsProvider} and in step scope will be registered with the + * service. The {@link StepContext} is then a source of aggregate statistics + * for the step. + * + * @param statisticsService the {@link StatisticsService} to set. Default is + * a {@link SimpleStatisticsService}. + */ + public void setStatisticsService(StatisticsService statisticsService) { + this.statisticsService = statisticsService; + } + + /** + * Injected strategy for transaction management + * @param transactionManager + */ public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } @@ -142,8 +165,8 @@ public class SimpleStepExecutor implements StepExecutor { * execution * @see StepExecutor#process(Step, StepExecution) */ - public ExitStatus process(final Step step, final StepExecution stepExecution) - throws BatchCriticalException, StepInterruptedException { + public ExitStatus process(final Step step, final StepExecution stepExecution) throws BatchCriticalException, + StepInterruptedException { final StepInstance stepInstance = stepExecution.getStep(); boolean isRestart = stepInstance.getStepExecutionCount() > 0 ? true : false; @@ -153,8 +176,10 @@ public class SimpleStepExecutor implements StepExecutor { ExitStatus status = ExitStatus.FAILED; - final SimpleStepContext stepScopeContext = StepSynchronizationManager.open(); - stepScopeContext.setStepExecution(stepExecution); + StepContext parentStepScopeContext = StepSynchronizationManager.getContext(); + final StepContext stepScopeContext = new SimpleStepContext(stepExecution, parentStepScopeContext, + statisticsService); + StepSynchronizationManager.register(stepScopeContext); // Add the job identifier so that it can be used to identify // the conversation in StepScope stepScopeContext.setAttribute(StepScope.ID_KEY, stepExecution.getJobExecution().getId()); @@ -198,8 +223,8 @@ public class SimpleStepExecutor implements StepExecutor { // TODO: Statistics are not thread safe // - we cannot guarantee that they are - // up to date. (Maybe we never can?) - Properties statistics = getStatistics(module); + // up to date. (Maybe we never can?) + Properties statistics = stepScopeContext.getStatistics(); contribution.setStatistics(statistics); contribution.incrementCommitCount(); // Apply the contribution to the step @@ -301,8 +326,8 @@ public class SimpleStepExecutor implements StepExecutor { * outside this method, so subclasses that override do not need to create a * transaction. * - * @param step the current step containing the {@link Tasklet} - * with the business logic. + * @param step the current step containing the {@link Tasklet} with the + * business logic. * @return true if there is more data to process. */ protected final ExitStatus processChunk(final Step step, final StepContribution contribution) { @@ -378,15 +403,6 @@ public class SimpleStepExecutor implements StepExecutor { } } - private Properties getStatistics(Tasklet tasklet) { - if (tasklet instanceof StatisticsProvider) { - return ((StatisticsProvider) tasklet).getStatistics(); - } - else { - return null; - } - } - /** * Setter for the {@link StepInterruptionPolicy}. The policy is used to * check whether an external request has been made to interrupt the job @@ -414,9 +430,9 @@ public class SimpleStepExecutor implements StepExecutor { *
    *
  • If the configuration is a {@link RepeatOperationsHolder} then we use * the provided {@link RepeatOperations} instances for chunk and step.
  • - *
  • If the configuration is a {@link SimpleStep} then we - * apply the commit interval at the chunk level and the exception handler at - * the step level, provided the existing repeat operations are instances of + *
  • If the configuration is a {@link SimpleStep} then we apply the + * commit interval at the chunk level and the exception handler at the step + * level, provided the existing repeat operations are instances of * {@link RepeatTemplate}. In addition if there is a non-zero skip limit * and no {@link ExceptionHandler} then we inject a * {@link SimpleLimitExceptionHandler} with that limit.
  • @@ -431,9 +447,7 @@ public class SimpleStepExecutor implements StepExecutor { RepeatOperationsHolder holder = (RepeatOperationsHolder) step; RepeatOperations chunkOperations = holder.getChunkOperations(); RepeatOperations stepOperations = holder.getStepOperations(); - Assert - .state(chunkOperations != null, - "Chunk operations obtained from step must be non-null."); + Assert.state(chunkOperations != null, "Chunk operations obtained from step must be non-null."); if (chunkOperations != null) { setChunkOperations(chunkOperations); diff --git a/spring-batch-execution/src/main/java/org/springframework/batch/execution/tasklet/ItemOrientedTasklet.java b/spring-batch-execution/src/main/java/org/springframework/batch/execution/tasklet/ItemOrientedTasklet.java index f5e573258..e3f38f30a 100644 --- a/spring-batch-execution/src/main/java/org/springframework/batch/execution/tasklet/ItemOrientedTasklet.java +++ b/spring-batch-execution/src/main/java/org/springframework/batch/execution/tasklet/ItemOrientedTasklet.java @@ -16,12 +16,6 @@ package org.springframework.batch.execution.tasklet; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - import org.springframework.batch.core.tasklet.Tasklet; import org.springframework.batch.io.Skippable; import org.springframework.batch.item.ItemProcessor; @@ -32,7 +26,6 @@ import org.springframework.batch.retry.RetryPolicy; import org.springframework.batch.retry.callback.ItemReaderRetryCallback; import org.springframework.batch.retry.policy.ItemReaderRetryPolicy; import org.springframework.batch.retry.support.RetryTemplate; -import org.springframework.batch.statistics.StatisticsProvider; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; @@ -40,13 +33,13 @@ import org.springframework.util.Assert; * A concrete implementation of the {@link Tasklet} interface that provides * 'split processing'. This type of processing is characterized by separating * the reading and processing of batch data into two separate classes: - * {@link ItemReader} and {@link ItemProcessor}. The {@link ItemReader} class provides a solid means - * for re-usability and enforces good architecture practices. Because an object - * must be returned by the {@link ItemReader} to continue - * processing, (returning null indicates processing should end) a developer is - * forced to read in all relevant data, place it into a domain object, and - * return that object. The {@link ItemProcessor} will then use this object for - * calculations and output.
    + * {@link ItemReader} and {@link ItemProcessor}. The {@link ItemReader} class + * provides a solid means for re-usability and enforces good architecture + * practices. Because an object must be returned by the + * {@link ItemReader} to continue processing, (returning null indicates + * processing should end) a developer is forced to read in all relevant data, + * place it into a domain object, and return that object. The + * {@link ItemProcessor} will then use this object for calculations and output.
    * * If a {@link RetryPolicy} is provided it will be used to construct a stateful * retry around the {@link ItemProcessor}, delegating identity concerns to the @@ -75,8 +68,7 @@ import org.springframework.util.Assert; * @author Robert Kasanicky * */ -public class ItemOrientedTasklet implements Tasklet, Skippable, - StatisticsProvider, InitializingBean { +public class ItemOrientedTasklet implements Tasklet, Skippable, InitializingBean { /** * Prefix added to statistics keys from processor if needed to avoid @@ -115,13 +107,11 @@ public class ItemOrientedTasklet implements Tasklet, Skippable, itemRecoverer = (ItemRecoverer) itemProvider; } - ItemReaderRetryPolicy itemProviderRetryPolicy = new ItemReaderRetryPolicy( - retryPolicy); + ItemReaderRetryPolicy itemProviderRetryPolicy = new ItemReaderRetryPolicy(retryPolicy); template.setRetryPolicy(itemProviderRetryPolicy); if (retryPolicy != null) { - retryCallback = new ItemReaderRetryCallback(itemProvider, - itemProcessor); + retryCallback = new ItemReaderRetryCallback(itemProvider, itemProcessor); retryCallback.setRecoverer(itemRecoverer); } @@ -150,7 +140,8 @@ public class ItemOrientedTasklet implements Tasklet, Skippable, } try { itemProcessor.process(item); - } catch (Exception e) { + } + catch (Exception e) { if (itemRecoverer != null) { itemRecoverer.recover(item, e); } @@ -211,77 +202,10 @@ public class ItemOrientedTasklet implements Tasklet, Skippable, } } - /** - * If the provider and / or processor are {@link StatisticsProvider} then - * delegate to them in that order. If they both implement - * {@link StatisticsProvider} then the property keys are prepended with - * special prefixes to avoid potential ambiguity. The prefixes are only - * prepended in the case of a duplicate key shared between provider and - * processor. - * - * @see org.springframework.batch.io.Skippable#skip() - */ - public Properties getStatistics() { - Properties stats = new Properties(); - if (this.itemProvider instanceof StatisticsProvider) { - stats = ((StatisticsProvider) this.itemProvider).getStatistics(); - } - if (this.itemProcessor instanceof StatisticsProvider) { - Properties props = ((StatisticsProvider) this.itemProcessor) - .getStatistics(); - if (!stats.isEmpty()) { - stats = prependKeys(stats, props, PROVIDER_STATISTICS_PREFIX, - PROCESSOR_STATISTICS_PREFIX); - } else { - stats.putAll(props); - } - } - return stats; - } - - /** - * @param props1 - * @param string - * @return - */ - private Properties prependKeys(Properties props1, Properties props2, - String prefix1, String prefix2) { - Properties result = new Properties(); - Set duplicates = new HashSet(); - for (Iterator iterator = props1.entrySet().iterator(); iterator - .hasNext();) { - Map.Entry entry = (Map.Entry) iterator.next(); - String key = (String) entry.getKey(); - String value = (String) entry.getValue(); - if (props2.containsKey(key)) { - duplicates.add(key); - continue; - } - result.setProperty(key, value); - } - for (Iterator iterator = props2.entrySet().iterator(); iterator - .hasNext();) { - Map.Entry entry = (Map.Entry) iterator.next(); - String key = (String) entry.getKey(); - String value = (String) entry.getValue(); - if (duplicates.contains(key)) { - continue; - } - result.setProperty(key, value); - } - for (Iterator iterator = duplicates.iterator(); iterator.hasNext();) { - String key = (String) iterator.next(); - result.setProperty(prefix1 + key, props1.getProperty(key)); - result.setProperty(prefix2 + key, props2.getProperty(key)); - } - return result; - } - /** * Public setter for the retryPolicy. * - * @param retyPolicy - * the retryPolicy to set + * @param retyPolicy the retryPolicy to set */ public void setRetryPolicy(RetryPolicy retryPolicy) { this.retryPolicy = retryPolicy; diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java deleted file mode 100644 index 63df8eb3c..000000000 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/bootstrap/support/CommandLineJobRunnerTests.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2006-2008 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.execution.bootstrap.support; - -import junit.framework.TestCase; - -import org.springframework.batch.core.domain.Job; -import org.springframework.batch.core.domain.JobExecution; -import org.springframework.batch.core.domain.JobParameters; -import org.springframework.batch.core.executor.ExitCodeExceptionClassifier; -import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; -import org.springframework.batch.execution.launch.JobLauncher; -import org.springframework.batch.repeat.ExitStatus; - -/** - * @author Lucas Ward - * - */ -public class CommandLineJobRunnerTests extends TestCase { - - private static final String JOB = "org/springframework/batch/execution/bootstrap/support/job.xml"; - private static final String TEST_BATCH_ENVIRONMENT = "org/springframework/batch/execution/bootstrap/support/test-environment.xml"; - private static final String JOB_NAME = "test-job"; - - private String jobPath = JOB; - private String environmentPath = TEST_BATCH_ENVIRONMENT; - private String jobName = JOB_NAME; - private String jobKey = "job.Key=myKey"; - private String scheduleDate = "schedule.Date=01/23/2008"; - private String vendorId = "vendor.id=33243243"; - - private String[] args = new String[]{jobPath, jobName, environmentPath, jobKey, scheduleDate, vendorId}; - - private JobExecution jobExecution; - - /* (non-Javadoc) - * @see junit.framework.TestCase#setUp() - */ - protected void setUp() throws Exception { - super.setUp(); - - jobExecution = new JobExecution(null, new Long(1)); - ExitStatus exitStatus = ExitStatus.FINISHED; - jobExecution.setExitStatus(exitStatus); - } - - public void testMain(){ - - StubJobLauncher.jobExecution = jobExecution; - - CommandLineJobRunner.main(args); - - assertEquals(0, StubSystemExiter.getStatus()); - } - - public void testJobAlreadyRunning(){ - - StubJobLauncher.throwExecutionRunningException = true; - - CommandLineJobRunner.main(args); - - assertTrue(StubExceptionClassifier.exception instanceof JobExecutionAlreadyRunningException); - } - - //can't test because it will cause the system to exit. -// public void testInvalidArgs(){ -// -// String[] args = new String[]{jobPath, jobName}; -// CommandLineJobRunner.main(args); -// } - - public void testWithNoParameters(){ - - String[] args = new String[]{jobPath, jobName, environmentPath}; - CommandLineJobRunner.main(args); - assertEquals(new JobParameters(), StubJobLauncher.jobParameters); - } - - protected void tearDown() throws Exception { - super.tearDown(); - - StubJobLauncher.tearDown(); - } - - public static class StubSystemExiter implements SystemExiter { - - public static int status; - - public void exit(int status) { - StubSystemExiter.status = status; - } - - public static int getStatus() { - return status; - } - } - - public static class StubJobLauncher implements JobLauncher{ - - public static JobExecution jobExecution; - public static boolean throwExecutionRunningException = false; - public static JobParameters jobParameters; - - public JobExecution run(Job job, JobParameters jobParameters) - throws JobExecutionAlreadyRunningException { - - StubJobLauncher.jobParameters = jobParameters; - - if(throwExecutionRunningException){ - throw new JobExecutionAlreadyRunningException(""); - } - - return jobExecution; - } - - public static void tearDown(){ - jobExecution = null; - throwExecutionRunningException = false; - jobParameters = null; - } - } - - public static class StubExceptionClassifier implements ExitCodeExceptionClassifier{ - - public static Throwable exception; - - public Object classify(Throwable throwable) { - return null; - } - - public Object getDefault() { - return null; - } - - public ExitStatus classifyForExitCode(Throwable throwable) { - exception = throwable; - return ExitStatus.FAILED; - } - - } -} diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/resource/BatchResourceFactoryBeanTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/resource/BatchResourceFactoryBeanTests.java index bf039d89f..82adaa448 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/resource/BatchResourceFactoryBeanTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/resource/BatchResourceFactoryBeanTests.java @@ -63,13 +63,12 @@ public class BatchResourceFactoryBeanTests extends TestCase { resourceFactory.setRootDirectory(rootDir); - SimpleStepContext context = new SimpleStepContext(); jobInstance = new JobInstance(new Long(0), new JobParameters()); jobInstance.setJob(new Job("testJob")); JobExecution jobExecution = new JobExecution(jobInstance); StepInstance step = new StepInstance(jobInstance, "bar"); StepExecution stepExecution = new StepExecution(step, jobExecution, null); - context.setStepExecution(stepExecution); + SimpleStepContext context = new SimpleStepContext(stepExecution); resourceFactory.setStepContext(context); resourceFactory.afterPropertiesSet(); diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/SimpleStepContextTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/SimpleStepContextTests.java index a4200fea0..c8e6eb0e0 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/SimpleStepContextTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/SimpleStepContextTests.java @@ -16,47 +16,57 @@ package org.springframework.batch.execution.scope; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Properties; import junit.framework.TestCase; import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.statistics.StatisticsService; +import org.springframework.batch.support.PropertiesConverter; /** * @author Dave Syer - * + * */ public class SimpleStepContextTests extends TestCase { - private SimpleStepContext context = new SimpleStepContext(new SimpleStepContext()); - + private SimpleStepContext context = new SimpleStepContext(null, new SimpleStepContext(null)); + /** - * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#StepScopeContext()}. + * Test method for + * {@link org.springframework.batch.execution.scope.SimpleStepContext#StepScopeContext()}. */ public void testStepScopeContext() { - assertNull(new SimpleStepContext().getParent()); + assertNull(new SimpleStepContext(null).getParent()); } /** - * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getParent()}. + * Test method for + * {@link org.springframework.batch.execution.scope.SimpleStepContext#getParent()}. */ public void testGetParent() { assertNotNull(context.getParent()); } /** - * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getStepExecution()}. + * Test method for + * {@link org.springframework.batch.execution.scope.SimpleStepContext#getStepExecution()}. */ - public void testGetJobIdentifier() { + public void testGetStepExecution() { assertNull(context.getStepExecution()); - context.setStepExecution(new StepExecution(null, null, null)); + context = new SimpleStepContext(new StepExecution(null, null, null)); assertNotNull(context.getStepExecution()); } private List list = new ArrayList(); /** - * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + * Test method for + * {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. */ public void testDestructionCallbackSunnyDay() throws Exception { SimpleStepContext context = new SimpleStepContext(null); @@ -72,7 +82,8 @@ public class SimpleStepContextTests extends TestCase { } /** - * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + * Test method for + * {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. */ public void testDestructionCallbackMissingAttribute() throws Exception { SimpleStepContext context = new SimpleStepContext(null); @@ -82,12 +93,14 @@ public class SimpleStepContextTests extends TestCase { } }); context.close(); - // Yes the callback should be called even if the attribute is missing - for inner beans + // Yes the callback should be called even if the attribute is missing - + // for inner beans assertEquals(1, list.size()); } /** - * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + * Test method for + * {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. */ public void testDestructionCallbackWithException() throws Exception { SimpleStepContext context = new SimpleStepContext(null); @@ -108,7 +121,8 @@ public class SimpleStepContextTests extends TestCase { try { context.close(); fail("Expected RuntimeException"); - } catch (RuntimeException e) { + } + catch (RuntimeException e) { // We don't care which one was thrown... assertEquals("fail!", e.getMessage()); } @@ -117,5 +131,56 @@ public class SimpleStepContextTests extends TestCase { assertTrue(list.contains("bar")); assertTrue(list.contains("spam")); } + + public void testStatisticsWithNullService() throws Exception { + assertEquals(0, context.getStatistics().size()); + } + + public void testStatisticsWithNotNullService() throws Exception { + Map map = new HashMap(); + context = new SimpleStepContext(null, null, new StubStatisticsService(map)); + assertEquals(1, context.getStatistics().size()); + assertEquals("bar", context.getStatistics().getProperty("foo")); + } + + public void testStatisticsServiceRegistration() throws Exception { + Map map = new HashMap(); + context = new SimpleStepContext(null, null, new StubStatisticsService(map)); + StubStatisticsProvider provider = new StubStatisticsProvider(); + context.setAttribute("foo", provider); + assertEquals(1, map.size()); + assertEquals(context, map.keySet().iterator().next()); + assertEquals(provider, map.values().iterator().next()); + } + + /** + * @author Dave Syer + * + */ + private class StubStatisticsService implements StatisticsService { + private final Map map; + + private StubStatisticsService(Map map) { + this.map = map; + } + + public Properties getStatistics(Object key) { + return PropertiesConverter.stringToProperties("foo=bar"); + } + + public void register(Object key, StatisticsProvider provider) { + map.put(key, provider); + } + } + + /** + * @author Dave Syer + * + */ + private class StubStatisticsProvider implements StatisticsProvider { + public Properties getStatistics() { + return null; + } + } } diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java index cc841721d..71c335a9a 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java @@ -40,7 +40,7 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBean() throws Exception { - StepSynchronizationManager.open(); + StepSynchronizationManager.register(new SimpleStepContext(null)); ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); TestBean bean = (TestBean) applicationContext.getBean("bean"); assertNotNull(bean); @@ -49,7 +49,7 @@ public class StepContextAwareStepScopeTests extends TestCase { public void testScopedBeanWithDestroyCallback() throws Exception { assertEquals(0, list.size()); - StepSynchronizationManager.open(); + StepSynchronizationManager.register(new SimpleStepContext(null)); ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); TestBean bean = (TestBean) applicationContext.getBean("bean"); assertNotNull(bean); @@ -58,7 +58,8 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBeanWithAware() throws Exception { - StepContext context = StepSynchronizationManager.open(); + StepContext context = new SimpleStepContext(null); + StepSynchronizationManager.register(context); ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); TestBeanAware bean = (TestBeanAware) applicationContext.getBean("aware"); assertNotNull(bean); @@ -67,7 +68,7 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBeanWithInner() throws Exception { - StepSynchronizationManager.open(); + StepSynchronizationManager.register(new SimpleStepContext(null)); ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext( "scope-tests.xml", getClass()); TestBean bean = ((TestBean) applicationContext.getBean("inner")).child; @@ -78,7 +79,8 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBeanWithProxy() throws Exception { - StepContext context = StepSynchronizationManager.open(); + StepContext context = new SimpleStepContext(null); + StepSynchronizationManager.register(context); ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); TestBeanAware bean = (TestBeanAware) applicationContext.getBean("proxy"); assertNotNull(bean); @@ -89,7 +91,7 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBeanWithProxyInThread() throws Exception { - StepSynchronizationManager.open(); + StepSynchronizationManager.register(new SimpleStepContext(null)); final ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); new Thread(new Runnable() { public void run() { @@ -109,7 +111,7 @@ public class StepContextAwareStepScopeTests extends TestCase { } public void testScopedBeanWithTwoProxiesInThreads() throws Exception { - StepSynchronizationManager.open(); + StepSynchronizationManager.register(new SimpleStepContext(null)); final ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); new Thread(new Runnable() { public void run() { diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java index 9f533dd66..812cb4c4b 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java @@ -41,7 +41,8 @@ public class StepScopeTests extends TestCase { */ protected void setUp() throws Exception { super.setUp(); - context = StepSynchronizationManager.open(); + context = new SimpleStepContext(null); + StepSynchronizationManager.register(context); } /* (non-Javadoc) @@ -104,7 +105,8 @@ public class StepScopeTests extends TestCase { * {@link org.springframework.batch.execution.scope.StepScope#get(java.lang.String, org.springframework.beans.factory.ObjectFactory)}. */ public void testGetWithSomethingAlreadyInParentContext() { - SimpleStepContext context = StepSynchronizationManager.open(); + StepContext context = new SimpleStepContext(null); + StepSynchronizationManager.register(context); context.setAttribute("foo", "bar"); Object value = scope.get("foo", new ObjectFactory() { public Object getObject() throws BeansException { diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepExecutorTests.java similarity index 90% rename from spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java rename to spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepExecutorTests.java index 5b45c4173..7f0e4af81 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepExecutorTests.java @@ -18,7 +18,10 @@ package org.springframework.batch.execution.step.simple; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Properties; import junit.framework.TestCase; @@ -50,8 +53,11 @@ import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; import org.springframework.batch.repeat.support.RepeatTemplate; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.statistics.StatisticsService; +import org.springframework.batch.support.PropertiesConverter; -public class DefaultStepExecutorTests extends TestCase { +public class SimpleStepExecutorTests extends TestCase { ArrayList processed = new ArrayList(); @@ -411,6 +417,43 @@ public class DefaultStepExecutorTests extends TestCase { assertEquals(1, list.size()); } + public void testStatisticsService() throws Exception { + StepInstance step = new StepInstance(new Long(1)); + step.setStepExecutionCount(1); + stepConfiguration.setTasklet(new Tasklet() { + public ExitStatus execute() throws Exception { + return ExitStatus.FINISHED; + } + }); + stepConfiguration.setSaveRestartData(true); + JobExecution jobExecution = new JobExecution(jobInstance); + StepExecution stepExecution = new StepExecution(step, jobExecution); + + assertEquals(null, stepExecution.getStatistics().getProperty("foo")); + + final Map map = new HashMap(); + stepExecutor.setStatisticsService(new StatisticsService() { + public Properties getStatistics(Object key) { + return PropertiesConverter.stringToProperties("foo=bar"); + } + public void register(Object key, StatisticsProvider provider) { + map.put(key, provider); + } + }); + + try { + stepExecutor.process(stepConfiguration, stepExecution); + } + catch (Throwable t) { + fail(); + } + + // At least once in that process the statistics service was asked for statistics... + assertEquals("bar", stepExecution.getStatistics().getProperty("foo")); + // ...but nothing was registered because nothing with step scoped. + assertEquals(0, map.size()); + } + private class MockRestartableTasklet implements Tasklet, Restartable { private boolean getRestartDataCalled = false; diff --git a/spring-batch-execution/src/test/java/org/springframework/batch/execution/tasklet/ItemOrientedTaskletTests.java b/spring-batch-execution/src/test/java/org/springframework/batch/execution/tasklet/ItemOrientedTaskletTests.java index 089f3199b..1e4d8be15 100644 --- a/spring-batch-execution/src/test/java/org/springframework/batch/execution/tasklet/ItemOrientedTaskletTests.java +++ b/spring-batch-execution/src/test/java/org/springframework/batch/execution/tasklet/ItemOrientedTaskletTests.java @@ -159,41 +159,6 @@ public class ItemOrientedTaskletTests extends TestCase { assertEquals(2, list.size()); } - public void testStatisticsProvider() throws Exception { - module.setItemReader(new SkippableItemReader()); - Properties stats = module.getStatistics(); - assertEquals(1, stats.size()); - assertEquals("bar", stats.getProperty("foo")); - } - - public void testStatisticsProcessor() throws Exception { - module.setItemProcessor(new SkippableItemProcessor()); - Properties stats = module.getStatistics(); - assertEquals(1, stats.size()); - assertEquals("bar", stats.getProperty("foo")); - } - - public void testStatisticsProviderProcessor() throws Exception { - module.setItemReader(new SkippableItemReader()); - module.setItemProcessor(new SkippableItemProcessor()); - Properties stats = module.getStatistics(); - assertEquals(2, stats.size()); - assertEquals("bar", stats.getProperty("provider.foo")); - assertEquals("bar", stats.getProperty("processor.foo")); - } - - public void testStatisticsProviderProcessorMergeDuplicates() - throws Exception { - module.setItemReader(new SkippableItemReader()); - module.setItemProcessor(new SkippableItemProcessor( - "foo=bar\nspam=bucket")); - Properties stats = module.getStatistics(); - assertEquals(3, stats.size()); - assertEquals("bar", stats.getProperty("provider.foo")); - assertEquals("bar", stats.getProperty("processor.foo")); - assertEquals("bucket", stats.getProperty("spam")); - } - public void testRecoverable() throws Exception { // set up and call execute diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/CompositeItemProcessor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/CompositeItemProcessor.java index e7b39d28b..b7829f298 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/CompositeItemProcessor.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/CompositeItemProcessor.java @@ -11,14 +11,13 @@ import org.springframework.batch.item.ItemReader; import org.springframework.batch.restart.GenericRestartData; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; -import org.springframework.batch.statistics.StatisticsProvider; /** * Runs a collection of ItemProcessors in fixed-order sequence. * * @author Robert Kasanicky */ -public class CompositeItemProcessor implements ItemProcessor, Restartable, StatisticsProvider { +public class CompositeItemProcessor implements ItemProcessor, Restartable { private static final String SEPARATOR = "#"; @@ -72,23 +71,6 @@ public class CompositeItemProcessor implements ItemProcessor, Restartable, Stati } - /** - * @return Properties containing statistics of all injected ItemProcessors, - * property keys are prefixed with the list index of the ItemProcessor. - */ - public Properties getStatistics() { - return createCompoundProperties(new PropertiesExtractor() { - public Properties extractProperties(Object o) { - if (o instanceof StatisticsProvider) { - return ((StatisticsProvider) o).getStatistics(); - } - else { - return null; - } - } - }); - } - public void setItemProcessors(List itemProcessors) { this.itemProcessors = itemProcessors; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/ItemWriterItemProcessor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/ItemWriterItemProcessor.java index 9a736f07f..96223c4cf 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/ItemWriterItemProcessor.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/processor/ItemWriterItemProcessor.java @@ -8,24 +8,23 @@ import org.springframework.batch.item.ItemWriter; import org.springframework.batch.restart.GenericRestartData; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; -import org.springframework.batch.statistics.StatisticsProvider; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; /** - * Simple wrapper around {@link ItemWriter} providing {@link Restartable} and - * {@link StatisticsProvider} where the {@link ItemWriter} does. + * Simple wrapper around {@link ItemWriter} providing {@link Restartable} where + * the {@link ItemWriter} does. To make sure * * @author Dave Syer * @author Robert Kasanicky */ -public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skippable, - StatisticsProvider, InitializingBean { +public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skippable, InitializingBean { private ItemWriter writer; /** - * Calls {@link #doProcess(Object)} and then writes the result to the {@link ItemWriter}. + * Calls {@link #doProcess(Object)} and then writes the result to the + * {@link ItemWriter}. * * @see org.springframework.batch.item.ItemProcessor#process(java.lang.Object) */ @@ -35,8 +34,8 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip } /** - * By default returns the argument. This method is an extension point - * meant to be overridden by subclasses that implement processing logic. + * By default returns the argument. This method is an extension point meant + * to be overridden by subclasses that implement processing logic. */ protected Object doProcess(Object item) throws Exception { return item; @@ -53,13 +52,13 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip * @see Restartable#getRestartData() */ public RestartData getRestartData() { - + Assert.state(writer != null, "Source must not be null."); - + if (writer instanceof Restartable) { return ((Restartable) writer).getRestartData(); } - else{ + else { return new GenericRestartData(new Properties()); } } @@ -68,35 +67,21 @@ public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skip * @see Restartable#restoreFrom(RestartData) */ public void restoreFrom(RestartData data) { - + Assert.state(writer != null, "Source must not be null."); - + if (writer instanceof Restartable) { ((Restartable) writer).restoreFrom(data); } - - } - /** - * @return delegates to the parent template of it is a - * {@link StatisticsProvider}, otherwise returns an empty - * {@link Properties} instance. - * @see StatisticsProvider#getStatistics() - */ - public Properties getStatistics() { - if (!(writer instanceof StatisticsProvider)) { - return new Properties(); - } - return ((StatisticsProvider) writer).getStatistics(); } public void skip() { if (writer instanceof Skippable) { - ((Skippable)writer).skip(); + ((Skippable) writer).skip(); } } - public void afterPropertiesSet() throws Exception { Assert.notNull(writer); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/reader/DelegatingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/reader/DelegatingItemReader.java index d592b5d90..6678ea358 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/reader/DelegatingItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/reader/DelegatingItemReader.java @@ -16,13 +16,10 @@ package org.springframework.batch.item.reader; -import java.util.Properties; - import org.springframework.batch.io.Skippable; import org.springframework.batch.item.ItemReader; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; -import org.springframework.batch.statistics.StatisticsProvider; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; @@ -33,7 +30,7 @@ import org.springframework.util.Assert; * * @author Dave Syer */ -public class DelegatingItemReader extends AbstractItemReader implements Restartable, StatisticsProvider, Skippable, InitializingBean{ +public class DelegatingItemReader extends AbstractItemReader implements Restartable, Skippable, InitializingBean{ private ItemReader inputSource; @@ -73,19 +70,6 @@ public class DelegatingItemReader extends AbstractItemReader implements Restarta ((Restartable) inputSource).restoreFrom(data); } - /** - * @return delegates to the parent template of it is a - * {@link StatisticsProvider}, otherwise returns an empty - * {@link Properties} instance. - * @see StatisticsProvider#getStatistics() - */ - public Properties getStatistics() { - if (!(inputSource instanceof StatisticsProvider)) { - return new Properties(); - } - return ((StatisticsProvider) inputSource).getStatistics(); - } - /** * Setter for input source. * @param source diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/SimpleStatisticsService.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/SimpleStatisticsService.java new file mode 100644 index 000000000..2bdb29ab9 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/SimpleStatisticsService.java @@ -0,0 +1,89 @@ +/* + * 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.statistics; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Simple {@link StatisticsService} that makes no attempt to aggregate or + * resolve conflicts between key names. All the contributions registered are + * simply polled and added "as is" to the aggregate properties. + * + * @author Dave Syer + * + */ +public class SimpleStatisticsService implements StatisticsService { + + private Map registry = new HashMap(); + + /** + * Simple aggregate statistics provider for the contributions registered + * under the given key. + * + * @see org.springframework.batch.statistics.StatisticsService#getStatistics(java.lang.Object) + */ + public Properties getStatistics(Object key) { + Set set = new LinkedHashSet(); + synchronized (registry) { + Collection collection = (Collection) registry.get(key); + if (collection != null) { + set = new LinkedHashSet(collection); + } + } + return aggregate(set); + } + + /** + * @param list a list of {@link StatisticsProvider}s + * @return aggregated statistics + */ + private Properties aggregate(Collection list) { + Properties result = new Properties(); + for (Iterator iterator = list.iterator(); iterator.hasNext();) { + StatisticsProvider provider = (StatisticsProvider) iterator.next(); + Properties properties = provider.getStatistics(); + if (properties != null) { + result.putAll(properties); + } + } + return result; + } + + /** + * Register a {@link StatisticsProvider} as one of the interesting providers + * under the provided key. + * + * @see org.springframework.batch.statistics.StatisticsService#register(java.lang.Object, + * org.springframework.batch.statistics.StatisticsProvider) + */ + public void register(Object key, StatisticsProvider provider) { + synchronized (registry) { + Set set = (Set) registry.get(key); + if (set == null) { + set = new LinkedHashSet(); + registry.put(key, set); + } + set.add(provider); + } + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java index db6bbf908..e50bcf194 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java @@ -20,7 +20,7 @@ import java.util.Properties; /** * Provides statistics for a given module run. Any class that implements - * this interface is garunteeing that it will provide Statistics. + * this interface is guaranteeing that it will provide Statistics. * * @author Lucas Ward * diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsService.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsService.java new file mode 100644 index 000000000..2d93838e9 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsService.java @@ -0,0 +1,50 @@ +/* + * 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.statistics; + +import java.util.Properties; + +/** + * Generalised statistics aggregation strategy. Clients register + * {@link StatisticsProvider} instances under a well-known key, and then when + * they ask for statistics by that key, they receive an aggregate of all the + * values given by registered providers. + * + * @author Dave Syer + * + */ +public interface StatisticsService { + + /** + * Register the {@link StatisticsProvider} instance as one of possibly + * several that are associated with the given key. + * + * @param key the key under which to add the provider + * @param provider a {@link StatisticsProvider} + */ + void register(Object key, StatisticsProvider provider); + + /** + * Extract and aggregate the statistics from all providers under this key. + * + * @param key the key under which {@link StatisticsProvider} instances might + * have been registered. + * @return {@link Properties} summarising the statistics of all providers + * registered under this key, or empty otherwise. + */ + Properties getStatistics(Object key); + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/CompositeItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/CompositeItemProcessorTests.java index 6809f10e5..8ba156bce 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/CompositeItemProcessorTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/CompositeItemProcessorTests.java @@ -58,24 +58,6 @@ public class CompositeItemProcessorTests extends TestCase { } } - /** - * Statistics of injected ItemProcessors should be returned under keys prefixed with their list index. - */ - public void testStatistics() { - final ItemProcessor p1 = new ItemProcessorStub(); - final ItemProcessor p2 = new ItemProcessorStub(); - - List itemProcessors = new ArrayList(){{ - add(p1); - add(p2); - }}; - - itemProcessor.setItemProcessors(itemProcessors); - Properties stats = itemProcessor.getStatistics(); - assertEquals(String.valueOf(p1.hashCode()), stats.getProperty("0#" + ItemProcessorStub.STATS_KEY)); - assertEquals(String.valueOf(p2.hashCode()), stats.getProperty("1#" + ItemProcessorStub.STATS_KEY)); - } - /** * All Restartable processors should be restarted, not-Restartable processors should be ignored. */ diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/ItemWriterItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/ItemWriterItemProcessorTests.java index 10dd0e0b8..49c11b105 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/ItemWriterItemProcessorTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/processor/ItemWriterItemProcessorTests.java @@ -23,7 +23,6 @@ import junit.framework.TestCase; import org.springframework.batch.io.Skippable; import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.processor.ItemWriterItemProcessor; import org.springframework.batch.restart.GenericRestartData; import org.springframework.batch.restart.RestartData; import org.springframework.batch.restart.Restartable; @@ -56,14 +55,6 @@ public class ItemWriterItemProcessorTests extends TestCase { assertEquals("test:foo", list.get(0)); } - /** - * Gets statistics from the input template - */ - public void testGetStatistics() { - Properties props = processor.getStatistics(); - assertEquals("b", props.getProperty("a")); - } - /** * Gets restart data from the input template */ @@ -111,15 +102,6 @@ public class ItemWriterItemProcessorTests extends TestCase { // expected } } - - /** - * Gets statistics from the input template - */ - public void testGetStatisticsWithoutStatisticsProvider() { - processor.setItemWriter(null); - Properties props = processor.getStatistics(); - assertEquals(null, props.getProperty("a")); - } public void testSkip() { processor.skip(); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/reader/DelegatingItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/reader/DelegatingItemReaderTests.java index 3964c4766..3a6fec2ab 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/reader/DelegatingItemReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/reader/DelegatingItemReaderTests.java @@ -72,14 +72,6 @@ public class DelegatingItemReaderTests extends TestCase { assertSame("domain object is provided by the input template", this, result); } - /** - * Gets statistics from the input template - */ - public void testGetStatistics() { - Properties props = itemProvider.getStatistics(); - assertEquals("b", props.getProperty("a")); - } - /** * Gets restart data from the input template */ diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/statistics/SimpleStatisticsServiceTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/statistics/SimpleStatisticsServiceTests.java new file mode 100644 index 000000000..d1a4b7d67 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/statistics/SimpleStatisticsServiceTests.java @@ -0,0 +1,72 @@ +/* + * 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.statistics; + +import java.util.Properties; + +import org.springframework.batch.support.PropertiesConverter; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class SimpleStatisticsServiceTests extends TestCase { + + private SimpleStatisticsService service = new SimpleStatisticsService(); + + public void testRegistration() throws Exception { + service.register("FOO", new StubStatisticsProvider()); + assertEquals("bar", service.getStatistics("FOO").getProperty("foo")); + } + + public void testAggregation() throws Exception { + service.register("FOO", new StubStatisticsProvider()); + service.register("FOO", new StubStatisticsProvider("spam=bucket")); + assertEquals("bar", service.getStatistics("FOO").getProperty("foo")); + assertEquals("bucket", service.getStatistics("FOO").getProperty("spam")); + } + + public void testDoubleAggregationOrder() throws Exception { + StubStatisticsProvider provider = new StubStatisticsProvider(); + service.register("FOO", provider); + service.register("FOO", provider); + assertEquals(1, service.getStatistics("FOO").size()); + } + + /** + * @author Dave Syer + * + */ + private class StubStatisticsProvider implements StatisticsProvider { + String values = "foo=bar"; + + public StubStatisticsProvider(String values) { + super(); + this.values = values; + } + + public StubStatisticsProvider() { + super(); + } + + public Properties getStatistics() { + return PropertiesConverter.stringToProperties(values); + } + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/tasklet/SimpleTradeTasklet.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/tasklet/SimpleTradeTasklet.java index a7de38394..cebd66fe4 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/sample/tasklet/SimpleTradeTasklet.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/tasklet/SimpleTradeTasklet.java @@ -86,7 +86,7 @@ public class SimpleTradeTasklet implements Tasklet, StatisticsProvider { public Properties getStatistics() { Properties statistics = new Properties(); - statistics.setProperty("Trade.Count", String.valueOf(tradeCount)); + statistics.setProperty("trade.count", String.valueOf(tradeCount)); statistics.putAll(inputSource.getStatistics()); return statistics; } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/processor/StagingItemProcessorTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/processor/StagingItemProcessorTests.java index 3301f746d..eb8677487 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/processor/StagingItemProcessorTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/processor/StagingItemProcessorTests.java @@ -11,8 +11,7 @@ import org.springframework.batch.execution.scope.StepSynchronizationManager; import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; import org.springframework.util.ClassUtils; -public class StagingItemProcessorTests extends - AbstractTransactionalDataSourceSpringContextTests { +public class StagingItemProcessorTests extends AbstractTransactionalDataSourceSpringContextTests { private StagingItemProcessor processor; @@ -21,25 +20,21 @@ public class StagingItemProcessorTests extends } protected String[] getConfigLocations() { - return new String[] { ClassUtils.addResourcePathToPackagePath( - StagingItemProcessor.class, "staging-test-context.xml") }; + return new String[] { ClassUtils.addResourcePathToPackagePath(StagingItemProcessor.class, + "staging-test-context.xml") }; } protected void prepareTestInstance() throws Exception { - SimpleStepContext stepScopeContext = StepSynchronizationManager - .open(); - stepScopeContext.setStepExecution(new StepExecution(new StepInstance( - new Long(11)), new JobExecution(new JobInstance(new Long(12), - new JobParameters(), new Job("job"))))); + SimpleStepContext stepScopeContext = new SimpleStepContext(new StepExecution(new StepInstance(new Long(11)), + new JobExecution(new JobInstance(new Long(12), new JobParameters(), new Job("job"))))); + StepSynchronizationManager.register(stepScopeContext); super.prepareTestInstance(); } public void testProcessInsertsNewItem() throws Exception { - int before = getJdbcTemplate().queryForInt( - "SELECT COUNT(*) from BATCH_STAGING"); + int before = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING"); processor.process("FOO"); - int after = getJdbcTemplate().queryForInt( - "SELECT COUNT(*) from BATCH_STAGING"); + int after = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING"); assertEquals(before + 1, after); } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/reader/StagingItemReaderTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/reader/StagingItemReaderTests.java index 427c2413e..b6dbcc406 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/reader/StagingItemReaderTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/sample/item/reader/StagingItemReaderTests.java @@ -6,21 +6,22 @@ import org.springframework.batch.core.domain.JobParameters; import org.springframework.batch.core.domain.StepExecution; import org.springframework.batch.core.domain.StepInstance; import org.springframework.batch.execution.scope.SimpleStepContext; +import org.springframework.batch.execution.scope.StepContext; import org.springframework.batch.execution.scope.StepSynchronizationManager; import org.springframework.batch.repeat.context.RepeatContextSupport; import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; import org.springframework.batch.sample.item.processor.StagingItemProcessor; -import org.springframework.batch.sample.item.reader.StagingItemReader; import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; import org.springframework.util.ClassUtils; -public class StagingItemReaderTests extends - AbstractTransactionalDataSourceSpringContextTests { +public class StagingItemReaderTests extends AbstractTransactionalDataSourceSpringContextTests { private StagingItemProcessor processor; + private StagingItemReader provider; - private Long jobId; + + private Long jobId = new Long(11); public void setProcessor(StagingItemProcessor processor) { this.processor = processor; @@ -31,16 +32,14 @@ public class StagingItemReaderTests extends } protected String[] getConfigLocations() { - return new String[] { ClassUtils.addResourcePathToPackagePath( - StagingItemProcessor.class, "staging-test-context.xml") }; + return new String[] { ClassUtils.addResourcePathToPackagePath(StagingItemProcessor.class, + "staging-test-context.xml") }; } protected void prepareTestInstance() throws Exception { - SimpleStepContext stepScopeContext = StepSynchronizationManager.open(); - jobId = new Long(11); - stepScopeContext.setStepExecution(new StepExecution(new StepInstance( - new Long(12)), new JobExecution(new JobInstance(jobId, - new JobParameters())))); + StepContext stepScopeContext = new SimpleStepContext(new StepExecution(new StepInstance(new Long(12)), + new JobExecution(new JobInstance(jobId, new JobParameters())))); + StepSynchronizationManager.register(stepScopeContext); RepeatSynchronizationManager.register(new RepeatContextSupport(null)); super.prepareTestInstance(); } @@ -59,36 +58,31 @@ public class StagingItemReaderTests extends public void testReaderUpdatesProcessIndicator() throws Exception { - long id = getJdbcTemplate().queryForLong( - "SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", + long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", new Object[] { jobId }); - String before = (String) getJdbcTemplate().queryForObject( - "SELECT PROCESSED from BATCH_STAGING where ID=?", + String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?", new Object[] { new Long(id) }, String.class); assertEquals(StagingItemProcessor.NEW, before); Object item = provider.read(); assertEquals("FOO", item); - String after = (String) getJdbcTemplate().queryForObject( - "SELECT PROCESSED from BATCH_STAGING where ID=?", + String after = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?", new Object[] { new Long(id) }, String.class); assertEquals(StagingItemProcessor.DONE, after); } - + public void testUpdateProcessIndicatorAfterCommit() throws Exception { testReaderUpdatesProcessIndicator(); setComplete(); endTransaction(); startNewTransaction(); - long id = getJdbcTemplate().queryForLong( - "SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", + long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", new Object[] { jobId }); - String before = (String) getJdbcTemplate().queryForObject( - "SELECT PROCESSED from BATCH_STAGING where ID=?", + String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?", new Object[] { new Long(id) }, String.class); - assertEquals(StagingItemProcessor.DONE, before); + assertEquals(StagingItemProcessor.DONE, before); } public void testProviderRollsBackMultipleTimes() throws Exception { @@ -96,14 +90,14 @@ public class StagingItemReaderTests extends setComplete(); endTransaction(); startNewTransaction(); - // After a rollback we have to resynchronize the TX to simulate a real batch + // After a rollback we have to resynchronize the TX to simulate a real + // batch BatchTransactionSynchronizationManager.resynchronize(); - int count = getJdbcTemplate().queryForInt( - "SELECT COUNT(*) from BATCH_STAGING where JOB_ID=? AND PROCESSED=?", + int count = getJdbcTemplate().queryForInt("SELECT COUNT(*) from BATCH_STAGING where JOB_ID=? AND PROCESSED=?", new Object[] { jobId, StagingItemProcessor.NEW }); assertEquals(4, count); - + Object item = provider.read(); assertEquals("FOO", item); item = provider.read(); @@ -112,7 +106,7 @@ public class StagingItemReaderTests extends endTransaction(); startNewTransaction(); BatchTransactionSynchronizationManager.resynchronize(); - + item = provider.read(); assertEquals("FOO", item); item = provider.read(); @@ -123,25 +117,24 @@ public class StagingItemReaderTests extends endTransaction(); startNewTransaction(); BatchTransactionSynchronizationManager.resynchronize(); - + item = provider.read(); assertEquals("FOO", item); } - + public void testProviderRollsBackProcessIndicator() throws Exception { setComplete(); endTransaction(); startNewTransaction(); - // After a rollback we have to resynchronize the TX to simulate a real batch + // After a rollback we have to resynchronize the TX to simulate a real + // batch BatchTransactionSynchronizationManager.resynchronize(); - long id = getJdbcTemplate().queryForLong( - "SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", + long id = getJdbcTemplate().queryForLong("SELECT MIN(ID) from BATCH_STAGING where JOB_ID=?", new Object[] { jobId }); - String before = (String) getJdbcTemplate().queryForObject( - "SELECT PROCESSED from BATCH_STAGING where ID=?", + String before = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?", new Object[] { new Long(id) }, String.class); assertEquals(StagingItemProcessor.NEW, before); @@ -150,11 +143,11 @@ public class StagingItemReaderTests extends endTransaction(); startNewTransaction(); - // After a rollback we have to resynchronize the TX to simulate a real batch + // After a rollback we have to resynchronize the TX to simulate a real + // batch BatchTransactionSynchronizationManager.resynchronize(); - String after = (String) getJdbcTemplate().queryForObject( - "SELECT PROCESSED from BATCH_STAGING where ID=?", + String after = (String) getJdbcTemplate().queryForObject("SELECT PROCESSED from BATCH_STAGING where ID=?", new Object[] { new Long(id) }, String.class); assertEquals(StagingItemProcessor.NEW, after);