[BATCH-429] Moved all code out of execution

This commit is contained in:
nebhale
2008-03-07 16:02:22 +00:00
parent 5a04a9ddcd
commit bfb9447d6b
153 changed files with 188 additions and 187 deletions

View File

@@ -0,0 +1,113 @@
/*
* 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.configuration;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.repository.DuplicateJobException;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.util.Assert;
/**
* A {@link BeanPostProcessor} that registers {@link Job} beans
* with a {@link JobRegistry}. Include a bean of this type along
* with your job configuration, and use the same
* {@link JobRegistry} as a {@link JobLocator} when
* you need to locate a {@link JobLocator} to launch.
*
* @author Dave Syer
*
*/
public class JobRegistryBeanPostProcessor implements BeanPostProcessor, InitializingBean, DisposableBean {
// It doesn't make sense for this to have a default value...
private JobRegistry jobConfigurationRegistry = null;
private Collection jobNames = new HashSet();
/**
* Injection setter for {@link JobRegistry}.
*
* @param jobRegistry the jobConfigurationRegistry to set
*/
public void setJobRegistry(JobRegistry jobRegistry) {
this.jobConfigurationRegistry = jobRegistry;
}
/**
* Make sure the registry is set before use.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jobConfigurationRegistry, "JobConfigurationRegistry must not be null");
}
/**
* De-register all the {@link Job} instances that were
* regsistered by this post processor.
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
for (Iterator iter = jobNames.iterator(); iter.hasNext();) {
String name = (String) iter.next();
jobConfigurationRegistry.unregister(name);
}
jobNames.clear();
}
/**
* If the bean is an instance of {@link Job} then register it.
* @throws FatalBeanException if there is a
* {@link DuplicateJobException}.
*
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object,
* java.lang.String)
*/
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof Job) {
Job job = (Job) bean;
try {
jobConfigurationRegistry.register(new ReferenceJobFactory(job));
jobNames.add(job.getName());
}
catch (DuplicateJobException e) {
throw new FatalBeanException("Cannot register job configuration", e);
}
}
return bean;
}
/**
* Do nothing.
*
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object,
* java.lang.String)
*/
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.configuration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.JobFactory;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.ListableJobRegistry;
import org.springframework.batch.core.repository.DuplicateJobException;
import org.springframework.batch.core.repository.NoSuchJobException;
import org.springframework.util.Assert;
/**
* Simple map-based implementation of {@link JobRegistry}. Access to the map is
* synchronized, guarded by an internal lock.
*
* @author Dave Syer
*
*/
public class MapJobRegistry implements ListableJobRegistry {
private Map map = new HashMap();
/*
* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.JobConfigurationRegistry#registerJobConfiguration(org.springframework.batch.container.common.configuration.JobConfiguration)
*/
public void register(JobFactory jobFactory) throws DuplicateJobException {
Assert.notNull(jobFactory);
String name = jobFactory.getJobName();
Assert.notNull(name, "Job configuration must have a name.");
synchronized (map) {
if (map.containsKey(name)) {
throw new DuplicateJobException("A job configuration with this name [" + name
+ "] was already registered");
}
map.put(name, jobFactory);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.JobConfigurationRegistry#unregister(org.springframework.batch.container.common.configuration.JobConfiguration)
*/
public void unregister(String name) {
Assert.notNull(name, "Job configuration must have a name.");
synchronized (map) {
map.remove(name);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.JobConfigurationLocator#getJobConfiguration(java.lang.String)
*/
public Job getJob(String name) throws NoSuchJobException {
synchronized (map) {
if (!map.containsKey(name)) {
throw new NoSuchJobException("No job configuration with the name [" + name + "] was registered");
}
return (Job) ((JobFactory) map.get(name)).createJob();
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.ListableJobConfigurationRegistry#getJobConfigurations()
*/
public Collection getJobNames() {
synchronized (map) {
return Collections.unmodifiableCollection(new HashSet(map.keySet()));
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.configuration;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.JobFactory;
/**
* A {@link JobFactory} that just keeps a reference to a {@link Job}. It never
* modifies its {@link Job}.
*
* @author Dave Syer
*
*/
public class ReferenceJobFactory implements JobFactory {
private Job job;
private String name;
/**
* @param job the {@link Job} to return from {@link #createJob()}.
*/
public ReferenceJobFactory(Job job) {
super();
this.job = job;
this.name = job.getName();
}
/**
* Just return the instance passed in on initialization.
*
* @see org.springframework.batch.core.configuration.JobFactory#createJob()
*/
public Job createJob() {
return job;
}
/**
* Returns the job name as passed in on initialization.
*
* @see org.springframework.batch.core.configuration.JobFactory#getJobName()
*/
public String getJobName() {
return name;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of configuration concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,124 @@
/*
* 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.job;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.util.ClassUtils;
/**
* Batch domain object representing a job. Job is an explicit abstraction
* representing the configuration of a job specified by a developer. It should
* be noted that restart policy is applied to the job as a whole and not to a
* step.
*
* @author Lucas Ward
* @author Dave Syer
*/
public abstract class AbstractJob implements BeanNameAware, Job {
private List steps = new ArrayList();
private String name;
private boolean restartable = false;
/**
* Default constructor.
*/
public AbstractJob() {
super();
}
/**
* Convenience constructor to immediately add name (which is mandatory but
* not final).
*
* @param name
*/
public AbstractJob(String name) {
super();
this.name = name;
}
/**
* Set the name property if it is not already set. Because of the order of
* the callbacks in a Spring container the name property will be set first
* if it is present. Care is needed with bean definition inheritance - if a
* parent bean has a name, then its children need an explicit name as well,
* otherwise they will not be unique.
*
* @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String)
*/
public void setBeanName(String name) {
if (this.name == null) {
this.name = name;
}
}
/**
* Set the name property. Always overrides the default value if this object
* is a Spring bean.
*
* @see #setBeanName(java.lang.String)
*/
public void setName(String name) {
this.name = name;
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.IJob#getName()
*/
public String getName() {
return name;
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.IJob#getSteps()
*/
public List getSteps() {
return steps;
}
public void setSteps(List steps) {
this.steps.clear();
this.steps.addAll(steps);
}
public void addStep(Step step) {
this.steps.add(step);
}
public void setRestartable(boolean restartable) {
this.restartable = restartable;
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.IJob#isRestartable()
*/
public boolean isRestartable() {
return restartable;
}
public String toString() {
return ClassUtils.getShortName(getClass()) + ": [name=" + name + "]";
}
}

View File

@@ -0,0 +1,203 @@
/*
* 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.job;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.JobListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.listener.CompositeJobListener;
import org.springframework.batch.repeat.ExitStatus;
/**
* Simple implementation of (@link Job} interface providing the ability to run a {@link JobExecution}. Sequentially
* executes a job by iterating through its list of steps.
*
* @author Lucas Ward
* @author Dave Syer
*/
public class SimpleJob extends AbstractJob {
private JobRepository jobRepository;
private CompositeJobListener listener = new CompositeJobListener();
/**
* Public setter for injecting {@link JobListener}s. They will all be given the {@link JobListener} callbacks at
* the appropriate point in the job.
*
* @param listeners the listeners to set.
*/
public void setJobListeners(JobListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
this.listener.register(listeners[i]);
}
}
/**
* Register a single listener for the {@link JobListener} callbacks.
*
* @param listener a {@link JobListener}
*/
public void registerListener(JobListener listener) {
this.listener.register(listener);
}
/**
* Run the specified job by looping through the steps and delegating to the {@link Step}.
*
* @see org.springframework.batch.core.Job#execute(org.springframework.batch.core.JobExecution)
*/
public void execute(JobExecution execution) throws JobExecutionException {
JobInstance jobInstance = execution.getJobInstance();
StepExecution currentStepExecution = null;
int startedCount = 0;
List steps = getSteps();
try {
// The job was already stopped before we even got this far. Deal
// with it in the same way as any other interruption.
if (execution.getStatus() == BatchStatus.STOPPING) {
throw new JobInterruptedException("JobExecution already stopped before being executed.");
}
execution.setStartTime(new Date());
updateStatus(execution, BatchStatus.STARTING);
listener.beforeJob(execution);
for (Iterator i = steps.iterator(); i.hasNext();) {
Step step = (Step) i.next();
if (shouldStart(jobInstance, step)) {
startedCount++;
updateStatus(execution, BatchStatus.STARTED);
currentStepExecution = execution.createStepExecution(step);
step.execute(currentStepExecution);
}
}
updateStatus(execution, BatchStatus.COMPLETED);
listener.afterJob(execution);
} catch (JobInterruptedException e) {
execution.setStatus(BatchStatus.STOPPED);
rethrow(e);
} catch (Throwable t) {
execution.setStatus(BatchStatus.FAILED);
rethrow(t);
} finally {
ExitStatus status = ExitStatus.FAILED;
if (startedCount == 0) {
if (steps.size() > 0) {
status = ExitStatus.NOOP
.addExitDescription("All steps already completed. No processing was done.");
} else {
status = ExitStatus.NOOP.addExitDescription("No steps configured for this job.");
}
} else if (currentStepExecution != null) {
status = currentStepExecution.getExitStatus();
}
execution.setEndTime(new Date());
execution.setExitStatus(status);
jobRepository.saveOrUpdate(execution);
}
}
private void updateStatus(JobExecution jobExecution, BatchStatus status) {
jobExecution.setStatus(status);
jobRepository.saveOrUpdate(jobExecution);
}
/*
* Given a step and configuration, return true if the step should start, false if it should not, and throw an
* exception if the job should finish.
*/
private boolean shouldStart(JobInstance jobInstance, Step step) throws JobExecutionException {
BatchStatus stepStatus;
// if the last execution is null, the step has never been executed.
StepExecution lastStepExecution = jobRepository.getLastStepExecution(jobInstance, step);
if (lastStepExecution == null) {
stepStatus = BatchStatus.STARTING;
} else {
stepStatus = lastStepExecution.getStatus();
}
if (stepStatus == BatchStatus.UNKNOWN) {
throw new JobExecutionException("Cannot restart step from UNKNOWN status. "
+ "The last execution ended with a failure that could not be rolled back, "
+ "so it may be dangerous to proceed. " + "Manual intervention is probably necessary.");
}
if (stepStatus == BatchStatus.COMPLETED && step.isAllowStartIfComplete() == false) {
// step is complete, false should be returned, indicating that the
// step should not be started
return false;
}
if (jobRepository.getStepExecutionCount(jobInstance, step) < step.getStartLimit()) {
// step start count is less than start max, return true
return true;
} else {
// start max has been exceeded, throw an exception.
throw new UnexpectedJobExecutionException("Maximum start limit exceeded for step: " + step.getName() + "StartMax: "
+ step.getStartLimit());
}
}
/**
* @param t
*/
private static void rethrow(Throwable t) throws RuntimeException {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new UnexpectedJobExecutionException(t);
}
}
/**
* Public setter for the {@link JobRepository} that is needed to manage the state of the batch meta domain (jobs,
* steps, executions) during the life of a job.
*
* @param jobRepository
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of job concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,54 @@
/*
* 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.launch;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
/**
* Simple interface for controlling jobs, including possible ad-hoc executions,
* based on different runtime identifiers. It is extremely important to note
* that this interface makes absolutely no guarantees about whether or not calls
* to it are executed synchronously or asynchronously. The javadocs for specific
* implementations should be checked to ensure callers fully understand how the
* job will be run.
*
* @author Lucas Ward
* @author Dave Syer
*/
public interface JobLauncher {
/**
* Start a job execution for the given {@link Job} and {@link JobParameters}.
*
* @return the exit code from the job if it returns synchronously. If the
* implementation is asynchronous, the status might well be unknown.
*
* @throws JobExecutionAlreadyRunningException if the JobInstance identified
* by the properties already has an execution running.
* @throws IllegalArgumentException if the job or jobInstanceProperties are
* null.
* @throws JobRestartException if the job has been run before and
* circumstances that preclude a re-start.
*/
public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
JobRestartException;
}

View File

@@ -0,0 +1,135 @@
/*
* 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.launch;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.Assert;
/**
* Simple implementation of the {@link JobLauncher} interface. The Spring Core
* {@link TaskExecutor} interface is used to launch a {@link Job}. This means
* that the type of executor set is very important. If a
* {@link SyncTaskExecutor} is used, then the job will be processed
* <strong>within the same thread that called the launcher.</strong> Care
* should be taken to ensure any users of this class understand fully whether or
* not the implementation of TaskExecutor used will start tasks synchronously or
* asynchronously. The default setting uses a synchronous task executor.
*
* There is only one required dependency of this Launcher, a
* {@link JobRepository}. The JobRepository is used to obtain a valid
* JobExecution. The Repository must be used because the provided {@link Job}
* could be a restart of an existing {@link JobInstance}, and only the
* Repository can reliably recreate it.
*
* @author Lucas Ward
* @since 1.0
* @see JobRepository
* @see TaskExecutor
*/
public class SimpleJobLauncher implements JobLauncher, InitializingBean {
protected static final Log logger = LogFactory.getLog(SimpleJobLauncher.class);
private JobRepository jobRepository;
private TaskExecutor taskExecutor = new SyncTaskExecutor();
/**
* Run the provided job with the given {@link JobParameters}. The
* {@link JobParameters} will be used to determine if this is an execution
* of an existing job instance, or if a new one should be created.
*
* @param job, the job to be run.
* @param jobParameters, the {@link JobParameters} for this particular
* execution.
* @return JobExecutionAlreadyRunningException if the JobInstance already
* exists and has an execution already running.
* @throws JobRestartException if the execution would be a re-start, but a
* re-start is either not allowed or not needed.
*/
public JobExecution run(final Job job, final JobParameters jobParameters)
throws JobExecutionAlreadyRunningException, JobRestartException {
Assert.notNull(job, "The Job must not be null.");
Assert.notNull(jobParameters, "The JobParameters must not be null.");
final JobExecution jobExecution = jobRepository.createJobExecution(job, jobParameters);
taskExecutor.execute(new Runnable() {
public void run() {
try {
logger.info("Job: [" + job + "] launched with the following parameters: [" + jobParameters + "]");
job.execute(jobExecution);
logger.info("Job: [" + job + "] completed successfully with the following parameters: ["
+ jobParameters + "]");
}
catch (Throwable t) {
logger.info("Job: [" + job + "] failed with the following parameters: [" + jobParameters + "]", t);
rethrow(t);
}
}
private void rethrow(Throwable t) {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
throw new RuntimeException(t);
}
});
return jobExecution;
}
/**
* Set the JobRepsitory.
*
* @param jobRepository
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Set the TaskExecutor. (Optional)
*
* @param taskExecutor
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Ensure the required dependencies of a {@link JobRepository} have been
* set.
*/
public void afterPropertiesSet() throws Exception {
Assert.state(jobRepository != null, "A JobRepository has not been set.");
logger.info("No TaskExecutor has been set, defaulting to synchronous executor.");
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of facade concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,259 @@
/*
* 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.launch.support;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.runtime.ExitStatusExceptionClassifier;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.execution.step.support.SimpleExitStatusExceptionClassifier;
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;
/**
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* The arguments to this class are roughly as follows:
* </p>
*
* <code>
* java jobPath jobName jobLauncherPath jobParameters...
* </code>
*
* <p>
* <ul>
* <li>jobPath: the xml application context containing a {@link Job}
* <li>jobName: the bean id of the job.
* <li>jobLauncherPath: the xml application context containing a
* {@link JobLauncher}
* <li>jobParameters: 0 to many parameters that will be used to launch a job.
* </ul>
* </p>
*
* <p>
* 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: "
*
* <p>
* <code>
* java org.springframework.batch.execution.bootstrap.support.CommandLineJobRunner testJob.xml
* testJob schedule.date=2008/01/24 vendor.id=3902483920
* <code></p>
*
* <p>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.</p>
*
* @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 ExitStatusExceptionClassifier exceptionClassifier = new SimpleExitStatusExceptionClassifier();
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 ExitStatusExceptionClassifier}
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(
ExitStatusExceptionClassifier 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 jobName, String[] parameters) {
try {
ApplicationContext context = new ClassPathXmlApplicationContext(jobPath);
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
* <p>
* <ul>
* <li>jobPath: the xml application context containing a
* {@link Job}
* <li>jobName: the bean id of the job.
* <li>jobLauncherPath: the xml application context containing a
* {@link JobLauncher}
* <li>jobParameters: 0 to many parameters that will be used to
* launch a job.
* </ul>
* </p>
*/
public static void main(String[] args) {
CommandLineJobRunner command = new CommandLineJobRunner();
if (args.length < 2) {
logger
.error("At least 2 arguments are required: JobPath and JobName.");
command.exit(1);
}
String jobPath = args[0];
String jobName = args[1];
String[] parameters = new String[args.length - 2];
System.arraycopy(args, 2, parameters, 0, args.length - 2);
int result = command.start(jobPath, jobName, parameters);
command.exit(result);
}
}

View File

@@ -0,0 +1,171 @@
/*
* 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.launch.support;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.util.StringUtils;
/**
* Factory for {@link JobParameters} instances using a simple naming convention
* for property keys. Key names ending with "(&lt;type&gt;)" where type is one
* of string, date, long are converted to the corresponding type. The default
* type is string. E.g.
*
* <pre>
* schedule.date(date)=2007/12/11
* department.id(long)=2345
* </pre>
*
* The literal values are converted to the correct type using the default Spring
* strategies, augmented if necessary by the custom editors provided.
*
* @author Dave Syer
*
*/
public class DefaultJobParametersFactory implements JobParametersFactory {
public static String DATE_TYPE = "(date)";
public static String STRING_TYPE = "(string)";
public static String LONG_TYPE = "(long)";
private DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
private NumberFormat numberFormat = new DecimalFormat("#");
/**
* Check for suffix on keys and use those to decide how to convert the
* value.
*
* @throws IllegalArgumentException if a number or date is passed in that
* cannot be parsed, or cast to the correct type.
*
* @see org.springframework.batch.core.runtime.JobParametersFactory#getJobParameters(java.util.Properties)
*/
public JobParameters getJobParameters(Properties props) {
if (props == null || props.isEmpty()) {
return new JobParameters();
}
JobParametersBuilder propertiesBuilder = new JobParametersBuilder();
for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (key.endsWith(DATE_TYPE)) {
Date date;
try {
date = dateFormat.parse(value);
}
catch (ParseException ex) {
String suffix = (dateFormat instanceof SimpleDateFormat) ? ", use "
+ ((SimpleDateFormat) dateFormat).toPattern() : "";
throw new IllegalArgumentException("Date format is invalid: [" + value + "]" + suffix, ex);
}
propertiesBuilder.addDate(StringUtils.replace(key, DATE_TYPE, ""), date);
}
else if (key.endsWith(LONG_TYPE)) {
Long result;
try {
result = (Long) numberFormat.parse(value);
}
catch (ParseException ex) {
String suffix = (numberFormat instanceof DecimalFormat) ? ", use "
+ ((DecimalFormat) numberFormat).toPattern() : "";
throw new IllegalArgumentException(
"Number format is invalid: [" + value + "], use " + suffix, ex);
}
catch (ClassCastException ex) {
throw new IllegalArgumentException("Number format is invalid: [" + value
+ "], use a format with no decimal places", ex);
}
propertiesBuilder.addLong(StringUtils.replace(key, LONG_TYPE, ""), result);
}
else if (StringUtils.endsWithIgnoreCase(key, STRING_TYPE)) {
propertiesBuilder.addString(StringUtils.replace(key, STRING_TYPE, ""), value);
}
else {
propertiesBuilder.addString(key.toString(), value.toString());
}
}
return propertiesBuilder.toJobParameters();
}
/**
* Use the same suffixes to create properties (omitting the string suffix
* because it is the default).
*
* @see org.springframework.batch.core.runtime.JobParametersFactory#getProperties(org.springframework.batch.core.JobParameters)
*/
public Properties getProperties(JobParameters params) {
if (params == null || params.isEmpty()) {
return new Properties();
}
Map parameters = params.getParameters();
Properties result = new Properties();
for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
Object value = parameters.get(key);
if (value instanceof Date) {
result.setProperty(key + DATE_TYPE, dateFormat.format(value));
}
else if (value instanceof Long) {
result.setProperty(key + LONG_TYPE, numberFormat.format(value));
}
else {
result.setProperty(key, "" + value);
}
}
return result;
}
/**
* Public setter for injecting a date format.
* @param dateFormat a {@link DateFormat}, defaults to "yyyy/MM/dd"
*/
public void setDateFormat(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
/**
* Public setter for the {@link NumberFormat}. Used to parse longs, so must
* not contain decimal place (e.g. use "#" or "#,###").
*
* @param numberFormat the {@link NumberFormat} to set
*/
public void setNumberFormat(NumberFormat numberFormat) {
this.numberFormat = numberFormat;
}
}

View File

@@ -0,0 +1,30 @@
package org.springframework.batch.execution.launch.support;
/**
*
* This interface should be implemented when an environment calling the batch famework has specific
* requirements regarding the process return codes.
*
* @param The type of returncode expected by the environment
* @author Stijn Maller
* @author Lucas Ward
* @author Dave Syer
*/
public interface ExitCodeMapper {
static int JVM_EXITCODE_COMPLETED = 0;
static int JVM_EXITCODE_GENERIC_ERROR = 1;
static int JVM_EXITCODE_JOB_ERROR = 2;
public static final String NO_SUCH_JOB = "NO_SUCH_JOB";
public static final String JOB_NOT_PROVIDED = "JOB_NOT_PROVIDED";
/**
* Transform the exitcode known by the batchframework into an exitcode in the
* format of the calling environment.
* @param exitCode The exitcode which is used internally by the batch framework.
* @return The corresponding exitcode as known by the calling environment.
*/
public int getExitCode(String exitCode);
}

View File

@@ -0,0 +1,74 @@
/*
* 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.launch.support;
import java.util.Properties;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.execution.launch.JobLauncher;
/**
* Interface to expose for remote management of jobs. Similar to
* {@link JobLauncher}, but replaces {@link JobExecution} and
* {@link JobIdentifier} with Strings in return types and method parameters, so
* it can be inspected by remote clients like the jconsole from the JRE without
* any links to Spring Batch.
*
* @author Dave Syer
*
*/
public interface ExportedJobLauncher {
/**
* Launch a job with the given name.
*
* @param name the name of the job to launch
* @return a representation of the {@link JobExecution} returned by a
* {@link JobLauncher}.
*/
String run(String name);
/**
* Launch a job with the given name and parameters.
*
* @param name the name of the job to launch
* @return a representation of the {@link JobExecution} returned by a
* {@link JobLauncher}.
*/
String run(String name, String params);
/**
* Stop all running jobs.
*/
void stop();
/**
* Enquire if any jobs launched here are still running.
*
* @return true if any jobs are running.
*/
boolean isRunning();
/**
* Query statistics of currently executing jobs.
*
* @return properties representing last known state of currently executing
* jobs
*/
public Properties getStatistics();
}

View File

@@ -0,0 +1,40 @@
/*
* 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.launch.support;
/**
* Implementation of the {@link SystemExiter} interface that calls the standards
* System.exit method. It should be noted that there will be no unit tests for
* this class, since there is only one line of actual code, that would only be
* testable by mocking System or Runtime.
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class JvmSystemExiter implements SystemExiter {
/**
* Delegate call to System.exit() with the argument provided. Do not use
* this at home children!
*
* @see org.springframework.batch.execution.launch.support.SystemExiter#exit(int)
*/
public void exit(int status) {
System.exit(status);
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.launch.support;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.runtime.JobParametersFactory;
/**
* @author Lucas Ward
*
*/
public class ScheduledJobParametersFactory implements JobParametersFactory {
public static String SCHEDULE_DATE_KEY = "schedule.date";
private DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
/*
* (non-Javadoc)
* @see org.springframework.batch.core.runtime.JobParametersFactory#getJobParameters(java.util.Properties)
*/
public JobParameters getJobParameters(Properties props) {
if(props == null || props.isEmpty()){
return new JobParameters();
}
JobParametersBuilder propertiesBuilder = new JobParametersBuilder();
for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
if (entry.getKey().equals(SCHEDULE_DATE_KEY)) {
Date scheduleDate;
try {
scheduleDate = dateFormat.parse(entry.getValue().toString());
}
catch (ParseException ex) {
throw new IllegalArgumentException("Date format is invalid: [" + entry.getValue() + "]",
ex);
}
propertiesBuilder.addDate(entry.getKey().toString(), scheduleDate);
}
else {
propertiesBuilder.addString(entry.getKey().toString(), entry.getValue().toString());
}
}
return propertiesBuilder.toJobParameters();
}
/**
* Convert schedule date to Date, and assume all other parameters can be
* represented by their default string value.
*
* @see org.springframework.batch.core.runtime.JobParametersFactory#getProperties(org.springframework.batch.core.JobParameters)
*/
public Properties getProperties(JobParameters params) {
if(params == null || params.isEmpty()){
return new Properties();
}
Map parameters = params.getParameters();
Properties result = new Properties();
for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
Object value = parameters.get(key);
if (key.equals(SCHEDULE_DATE_KEY)) {
result.setProperty(key, dateFormat.format(value));
} else {
result.setProperty(key,""+value);
}
}
return result;
}
/**
* Public setter for injecting a date format.
* @param dateFormat a {@link DateFormat}, defaults to "yyyy/MM/dd"
*/
public void setDateFormat(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.launch.support;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.NoSuchJobException;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.support.PropertiesConverter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* @author Dave Syer
*
*/
public class SimpleExportedJobLauncher implements ExportedJobLauncher, InitializingBean {
private JobLauncher launcher;
private JobLocator jobLocator;
private Map registry = new HashMap();
private JobParametersFactory jobParametersFactory = new DefaultJobParametersFactory();
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(launcher, "JobLauncher must be provided.");
Assert.notNull(jobLocator, "JobLocator must be provided.");
}
/**
* Public setter for the {@link JobLauncher}.
* @param launcher the launcher to set
*/
public void setLauncher(JobLauncher launcher) {
this.launcher = launcher;
}
/**
* Public setter for the JobLocator.
* @param jobLocator the jobLocator to set
*/
public void setJobLocator(JobLocator jobLocator) {
this.jobLocator = jobLocator;
}
/**
* Public setter for the JobParametersFactory.
* @param jobParametersFactory the jobParametersFactory to set
*/
public void setJobParametersFactory(JobParametersFactory jobParametersFactory) {
this.jobParametersFactory = jobParametersFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.bootstrap.support.ExportedJobLauncher#getStatistics()
*/
public Properties getStatistics() {
Properties result = new Properties();
int i = 0;
for (Iterator iterator = registry.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
JobExecution execution = (JobExecution) registry.get(key);
addStatistics(result, execution, "job" + i + ".");
i++;
}
return result;
}
/**
* @param result
* @param execution
*/
private void addStatistics(Properties result, JobExecution execution, String prefix) {
int i = 0;
for (Iterator iterator = execution.getStepExecutions().iterator(); iterator.hasNext();) {
StepExecution stepExecution = (StepExecution) iterator.next();
Properties statistics = stepExecution.getExecutionContext().getProperties();
for (Iterator iter = statistics.keySet().iterator(); iter.hasNext();) {
String key = (String) iter.next();
result.setProperty(prefix + "step" + i + "." + key, statistics.getProperty(key));
}
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.bootstrap.support.ExportedJobLauncher#isRunning()
*/
public boolean isRunning() {
for (Iterator iterator = registry.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
JobExecution execution = (JobExecution) registry.get(key);
if (execution.isRunning()) {
return true;
}
}
return false;
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.bootstrap.support.ExportedJobLauncher#run(java.lang.String)
*/
public String run(String name) {
return run(name, null);
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.bootstrap.support.ExportedJobLauncher#run(java.lang.String,
* java.lang.String)
*/
public String run(String name, String params) {
Job job;
try {
job = jobLocator.getJob(name);
}
catch (NoSuchJobException e) {
return e.getClass().getName() + ": " + e.getMessage();
}
JobParameters jobParameters = new JobParameters();
if (params != null) {
jobParameters = jobParametersFactory.getJobParameters(PropertiesConverter.stringToProperties(params));
}
JobExecution execution;
try {
execution = launcher.run(job, jobParameters);
}
catch (JobExecutionAlreadyRunningException e) {
return e.getClass().getName() + ": " + e.getMessage();
}
catch (JobRestartException e) {
return e.getClass().getName() + ": " + e.getMessage();
}
registry.put(name + params, execution);
return execution.toString();
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.bootstrap.support.ExportedJobLauncher#stop()
*/
public void stop() {
for (Iterator iterator = registry.keySet().iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
JobExecution execution = (JobExecution) registry.get(key);
execution.stop();
}
registry.clear();
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.launch.support;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.repeat.ExitStatus;
/**
* An implementation of {@link ExitCodeMapper} that can be configured
* through a map from batch exit codes (String) to integer results.
*
* @author Stijn Maller
* @author Lucas Ward
* @author Dave Syer
*/
public class SimpleJvmExitCodeMapper implements ExitCodeMapper {
protected Log logger = LogFactory.getLog(getClass());
private Map mapping;
public SimpleJvmExitCodeMapper(){
mapping = new HashMap();
mapping.put(ExitStatus.FINISHED.getExitCode(),
new Integer(JVM_EXITCODE_COMPLETED));
mapping.put(ExitStatus.FAILED.getExitCode(),
new Integer(JVM_EXITCODE_GENERIC_ERROR));
mapping.put(ExitCodeMapper.JOB_NOT_PROVIDED,
new Integer(JVM_EXITCODE_JOB_ERROR));
mapping.put(ExitCodeMapper.NO_SUCH_JOB,
new Integer(JVM_EXITCODE_JOB_ERROR));
}
public Map getMapping() {
return mapping;
}
/**
* Supply the ExitCodeMappings
* @param exitCodeMap A set of mappings between environment specific exit codes
* and batch framework internal exit codes
*/
public void setMapping(Map exitCodeMap) {
mapping.putAll(exitCodeMap);
}
/**
* Get the JVM exitcode that matches a certain Batch Framework Exitcode
* @param exitCode The exitcode of the Batch Job as known by the Batch Framework
* @return The exitCode of the Batch Job as known by the JVM
*/
public int getExitCode(String exitCode) {
Integer statusCode = null;
try{
statusCode = (Integer)mapping.get(exitCode);
}
catch(RuntimeException ex){
//We still need to return an exit code, even if there is an issue with
//the mapper.
logger.fatal("Error mapping exit code, generic exit code returned.", ex);
}
return (statusCode != null) ? statusCode.intValue() : JVM_EXITCODE_GENERIC_ERROR;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.launch.support;
/**
* Interface for exiting the JVM. This abstraction is only
* useful in order to allow classes that make System.exit calls
* to be testable, since calling System.exit during a unit
* test would cause the entire jvm to finish.
*
* @author Lucas Ward
*
*/
public interface SystemExiter {
/**
* Terminate the currently running Java Virtual Machine.
*
* @param status exit status.
* @throws SecurityException
* if a security manager exists and its <code>checkExit</code>
* method doesn't allow exit with the specified status.
* @see System.exit
*/
void exit(int status);
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Support classes for use in bootstrap implementations or configurations.
</p>
</body>
</html>

View File

@@ -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.execution.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.ChunkListener;
/**
* @author Lucas Ward
*
*/
public class CompositeChunkListener implements ChunkListener {
private List listeners = new ArrayList();
/**
* Public setter for the listeners.
*
* @param listeners
*/
public void setListeners(ChunkListener[] listeners) {
this.listeners = Arrays.asList(listeners);
}
/**
* Register additional listener.
*
* @param stepListener
*/
public void register(ChunkListener chunkListener) {
if (!listeners.contains(chunkListener)) {
listeners.add(chunkListener);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.ChunkListener#afterChunk()
*/
public void afterChunk() {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ChunkListener listener = (ChunkListener) iterator.next();
listener.afterChunk();
}
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.ChunkListener#beforeChunk()
*/
public void beforeChunk() {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ChunkListener listener = (ChunkListener) iterator.next();
listener.beforeChunk();
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.ChunkListener;
import org.springframework.batch.core.ItemReadListener;
/**
* @author Lucas Ward
*
*/
public class CompositeItemReadListener implements ItemReadListener {
private List listeners = new ArrayList();
/**
* Public setter for the listeners.
*
* @param listeners
*/
public void setListeners(ChunkListener[] listeners) {
this.listeners = Arrays.asList(listeners);
}
/**
* Register additional listener.
*
* @param itemReaderListener
*/
public void register(ItemReadListener itemReaderListener) {
if (!listeners.contains(itemReaderListener)) {
listeners.add(itemReaderListener);
}
}
public void afterRead(Object item) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemReadListener listener = (ItemReadListener) iterator.next();
listener.afterRead(item);
}
}
public void beforeRead() {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemReadListener listener = (ItemReadListener) iterator.next();
listener.beforeRead();
}
}
public void onReadError(Exception ex) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemReadListener listener = (ItemReadListener) iterator.next();
listener.onReadError(ex);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.ChunkListener;
import org.springframework.batch.core.ItemWriteListener;
/**
* @author Lucas Ward
*
*/
public class CompositeItemWriteListener implements ItemWriteListener {
private List listeners = new ArrayList();
/**
* Public setter for the listeners.
*
* @param listeners
*/
public void setListeners(ChunkListener[] listeners) {
this.listeners = Arrays.asList(listeners);
}
/**
* Register additional listener.
*
* @param itemReaderListener
*/
public void register(ItemWriteListener itemReaderListener) {
if (!listeners.contains(itemReaderListener)) {
listeners.add(itemReaderListener);
}
}
public void afterWrite() {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemWriteListener listener = (ItemWriteListener) iterator.next();
listener.afterWrite();
}
}
public void beforeWrite(Object item) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemWriteListener listener = (ItemWriteListener) iterator.next();
listener.beforeWrite(item);
}
}
public void onWriteError(Exception ex, Object item) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
ItemWriteListener listener = (ItemWriteListener) iterator.next();
listener.onWriteError(ex, item);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobListener;
/**
* @author Dave Syer
*
*/
public class CompositeJobListener implements JobListener {
private List listeners = new ArrayList();
/**
* Public setter for the listeners.
*
* @param listeners
*/
public void setListeners(JobListener[] listeners) {
this.listeners = Arrays.asList(listeners);
}
/**
* Register additional listener.
*
* @param jobListener
*/
public void register(JobListener jobListener) {
if (!listeners.contains(jobListener)) {
listeners.add(jobListener);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.StepListener#close()
*/
public void afterJob(JobExecution jobExecution) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
JobListener listener = (JobListener) iterator.next();
listener.afterJob(jobExecution);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.StepListener#open(org.springframework.batch.core.domain.JobParameters)
*/
public void beforeJob(JobExecution jobExecution) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
JobListener listener = (JobListener) iterator.next();
listener.beforeJob(jobExecution);
}
}
}

View File

@@ -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.execution.listener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.repeat.ExitStatus;
/**
* @author Lucas Ward
*
*/
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);
}
/**
* 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 afterStep(StepExecution stepExecution) {
ExitStatus status = null;
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
StepListener listener = (StepListener) iterator.next();
ExitStatus close = listener.afterStep(stepExecution);
status = status!=null ? status.and(close): close;
}
return status;
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.StepListener#open(org.springframework.batch.core.domain.JobParameters)
*/
public void beforeStep(StepExecution stepExecution) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
StepListener listener = (StepListener) iterator.next();
listener.beforeStep(stepExecution);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.core.domain.StepListener#onError(java.lang.Throwable)
*/
public ExitStatus onErrorInStep(StepExecution stepExecution, Throwable e) {
ExitStatus status = null;
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
StepListener listener = (StepListener) iterator.next();
ExitStatus close = listener.onErrorInStep(stepExecution, e);
status = status!=null ? status.and(close): close;
}
return status;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Reference implementation of the Spring Batch Core.
</p>
</body>
</html>

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2002-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.repository;
import javax.sql.DataSource;
import org.springframework.batch.execution.repository.dao.JdbcJobExecutionDao;
import org.springframework.batch.execution.repository.dao.JdbcJobInstanceDao;
import org.springframework.batch.execution.repository.dao.JdbcStepExecutionDao;
import org.springframework.batch.execution.repository.dao.JobExecutionDao;
import org.springframework.batch.execution.repository.dao.JobInstanceDao;
import org.springframework.batch.execution.repository.dao.StepExecutionDao;
import org.springframework.batch.item.database.support.DataFieldMaxValueIncrementerFactory;
import org.springframework.batch.item.database.support.DefaultDataFieldMaxValueIncrementerFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.Assert;
/**
* A {@link FactoryBean} that automates the creation of a {@link SimpleJobRepository}. Requires the user
* to describe what kind of database they are using.
*
* @author Ben Hale
* @author Lucas Ward
*/
public class JobRepositoryFactoryBean implements FactoryBean, InitializingBean {
private DataSource dataSource;
private String databaseType;
private DataFieldMaxValueIncrementerFactory incrementerFactory;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void setDatabaseType(String dbType) {
this.databaseType = dbType;
}
public void setIncrementerFactory(
DataFieldMaxValueIncrementerFactory incrementerFactory) {
this.incrementerFactory = incrementerFactory;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(dataSource, "Datasource must not be null.");
if(incrementerFactory == null){
incrementerFactory = new DefaultDataFieldMaxValueIncrementerFactory(dataSource);
}
Assert.isTrue(incrementerFactory.isSupportedIncrementerType(databaseType), "Unsupported database type");
}
public Object getObject() throws Exception {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
JobInstanceDao jobInstanceDao = createJobInstanceDao(jdbcTemplate);
JobExecutionDao jobExecutionDao = createJobExecutionDao(jdbcTemplate);
StepExecutionDao stepExecutionDao = createStepExecutionDao(jdbcTemplate);
return new SimpleJobRepository(jobInstanceDao, jobExecutionDao, stepExecutionDao);
}
public Class getObjectType() {
return SimpleJobRepository.class;
}
public boolean isSingleton() {
return true;
}
private JobInstanceDao createJobInstanceDao(JdbcTemplate jdbcTemplate) throws Exception {
JdbcJobInstanceDao dao = new JdbcJobInstanceDao();
dao.setJdbcTemplate(jdbcTemplate);
dao.setJobIncrementer(incrementerFactory.getIncrementer(databaseType, "BATCH_JOB_SEQ"));
dao.afterPropertiesSet();
return dao;
}
private JobExecutionDao createJobExecutionDao(JdbcTemplate jdbcTemplate) throws Exception {
JdbcJobExecutionDao dao = new JdbcJobExecutionDao();
dao.setJdbcTemplate(jdbcTemplate);
dao.setJobExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, "BATCH_JOB_EXECUTION_SEQ"));
dao.afterPropertiesSet();
return dao;
}
private StepExecutionDao createStepExecutionDao(JdbcTemplate jdbcTemplate) throws Exception {
JdbcStepExecutionDao dao = new JdbcStepExecutionDao();
dao.setJdbcTemplate(jdbcTemplate);
dao.setStepExecutionIncrementer(incrementerFactory.getIncrementer(databaseType, "BATCH_STEP_EXECUTION_SEQ"));
dao.afterPropertiesSet();
return dao;
}
}

View File

@@ -0,0 +1,285 @@
/*
* 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.repository;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.repository.dao.JobExecutionDao;
import org.springframework.batch.execution.repository.dao.JobInstanceDao;
import org.springframework.batch.execution.repository.dao.StepExecutionDao;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.util.Assert;
/**
*
* <p>
* Implementation of {@link JobRepository} that stores JobInstances,
* JobExecutions, and StepExecutions using the injected DAOs.
* <p>
*
* @author Lucas Ward
* @author Dave Syer
* @author Robert Kasanicky
*
* @see JobRepository
* @see JobInstanceDao
* @see JobExecutionDao
* @see StepExecutionDao
*
*/
public class SimpleJobRepository implements JobRepository {
private JobInstanceDao jobInstanceDao;
private JobExecutionDao jobExecutionDao;
private StepExecutionDao stepExecutionDao;
/**
* Provide default constructor with low visibility in case user wants to use
* use aop:proxy-target-class="true" for transaction interceptor.
*/
SimpleJobRepository() {
}
public SimpleJobRepository(JobInstanceDao jobInstanceDao, JobExecutionDao jobExecutionDao,
StepExecutionDao stepExecutionDao) {
super();
this.jobInstanceDao = jobInstanceDao;
this.jobExecutionDao = jobExecutionDao;
this.stepExecutionDao = stepExecutionDao;
}
/**
* <p>
* Create a {@link JobExecution} based on the passed in {@link Job} and
* {@link JobParameters}. However, unique identification of a job can only
* come from the database, and therefore must come from JobDao by either
* creating a new job instance or finding an existing one, which will ensure
* that the id of the job instance is populated with the correct value.
* </p>
*
* <p>
* There are two ways in which the method determines if a job should be
* created or an existing one should be returned. The first is
* restartability. The {@link Job} restartable property will be checked
* first. If it is false, a new job will be created, regardless of whether
* or not one exists. If it is true, the {@link JobInstanceDao} will be
* checked to determine if the job already exists, if it does, it's steps
* will be populated (there must be at least 1) and a new
* {@link JobExecution} will be returned. If no job instance is found, a new
* one will be created.
* </p>
*
* <p>
* A check is made to see if any job executions are already running, and an
* exception will be thrown if one is detected. To detect a running job
* execution we use the {@link JobExecutionDao}:
* <ol>
* <li>First we find all jobs which match the given {@link JobParameters}
* and job name</li>
* <li>What happens then depends on how many existing job instances we
* find:
* <ul>
* <li>If there are none, or the {@link Job} is marked restartable, then we
* create a new {@link JobInstance}</li>
* <li>If there is more than one and the {@link Job} is not marked as
* restartable, it is an error. This could be caused by a job whose
* restartable flag has changed to be more strict (true not false)
* <em>after</em> it has been executed at least once.</li>
* <li>If there is precisely one existing {@link JobInstance} then we check
* the {@link JobExecution} instances for that job, and if any of them tells
* us it is running (see {@link JobExecution#isRunning()}) then it is an
* error.</li>
* </ul>
* </li>
* </ol>
* If this method is run in a transaction (as it normally would be) with
* isolation level at {@link Isolation#REPEATABLE_READ} or better, then this
* method should block if another transaction is already executing it (for
* the same {@link JobParameters} and job name). The first transaction to
* complete in this scenario obtains a valid {@link JobExecution}, and
* others throw {@link JobExecutionAlreadyRunningException} (or timeout).
* There are no such guarantees if the {@link JobDao} does not respect the
* transaction isolation levels (e.g. if using a non-relational data-store,
* or if the platform does not support the higher isolation levels).
* </p>
*
* @see JobRepository#createJobExecution(Job, JobParameters)
*
*/
public JobExecution createJobExecution(Job job, JobParameters jobParameters)
throws JobExecutionAlreadyRunningException, JobRestartException {
Assert.notNull(job, "Job must not be null.");
Assert.notNull(jobParameters, "JobParameters must not be null.");
/*
* Find all jobs matching the runtime information.
*
* If this method is transactional, and the isolation level is
* REPEATABLE_READ or better, another launcher trying to start the same
* job in another thread or process will block until this transaction
* has finished.
*/
JobInstance jobInstance = jobInstanceDao.getJobInstance(job, jobParameters);
// existing job instance found
if (jobInstance != null) {
if (!job.isRestartable()) {
throw new JobRestartException("JobInstance already exists and is not restartable");
}
List executions = jobExecutionDao.findJobExecutions(jobInstance);
// check for running executions and find the last started
for (Iterator iterator = executions.iterator(); iterator.hasNext();) {
JobExecution execution = (JobExecution) iterator.next();
if (execution.isRunning()) {
throw new JobExecutionAlreadyRunningException("A job execution for this job is already running: "
+ jobInstance);
}
}
}
else {
// no job found, create one
jobInstance = jobInstanceDao.createJobInstance(job, jobParameters);
}
JobExecution jobExecution = new JobExecution(jobInstance);
// Save the JobExecution so that it picks up an ID (useful for clients
// monitoring asynchronous executions):
saveOrUpdate(jobExecution);
return jobExecution;
}
/**
* Save or Update a JobExecution. A JobExecution is considered one
* 'execution' of a particular job. Therefore, it must have it's jobId field
* set before it is passed into this method. It also has it's own unique
* identifer, because it must be updatable separately. If an id isn't found,
* a new JobExecution is created, if one is found, the current row is
* updated.
*
* @param JobExecution to be stored.
* @throws IllegalArgumentException if jobExecution is null.
*/
public void saveOrUpdate(JobExecution jobExecution) {
Assert.notNull(jobExecution, "JobExecution cannot be null.");
Assert.notNull(jobExecution.getJobId(), "JobExecution must have a Job ID set.");
if (jobExecution.getId() == null) {
// existing instance
jobExecutionDao.saveJobExecution(jobExecution);
}
else {
// new execution
jobExecutionDao.updateJobExecution(jobExecution);
}
}
/**
* Save or Update the given StepExecution. If it's id is null, it will be
* saved and an id will be set, otherwise it will be updated. It should be
* noted that assigning an ID randomly will likely cause an exception
* depending on the StepDao implementation.
*
* @param StepExecution to be saved.
* @throws IllegalArgumentException if stepExecution is null.
*/
public void saveOrUpdate(StepExecution stepExecution) {
Assert.notNull(stepExecution, "StepExecution cannot be null.");
Assert.notNull(stepExecution.getStepName(), "StepExecution's step name cannot be null.");
Assert.notNull(stepExecution.getJobExecutionId(), "StepExecution must belong to persisted JobExecution");
if (stepExecution.getId() == null) {
stepExecutionDao.saveStepExecution(stepExecution);
}
else {
// existing execution, update
stepExecutionDao.updateStepExecution(stepExecution);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.core.repository.JobRepository#saveOrUpdateExecutionContext(org.springframework.batch.core.domain.StepExecution)
*/
public void saveOrUpdateExecutionContext(StepExecution stepExecution) {
saveOrUpdate(stepExecution);
stepExecutionDao.saveOrUpdateExecutionContext(stepExecution);
}
/**
* @return the last execution of the step within given job instance
*/
public StepExecution getLastStepExecution(JobInstance jobInstance, Step step) {
List jobExecutions = jobExecutionDao.findJobExecutions(jobInstance);
List stepExecutions = new ArrayList(jobExecutions.size());
for (Iterator iterator = jobExecutions.iterator(); iterator.hasNext();) {
JobExecution jobExecution = (JobExecution) iterator.next();
StepExecution stepExecution = stepExecutionDao.getStepExecution(jobExecution, step);
if (stepExecution != null) {
stepExecutions.add(stepExecution);
}
}
StepExecution latest = null;
for (Iterator iterator = stepExecutions.iterator(); iterator.hasNext();) {
StepExecution stepExecution = (StepExecution) iterator.next();
if (latest == null) {
latest = stepExecution;
}
if (latest.getStartTime().getTime() < stepExecution.getStartTime().getTime()) {
latest = stepExecution;
}
}
return latest;
}
/**
* @return number of executions of the step within given job instance
*/
public int getStepExecutionCount(JobInstance jobInstance, Step step) {
int count = 0;
List jobExecutions = jobExecutionDao.findJobExecutions(jobInstance);
for (Iterator iterator = jobExecutions.iterator(); iterator.hasNext();) {
JobExecution jobExecution = (JobExecution) iterator.next();
if (stepExecutionDao.getStepExecution(jobExecution, step) != null) {
count++;
}
}
return count;
}
}

View File

@@ -0,0 +1,52 @@
package org.springframework.batch.execution.repository.dao;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Encapsulates common functionality needed by JDBC batch metadata DAOs -
* provides jdbcTemplate for subclasses and handles table prefixes.
*
* @author Robert Kasanicky
*/
public abstract class AbstractJdbcBatchMetadataDao implements InitializingBean {
/**
* Default value for the table prefix property.
*/
public static final String DEFAULT_TABLE_PREFIX = "BATCH_";
private String tablePrefix = DEFAULT_TABLE_PREFIX;
private JdbcOperations jdbcTemplate;
protected String getQuery(String base) {
return StringUtils.replace(base, "%PREFIX%", tablePrefix);
}
/**
* Public setter for the table prefix property. This will be prefixed to all
* the table names before queries are executed. Defaults to
* {@value #DEFAULT_TABLE_PREFIX}.
*
* @param tablePrefix the tablePrefix to set
*/
public void setTablePrefix(String tablePrefix) {
this.tablePrefix = tablePrefix;
}
public void setJdbcTemplate(JdbcOperations jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
protected JdbcOperations getJdbcTemplate() {
return jdbcTemplate;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(jdbcTemplate);
}
}

View File

@@ -0,0 +1,219 @@
package org.springframework.batch.execution.repository.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.util.Assert;
/**
* Jdbc implementation of {@link JobExecutionDao}. Uses sequences (via Spring's
* {@link DataFieldMaxValueIncrementer} abstraction) to create all primary keys
* before inserting a new row. Objects are checked to ensure all mandatory
* fields to be stored are not null. If any are found to be null, an
* IllegalArgumentException will be thrown. This could be left to JdbcTemplate,
* however, the exception will be fairly vague, and fails to highlight which
* field caused the exception.
*
* @author Lucas Ward
* @author Dave Syer
* @author Robert Kasanicky
*/
public class JdbcJobExecutionDao extends AbstractJdbcBatchMetadataDao implements JobExecutionDao, InitializingBean {
private static final Log logger = LogFactory.getLog(JdbcJobExecutionDao.class);
private static final int EXIT_MESSAGE_LENGTH = 250;
private static final String GET_JOB_EXECUTION_COUNT = "SELECT count(JOB_EXECUTION_ID) from %PREFIX%JOB_EXECUTION "
+ "where JOB_INSTANCE_ID = ?";
private static final String SAVE_JOB_EXECUTION = "INSERT into %PREFIX%JOB_EXECUTION(JOB_EXECUTION_ID, JOB_INSTANCE_ID, START_TIME, "
+ "END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE) values (?, ?, ?, ?, ?, ?, ?, ?)";
private static final String CHECK_JOB_EXECUTION_EXISTS = "SELECT COUNT(*) FROM %PREFIX%JOB_EXECUTION WHERE JOB_EXECUTION_ID = ?";
private static final String UPDATE_JOB_EXECUTION = "UPDATE %PREFIX%JOB_EXECUTION set START_TIME = ?, END_TIME = ?, "
+ " STATUS = ?, CONTINUABLE = ?, EXIT_CODE = ?, EXIT_MESSAGE = ? where JOB_EXECUTION_ID = ?";
private static final String FIND_JOB_EXECUTIONS = "SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%JOB_EXECUTION"
+ " where JOB_INSTANCE_ID = ?";
private static final String GET_LAST_EXECUTION = "SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%JOB_EXECUTION"
+ " where JOB_INSTANCE_ID = ? and START_TIME = (SELECT max(START_TIME) from %PREFIX%JOB_EXECUTION where JOB_INSTANCE_ID = ?)";
private DataFieldMaxValueIncrementer jobExecutionIncrementer;
public List findJobExecutions(final JobInstance job) {
Assert.notNull(job, "Job cannot be null.");
Assert.notNull(job.getId(), "Job Id cannot be null.");
return getJdbcTemplate().query(getQuery(FIND_JOB_EXECUTIONS), new Object[] { job.getId() },
new JobExecutionRowMapper(job));
}
/**
* @see JobDao#getJobExecutionCount(JobInstance)
* @throws IllegalArgumentException if jobId is null.
*/
public int getJobExecutionCount(JobInstance jobInstance) {
Long jobId = jobInstance.getId();
Assert.notNull(jobId, "JobId cannot be null");
Object[] parameters = new Object[] { jobId };
return getJdbcTemplate().queryForInt(getQuery(GET_JOB_EXECUTION_COUNT), parameters);
}
/**
*
* SQL implementation using Sequences via the Spring incrementer
* abstraction. Once a new id has been obtained, the JobExecution is saved
* via a SQL INSERT statement.
*
* @see JobDao#saveJobExecution(JobExecution)
* @throws IllegalArgumentException if jobExecution is null, as well as any
* of it's fields to be persisted.
*/
public void saveJobExecution(JobExecution jobExecution) {
validateJobExecution(jobExecution);
jobExecution.setId(new Long(jobExecutionIncrementer.nextLongValue()));
Object[] parameters = new Object[] { jobExecution.getId(), jobExecution.getJobId(),
jobExecution.getStartTime(), jobExecution.getEndTime(), jobExecution.getStatus().toString(),
jobExecution.getExitStatus().isContinuable() ? "Y" : "N", jobExecution.getExitStatus().getExitCode(),
jobExecution.getExitStatus().getExitDescription() };
getJdbcTemplate().update(
getQuery(SAVE_JOB_EXECUTION),
parameters,
new int[] { Types.INTEGER, Types.INTEGER, Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.CHAR,
Types.VARCHAR, Types.VARCHAR });
}
/**
* Validate JobExecution. At a minimum, JobId, StartTime, EndTime, and
* Status cannot be null.
*
* @param jobExecution
* @throws IllegalArgumentException
*/
private void validateJobExecution(JobExecution jobExecution) {
Assert.notNull(jobExecution);
Assert.notNull(jobExecution.getJobId(), "JobExecution Job-Id cannot be null.");
Assert.notNull(jobExecution.getStatus(), "JobExecution status cannot be null.");
}
/**
* Update given JobExecution using a SQL UPDATE statement. The JobExecution
* is first checked to ensure all fields are not null, and that it has an
* ID. The database is then queried to ensure that the ID exists, which
* ensures that it is valid.
*
* @see JobDao#updateJobExecution(JobExecution)
*/
public void updateJobExecution(JobExecution jobExecution) {
validateJobExecution(jobExecution);
String exitDescription = jobExecution.getExitStatus().getExitDescription();
if (exitDescription != null && exitDescription.length() > EXIT_MESSAGE_LENGTH) {
exitDescription = exitDescription.substring(0, EXIT_MESSAGE_LENGTH);
logger.debug("Truncating long message before update of JobExecution: " + jobExecution);
}
Object[] parameters = new Object[] { jobExecution.getStartTime(), jobExecution.getEndTime(),
jobExecution.getStatus().toString(), jobExecution.getExitStatus().isContinuable() ? "Y" : "N",
jobExecution.getExitStatus().getExitCode(), exitDescription, jobExecution.getId() };
if (jobExecution.getId() == null) {
throw new IllegalArgumentException("JobExecution ID cannot be null. JobExecution must be saved "
+ "before it can be updated.");
}
// Check if given JobExecution's Id already exists, if none is found it
// is invalid and
// an exception should be thrown.
if (getJdbcTemplate().queryForInt(getQuery(CHECK_JOB_EXECUTION_EXISTS), new Object[] { jobExecution.getId() }) != 1) {
throw new NoSuchObjectException("Invalid JobExecution, ID " + jobExecution.getId()
+ " not found.");
}
getJdbcTemplate().update(
getQuery(UPDATE_JOB_EXECUTION),
parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.CHAR, Types.VARCHAR, Types.VARCHAR,
Types.INTEGER });
}
/**
* Setter for {@link DataFieldMaxValueIncrementer} to be used when
* generating primary keys for {@link JobExecution} instances.
*
* @param jobExecutionIncrementer the {@link DataFieldMaxValueIncrementer}
*/
public void setJobExecutionIncrementer(DataFieldMaxValueIncrementer jobExecutionIncrementer) {
this.jobExecutionIncrementer = jobExecutionIncrementer;
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(jobExecutionIncrementer);
}
/**
* Re-usable mapper for {@link JobExecution} instances.
*
* @author Dave Syer
*
*/
private static class JobExecutionRowMapper implements RowMapper {
private JobInstance job;
public JobExecutionRowMapper(JobInstance job) {
super();
this.job = job;
}
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
JobExecution jobExecution = new JobExecution(job);
jobExecution.setId(new Long(rs.getLong(1)));
jobExecution.setStartTime(rs.getTimestamp(2));
jobExecution.setEndTime(rs.getTimestamp(3));
jobExecution.setStatus(BatchStatus.getStatus(rs.getString(4)));
jobExecution.setExitStatus(new ExitStatus("Y".equals(rs.getString(5)), rs.getString(6), rs.getString(7)));
return jobExecution;
}
}
public JobExecution getLastJobExecution(JobInstance jobInstance) {
Long id = jobInstance.getId();
List executions = getJdbcTemplate().query(getQuery(GET_LAST_EXECUTION), new Object[] { id, id },
new JobExecutionRowMapper(jobInstance));
Assert.state(executions.size() <= 1, "There must be at most one latest job execution");
if (executions.isEmpty()) {
return null;
}
else {
return (JobExecution) executions.get(0);
}
}
}

View File

@@ -0,0 +1,236 @@
package org.springframework.batch.execution.repository.dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.util.Assert;
/**
* Jdbc implementation of {@link JobInstanceDao}. Uses sequences (via Spring's
* {@link DataFieldMaxValueIncrementer} abstraction) to create all primary keys
* before inserting a new row. Objects are checked to ensure all mandatory
* fields to be stored are not null. If any are found to be null, an
* IllegalArgumentException will be thrown. This could be left to JdbcTemplate,
* however, the exception will be fairly vague, and fails to highlight which
* field caused the exception.
*
* @author Lucas Ward
* @author Dave Syer
* @author Robert Kasanicky
*/
public class JdbcJobInstanceDao extends AbstractJdbcBatchMetadataDao implements JobInstanceDao, InitializingBean {
private static final String CREATE_JOB_INSTANCE = "INSERT into %PREFIX%JOB_INSTANCE(JOB_INSTANCE_ID, JOB_NAME, JOB_KEY)"
+ " values (?, ?, ?)";
private static final String CREATE_JOB_PARAMETERS = "INSERT into %PREFIX%JOB_PARAMS(JOB_INSTANCE_ID, KEY_NAME, TYPE_CD, "
+ "STRING_VAL, DATE_VAL, LONG_VAL, DOUBLE_VAL) values (?, ?, ?, ?, ?, ?, ?)";
private static final String FIND_JOBS = "SELECT JOB_INSTANCE_ID from %PREFIX%JOB_INSTANCE where JOB_NAME = ? and JOB_KEY = ?";
private DataFieldMaxValueIncrementer jobIncrementer;
/**
* In this jdbc implementation a job id is obtained by asking the
* jobIncrementer (which is likely a sequence) for the nextLong, and then
* passing the Id and parameter values into an INSERT statement.
*
* @see JobDao#createJob(JobIdentifier)
* @throws IllegalArgumentException if any {@link JobIdentifier} fields are
* null.
*/
public JobInstance createJobInstance(Job job, JobParameters jobParameters) {
Assert.notNull(job, "Job must not be null.");
Assert.hasLength(job.getName(), "Job must have a name");
Assert.notNull(jobParameters, "JobParameters must not be null.");
Assert.state(getJobInstance(job, jobParameters) == null, "JobInstance must not already exist");
Long jobId = new Long(jobIncrementer.nextLongValue());
Object[] parameters = new Object[] { jobId, job.getName(), createJobKey(jobParameters) };
getJdbcTemplate().update(getQuery(CREATE_JOB_INSTANCE), parameters,
new int[] { Types.INTEGER, Types.VARCHAR, Types.VARCHAR });
insertJobParameters(jobId, jobParameters);
JobInstance jobInstance = new JobInstance(jobId, jobParameters, job);
return jobInstance;
}
private String createJobKey(JobParameters jobParameters) {
Map props = jobParameters.getParameters();
StringBuffer stringBuffer = new StringBuffer();
for (Iterator it = props.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
stringBuffer.append(entry.toString() + ";");
}
return stringBuffer.toString();
}
/**
* Convenience method that inserts all parameters from the provided
* JobParameters.
*
*/
private void insertJobParameters(Long jobId, JobParameters jobParameters) {
Map parameters = jobParameters.getStringParameters();
if (!parameters.isEmpty()) {
for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
insertParameter(jobId, ParameterType.STRING, entry.getKey().toString(), entry.getValue());
}
}
parameters = jobParameters.getLongParameters();
if (!parameters.isEmpty()) {
for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
insertParameter(jobId, ParameterType.LONG, entry.getKey().toString(), entry.getValue());
}
}
parameters = jobParameters.getDoubleParameters();
if (!parameters.isEmpty()) {
for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
insertParameter(jobId, ParameterType.DOUBLE, entry.getKey().toString(), entry.getValue());
}
}
parameters = jobParameters.getDateParameters();
if (!parameters.isEmpty()) {
for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
insertParameter(jobId, ParameterType.DATE, entry.getKey().toString(), entry.getValue());
}
}
}
/**
* Convenience method that inserts an individual records into the
* JobParameters table.
*/
private void insertParameter(Long jobId, ParameterType type, String key, Object value) {
Object[] args = new Object[0];
int[] argTypes = new int[] { Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, Types.TIMESTAMP,
Types.INTEGER, Types.DOUBLE };
if (type == ParameterType.STRING) {
args = new Object[] { jobId, key, type, value, new Timestamp(0L), new Long(0), new Double(0) };
}
else if (type == ParameterType.LONG) {
args = new Object[] { jobId, key, type, "", new Timestamp(0L), value, new Double(0) };
}
else if (type == ParameterType.DOUBLE) {
args = new Object[] { jobId, key, type, "", new Timestamp(0L), new Long(0), value };
}
else if (type == ParameterType.DATE) {
args = new Object[] { jobId, key, type, "", value, new Long(0), new Double(0) };
}
getJdbcTemplate().update(getQuery(CREATE_JOB_PARAMETERS), args, argTypes);
}
/**
* The job table is queried for <strong>any</strong> jobs that match the
* given identifier, adding them to a list via the RowMapper callback.
*
* @see JobDao#findJobInstances(JobIdentifier)
* @throws IllegalArgumentException if any {@link JobIdentifier} fields are
* null.
*/
public JobInstance getJobInstance(final Job job, final JobParameters jobParameters) {
Assert.notNull(job, "Job must not be null.");
Assert.hasLength(job.getName(), "Job must have a name");
Assert.notNull(jobParameters, "JobParameters must not be null.");
Object[] parameters = new Object[] { job.getName(), createJobKey(jobParameters) };
RowMapper rowMapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
JobInstance jobInstance = new JobInstance(new Long(rs.getLong(1)), jobParameters, job);
return jobInstance;
}
};
List instances = getJdbcTemplate().query(getQuery(FIND_JOBS), parameters, rowMapper);
if (instances.isEmpty()) {
return null;
} else {
Assert.state(instances.size() == 1);
return (JobInstance) instances.get(0);
}
}
/**
* Setter for {@link DataFieldMaxValueIncrementer} to be used when
* generating primary keys for {@link JobInstance} instances.
*
* @param jobIncrementer the {@link DataFieldMaxValueIncrementer}
*/
public void setJobIncrementer(DataFieldMaxValueIncrementer jobIncrementer) {
this.jobIncrementer = jobIncrementer;
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(jobIncrementer);
}
private static class ParameterType {
private final String type;
private ParameterType(String type) {
this.type = type;
}
public String toString() {
return type;
}
public static final ParameterType STRING = new ParameterType("STRING");
public static final ParameterType DATE = new ParameterType("DATE");
public static final ParameterType LONG = new ParameterType("LONG");
public static final ParameterType DOUBLE = new ParameterType("DOUBLE");
private static final ParameterType[] VALUES = { STRING, DATE, LONG, DOUBLE };
public static ParameterType getType(String typeAsString) {
for (int i = 0; i < VALUES.length; i++) {
if (VALUES[i].toString().equals(typeAsString)) {
return (ParameterType) VALUES[i];
}
}
return null;
}
}
}

View File

@@ -0,0 +1,425 @@
package org.springframework.batch.execution.repository.dao;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.jdbc.core.PreparedStatementCallback;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobCreator;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.util.Assert;
/**
* Jdbc implementation of {@link StepExecutionDao}.<br/>
*
* Allows customization of the tables names used by Spring Batch for step meta
* data via a prefix property.<br/>
*
* Uses sequences or tables (via Spring's {@link DataFieldMaxValueIncrementer}
* abstraction) to create all primary keys before inserting a new row. All
* objects are checked to ensure all fields to be stored are not null. If any
* are found to be null, an IllegalArgumentException will be thrown. This could
* be left to JdbcTemplate, however, the exception will be fairly vague, and
* fails to highlight which field caused the exception.<br/>
*
* @author Lucas Ward
* @author Dave Syer
* @author Robert Kasanicky
*
* @see StepExecutionDao
*/
public class JdbcStepExecutionDao extends AbstractJdbcBatchMetadataDao implements StepExecutionDao, InitializingBean {
private static final Log logger = LogFactory.getLog(JdbcStepExecutionDao.class);
private static final String FIND_STEP_EXECUTION_CONTEXT = "SELECT TYPE_CD, KEY_NAME, STRING_VAL, DOUBLE_VAL, LONG_VAL, OBJECT_VAL "
+ "from %PREFIX%STEP_EXECUTION_CONTEXT where STEP_EXECUTION_ID = ?";
private static final String INSERT_STEP_EXECUTION_CONTEXT = "INSERT into %PREFIX%STEP_EXECUTION_CONTEXT(STEP_EXECUTION_ID, TYPE_CD,"
+ " KEY_NAME, STRING_VAL, DOUBLE_VAL, LONG_VAL, OBJECT_VAL) values(?,?,?,?,?,?,?)";
private static final String SAVE_STEP_EXECUTION = "INSERT into %PREFIX%STEP_EXECUTION(STEP_EXECUTION_ID, VERSION, STEP_NAME, JOB_EXECUTION_ID, START_TIME, "
+ "END_TIME, STATUS, COMMIT_COUNT, TASK_COUNT, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE) "
+ "values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_STEP_EXECUTION_CONTEXT = "UPDATE %PREFIX%STEP_EXECUTION_CONTEXT set "
+ "TYPE_CD = ?, STRING_VAL = ?, DOUBLE_VAL = ?, LONG_VAL = ?, OBJECT_VAL = ? where STEP_EXECUTION_ID = ? and KEY_NAME = ?";
private static final String UPDATE_STEP_EXECUTION = "UPDATE %PREFIX%STEP_EXECUTION set START_TIME = ?, END_TIME = ?, "
+ "STATUS = ?, COMMIT_COUNT = ?, TASK_COUNT = ?, CONTINUABLE = ? , EXIT_CODE = ?, "
+ "EXIT_MESSAGE = ?, VERSION = ? where STEP_EXECUTION_ID = ? and VERSION = ?";
private static final String GET_STEP_EXECUTION = "SELECT STEP_EXECUTION_ID, STEP_NAME, START_TIME, END_TIME, STATUS, COMMIT_COUNT,"
+ " TASK_COUNT, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%STEP_EXECUTION where STEP_NAME = ? and JOB_EXECUTION_ID = ?";
private static final String CURRENT_VERSION_STEP_EXECUTION = "SELECT VERSION FROM %PREFIX%STEP_EXECUTION WHERE STEP_EXECUTION_ID=?";
private static final int EXIT_MESSAGE_LENGTH = 250;
private LobHandler lobHandler = new DefaultLobHandler();
private DataFieldMaxValueIncrementer stepExecutionIncrementer;
public ExecutionContext findExecutionContext(final StepExecution stepExecution) {
final Long executionId = stepExecution.getId();
Assert.notNull(executionId, "ExecutionId must not be null.");
final ExecutionContext executionContext = new ExecutionContext();
RowCallbackHandler callback = new RowCallbackHandler() {
public void processRow(ResultSet rs) throws SQLException {
String typeCd = rs.getString("TYPE_CD");
AttributeType type = AttributeType.getType(typeCd);
String key = rs.getString("KEY_NAME");
if (type == AttributeType.STRING) {
executionContext.putString(key, rs.getString("STRING_VAL"));
}
else if (type == AttributeType.LONG) {
executionContext.putLong(key, rs.getLong("LONG_VAL"));
}
else if (type == AttributeType.DOUBLE) {
executionContext.putDouble(key, rs.getDouble("DOUBLE_VAL"));
}
else if (type == AttributeType.OBJECT) {
executionContext.put(key, rs.getObject("OBJECT_VAL"));
}
else {
throw new UnexpectedJobExecutionException("Invalid type found: [" + typeCd + "] for execution id: ["
+ executionId + "]");
}
}
};
getJdbcTemplate().query(getQuery(FIND_STEP_EXECUTION_CONTEXT), new Object[] { executionId }, callback);
return executionContext;
}
private void insertExecutionAttribute(final Long executionId, final String key, final Object value,
final AttributeType type) {
PreparedStatementCallback callback = new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException,
DataAccessException {
ps.setLong(1, executionId.longValue());
ps.setString(3, key);
if (type == AttributeType.STRING) {
ps.setString(2, AttributeType.STRING.toString());
ps.setString(4, value.toString());
ps.setDouble(5, 0.0);
ps.setLong(6, 0);
lobCreator.setBlobAsBytes(ps, 7, null);
}
else if (type == AttributeType.DOUBLE) {
ps.setString(2, AttributeType.DOUBLE.toString());
ps.setString(4, null);
ps.setDouble(5, ((Double) value).doubleValue());
ps.setLong(6, 0);
lobCreator.setBlobAsBytes(ps, 7, null);
}
else if (type == AttributeType.LONG) {
ps.setString(2, AttributeType.LONG.toString());
ps.setString(4, null);
ps.setDouble(5, 0.0);
ps.setLong(6, ((Long) value).longValue());
lobCreator.setBlobAsBytes(ps, 7, null);
}
else {
ps.setString(2, AttributeType.OBJECT.toString());
ps.setString(4, null);
ps.setDouble(5, 0.0);
ps.setLong(6, 0);
lobCreator.setBlobAsBytes(ps, 7, SerializationUtils.serialize((Serializable) value));
}
}
};
getJdbcTemplate().execute(getQuery(INSERT_STEP_EXECUTION_CONTEXT), callback);
}
/**
* Save a StepExecution. A unique id will be generated by the
* stepExecutionIncrementor, and then set in the StepExecution. All values
* will then be stored via an INSERT statement.
*
* @see StepDao#saveStepExecution(StepExecution)
*/
public void saveStepExecution(StepExecution stepExecution) {
validateStepExecution(stepExecution);
stepExecution.setId(new Long(stepExecutionIncrementer.nextLongValue()));
stepExecution.incrementVersion(); // should be 0 now
Object[] parameters = new Object[] { stepExecution.getId(), stepExecution.getVersion(),
stepExecution.getStepName(), stepExecution.getJobExecutionId(), stepExecution.getStartTime(),
stepExecution.getEndTime(), stepExecution.getStatus().toString(), stepExecution.getCommitCount(),
stepExecution.getTaskCount(), stepExecution.getExitStatus().isContinuable() ? "Y" : "N",
stepExecution.getExitStatus().getExitCode(), stepExecution.getExitStatus().getExitDescription() };
getJdbcTemplate().update(
getQuery(SAVE_STEP_EXECUTION),
parameters,
new int[] { Types.INTEGER, Types.INTEGER, Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP,
Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER, Types.INTEGER, Types.CHAR, Types.VARCHAR,
Types.VARCHAR });
}
/**
* Validate StepExecution. At a minimum, JobId, StartTime, and Status cannot
* be null. EndTime can be null for an unfinished job.
*
* @param jobExecution
* @throws IllegalArgumentException
*/
private void validateStepExecution(StepExecution stepExecution) {
Assert.notNull(stepExecution);
Assert.notNull(stepExecution.getStepName(), "StepExecution step name cannot be null.");
Assert.notNull(stepExecution.getStartTime(), "StepExecution start time cannot be null.");
Assert.notNull(stepExecution.getStatus(), "StepExecution status cannot be null.");
}
/**
* Save or update execution attributes. A lob creator must be used, since
* any attributes that don't match a provided type must be serialized into a
* blob.
*
* @see {@link LobCreator}
*/
public void saveOrUpdateExecutionContext(final StepExecution stepExecution) {
Long executionId = stepExecution.getId();
ExecutionContext executionContext = stepExecution.getExecutionContext();
Assert.notNull(executionId, "ExecutionId must not be null.");
Assert.notNull(executionContext, "The ExecutionContext must not be null.");
for (Iterator it = executionContext.entrySet().iterator(); it.hasNext();) {
Entry entry = (Entry) it.next();
final String key = entry.getKey().toString();
final Object value = entry.getValue();
if (value instanceof String) {
updateExecutionAttribute(executionId, key, value, AttributeType.STRING);
}
else if (value instanceof Double) {
updateExecutionAttribute(executionId, key, value, AttributeType.DOUBLE);
}
else if (value instanceof Long) {
updateExecutionAttribute(executionId, key, value, AttributeType.LONG);
}
else {
updateExecutionAttribute(executionId, key, value, AttributeType.OBJECT);
}
}
}
private void updateExecutionAttribute(final Long executionId, final String key, final Object value,
final AttributeType type) {
PreparedStatementCallback callback = new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException,
DataAccessException {
ps.setLong(6, executionId.longValue());
ps.setString(7, key);
if (type == AttributeType.STRING) {
ps.setString(1, AttributeType.STRING.toString());
ps.setString(2, value.toString());
ps.setDouble(3, 0.0);
ps.setLong(4, 0);
lobCreator.setBlobAsBytes(ps, 5, null);
}
else if (type == AttributeType.DOUBLE) {
ps.setString(1, AttributeType.DOUBLE.toString());
ps.setString(2, null);
ps.setDouble(3, ((Double) value).doubleValue());
ps.setLong(4, 0);
lobCreator.setBlobAsBytes(ps, 5, null);
}
else if (type == AttributeType.LONG) {
ps.setString(1, AttributeType.LONG.toString());
ps.setString(2, null);
ps.setDouble(3, 0.0);
ps.setLong(4, ((Long) value).longValue());
lobCreator.setBlobAsBytes(ps, 5, null);
}
else {
ps.setString(1, AttributeType.OBJECT.toString());
ps.setString(2, null);
ps.setDouble(3, 0.0);
ps.setLong(4, 0);
lobCreator.setBlobAsBytes(ps, 5, SerializationUtils.serialize((Serializable) value));
}
}
};
// LobCreating callbacks always return the affect row count for SQL DML
// statements, if less than 1 row
// is affected, then this row is new and should be inserted.
Integer affectedRows = (Integer) getJdbcTemplate().execute(getQuery(UPDATE_STEP_EXECUTION_CONTEXT), callback);
if (affectedRows.intValue() < 1) {
insertExecutionAttribute(executionId, key, value, type);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.execution.repository.dao.StepExecutionDao#updateStepExecution(org.springframework.batch.core.domain.StepExecution)
*/
public void updateStepExecution(StepExecution stepExecution) {
validateStepExecution(stepExecution);
Assert.notNull(stepExecution.getId(), "StepExecution Id cannot be null. StepExecution must saved"
+ " before it can be updated.");
// Do not check for existence of step execution considering
// it is saved at every commit point.
String exitDescription = stepExecution.getExitStatus().getExitDescription();
if (exitDescription != null && exitDescription.length() > EXIT_MESSAGE_LENGTH) {
exitDescription = exitDescription.substring(0, EXIT_MESSAGE_LENGTH);
logger.debug("Truncating long message before update of StepExecution: " + stepExecution);
}
// Attempt to prevent concurrent modification errors by blocking here if
// someone is already trying to do it.
synchronized (stepExecution) {
Integer version = new Integer(stepExecution.getVersion().intValue() + 1);
Object[] parameters = new Object[] { stepExecution.getStartTime(), stepExecution.getEndTime(),
stepExecution.getStatus().toString(), stepExecution.getCommitCount(), stepExecution.getTaskCount(),
stepExecution.getExitStatus().isContinuable() ? "Y" : "N",
stepExecution.getExitStatus().getExitCode(), exitDescription, version, stepExecution.getId(),
stepExecution.getVersion() };
int count = getJdbcTemplate().update(
getQuery(UPDATE_STEP_EXECUTION),
parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER, Types.INTEGER,
Types.CHAR, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.INTEGER, Types.INTEGER });
// Avoid concurrent modifications...
if (count == 0) {
int curentVersion = getJdbcTemplate().queryForInt(
getQuery(CURRENT_VERSION_STEP_EXECUTION),
new Object[] { stepExecution.getId() });
throw new OptimisticLockingFailureException("Attempt to update step execution id="
+ stepExecution.getId() + " with wrong version (" + stepExecution.getVersion() + "), where current version is "+curentVersion);
}
stepExecution.incrementVersion();
}
}
private class StepExecutionRowMapper implements RowMapper {
private final JobExecution jobExecution;
private final Step step;
public StepExecutionRowMapper(JobExecution jobExecution, Step step) {
this.jobExecution = jobExecution;
this.step = step;
}
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
StepExecution stepExecution = new StepExecution(step, jobExecution, new Long(rs.getLong(1)));
stepExecution.setStartTime(rs.getTimestamp(3));
stepExecution.setEndTime(rs.getTimestamp(4));
stepExecution.setStatus(BatchStatus.getStatus(rs.getString(5)));
stepExecution.setCommitCount(rs.getInt(6));
stepExecution.setTaskCount(rs.getInt(7));
stepExecution.setExitStatus(new ExitStatus("Y".equals(rs.getString(8)), rs.getString(9), rs.getString(10)));
stepExecution.setExecutionContext(findExecutionContext(stepExecution));
return stepExecution;
}
}
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
public void setStepExecutionIncrementer(DataFieldMaxValueIncrementer stepExecutionIncrementer) {
this.stepExecutionIncrementer = stepExecutionIncrementer;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(stepExecutionIncrementer, "StepExecutionIncrementer cannot be null.");
}
public static class AttributeType {
private final String type;
private AttributeType(String type) {
this.type = type;
}
public String toString() {
return type;
}
public static final AttributeType STRING = new AttributeType("STRING");
public static final AttributeType LONG = new AttributeType("LONG");
public static final AttributeType OBJECT = new AttributeType("OBJECT");
public static final AttributeType DOUBLE = new AttributeType("DOUBLE");
private static final AttributeType[] VALUES = { STRING, OBJECT, LONG, DOUBLE };
public static AttributeType getType(String typeAsString) {
for (int i = 0; i < VALUES.length; i++) {
if (VALUES[i].toString().equals(typeAsString)) {
return (AttributeType) VALUES[i];
}
}
return null;
}
}
public StepExecution getStepExecution(JobExecution jobExecution, Step step) {
List executions = getJdbcTemplate().query(getQuery(GET_STEP_EXECUTION),
new Object[] { step.getName(), jobExecution.getId() }, new StepExecutionRowMapper(jobExecution, step));
Assert.state(executions.size() <= 1,
"There can be at most one step execution with given name for single job execution");
if (executions.isEmpty()) {
return null;
}
else {
return (StepExecution) executions.get(0);
}
}
}

View File

@@ -0,0 +1,55 @@
package org.springframework.batch.execution.repository.dao;
import java.util.List;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
/**
* Data Access Object for job executions.
*
* @author Lucas Ward
* @author Robert Kasanicky
*/
public interface JobExecutionDao {
/**
* Save a new JobExecution.
*
* Preconditions: jobInstance the jobExecution belongs to must have a jobInstanceId.
*
* @param jobExecution
*/
void saveJobExecution(JobExecution jobExecution);
/**
* Update and existing JobExecution.
*
* Preconditions: jobExecution must have an Id (which can be obtained by the
* save method) and a jobInstanceId.
*
* @param jobExecution
*/
void updateJobExecution(JobExecution jobExecution);
/**
* Return the number of JobExecutions for the given JobInstance
*
* Preconditions: jobInstance must have an id.
*/
int getJobExecutionCount(JobInstance jobInstance);
/**
* Return list of JobExecutions for given JobInstance.
*
* @param jobInstance
* @return list of jobExecutions.
*/
List findJobExecutions(JobInstance jobInstance);
/**
* @return last JobExecution for given JobInstance.
*/
JobExecution getLastJobExecution(JobInstance jobInstance);
}

View File

@@ -0,0 +1,42 @@
package org.springframework.batch.execution.repository.dao;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
/**
* Data Access Object for job instances.
*
* @author Lucas Ward
* @author Robert Kasanicky
*
*/
public interface JobInstanceDao {
/**
* Create a JobInstance with given name and parameters.
*
* PreConditions: JobInstance for given name and parameters must not already exist
*
* PostConditions: A valid job instancewill be returned which has been persisted and
* contains an unique Id.
*
* @param jobName
* @param jobParameters
* @return JobInstance
*/
JobInstance createJobInstance(Job job, JobParameters jobParameters);
/**
* Find all job instances that match the given name and parameters. If no
* matching job instances are found, then a list of size 0 will be
* returned.
*
* @param jobName
* @param jobParameters
* @return List of {@link JobInstance} objects matching
* {@link JobIdentifier}
*/
JobInstance getJobInstance(Job job, JobParameters jobParameters);
}

View File

@@ -0,0 +1,76 @@
package org.springframework.batch.execution.repository.dao;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
/**
* In-memory implementation of {@link JobExecutionDao}.
*
*/
public class MapJobExecutionDao implements JobExecutionDao {
private static Map executionsByJobInstanceId = TransactionAwareProxyFactory.createTransactionalMap();
private static long currentId;
public static void clear() {
executionsByJobInstanceId.clear();
}
public int getJobExecutionCount(JobInstance jobInstance) {
Set executions = (Set) executionsByJobInstanceId.get(jobInstance.getId());
if (executions == null) {
return 0;
}
return executions.size();
}
public void saveJobExecution(JobExecution jobExecution) {
Set executions = (Set) executionsByJobInstanceId.get(jobExecution.getJobId());
if (executions == null) {
executions = TransactionAwareProxyFactory.createTransactionalSet();
executionsByJobInstanceId.put(jobExecution.getJobId(), executions);
}
executions.add(jobExecution);
jobExecution.setId(new Long(currentId++));
}
public List findJobExecutions(JobInstance jobInstance) {
Set executions = (Set) executionsByJobInstanceId.get(jobInstance.getId());
if (executions == null) {
return new ArrayList();
}
else {
return new ArrayList(executions);
}
}
public void updateJobExecution(JobExecution jobExecution) {
// no-op
}
public JobExecution getLastJobExecution(JobInstance jobInstance) {
Set executions = (Set) executionsByJobInstanceId.get(jobInstance.getId());
if (executions == null) {
return null;
}
JobExecution lastExec = null;
for (Iterator iterator = executions.iterator(); iterator.hasNext();) {
JobExecution exec = (JobExecution) iterator.next();
if (lastExec == null) {
lastExec = exec;
}
if (lastExec.getStartTime().getTime() < exec.getStartTime().getTime()) {
lastExec = exec;
}
}
return lastExec;
}
}

View File

@@ -0,0 +1,44 @@
package org.springframework.batch.execution.repository.dao;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
import org.springframework.util.Assert;
public class MapJobInstanceDao implements JobInstanceDao {
private static Collection jobInstances = TransactionAwareProxyFactory.createTransactionalList();
private long currentId = 0;
public static void clear() {
jobInstances.clear();
}
public JobInstance createJobInstance(Job job, JobParameters jobParameters) {
Assert.state(getJobInstance(job, jobParameters) == null, "JobInstance must not already exist");
JobInstance jobInstance = new JobInstance(new Long(currentId++), jobParameters, job);
jobInstances.add(jobInstance);
return jobInstance;
}
public JobInstance getJobInstance(Job job, JobParameters jobParameters) {
for (Iterator iterator = jobInstances.iterator(); iterator.hasNext();) {
JobInstance instance = (JobInstance) iterator.next();
if (instance.getJobName().equals(job.getName()) && instance.getJobParameters().equals(jobParameters)) {
return instance;
}
}
return null;
}
}

View File

@@ -0,0 +1,101 @@
/*
* 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.repository.dao;
import java.util.Map;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.util.Assert;
/**
* In-memory implementation of {@link StepExecutionDao}.
*/
public class MapStepExecutionDao implements StepExecutionDao {
private static Map executionsByJobExecutionId;
private static Map contextsByStepExecutionId;
private static long currentId = 0;
static {
executionsByJobExecutionId = TransactionAwareProxyFactory.createTransactionalMap();
contextsByStepExecutionId = TransactionAwareProxyFactory.createTransactionalMap();
}
public static void clear() {
executionsByJobExecutionId.clear();
}
public ExecutionContext findExecutionContext(StepExecution stepExecution) {
return (ExecutionContext) contextsByStepExecutionId.get(stepExecution.getId());
}
public void saveStepExecution(StepExecution stepExecution) {
Assert.state(stepExecution.getId() == null);
Assert.state(stepExecution.getVersion() == null);
Assert.notNull(stepExecution.getJobExecutionId(), "JobExecution must be saved already.");
Map executions = (Map) executionsByJobExecutionId.get(stepExecution.getJobExecutionId());
if (executions == null) {
executions = TransactionAwareProxyFactory.createTransactionalMap();
executionsByJobExecutionId.put(stepExecution.getJobExecutionId(), executions);
}
stepExecution.incrementVersion();
stepExecution.setId(new Long(currentId++));
executions.put(stepExecution.getStepName(), stepExecution);
}
public void updateStepExecution(StepExecution stepExecution) {
Assert.notNull(stepExecution.getJobExecutionId());
Map executions = (Map) executionsByJobExecutionId.get(stepExecution.getJobExecutionId());
Assert.notNull(executions, "step executions for given job execution are expected to be already saved");
StepExecution persistedExecution = (StepExecution) executions.get(stepExecution.getStepName());
Assert.notNull(persistedExecution, "step execution is expected to be already saved");
if (!persistedExecution.getVersion().equals(stepExecution.getVersion())) {
throw new OptimisticLockingFailureException("Attempt to update step execution id=" + stepExecution.getId()
+ " with wrong version (" + stepExecution.getVersion() + "), where current version is "
+ persistedExecution.getVersion());
}
stepExecution.incrementVersion();
executions.put(stepExecution.getStepName(), stepExecution);
}
public StepExecution getStepExecution(JobExecution jobExecution, Step step) {
Map executions = (Map) executionsByJobExecutionId.get(jobExecution.getId());
if (executions == null) {
return null;
}
return (StepExecution) executions.get(step.getName());
}
public void saveOrUpdateExecutionContext(StepExecution stepExecution) {
contextsByStepExecutionId.put(stepExecution.getId(), stepExecution.getExecutionContext());
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.repository.dao;
/**
* This exception identifies that a batch domain object is invalid, which
* is generally caused by an invalid ID. (An ID which doesn't exist in the database).
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class NoSuchObjectException extends RuntimeException {
private static final long serialVersionUID = 4399621765157283111L;
public NoSuchObjectException(String message){
super(message);
}
}

View File

@@ -0,0 +1,48 @@
package org.springframework.batch.execution.repository.dao;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.ExecutionContext;
public interface StepExecutionDao {
/**
* Save the given StepExecution.
*
* Preconditions: Id must be null.
*
* Postconditions: Id will be set to a unique Long.
*
* @param stepExecution
*/
void saveStepExecution(StepExecution stepExecution);
/**
* Update the given StepExecution
*
* Preconditions: Id must not be null.
*
* @param stepExecution
*/
void updateStepExecution(StepExecution stepExecution);
/**
* Find all {@link ExecutionContext} for the given {@link StepExecution}.
*
* @throws IllegalArgumentException if the id is null.
*/
ExecutionContext findExecutionContext(StepExecution stepExecution);
/**
* Save the {@link ExecutionContext} of the given {@link StepExecution}.
*
* @param stepExecution the {@link StepExecution} containing the
* {@link ExecutionContext} to be saved.
* @throws IllegalArgumentException if the attributes are null.
*/
void saveOrUpdateExecutionContext(StepExecution stepExecution);
StepExecution getStepExecution(JobExecution jobExecution, Step step);
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of dao concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of repository concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,289 @@
/*
* 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.resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.Iterator;
import java.util.Properties;
import java.util.Map.Entry;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.core.listener.StepListenerSupport;
import org.springframework.batch.core.runtime.JobParametersFactory;
import org.springframework.batch.execution.launch.support.DefaultJobParametersFactory;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Strategy for locating different resources on the file system. For each unique
* step execution, the same file handle will be returned. A unique step is
* defined as having the same job instance and step name. An external file mover
* (such as an EAI solution) should rename and move any input files to conform
* to the pattern defined here.<br/>
*
* If no pattern is passed in, then following default is used:
*
* <pre>
* data/%JOB_NAME%/%STEP_NAME%.txt
* </pre>
*
* The %% variables are replaced with the corresponding bean property at run
* time, when the factory method is executed. To insert {@link JobParameters}
* use a pattern with the parameter key surrounded by %%, e.g.
*
* <pre>
* //home/jobs/data/%JOB_NAME%/%STEP_NAME%-%schedule.date%.txt
* </pre>
*
* Note that the default pattern does not start with a separator. Because of the
* implementation of the Spring Core Resource abstractions, it would need to
* start with a double forward slash "//" to resolve to an absolute directory.<br/>
*
* To use this resource it must be initialised with a {@link StepExecution}.
* The best way to do that is to register it as a listener in the step that is
* going to need it. It is to enable this usage that the resource implements
* {@link StepListener}.
*
* @author Tomas Slanina
* @author Lucas Ward
* @author Dave Syer
*
* @see Resource
*/
public class StepExecutionProxyResource extends StepListenerSupport implements Resource, ResourceLoaderAware,
StepListener {
private static final String JOB_NAME_PATTERN = "%JOB_NAME%";
private static final String STEP_NAME_PATTERN = "%STEP_NAME%";
private static final String DEFAULT_PATTERN = "data/%JOB_NAME%/" + "%STEP_NAME%.txt";
private String filePattern = DEFAULT_PATTERN;
private JobParametersFactory jobParametersFactory = new DefaultJobParametersFactory();
private ResourceLoader resourceLoader = new FileSystemResourceLoader();
private Resource delegate;
/**
* @param relativePath
* @return
* @throws IOException
* @see org.springframework.core.io.Resource#createRelative(java.lang.String)
*/
public Resource createRelative(String relativePath) throws IOException {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.createRelative(relativePath);
}
/**
* @return
* @see org.springframework.core.io.Resource#exists()
*/
public boolean exists() {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.exists();
}
/**
* @return
* @see org.springframework.core.io.Resource#getDescription()
*/
public String getDescription() {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getDescription();
}
/**
* @return
* @throws IOException
* @see org.springframework.core.io.Resource#getFile()
*/
public File getFile() throws IOException {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getFile();
}
/**
* @return
* @see org.springframework.core.io.Resource#getFilename()
*/
public String getFilename() {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getFilename();
}
/**
* @return
* @throws IOException
* @see org.springframework.core.io.InputStreamSource#getInputStream()
*/
public InputStream getInputStream() throws IOException {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getInputStream();
}
/**
* @return
* @throws IOException
* @see org.springframework.core.io.Resource#getURI()
*/
public URI getURI() throws IOException {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getURI();
}
/**
* @return
* @throws IOException
* @see org.springframework.core.io.Resource#getURL()
*/
public URL getURL() throws IOException {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.getURL();
}
/**
* @return
* @see org.springframework.core.io.Resource#isOpen()
*/
public boolean isOpen() {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.isOpen();
}
/**
* @see org.springframework.core.io.Resource#isReadable()
*/
public boolean isReadable() {
Assert.state(delegate != null, "The delegate resource has not been initialised. "
+ "Remember to register this object as a StepListener.");
return delegate.isReadable();
}
/**
* Public setter for the {@link JobParametersFactory} used to translate
* {@link JobParameters} into {@link Properties}. Defaults to a
* {@link DefaultJobParametersFactory}.
* @param jobParametersFactory the {@link JobParametersFactory} to set
*/
public void setJobParametersFactory(JobParametersFactory jobParametersFactory) {
this.jobParametersFactory = jobParametersFactory;
}
/**
* Always false because we are expecting to be step scoped.
*
* @see org.springframework.beans.factory.config.AbstractFactoryBean#isSingleton()
*/
public boolean isSingleton() {
return false;
}
/*
* (non-Javadoc)
*
* @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader)
*/
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* helper method for <code>createFileName()</code>
*/
private String replacePattern(String string, String pattern, String replacement) {
if (string == null)
return null;
// check to ensure pattern exists in string.
if (string.indexOf(pattern) != -1) {
return StringUtils.replace(string, pattern, replacement);
}
return string;
}
/**
* Creates a filename given a pattern and step context information.
*
* Deliberate package access, so that the method can be accessed by unit
* tests
* @param jobName
* @param stepName
* @param properties
*/
private String createFileName(String jobName, String stepName, Properties properties) {
Assert.notNull(filePattern, "filename pattern is null");
String fileName = filePattern;
fileName = replacePattern(fileName, JOB_NAME_PATTERN, jobName == null ? "job" : jobName);
fileName = replacePattern(fileName, STEP_NAME_PATTERN, stepName);
if (properties != null) {
for (Iterator iterator = properties.entrySet().iterator(); iterator.hasNext();) {
Entry entry = (Entry) iterator.next();
String key = (String) entry.getKey();
fileName = replacePattern(fileName, "%" + key + "%", (String) entry.getValue());
}
}
return fileName;
}
public void setFilePattern(String filePattern) {
this.filePattern = replacePattern(filePattern, "\\", File.separator);
}
/**
* Collect the properties of the enclosing {@link StepExecution} that will
* be needed to create a file name.
*
* @see org.springframework.batch.core.StepListener#beforeStep(org.springframework.batch.core.StepExecution)
*/
public void beforeStep(StepExecution execution) {
String stepName = execution.getStepName();
String jobName = execution.getJobExecution().getJobInstance().getJobName();
Properties properties = jobParametersFactory.getProperties(execution.getJobExecution().getJobInstance()
.getJobParameters());
delegate = resourceLoader.getResource(createFileName(jobName, stepName, properties));
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.step;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
/**
* A {@link Step} implementation that provides common behaviour to subclasses.
*
* @author Dave Syer
* @author Ben Hale
*/
public abstract class AbstractStep implements Step {
protected String name;
protected int startLimit = Integer.MAX_VALUE;
protected boolean allowStartIfComplete;
/**
* Default constructor.
*/
public AbstractStep() {
super();
}
public String getName() {
return this.name;
}
/**
* Set the name property. Always overrides the default value if this object
* is a Spring bean.
*
* @see #setBeanName(java.lang.String)
*/
public void setName(String name) {
this.name = name;
}
public int getStartLimit() {
return this.startLimit;
}
/**
* Public setter for the startLimit.
*
* @param startLimit the startLimit to set
*/
public void setStartLimit(int startLimit) {
this.startLimit = startLimit;
}
public boolean isAllowStartIfComplete() {
return this.allowStartIfComplete;
}
/**
* Public setter for the shouldAllowStartIfComplete.
*
* @param allowStartIfComplete the shouldAllowStartIfComplete to set
*/
public void setAllowStartIfComplete(boolean allowStartIfComplete) {
this.allowStartIfComplete = allowStartIfComplete;
}
/**
* Convenient constructor for setting only the name property.
*
* @param name
*/
public AbstractStep(String name) {
this.name = name;
}
public abstract void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException;
}

View File

@@ -0,0 +1,78 @@
/*
* 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.step;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.item.ClearFailedException;
import org.springframework.batch.item.FlushFailedException;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.MarkFailedException;
import org.springframework.batch.item.ResetFailedException;
import org.springframework.batch.repeat.ExitStatus;
/**
* Strategy for processing a single item in an item-oriented step. Extends
* {@link ItemReader} and {@link ItemWriter} because part of the contract of the
* processor is that it should delegate calls to those interfaces.
*
* @author Dave Syer
*
*/
public interface ItemHandler {
/**
* Given the current context in the form of a step contribution, do whatever
* is necessary to process this unit inside a chunk. Implementations obtain
* the item and return {@link ExitStatus#FINISHED} if it is null. If it is
* not null process the item and return {@link ExitStatus#CONTINUABLE}. On
* failure throws an exception.
*
* @param contribution the current step context
* @return an {@link ExitStatus} indicating whether processing is
* continuable.
*/
ExitStatus handle(StepContribution contribution) throws Exception;
/**
* Implementations should delegate to an {@link ItemReader}.
*
* @see org.springframework.batch.item.ItemReader#mark()
*/
void mark() throws MarkFailedException;
/**
* Implementations should delegate to an {@link ItemReader}.
*
* @see org.springframework.batch.item.ItemReader#reset()
*/
void reset() throws ResetFailedException;
/**
* Implementations should delegate to an {@link ItemWriter}.
*
* @see org.springframework.batch.item.ItemWriter#flush()
*/
public void flush() throws FlushFailedException;
/**
* Implementations should delegate to an {@link ItemWriter}.
*
* @see org.springframework.batch.item.ItemWriter#clear()
*/
public void clear() throws ClearFailedException;
}

View File

@@ -0,0 +1,504 @@
/*
* 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.step;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.runtime.ExitStatusExceptionClassifier;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.listener.CompositeStepListener;
import org.springframework.batch.execution.step.support.DefaultStepExecutionSynchronizer;
import org.springframework.batch.execution.step.support.SimpleExitStatusExceptionClassifier;
import org.springframework.batch.execution.step.support.StepExecutionSynchronizer;
import org.springframework.batch.execution.step.support.StepInterruptionPolicy;
import org.springframework.batch.execution.step.support.ThreadStepInterruptionPolicy;
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.stream.CompositeItemStream;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatCallback;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.RepeatOperations;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* Simple implementation of executing the step as a set of chunks, each chunk surrounded by a transaction. The structure
* is therefore that of two nested loops, with transaction boundary around the whole inner loop. The outer loop is
* controlled by the step operations ({@link #setStepOperations(RepeatOperations)}), and the inner loop by the chunk
* operations ({@link #setChunkOperations(RepeatOperations)}). The inner loop should always be executed in a single
* thread, so the chunk operations should not do any concurrent execution. N.B. usually that means that the chunk
* operations should be a {@link RepeatTemplate} (which is the default).<br/>
*
* Clients can use interceptors in the step operations to intercept or listen to the iteration on a step-wide basis, for
* instance to get a callback when the step is complete. Those that want callbacks at the level of an individual tasks,
* can specify interceptors for the chunk operations.
*
* @author Dave Syer
* @author Lucas Ward
* @author Ben Hale
*/
public class ItemOrientedStep extends AbstractStep {
private static final Log logger = LogFactory.getLog(ItemOrientedStep.class);
private RepeatOperations chunkOperations = new RepeatTemplate();
private RepeatOperations stepOperations = new RepeatTemplate();
private ExitStatusExceptionClassifier exceptionClassifier = new SimpleExitStatusExceptionClassifier();
// default to checking current thread for interruption.
private StepInterruptionPolicy interruptionPolicy = new ThreadStepInterruptionPolicy();
private CompositeItemStream stream = new CompositeItemStream();
private CompositeStepListener listener = new CompositeStepListener();
private JobRepository jobRepository;
private PlatformTransactionManager transactionManager;
private ItemHandler itemHandler;
private StepExecutionSynchronizer synchronizer = new DefaultStepExecutionSynchronizer();
/**
* @param name
*/
public ItemOrientedStep(String name) {
super(name);
}
/**
* Public setter for {@link JobRepository}.
*
* @param jobRepository is a mandatory dependence (no default).
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Public setter for the {@link PlatformTransactionManager}.
*
* @param transactionManager the transaction manager to set
*/
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Public setter for the {@link ItemHandler}.
*
* @param itemHandler the {@link ItemHandler} to set
*/
public void setItemHandler(ItemHandler itemHandler) {
this.itemHandler = itemHandler;
}
/**
* Register each of the streams for callbacks at the appropriate time in the step. The {@link ItemReader} and
* {@link ItemWriter} are automatically registered, but it doesn't hurt to also register them here. Injected
* dependencies of the reader and writer are not automatically registered, so if you implement {@link ItemWriter}
* using delegation to another object which itself is a {@link ItemStream}, you need to register the delegate here.
*
* @param streams an array of {@link ItemStream} objects.
*/
public void setStreams(ItemStream[] streams) {
for (int i = 0; i < streams.length; i++) {
registerStream(streams[i]);
}
}
/**
* Register a single {@link ItemStream} for callbacks to the stream interface.
*
* @param stream
*/
public void registerStream(ItemStream stream) {
this.stream.register(stream);
}
/**
* Register each of the objects as listeners. If the {@link ItemReader} or {@link ItemWriter} themselves implements
* this interface they will be registered automatically, but their injected dependencies will not be. This is a good
* way to get access to job parameters and execution context if the tasklet is parameterised.
*
* @param listeners an array of listener objects of known types.
*/
public void setStepListeners(StepListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
registerStepListener(listeners[i]);
}
}
/**
* Register a step listener for callbacks at the appropriate stages in a step execution.
*
* @param listener a {@link StepListener}
*/
public void registerStepListener(StepListener listener) {
this.listener.register(listener);
}
/**
* The {@link RepeatOperations} to use for the outer loop of the batch processing. Should be set up by the caller
* through a factory. Defaults to a plain {@link RepeatTemplate}.
*
* @param stepOperations a {@link RepeatOperations} instance.
*/
public void setStepOperations(RepeatOperations stepOperations) {
this.stepOperations = stepOperations;
}
/**
* The {@link RepeatOperations} to use for the inner loop of the batch processing. should be set up by the caller
* through a factory. defaults to a plain {@link RepeatTemplate}.
*
* @param chunkOperations a {@link RepeatOperations} instance.
*/
public void setChunkOperations(RepeatOperations chunkOperations) {
this.chunkOperations = chunkOperations;
}
/**
* Setter for the {@link StepInterruptionPolicy}. The policy is used to check whether an external request has been
* made to interrupt the job execution.
*
* @param interruptionPolicy a {@link StepInterruptionPolicy}
*/
public void setInterruptionPolicy(StepInterruptionPolicy interruptionPolicy) {
this.interruptionPolicy = interruptionPolicy;
}
/**
* Setter for the {@link ExitStatusExceptionClassifier} that will be used to classify any exception that causes a
* job to fail.
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(ExitStatusExceptionClassifier exceptionClassifier) {
this.exceptionClassifier = exceptionClassifier;
}
/**
* Mostly useful for testing, but could be used to remove dependence on backport concurrency utilities. Public
* setter for the {@link StepExecutionSynchronizer}.
*
* @param synchronizer the {@link StepExecutionSynchronizer} to set
*/
public void setSynchronizer(StepExecutionSynchronizer synchronizer) {
this.synchronizer = synchronizer;
}
/**
* Process the step and update its context so that progress can be monitored by the caller. The step is broken down
* into chunks, each one executing in a transaction. The step and its execution and execution context are all given
* an up to date {@link BatchStatus}, and the {@link JobRepository} is used to store the result. Various reporting
* information are also added to the current context (the {@link RepeatContext} governing the step execution, which
* would normally be available to the caller somehow through the step's {@link JobExecutionContext}.<br/>
*
* @throws JobInterruptedException if the step or a chunk is interrupted
* @throws RuntimeException if there is an exception during a chunk execution
* @see StepExecutor#execute(StepExecution)
*/
public void execute(final StepExecution stepExecution) throws UnexpectedJobExecutionException, JobInterruptedException {
JobInstance jobInstance = stepExecution.getJobExecution().getJobInstance();
StepExecution lastStepExecution = jobRepository.getLastStepExecution(jobInstance, this);
boolean isRestart = jobRepository.getStepExecutionCount(jobInstance, this) > 0 ? true : false;
ExitStatus status = ExitStatus.FAILED;
final ExceptionHolder fatalException = new ExceptionHolder();
try {
stepExecution.setStartTime(new Date(System.currentTimeMillis()));
// We need to save the step execution right away, before we start
// using its ID. It would be better to make the creation atomic in
// the caller.
fatalException.setException(updateStatus(stepExecution, BatchStatus.STARTED));
if (isRestart && lastStepExecution != null) {
stepExecution.setExecutionContext(lastStepExecution.getExecutionContext());
} else {
stepExecution.setExecutionContext(new ExecutionContext());
}
// Execute step level listeners *after* the execution context is
// fixed in the step. E.g. ItemStream instances need the the same
// reference to the ExecutionContext as the step execution.
listener.beforeStep(stepExecution);
stream.open(stepExecution.getExecutionContext());
status = stepOperations.iterate(new RepeatCallback() {
public ExitStatus doInIteration(final RepeatContext context) throws Exception {
final StepContribution contribution = stepExecution.createStepContribution();
contribution.setExecutionContext(stepExecution.getExecutionContext());
// Before starting a new transaction, check for
// interruption.
interruptionPolicy.checkInterrupted(context);
ExitStatus result = ExitStatus.CONTINUABLE;
TransactionStatus transaction = transactionManager
.getTransaction(new DefaultTransactionDefinition());
try {
itemHandler.mark();
result = processChunk(contribution);
contribution.incrementCommitCount();
// If the step operations are asynchronous then we need
// to synchronize changes to the step execution (at a
// minimum).
try {
synchronizer.lock(stepExecution);
} catch (InterruptedException e) {
stepExecution.setStatus(BatchStatus.STOPPED);
Thread.currentThread().interrupt();
}
// Apply the contribution to the step
// only if chunk was successful
stepExecution.apply(contribution);
stream.update(stepExecution.getExecutionContext());
try {
jobRepository.saveOrUpdateExecutionContext(stepExecution);
} catch (Exception e) {
fatalException.setException(e);
stepExecution.setStatus(BatchStatus.UNKNOWN);
throw new CommitFailedException(
"Fatal error detected during save of step execution context", e);
}
try {
itemHandler.mark();
itemHandler.flush();
transactionManager.commit(transaction);
} catch (Exception e) {
fatalException.setException(e);
stepExecution.setStatus(BatchStatus.UNKNOWN);
throw new CommitFailedException("Fatal error detected during commit", e);
}
} catch (CommitFailedException e) {
throw e;
} catch (Throwable t) {
/*
* Any exception thrown within the transaction template will automatically cause the transaction
* to rollback. We need to include exceptions during an attempted commit (e.g. Hibernate flush)
* so this catch block comes outside the transaction.
*/
stepExecution.rollback();
try {
itemHandler.reset();
itemHandler.clear();
transactionManager.rollback(transaction);
} catch (Exception e) {
fatalException.setException(e);
stepExecution.setStatus(BatchStatus.UNKNOWN);
}
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RuntimeException(t);
}
} finally {
synchronizer.release(stepExecution);
}
// Check for interruption after transaction as well, so that
// the interrupted exception is correctly propagated up to
// caller
interruptionPolicy.checkInterrupted(context);
return result;
}
});
fatalException.setException(updateStatus(stepExecution, BatchStatus.COMPLETED));
} catch (CommitFailedException e) {
logger.error("Fatal error detected during commit.");
throw e;
} catch (RuntimeException e) {
// classify exception so an exit code can be stored.
status = exceptionClassifier.classifyForExitCode(e);
if (e.getCause() instanceof JobInterruptedException) {
updateStatus(stepExecution, BatchStatus.STOPPED);
throw (JobInterruptedException) e.getCause();
} else if (!fatalException.hasException()) {
try {
status = status.and(listener.onErrorInStep(stepExecution, e));
} catch (RuntimeException ex) {
logger.error("Unexpected error in listener on error in step.", ex);
}
updateStatus(stepExecution, BatchStatus.FAILED);
throw e;
} else {
logger.error("Fatal error detected during rollback caused by underlying exception: ", e);
throw e;
}
} finally {
try {
status = status.and(listener.afterStep(stepExecution));
} catch (RuntimeException e) {
logger.error("Unexpected error in listener after step.", e);
}
stepExecution.setExitStatus(status);
stepExecution.setEndTime(new Date(System.currentTimeMillis()));
try {
jobRepository.saveOrUpdate(stepExecution);
} catch (RuntimeException e) {
String msg = "Fatal error detected during final save of meta data";
logger.error(msg, e);
if (!fatalException.hasException()) {
fatalException.setException(e);
}
throw new UnexpectedJobExecutionException(msg, fatalException.getException());
}
try {
stream.close(stepExecution.getExecutionContext());
} catch (RuntimeException e) {
String msg = "Fatal error detected during close of streams. "
+ "The job execution completed (possibly unsuccessfully but with consistent meta-data).";
logger.error(msg, e);
if (!fatalException.hasException()) {
fatalException.setException(e);
}
throw new UnexpectedJobExecutionException(msg, fatalException.getException());
}
if (fatalException.hasException()) {
throw new UnexpectedJobExecutionException("Encountered an error saving batch meta data.", fatalException
.getException());
}
}
}
/**
* Execute a bunch of identical business logic operations all within a transaction. The transaction is
* programmatically started and stopped 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.
* @return true if there is more data to process.
*/
protected ExitStatus processChunk(final StepContribution contribution) {
ExitStatus result = chunkOperations.iterate(new RepeatCallback() {
public ExitStatus doInIteration(final RepeatContext context) throws Exception {
if (contribution.isTerminateOnly()) {
context.setTerminateOnly();
}
// check for interruption before each item as well
interruptionPolicy.checkInterrupted(context);
ExitStatus exitStatus = itemHandler.handle(contribution);
contribution.incrementTaskCount();
// check for interruption after each item as well
interruptionPolicy.checkInterrupted(context);
return exitStatus;
}
});
return result;
}
/**
* Convenience method to update the status in all relevant places.
*
* @param stepInstance the current step
* @param stepExecution the current stepExecution
* @param status the status to set
*/
private Exception updateStatus(StepExecution stepExecution, BatchStatus status) {
stepExecution.setStatus(status);
try {
jobRepository.saveOrUpdate(stepExecution);
return null;
} catch (Exception e) {
return e;
}
}
/**
* @author Dave Syer
*
*/
private static class ExceptionHolder {
private Exception exception;
public boolean hasException() {
return exception != null;
}
/**
* @param exception
*/
public void setException(Exception exception) {
this.exception = exception;
}
/**
* @return
*/
public Exception getException() {
return this.exception;
}
}
private class CommitFailedException extends RuntimeException {
public CommitFailedException(String string, Exception e) {
super(string, e);
}
}
}

View File

@@ -0,0 +1,193 @@
/*
* 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.step;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.UnexpectedJobExecutionException;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.listener.CompositeStepListener;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* A {@link Step} that executes a {@link Tasklet} directly. This step does not
* manage transactions or any looping functionality. The tasklet should do this
* on its own.
*
* @author Ben Hale
*/
public class TaskletStep extends AbstractStep implements Step, InitializingBean, BeanNameAware {
private static final Log logger = LogFactory.getLog(TaskletStep.class);
private Tasklet tasklet;
private JobRepository jobRepository;
private CompositeStepListener listener = new CompositeStepListener();
/**
* Set the name property if it is not already set. Because of the order of
* the callbacks in a Spring container the name property will be set first
* if it is present. Care is needed with bean definition inheritance - if a
* parent bean has a name, then its children need an explicit name as well,
* otherwise they will not be unique.
*
* @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String)
*/
public void setBeanName(String name) {
if (this.name == null) {
this.name = name;
}
}
/**
* Register each of the objects as listeners. If the {@link Tasklet} itself
* implements this interface it will be registered automatically, but its
* injected dependencies will not be. This is a good way to get access to
* job parameters and execution context if the tasklet is parameterised.
*
* @param listeners an array of listener objects of known types.
*/
public void setStepListeners(StepListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
this.listener.register(listeners[i]);
}
}
/**
* Check mandatory properties.
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jobRepository, "JobRepository is mandatory for TaskletStep");
Assert.notNull(tasklet, "Tasklet is mandatory for TaskletStep");
if (tasklet instanceof StepListener) {
listener.register((StepListener) tasklet);
}
}
/**
* Default constructor is useful for XML configuration.
*/
public TaskletStep() {
super();
}
/**
* Creates a new <code>Step</code> for executing a <code>Tasklet</code>
*
* @param tasklet The <code>Tasklet</code> to execute
* @param jobRepository The <code>JobRepository</code> to use for
* persistence of incremental state
*/
public TaskletStep(Tasklet tasklet, JobRepository jobRepository) {
this();
this.tasklet = tasklet;
this.jobRepository = jobRepository;
}
/**
* Public setter for the {@link Tasklet}.
* @param tasklet the {@link Tasklet} to set
*/
public void setTasklet(Tasklet tasklet) {
this.tasklet = tasklet;
}
/**
* Public setter for the {@link JobRepository}.
* @param jobRepository the {@link JobRepository} to set
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
public void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException {
stepExecution.setStartTime(new Date());
updateStatus(stepExecution, BatchStatus.STARTED);
ExitStatus exitStatus = ExitStatus.FAILED;
Exception fatalException = null;
try {
listener.beforeStep(stepExecution);
exitStatus = tasklet.execute();
try {
exitStatus = exitStatus.and(listener.afterStep(stepExecution));
}
catch (Exception e) {
logger.error("Encountered an error on listener close.", e);
}
try {
jobRepository.saveOrUpdateExecutionContext(stepExecution);
updateStatus(stepExecution, BatchStatus.COMPLETED);
}
catch (Exception e) {
fatalException = e;
updateStatus(stepExecution, BatchStatus.UNKNOWN);
}
}
catch (Exception e) {
logger.error("Encountered an error running the tasklet");
updateStatus(stepExecution, BatchStatus.FAILED);
try {
exitStatus = exitStatus.and(listener.onErrorInStep(stepExecution, e));
}
catch (Exception ex) {
logger.error("Encountered an error on listener close.", ex);
}
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw new UnexpectedJobExecutionException(e);
}
finally {
stepExecution.setExitStatus(exitStatus);
stepExecution.setEndTime(new Date());
try {
jobRepository.saveOrUpdate(stepExecution);
}
catch (Exception e) {
fatalException = e;
}
if (fatalException != null) {
logger.error("Encountered an error saving batch meta data."
+ "This job is now in an unknown state and should not be restarted.", fatalException);
throw new UnexpectedJobExecutionException("Encountered an error saving batch meta data.", fatalException);
}
}
}
private void updateStatus(StepExecution stepExecution, BatchStatus status) {
stepExecution.setStatus(status);
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of step concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,198 @@
/*
* 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.step.support;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.Assert;
/**
* Base class for factory beans for {@link ItemOrientedStep}. Ensures that all
* the mandatory properties are set, and provides basic support for the
* {@link Step} interface responsibilities like start limit.
*
* @author Dave Syer
*
*/
public abstract class AbstractStepFactoryBean implements FactoryBean, BeanNameAware {
private String name;
private int startLimit = Integer.MAX_VALUE;
private boolean allowStartIfComplete;
private ItemReader itemReader;
private ItemWriter itemWriter;
private PlatformTransactionManager transactionManager;
private JobRepository jobRepository;
private boolean singleton = true;
/**
*
*/
public AbstractStepFactoryBean() {
super();
}
/**
* Set the bean name property, which will become the name of the
* {@link Step} when it is created.
*
* @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String)
*/
public void setBeanName(String name) {
this.name = name;
}
/**
* Public getter for the String.
* @return the name
*/
public String getName() {
return name;
}
/**
* Public setter for the startLimit.
*
* @param startLimit the startLimit to set
*/
public void setStartLimit(int startLimit) {
this.startLimit = startLimit;
}
/**
* Public setter for the shouldAllowStartIfComplete.
*
* @param allowStartIfComplete the shouldAllowStartIfComplete to set
*/
public void setAllowStartIfComplete(boolean allowStartIfComplete) {
this.allowStartIfComplete = allowStartIfComplete;
}
/**
* @param itemReader the itemReader to set
*/
public void setItemReader(ItemReader itemReader) {
this.itemReader = itemReader;
}
/**
* @param itemWriter the itemWriter to set
*/
public void setItemWriter(ItemWriter itemWriter) {
this.itemWriter = itemWriter;
}
/**
* Protected getter for the {@link ItemReader} for subclasses to use.
* @return the itemReader
*/
protected ItemReader getItemReader() {
return itemReader;
}
/**
* Protected getter for the {@link ItemWriter} for subclasses to use
* @return the itemWriter
*/
protected ItemWriter getItemWriter() {
return itemWriter;
}
/**
* Public setter for {@link JobRepository}.
*
* @param jobRepository is a mandatory dependence (no default).
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Public setter for the {@link PlatformTransactionManager}.
*
* @param transactionManager the transaction manager to set
*/
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Create a {@link Step} from the configuration provided.
*
* @see org.springframework.beans.factory.FactoryBean#getObject()
*/
public final Object getObject() throws Exception {
ItemOrientedStep step = new ItemOrientedStep(getName());
applyConfiguration(step);
return step;
}
/**
* @param step
*
*/
protected void applyConfiguration(ItemOrientedStep step) {
Assert.notNull(getItemReader(), "ItemReader must be provided");
Assert.notNull(getItemWriter(), "ItemWriter must be provided");
Assert.notNull(jobRepository, "JobRepository must be provided");
Assert.notNull(transactionManager, "TransactionManager must be provided");
step.setItemHandler(new SimpleItemHandler(itemReader, itemWriter));
step.setTransactionManager(transactionManager);
step.setJobRepository(jobRepository);
step.setStartLimit(startLimit);
step.setAllowStartIfComplete(allowStartIfComplete);
}
public Class getObjectType() {
return Step.class;
}
/**
* Returns true by default, but in most cases a {@link Step} should not be
* treated as thread safe. Clients are recommended to create a new step for
* each job execution.
*
* @see org.springframework.beans.factory.FactoryBean#isSingleton()
*/
public boolean isSingleton() {
return this.singleton;
}
/**
* Public setter for the singleton flag.
* @param singleton the value to set. Defaults to true.
*/
public void setSingleton(boolean singleton) {
this.singleton = singleton;
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.step.support;
import org.springframework.batch.core.ItemSkipPolicy;
/**
* Implementation of the {@link ItemSkipPolicy} interface that
* will always return that an item should be skipped.
*
* @author Ben Hale
* @author Lucas Ward
*/
public class AlwaysSkipItemSkipPolicy implements ItemSkipPolicy {
public boolean shouldSkip(Throwable t, int skipCount) {
return true;
}
}

View File

@@ -0,0 +1,173 @@
/*
* 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.step.support;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.core.BatchListener;
import org.springframework.batch.core.ChunkListener;
import org.springframework.batch.core.ItemReadListener;
import org.springframework.batch.core.ItemWriteListener;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.execution.listener.CompositeChunkListener;
import org.springframework.batch.execution.listener.CompositeItemReadListener;
import org.springframework.batch.execution.listener.CompositeItemWriteListener;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.reader.DelegatingItemReader;
import org.springframework.batch.item.writer.DelegatingItemWriter;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.RepeatOperations;
import org.springframework.batch.repeat.listener.RepeatListenerSupport;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.util.Assert;
/**
* Package private helper for step factory beans.
*
* @author Dave Syer
*
*/
class BatchListenerFactoryHelper {
/**
* @param itemReader2
* @param listeners
* @return
*/
public ItemReader getItemReader(ItemReader itemReader, BatchListener[] listeners) {
final CompositeItemReadListener multicaster = new CompositeItemReadListener();
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof ItemReadListener) {
multicaster.register((ItemReadListener) listener);
}
}
itemReader = new DelegatingItemReader(itemReader) {
public Object read() throws Exception {
try {
multicaster.beforeRead();
Object item = super.read();
multicaster.afterRead(item);
return item;
}
catch (Exception e) {
multicaster.onReadError(e);
throw e;
}
}
};
return itemReader;
}
/**
* @param itemWriter2
* @param listeners
* @return
*/
public ItemWriter getItemWriter(ItemWriter itemWriter, BatchListener[] listeners) {
final CompositeItemWriteListener multicaster = new CompositeItemWriteListener();
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof ItemWriteListener) {
multicaster.register((ItemWriteListener) listener);
}
}
itemWriter = new DelegatingItemWriter(itemWriter) {
public void write(Object item) throws Exception {
try {
multicaster.beforeWrite(item);
super.write(item);
multicaster.afterWrite();
}
catch (Exception e) {
multicaster.onWriteError(e, item);
throw e;
}
}
};
return itemWriter;
}
/**
* @param stepOperations
* @param listeners
* @return
*/
public RepeatOperations addChunkListeners(RepeatOperations stepOperations, BatchListener[] listeners) {
final CompositeChunkListener multicaster = new CompositeChunkListener();
boolean hasChunkListener = false;
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof ChunkListener) {
hasChunkListener = true;
}
if (listener instanceof ChunkListener) {
multicaster.register((ChunkListener) listener);
}
}
if (hasChunkListener) {
Assert.state(stepOperations instanceof RepeatTemplate,
"Step operations is injected but not a RepeatTemplate, so chunk listeners cannot also be registered. "
+ "Either inject a RepeatTemplate, or remove the ChunkListener.");
RepeatTemplate stepTemplate = (RepeatTemplate) stepOperations;
stepTemplate.registerListener(new RepeatListenerSupport() {
public void before(RepeatContext context) {
multicaster.beforeChunk();
}
public void after(RepeatContext context, ExitStatus result) {
multicaster.afterChunk();
}
});
}
return stepOperations;
}
/**
* @param listeners
* @return
*/
public StepListener[] getStepListeners(BatchListener[] listeners) {
List list = new ArrayList();
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof StepListener) {
list.add(listener);
}
}
return (StepListener[]) list.toArray(new StepListener[list.size()]);
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.step.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.listener.ItemListenerSupport;
/**
* Default implementation of the {@link ItemListenerSupport} class that
* writes all exceptions via commons logging. Since generics can't be used to
* ensure the list contains exceptions, any non exceptions will be logged out by
* calling toString on the object.
*
* @author Lucas Ward
*
*/
public class DefaultItemFailureHandler extends ItemListenerSupport {
protected static final Log logger = LogFactory
.getLog(DefaultItemFailureHandler.class);
/*
* (non-Javadoc)
*
* @see org.springframework.batch.core.domain.ItemFailureLog#log(java.util.List)
*/
public void onReadError(Exception ex) {
try {
logger.error("Error encountered while reading", ex);
} catch (Exception exception) {
logger.error("Invalid type for logging: [" + exception.toString()
+ "]");
}
}
public void onWriteError(Exception ex, Object item) {
try {
logger.error("Error encountered while writing item: [ " + item + "]", ex);
} catch (Exception exception) {
logger.error("Invalid type for logging: [" + exception.toString()
+ "]");
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.step.support;
import org.springframework.batch.core.StepExecution;
import edu.emory.mathcs.backport.java.util.concurrent.Semaphore;
/**
* @author Dave Syer
*
*/
public class DefaultStepExecutionSynchronizer implements StepExecutionSynchronizer {
private Semaphore semaphore = new Semaphore(1);
/* (non-Javadoc)
* @see org.springframework.batch.execution.step.support.StepExecutionSynchronizer#lock(org.springframework.batch.core.domain.StepExecution)
*/
public void lock(StepExecution stepExecution) throws InterruptedException {
semaphore.acquire();
}
/* (non-Javadoc)
* @see org.springframework.batch.execution.step.support.StepExecutionSynchronizer#release(org.springframework.batch.core.domain.StepExecution)
*/
public void release(StepExecution stepExecution) {
semaphore.release();
}
}

View File

@@ -0,0 +1,245 @@
/*
* 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.step.support;
import org.springframework.batch.core.BatchListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.execution.step.ItemHandler;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.exception.ExceptionHandler;
import org.springframework.batch.repeat.exception.SimpleLimitExceptionHandler;
import org.springframework.batch.repeat.policy.SimpleCompletionPolicy;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.repeat.support.TaskExecutorRepeatTemplate;
import org.springframework.core.task.TaskExecutor;
/**
* Most common configuration options for simple steps should be found here. Use
* this factory bean instead of creating a {@link Step} implementation manually.
*
* @author Dave Syer
*
*/
public class DefaultStepFactoryBean extends AbstractStepFactoryBean {
private int skipLimit = 0;
private int commitInterval = 0;
private ItemStream[] streams = new ItemStream[0];
private BatchListener[] listeners = new BatchListener[0];
private ListenerMulticaster listener = new ListenerMulticaster();
private TaskExecutor taskExecutor;
private ItemHandler itemHandler;
private RepeatTemplate stepOperations;
private SimpleLimitExceptionHandler exceptionHandler;
/**
* Set the commit interval.
*
* @param commitInterval
*/
public void setCommitInterval(int commitInterval) {
this.commitInterval = commitInterval;
}
/**
* The streams to inject into the {@link Step}. Any instance of
* {@link ItemStream} can be used, and will then receive callbacks at the
* appropriate stage in the step.
*
* @param streams an array of listeners
*/
public void setStreams(ItemStream[] streams) {
this.streams = streams;
}
/**
* Public setter for a limit that determines skip policy. If this value is
* positive then an exception in chunk processing will cause the item to be
* skipped and no exception propagated until the limit is reached. If it is
* zero then all exceptions will be propagated from the chunk and cause the
* step to abort.
*
* @param skipLimit the value to set. Default is 0 (never skip).
*/
public void setSkipLimit(int skipLimit) {
this.skipLimit = skipLimit;
}
/**
* The listeners to inject into the {@link Step}. Any instance of
* {@link BatchListener} can be used, and will then receive callbacks at the
* appropriate stage in the step.
*
* @param listeners an array of listeners
*/
public void setListeners(BatchListener[] listeners) {
this.listeners = listeners;
}
/**
* Protected getter for the step operations to make them available in
* subclasses.
* @return the step operations
*/
protected RepeatTemplate getStepOperations() {
return stepOperations;
}
/**
* Public setter for the SimpleLimitExceptionHandler.
* @param exceptionHandler the exceptionHandler to set
*/
public void setExceptionHandler(SimpleLimitExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
/**
* Protected getter for the {@link ExceptionHandler}.
* @return the {@link ExceptionHandler}
*/
protected ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
/**
* Public setter for the {@link TaskExecutor}. If this is set, then it will
* be used to execute the chunk processing inside the {@link Step}.
*
* @param taskExecutor the taskExecutor to set
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Public getter for the ItemProcessor.
* @return the itemProcessor
*/
protected ItemHandler getItemHandler() {
return itemHandler;
}
/**
* Public setter for the ItemProcessor.
* @param itemHandler the itemProcessor to set
*/
protected void setItemHandler(ItemHandler itemHandler) {
this.itemHandler = itemHandler;
}
/**
* @param step
*
*/
protected void applyConfiguration(ItemOrientedStep step) {
super.applyConfiguration(step);
step.setStreams(streams);
for (int i = 0; i < listeners.length; i++) {
BatchListener listener = listeners[i];
if (listener instanceof StepListener) {
step.registerStepListener((StepListener) listener);
}
else {
this.listener.register(listener);
}
}
ItemReader itemReader = getItemReader();
ItemWriter itemWriter = getItemWriter();
// Since we are going to wrap these things with listener callbacks we
// need to register them here because the step will not know we did
// that.
if (itemReader instanceof ItemStream) {
step.registerStream((ItemStream) itemReader);
}
if (itemReader instanceof StepListener) {
step.registerStepListener((StepListener) itemReader);
}
if (itemWriter instanceof ItemStream) {
step.registerStream((ItemStream) itemWriter);
}
if (itemWriter instanceof StepListener) {
step.registerStepListener((StepListener) itemWriter);
}
BatchListenerFactoryHelper helper = new BatchListenerFactoryHelper();
if (commitInterval > 0) {
RepeatTemplate chunkOperations = new RepeatTemplate();
chunkOperations.setCompletionPolicy(new SimpleCompletionPolicy(commitInterval));
helper.addChunkListeners(chunkOperations, listeners);
step.setChunkOperations(chunkOperations);
}
StepListener[] stepListeners = helper.getStepListeners(listeners);
itemReader = helper.getItemReader(itemReader, listeners);
itemWriter = helper.getItemWriter(itemWriter, listeners);
stepOperations = new RepeatTemplate();
// In case they are used by subclasses:
setItemReader(itemReader);
setItemWriter(itemWriter);
step.setStepListeners(stepListeners);
if (taskExecutor != null) {
TaskExecutorRepeatTemplate repeatTemplate = new TaskExecutorRepeatTemplate();
repeatTemplate.setTaskExecutor(taskExecutor);
stepOperations = repeatTemplate;
}
step.setStepOperations(stepOperations);
ItemSkipPolicyItemHandler itemProcessor = new ItemSkipPolicyItemHandler(itemReader, itemWriter);
if (skipLimit > 0) {
/*
* If there is a skip limit (not the default) then we are prepared
* to absorb exceptions at the step level because the failed items
* will never re-appear after a rollback.
*/
itemProcessor.setItemSkipPolicy(new LimitCheckingItemSkipPolicy(skipLimit));
setExceptionHandler(new SimpleLimitExceptionHandler(skipLimit));
stepOperations.setExceptionHandler(getExceptionHandler());
step.setStepOperations(stepOperations);
}
else {
// This is the default in ItemOrientedStep anyway...
itemProcessor.setItemSkipPolicy(new NeverSkipItemSkipPolicy());
}
setItemHandler(itemProcessor);
step.setItemHandler(itemProcessor);
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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.step.support;
import org.springframework.batch.core.ItemSkipPolicy;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.Skippable;
import org.springframework.batch.repeat.ExitStatus;
/**
* @author Dave Syer
*
*/
public class ItemSkipPolicyItemHandler extends SimpleItemHandler {
private ItemSkipPolicy itemSkipPolicy = new NeverSkipItemSkipPolicy();
/**
* @param itemReader
* @param itemWriter
*/
public ItemSkipPolicyItemHandler(ItemReader itemReader, ItemWriter itemWriter) {
super(itemReader, itemWriter);
}
/**
* @param itemSkipPolicy
*/
public void setItemSkipPolicy(ItemSkipPolicy itemSkipPolicy) {
this.itemSkipPolicy = itemSkipPolicy;
}
/**
* Execute the business logic, delegating to the reader and writer.
* Subclasses could extend the behaviour as long as they always return the
* value of this method call in their superclass.<br/>
*
* Read from the {@link ItemReader} and process (if not null) with the
* {@link ItemWriter}.<br/>
*
* If there is an exception and the reader or writer implements
* {@link Skippable} then the skip method is called.
*
* @param contribution the current step
* @return {@link ExitStatus#CONTINUABLE} if there is more processing to do
* @throws Exception if there is an error
*/
public ExitStatus handle(StepContribution contribution) throws Exception {
ExitStatus exitStatus = ExitStatus.CONTINUABLE;
try {
exitStatus = super.handle(contribution);
}
catch (Exception e) {
if (itemSkipPolicy.shouldSkip(e, contribution.getSkipCount())) {
contribution.incrementSkipCount();
skip();
}
else {
// Rethrow so that outer transaction is rolled back properly
throw e;
}
}
return exitStatus;
}
/**
* Mark the current item as skipped if possible. If the reader and / or
* writer are {@link Skippable} then delegate to them in that order.
*
* @see org.springframework.batch.item.Skippable#skip()
*/
private void skip() {
if (getItemReader() instanceof Skippable) {
((Skippable) getItemReader()).skip();
}
if (getItemWriter() instanceof Skippable) {
((Skippable) getItemWriter()).skip();
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.step.support;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.springframework.batch.core.ItemSkipPolicy;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.item.file.FlatFileParseException;
import org.springframework.batch.support.ExceptionClassifier;
import org.springframework.batch.support.SubclassExceptionClassifier;
/**
* <p>
* {@link ItemSkipPolicy} that determines whether or not reading should continue
* based upon how many items have been skipped. This is extremely useful
* behavior, as it allows you to skip records, but will throw a
* {@link SkipLimitExceededException} if a set limit has been exceeded. For
* example, it is generally advisable to skip {@link FlatFileParseException}s,
* however, if the vast majority of records are causing exceptions, the file is
* likely bad.
* </p>
*
* <p>
* Furthermore, it is also likely that you only want to skip certain exceptions.
* {@link FlatFileParseException} is a good example of an exception you will
* likely want to skip, but a {@link FileNotFoundException} should cause
* immediate termination of the {@link Step}. Because it would be impossible
* for a general purpose policy to determine all the types of exceptions that
* should be skipped from those that shouldn't, a list must be passed in, with
* all of the exceptions that are 'skippable'
* </p>
*
* @author Ben Hale
* @author Lucas Ward
*/
public class LimitCheckingItemSkipPolicy implements ItemSkipPolicy {
/**
* Label for classifying skippable exceptions.
*/
private static final String SKIP = "skip";
private final int skipLimit;
private ExceptionClassifier exceptionClassifier;
public LimitCheckingItemSkipPolicy(int skipLimit) {
this(skipLimit, new ArrayList(0));
}
public LimitCheckingItemSkipPolicy(int skipLimit, List skippableExceptions) {
this.skipLimit = skipLimit;
SubclassExceptionClassifier exceptionClassifier = new SubclassExceptionClassifier();
Map typeMap = new HashMap();
for (Iterator iterator = skippableExceptions.iterator(); iterator.hasNext();) {
Class throwable = (Class) iterator.next();
typeMap.put(throwable, SKIP);
}
exceptionClassifier.setTypeMap(typeMap);
this.exceptionClassifier = exceptionClassifier;
}
/**
* Given the provided exception and skip count, determine whether or not
* processing should continue for the given exception. If the exception is
* not within the list of 'skippable exceptions', false will be returned. If
* the exception is within the list, and {@link StepExecution} skipCount is
* greater than the skipLimit, then a {@link SkipLimitExceededException}
* will be thrown.
*/
public boolean shouldSkip(Throwable t, int skipCount) {
if (exceptionClassifier.classify(t).equals(SKIP)) {
if (skipCount < skipLimit) {
return true;
}
else {
throw new SkipLimitExceededException(skipLimit, t);
}
}
else {
return false;
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.step.support;
import org.springframework.batch.core.BatchListener;
import org.springframework.batch.core.ChunkListener;
import org.springframework.batch.core.ItemReadListener;
import org.springframework.batch.core.ItemWriteListener;
import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.execution.listener.CompositeChunkListener;
import org.springframework.batch.execution.listener.CompositeItemReadListener;
import org.springframework.batch.execution.listener.CompositeItemWriteListener;
import org.springframework.batch.execution.listener.CompositeStepListener;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.repeat.ExitStatus;
/**
* @author Dave Syer
*
*/
public class ListenerMulticaster implements StepListener, ChunkListener, ItemReadListener,
ItemWriteListener {
private CompositeStepListener stepListener = new CompositeStepListener();
private CompositeChunkListener chunkListener = new CompositeChunkListener();
private CompositeItemReadListener itemReadListener = new CompositeItemReadListener();
private CompositeItemWriteListener itemWriteListener = new CompositeItemWriteListener();
/**
* Initialise the listener instance.
*/
public ListenerMulticaster() {
super();
}
/**
* Register each of the objects as listeners. Once registered, calls to the
* {@link ListenerMulticaster} broadcast to the individual listeners.
*
* @param listeners an array of listener objects of types known to the
* multicaster.
*/
public void setListeners(BatchListener[] listeners) {
for (int i = 0; i < listeners.length; i++) {
register(listeners[i]);
}
}
/**
* Register the listener for callbacks on the appropriate interfaces
* implemented. Any {@link BatchListener} can be provided, or an
* {@link ItemStream}. Other types will be ignored.
*/
public void register(BatchListener listener) {
if (listener instanceof StepListener) {
this.stepListener.register((StepListener) listener);
}
if (listener instanceof ChunkListener) {
this.chunkListener.register((ChunkListener) listener);
}
if (listener instanceof ItemReadListener) {
this.itemReadListener.register((ItemReadListener) listener);
}
if (listener instanceof ItemWriteListener) {
this.itemWriteListener.register((ItemWriteListener) listener);
}
}
/**
* @return
* @see org.springframework.batch.execution.listener.CompositeStepListener#afterStep()
*/
public ExitStatus afterStep(StepExecution stepExecution) {
return stepListener.afterStep(stepExecution);
}
/**
* @param stepExecution
* @see org.springframework.batch.execution.listener.CompositeStepListener#beforeStep(org.springframework.batch.core.StepExecution)
*/
public void beforeStep(StepExecution stepExecution) {
stepListener.beforeStep(stepExecution);
}
/**
* @param e
* @return
* @see org.springframework.batch.execution.listener.CompositeStepListener#onErrorInStep(java.lang.Throwable)
*/
public ExitStatus onErrorInStep(StepExecution stepExecution, Throwable e) {
return stepListener.onErrorInStep(stepExecution, e);
}
/**
*
* @see org.springframework.batch.execution.listener.CompositeChunkListener#afterChunk()
*/
public void afterChunk() {
chunkListener.afterChunk();
}
/**
*
* @see org.springframework.batch.execution.listener.CompositeChunkListener#beforeChunk()
*/
public void beforeChunk() {
chunkListener.beforeChunk();
}
/**
* @param item
* @see org.springframework.batch.execution.listener.CompositeItemReadListener#afterRead(java.lang.Object)
*/
public void afterRead(Object item) {
itemReadListener.afterRead(item);
}
/**
*
* @see org.springframework.batch.execution.listener.CompositeItemReadListener#beforeRead()
*/
public void beforeRead() {
itemReadListener.beforeRead();
}
/**
* @param ex
* @see org.springframework.batch.execution.listener.CompositeItemReadListener#onReadError(java.lang.Exception)
*/
public void onReadError(Exception ex) {
itemReadListener.onReadError(ex);
}
/**
*
* @see org.springframework.batch.execution.listener.CompositeItemWriteListener#afterWrite()
*/
public void afterWrite() {
itemWriteListener.afterWrite();
}
/**
* @param item
* @see org.springframework.batch.execution.listener.CompositeItemWriteListener#beforeWrite(java.lang.Object)
*/
public void beforeWrite(Object item) {
itemWriteListener.beforeWrite(item);
}
/**
* @param ex
* @param item
* @see org.springframework.batch.execution.listener.CompositeItemWriteListener#onWriteError(java.lang.Exception,
* java.lang.Object)
*/
public void onWriteError(Exception ex, Object item) {
itemWriteListener.onWriteError(ex, item);
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.step.support;
import org.springframework.batch.core.ItemSkipPolicy;
/**
* {@link ItemSkipPolicy} implementation that always returns false,
* indicating that an item should not be skipped.
*
* @author Lucas Ward
*/
public class NeverSkipItemSkipPolicy implements ItemSkipPolicy{
public boolean shouldSkip(Throwable t, int skipCount) {
return false;
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.step.support;
import org.springframework.batch.core.BatchListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepListener;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.RepeatOperations;
import org.springframework.batch.repeat.support.RepeatTemplate;
/**
* Factory bean for {@link Step} implementations allowing registration of
* listeners and also direct injection of the {@link RepeatOperations} needed at
* step and chunk level.
*
* @author Dave Syer
*
*/
public class RepeatOperationsStepFactoryBean extends AbstractStepFactoryBean {
private ItemStream[] streams = new ItemStream[0];
private BatchListener[] listeners = new BatchListener[0];
private RepeatOperations chunkOperations = new RepeatTemplate();
private RepeatOperations stepOperations = new RepeatTemplate();
/**
* The streams to inject into the {@link Step}. Any instance of
* {@link ItemStream} can be used, and will then receive callbacks at the
* appropriate stage in the step.
*
* @param streams an array of listeners
*/
public void setStreams(ItemStream[] streams) {
this.streams = streams;
}
/**
* The listeners to inject into the {@link Step}. Any instance of
* {@link BatchListener} can be used, and will then receive callbacks at the
* appropriate stage in the step.
*
* @param listeners an array of listeners
*/
public void setListeners(BatchListener[] listeners) {
this.listeners = listeners;
}
/**
* The {@link RepeatOperations} to use for the outer loop of the batch
* processing. Should be set up by the caller through a factory. Defaults to
* a plain {@link RepeatTemplate}.
*
* @param stepOperations a {@link RepeatOperations} instance.
*/
public void setStepOperations(RepeatOperations stepOperations) {
this.stepOperations = stepOperations;
}
/**
* The {@link RepeatOperations} to use for the inner loop of the batch
* processing. should be set up by the caller through a factory. defaults to
* a plain {@link RepeatTemplate}.
*
* @param chunkOperations a {@link RepeatOperations} instance.
*/
public void setChunkOperations(RepeatOperations chunkOperations) {
this.chunkOperations = chunkOperations;
}
/**
* @param step
*
*/
protected void applyConfiguration(ItemOrientedStep step) {
super.applyConfiguration(step);
step.setStreams(streams);
ItemReader itemReader = getItemReader();
ItemWriter itemWriter = getItemWriter();
/*
* Since we are going to wrap these things with listener callbacks we
* need to register them here because the step will not know we did
* that.
*/
if (itemReader instanceof ItemStream) {
step.registerStream((ItemStream) itemReader);
}
if (itemReader instanceof StepListener) {
step.registerStepListener((StepListener) itemReader);
}
if (itemWriter instanceof ItemStream) {
step.registerStream((ItemStream) itemWriter);
}
if (itemWriter instanceof StepListener) {
step.registerStepListener((StepListener) itemWriter);
}
BatchListenerFactoryHelper helper = new BatchListenerFactoryHelper();
StepListener[] stepListeners = helper.getStepListeners(listeners);
itemReader = helper.getItemReader(itemReader, listeners);
itemWriter = helper.getItemWriter(itemWriter, listeners);
RepeatOperations stepOperations = helper.addChunkListeners(this.stepOperations, listeners);
// In case they are used by subclasses:
setItemReader(itemReader);
setItemWriter(itemWriter);
step.setStepListeners(stepListeners);
step.setItemHandler(new SimpleItemHandler(itemReader, itemWriter));
step.setChunkOperations(chunkOperations);
step.setStepOperations(stepOperations);
}
}

View File

@@ -0,0 +1,83 @@
/*
* 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.step.support;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.repository.NoSuchJobException;
import org.springframework.batch.core.runtime.ExitStatusExceptionClassifier;
import org.springframework.batch.execution.launch.support.ExitCodeMapper;
import org.springframework.batch.repeat.ExitStatus;
/**
* <p>
* Simple implementation of {@link ExitStatusExceptionClassifier} that returns
* basic String exit codes, and defaults to the class name of the throwable for
* the message. Most users will want to write their own implementation that
* creates more specific exit codes for different exception types.
* </p>
*
* @author Lucas Ward
*
*/
public class SimpleExitStatusExceptionClassifier implements
ExitStatusExceptionClassifier {
/* (non-Javadoc)
* @see org.springframework.batch.core.executor.ExitCodeExceptionClassifier#classifyForExitCode(java.lang.Throwable)
*/
public ExitStatus classifyForExitCode(Throwable throwable) {
return (ExitStatus) classify(throwable);
}
/* (non-Javadoc)
* @see org.springframework.batch.common.ExceptionClassifier#classify(java.lang.Throwable)
*/
public Object classify(Throwable throwable) {
ExitStatus exitStatus = ExitStatus.FAILED;
if (throwable instanceof JobInterruptedException) {
exitStatus = new ExitStatus(false, JOB_INTERRUPTED,
JobInterruptedException.class.getName());
} else if( throwable instanceof NoSuchJobException ) {
exitStatus = new ExitStatus(false, ExitCodeMapper.NO_SUCH_JOB);
} else {
String message = "";
if (throwable!=null) {
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
message = writer.toString();
}
exitStatus = new ExitStatus(false, FATAL_EXCEPTION, message);
}
return exitStatus;
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.common.ExceptionClassifier#getDefault()
*/
public Object getDefault() {
// return without message since we don't know what the exception is
return new ExitStatus(false, FATAL_EXCEPTION);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.step.support;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.execution.step.ItemHandler;
import org.springframework.batch.item.ClearFailedException;
import org.springframework.batch.item.FlushFailedException;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.MarkFailedException;
import org.springframework.batch.item.ResetFailedException;
import org.springframework.batch.repeat.ExitStatus;
/**
* Simplest possible implementation of {@link ItemHandler} with no skipping or
* recovering. Just delegates all calls to the provided {@link ItemReader} and
* {@link ItemWriter}.
*
* @author Dave Syer
*
*/
public class SimpleItemHandler implements ItemHandler {
private ItemReader itemReader;
private ItemWriter itemWriter;
/**
* @param itemReader
* @param itemWriter
*/
public SimpleItemHandler(ItemReader itemReader, ItemWriter itemWriter) {
super();
this.itemReader = itemReader;
this.itemWriter = itemWriter;
}
/**
* Public getter for the ItemReader.
* @return the itemReader
*/
public ItemReader getItemReader() {
return itemReader;
}
/**
* Public getter for the ItemWriter.
* @return the itemWriter
*/
public ItemWriter getItemWriter() {
return itemWriter;
}
/**
* Read from the {@link ItemReader} and process (if not null) with the
* {@link ItemWriter}.
*
* @see org.springframework.batch.execution.step.ItemHandler#handle(org.springframework.batch.core.StepContribution)
*/
public ExitStatus handle(StepContribution contribution) throws Exception {
Object item = itemReader.read();
if (item == null) {
return ExitStatus.FINISHED;
}
itemWriter.write(item);
return ExitStatus.CONTINUABLE;
}
/**
* @throws MarkFailedException
* @see org.springframework.batch.item.ItemReader#mark()
*/
public void mark() throws MarkFailedException {
itemReader.mark();
}
/**
* @throws ResetFailedException
* @see org.springframework.batch.item.ItemReader#reset()
*/
public void reset() throws ResetFailedException {
itemReader.reset();
}
/**
* @throws ClearFailedException
* @see org.springframework.batch.item.ItemWriter#clear()
*/
public void clear() throws ClearFailedException {
itemWriter.clear();
}
/**
* @throws FlushFailedException
* @see org.springframework.batch.item.ItemWriter#flush()
*/
public void flush() throws FlushFailedException {
itemWriter.flush();
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.step.support;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.exception.ExceptionHandler;
import org.springframework.batch.repeat.synch.RepeatSynchronizationManager;
import org.springframework.batch.retry.RetryCallback;
import org.springframework.batch.retry.RetryContext;
import org.springframework.batch.retry.RetryPolicy;
import org.springframework.batch.retry.listener.RetryListenerSupport;
/**
* @author Dave Syer
*
*/
public class SimpleRetryExceptionHandler extends RetryListenerSupport implements ExceptionHandler {
/**
* Attribute key, whose existence signals an exhausted retry.
*/
private static final String EXHAUSTED = SimpleRetryExceptionHandler.class.getName() + ".RETRY_EXHAUSTED";
private RetryPolicy retryPolicy;
private ExceptionHandler exceptionHandler;
/**
* @param retryPolicy
* @param exceptionHandler
*/
public SimpleRetryExceptionHandler(RetryPolicy retryPolicy, ExceptionHandler exceptionHandler) {
this.retryPolicy = retryPolicy;
this.exceptionHandler = exceptionHandler;
}
/*
* (non-Javadoc)
* @see org.springframework.batch.repeat.exception.handler.ExceptionHandler#handleException(org.springframework.batch.repeat.RepeatContext,
* java.lang.Throwable)
*/
public void handleException(RepeatContext context, Throwable throwable) throws RuntimeException {
// Only bother to check the delegate exception handler if we know that
// retry is exhausted
if (context.hasAttribute(EXHAUSTED)) {
exceptionHandler.handleException(context, throwable);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.retry.RetryListener#close(org.springframework.batch.retry.RetryContext,
* org.springframework.batch.retry.RetryCallback, java.lang.Throwable)
*/
public void close(RetryContext context, RetryCallback callback, Throwable throwable) {
if (!retryPolicy.canRetry(context)) {
getRepeatContext().setAttribute(EXHAUSTED, "true");
}
}
/**
* Get the parent context (the retry is in an inner "chunk" loop and we want
* the exception to be handled at the outer "step" level).
* @return the {@link RepeatContext} that should hold the exhausted flag.
*/
private RepeatContext getRepeatContext() {
RepeatContext context = RepeatSynchronizationManager.getContext();
if (context.getParent() != null) {
return context.getParent();
}
return context;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.step.support;
import org.springframework.batch.core.UnexpectedJobExecutionException;
/**
* Exception indicating that the skip limit for a particular {@Step} has
* been exceeded.
*
* @author Ben Hale
* @author Lucas Ward
*/
public class SkipLimitExceededException extends UnexpectedJobExecutionException {
private final int skipLimit;
public SkipLimitExceededException(int skipLimit, Throwable t) {
super("Skip limit of '" + skipLimit + "' exceeded", t);
this.skipLimit = skipLimit;
}
public int getSkipLimit() {
return skipLimit;
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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.step.support;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.execution.step.ItemOrientedStep;
import org.springframework.batch.item.ItemKeyGenerator;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemRecoverer;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.retry.RetryOperations;
import org.springframework.batch.retry.RetryPolicy;
import org.springframework.batch.retry.backoff.BackOffPolicy;
import org.springframework.batch.retry.callback.ItemReaderRetryCallback;
import org.springframework.batch.retry.policy.ItemReaderRetryPolicy;
import org.springframework.batch.retry.policy.SimpleRetryPolicy;
import org.springframework.batch.retry.support.RetryTemplate;
/**
* Factory bean for step that executes its item processing with a stateful
* retry. Failed items are never skipped, but always cause a rollback. Before a
* rollback, the {@link Step} makes a record of the failed item, caching it
* under a key given by the {@link ItemKeyGenerator}. Then when it is
* re-presented by the {@link ItemReader} it is recognised and retried up to a
* limit given by the {@link RetryPolicy}. When the retry is exhausted instead
* of the item being skipped it is handled by an {@link ItemRecoverer}.<br/>
*
* The skipLimit property is still used to control the overall exception
* handling policy. Only exhausted retries count against the exception handler,
* instead of counting all exceptions.
*
* @author Dave Syer
*
*/
public class StatefulRetryStepFactoryBean extends DefaultStepFactoryBean {
private ItemKeyGenerator itemKeyGenerator;
private ItemRecoverer itemRecoverer;
private int retryLimit;
private Class[] retryableExceptionClasses;
private BackOffPolicy backOffPolicy;
/**
* Public setter for the retry limit. Each item can be retried up to this
* limit.
* @param retryLimit the retry limit to set
*/
public void setRetryLimit(int retryLimit) {
this.retryLimit = retryLimit;
}
/**
* Public setter for the Class[].
* @param retryableExceptionClasses the retryableExceptionClasses to set
*/
public void setRetryableExceptionClasses(Class[] retryableExceptionClasses) {
this.retryableExceptionClasses = retryableExceptionClasses;
}
/**
* Public setter for the {@link BackOffPolicy}.
* @param backOffPolicy the {@link BackOffPolicy} to set
*/
public void setBackOffPolicy(BackOffPolicy backOffPolicy) {
this.backOffPolicy = backOffPolicy;
}
/**
* Public setter for the {@link ItemKeyGenerator} which will be used to
* cache failed items between transactions. If it is not injected but the
* reader or writer implement {@link ItemKeyGenerator}, one of those will
* be used instead (preferring the reader to the writer if both would be
* appropriate). If neither can be used, then the default will be to just
* use the item itself as a cache key.
*
* @param itemKeyGenerator the {@link ItemKeyGenerator} to set
*/
public void setItemKeyGenerator(ItemKeyGenerator itemKeyGenerator) {
this.itemKeyGenerator = itemKeyGenerator;
}
/**
* Public setter for the {@link ItemRecoverer}. If this is set the
* {@link ItemRecoverer#recover(Object, Throwable)} will be called when
* retry is exhausted, and within the business transaction (which will not
* roll back because of any other item-related errors).
*
* @param itemRecoverer the {@link ItemRecoverer} to set
*/
public void setItemRecoverer(ItemRecoverer itemRecoverer) {
this.itemRecoverer = itemRecoverer;
}
/**
* @param step
*
*/
protected void applyConfiguration(ItemOrientedStep step) {
super.applyConfiguration(step);
if (retryLimit > 0) {
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(retryLimit);
if (retryableExceptionClasses != null) {
retryPolicy.setRetryableExceptionClasses(retryableExceptionClasses);
}
// Co-ordinate the retry policy with the exception handler:
getStepOperations()
.setExceptionHandler(new SimpleRetryExceptionHandler(retryPolicy, getExceptionHandler()));
ItemReaderRetryCallback retryCallback = new ItemReaderRetryCallback(getItemReader(), itemKeyGenerator,
getItemWriter());
retryCallback.setRecoverer(itemRecoverer);
ItemReaderRetryPolicy itemProviderRetryPolicy = new ItemReaderRetryPolicy(retryPolicy);
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(itemProviderRetryPolicy);
if (backOffPolicy != null) {
retryTemplate.setBackOffPolicy(backOffPolicy);
}
StatefulRetryItemHandler itemProcessor = new StatefulRetryItemHandler(getItemReader(), getItemWriter(),
retryTemplate, retryCallback);
step.setItemHandler(itemProcessor);
}
}
private static class StatefulRetryItemHandler extends SimpleItemHandler {
final private RetryOperations retryOperations;
final private ItemReaderRetryCallback retryCallback;
/**
* @param itemReader
* @param itemWriter
* @param retryCallback
* @param retryTemplate
*/
public StatefulRetryItemHandler(ItemReader itemReader, ItemWriter itemWriter, RetryOperations retryTemplate,
ItemReaderRetryCallback retryCallback) {
super(itemReader, itemWriter);
this.retryOperations = retryTemplate;
this.retryCallback = retryCallback;
}
/**
* Execute the business logic, delegating to the reader and writer.
* Subclasses could extend the behaviour as long as they always return
* the value of this method call in their superclass.<br/>
*
* Read from the {@link ItemReader} and process (if not null) with the
* {@link ItemWriter}. The call to {@link ItemWriter} is wrapped in a
* stateful retry. In that case the {@link ItemRecoverer} is used (if
* provided) in the case of an exception to apply alternate processing
* to the item. If the stateful retry is in place then the recovery will
* happen in the next transaction automatically, otherwise it might be
* necessary for clients to make the recover method transactional with
* appropriate propagation behaviour (probably REQUIRES_NEW because the
* call will happen in the context of a transaction that is about to
* rollback).<br/>
*
* @param contribution the current step
* @return {@link ExitStatus#CONTINUABLE} if there is more processing to
* do
* @throws Exception if there is an error
*/
public ExitStatus handle(StepContribution contribution) throws Exception {
return new ExitStatus(retryOperations.execute(retryCallback) != null);
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.step.support;
import org.springframework.batch.core.StepExecution;
/**
* Strategy for blocking while a step execution is being updated.
*
* @author Dave Syer
*
*/
public interface StepExecutionSynchronizer {
/**
* Lock the step execution, blocking if it has been locked by another thread.
*
* @param stepExecution the {@link StepExecution} that is in progress
* @throws InterruptedException if the thread is interrupted while waiting
*/
void lock(StepExecution stepExecution) throws InterruptedException;
/**
* Release the lock. Use this in a finally block.
*
* @param stepExecution
*/
void release(StepExecution stepExecution);
}

View File

@@ -0,0 +1,40 @@
/*
* 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.step.support;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.core.Step;
import org.springframework.batch.repeat.RepeatContext;
/**
* Strategy interface for an interruption policy. This policy allows
* {@link Step} implementations to check if a job has been interrupted.
*
* @author Lucas Ward
*
*/
public interface StepInterruptionPolicy {
/**
* Has the job been interrupted? If so then throw a
* {@link JobInterruptedException}.
* @param context the current context of the running step.
*
* @throws JobInterruptedException when the job has been interrupted.
*/
void checkInterrupted(RepeatContext context) throws JobInterruptedException;
}

View File

@@ -0,0 +1,61 @@
/*
* 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.step.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.JobInterruptedException;
import org.springframework.batch.repeat.RepeatContext;
/**
* Policy that checks the current thread to see if it has been interrupted.
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class ThreadStepInterruptionPolicy implements StepInterruptionPolicy {
protected static final Log logger = LogFactory
.getLog(ThreadStepInterruptionPolicy.class);
/**
* Returns if the current job lifecycle has been interrupted by checking if
* the current thread is interrupted.
*/
public void checkInterrupted(RepeatContext context) throws JobInterruptedException {
if (isInterrupted(context)) {
throw new JobInterruptedException("Job interrupted status detected.");
}
}
/**
* @param context the current context
* @return true if the job has been interrupted
*/
private boolean isInterrupted(RepeatContext context) {
boolean interrupted = (Thread.currentThread().isInterrupted() || context.isTerminateOnly());
if(interrupted){
logger.error("Step interrupted");
}
return interrupted;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of simple concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,65 @@
/*
* 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.tasklet;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.item.adapter.AbstractMethodInvokingDelegator;
import org.springframework.batch.repeat.ExitStatus;
/**
* A {@link Tasklet} that wraps a method in a POJO. By default the
* {@link ExitStatus} is determined by comparing the return value from the POJO
* with null. The POJO method is usually going to have no arguments, but a
* static argument or array of arguments can be used by setting the arguments
* property.
*
* @see AbstractMethodInvokingDelegator
*
* @author Dave Syer
*
*/
public class TaskletAdapter extends AbstractMethodInvokingDelegator implements Tasklet {
/**
* Delegate execution to the target object and translate the return value to
* an {@link ExitStatus} by invoking a method in the delegate POJO. N.B. the
* delegate method should not be void, otherwise there is no way to
* determine when the result indicates a finished job.
*
* @see org.springframework.batch.core.tasklet.Tasklet#execute()
*/
public ExitStatus execute() throws Exception {
return mapResult(invokeDelegateMethod());
}
/**
* If the result is an {@link ExitStatus} already just return that,
* otherwise return {@link ExitStatus#FINISHED} if the result is null and
* {@link ExitStatus#CONTINUABLE} if not.
* @param result the value returned by the delegate method
* @return an {@link ExitStatus} consistent with the result
*/
protected ExitStatus mapResult(Object result) {
if (result instanceof ExitStatus) {
return (ExitStatus) result;
}
if (result == null) {
return ExitStatus.FINISHED;
}
return ExitStatus.CONTINUABLE;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of tasklet concerns.
</p>
</body>
</html>