Add spring-batch- to module directory names (so folks can use mvn eclipse:eclipse if they want to).

BATCH-238: Remove hibernate support for the Daos.
This commit is contained in:
dsyer
2007-12-10 21:23:48 +00:00
parent 17705f27ab
commit 8ea331bfc7
884 changed files with 956 additions and 2352 deletions

View File

@@ -0,0 +1,45 @@
/*
* 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.bootstrap;
import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent;
import org.springframework.context.ApplicationEvent;
/**
* {@link ApplicationEvent} that encodes a request from the execution layer to a
* running job.
*
* @author Dave Syer
*
*/
public class BatchExecutionRequestEvent extends ApplicationEvent {
/**
* Constructor for {@link BatchExecutionRequestEvent}. The source is the
* execution layer service implementation that is sending the signal.<br/>
*
* TODO: the source should be Serializable so really it should be just a
* message about the request?
*
* Currently encodes a request to publish back a
* {@link RepeatOperationsApplicationEvent}. Could be extended in the
* future to narrow the request to ask for specific information to be
* published back.
*/
public BatchExecutionRequestEvent(Object source) {
super(source);
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.bootstrap;
import javax.management.Notification;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.jmx.export.notification.NotificationPublisher;
import org.springframework.jmx.export.notification.NotificationPublisherAware;
/**
* JMX notification broadcaster
*
* @author Dave Syer
* @since 2.1
*/
public class JobExecutionNotificationPublisher implements ApplicationListener,
NotificationPublisherAware {
protected static final Log logger = LogFactory
.getLog(JobExecutionNotificationPublisher.class);
private NotificationPublisher notificationPublisher;
private int notificationCount = 0;
/**
* Injection setter.
*
* @see org.springframework.jmx.export.notification.NotificationPublisherAware#setNotificationPublisher(org.springframework.jmx.export.notification.NotificationPublisher)
*/
public void setNotificationPublisher(
NotificationPublisher notificationPublisher) {
this.notificationPublisher = notificationPublisher;
}
/**
* If the event is a {@link RepeatOperationsApplicationEvent} for open and
* close we log the event at INFO level and send a JMX notification if we
* are also an MBean.
*
* @see org.springframework.batch.execution.launch.SimpleJobLauncher#onApplicationEvent(org.springframework.context.ApplicationEvent)
*/
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if (applicationEvent instanceof RepeatOperationsApplicationEvent) {
RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) applicationEvent;
int type = event.getType();
if (type == RepeatOperationsApplicationEvent.OPEN
|| type == RepeatOperationsApplicationEvent.CLOSE
|| type == RepeatOperationsApplicationEvent.ERROR) {
String message = event.getMessage() + "; source="
+ event.getSource();
logger.info(message);
publish(message);
}
return;
}
}
/**
* Publish the provided message to an external listener if there is one.
*
* @param message
* the message to publish
*/
private void publish(String message) {
if (notificationPublisher != null) {
Notification notification = new Notification(
"RepeatOperationsApplicationEvent", this,
notificationCount++, message);
/*
* We can't create a notification with a null source, but we can set
* it to null after creation(!). We want it to be null so that
* Spring will replace it automatically with the ObjectName (in
* ModelMBeanNotificationPublisher).
*/
notification.setSource(null);
notificationPublisher.sendNotification(notification);
}
}
}

View File

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

View File

@@ -0,0 +1,279 @@
/*
* 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.bootstrap.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.configuration.NoSuchJobConfigurationException;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.Assert;
/**
* <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 minimum of two contexts
* must be loaded. One is the context containing the JobConfiguration, the other
* contains the 'Execution Environment'. That is, the JobExecutorFacade (which
* contains all the executors, plus the repository), the JobIdentifierFactory,
* and a normal JobLauncher. This command line launcher loads these application
* contexts by first loading the execution environment context via a
* {@link ContextSingletonBeanFactoryLocator}, which will search for the
* default key from classpath*:beanRefContext.xml to return the context. This
* will then be used as the parent to the JobConfiguration context. All required
* 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 JobLauncher. 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>
* One odd field might be noticed in the launcher, 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>
* VM Arguments vs. Program arguments: Because all of the arguments to the main
* method are optional, VM arguments are used:
*
* <ul>
* <li>-Djob.configuration.path: the classpath location of the JobConfiguration
* to use
* <li>-Djob.name: job name to be passed to the {@link JobLauncher}
* <li>-Dbatch.execution.environment.key: the key in beanRefContext.xml used to
* load the execution envrionement.
* </ul>
*
* @author Dave Syer
* @author Lucas Ward
* @since 2.1
*/
public class BatchCommandLineLauncher {
protected static final Log logger = LogFactory
.getLog(BatchCommandLineLauncher.class);
/**
* The default key for the parent context.
*/
public static final String DEFAULT_PARENT_KEY = "batchExecutionEnvironment";
/**
* The default path to the job configuration.
*/
public static final String DEFAULT_JOB_CONFIGURATION_PATH = "job-configuration.xml";
/**
* The default path to the bean reference context.
*/
public static final String DEFAULT_BEAN_REF_CONTEXT_PATH = "beanRefContext.xml";
private static final String JOB_CONFIGURATION_PATH_KEY = "job.configuration.path";
private static final String JOB_NAME_KEY = "job.name";
private static final String BATCH_EXECUTION_ENVIRONMENT_KEY = "batch.execution.environment.key";
private static final String BEAN_REF_CONTEXT_KEY = "bean.ref.context";
private BeanFactoryLocator beanFactoryLocator;
private ExitCodeMapper exitCodeMapper = new SimpleJvmExitCodeMapper();
private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier();
private JobLauncher launcher;
private SystemExiter systemExiter = new JvmSystemExiter();
public BatchCommandLineLauncher(String beanRefContextPath) {
beanFactoryLocator = ContextSingletonBeanFactoryLocator
.getInstance(beanRefContextPath);
}
/**
* Injection setter for the {@link JobLauncher}.
*
* @param launcher
* the launcher to set
*/
public void setLauncher(JobLauncher launcher) {
this.launcher = launcher;
}
/**
* Injection setter for the {@link ExitCodeExceptionClassifier}
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(
ExitCodeExceptionClassifier exceptionClassifier) {
this.exceptionClassifier = exceptionClassifier;
}
/**
* Injection setter for the {@link JvmExitCodeMapper}.
*
* @param exitCodeMapper
* the exitCodeMapper to set
*/
public void setExitCodeMapper(ExitCodeMapper exitCodeMapper) {
this.exitCodeMapper = exitCodeMapper;
}
/**
* Injection setter for the {@link SystemExiter}.
*
* @param systemExitor
*/
public void setSystemExiter(SystemExiter systemExitor) {
this.systemExiter = systemExitor;
}
/**
* Delegate to the exiter to (possibly) exit the VM gracefully.
*
* @param status
*/
public void exit(int status) {
systemExiter.exit(status);
}
/**
* @param path
* the path to a Spring context configuration for this job
* @param jobName
* the name of the job execution to use
* @parm parentKey the key to be loaded by
* ContextSingletonBeanFactoryLocator and used as the parent context.
* @throws NoSuchJobConfigurationException
* @throws IllegalStateException
* if JobLauncher is not autowired by the ApplicationContext
*/
int start(String path, String jobName, String parentKey) {
ExitStatus status = ExitStatus.FAILED;
ClassPathXmlApplicationContext context = null;
try {
ConfigurableApplicationContext parent = (ConfigurableApplicationContext) beanFactoryLocator
.useBeanFactory(parentKey).getFactory();
parent.getAutowireCapableBeanFactory().autowireBeanProperties(this,
AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
if (!path.endsWith(".xml")) {
path = path + ".xml";
}
context = new ClassPathXmlApplicationContext(new String[] { path },
parent);
context.getAutowireCapableBeanFactory().autowireBeanProperties(
this, AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, false);
Assert
.state(
launcher != null,
"JobLauncher must be provided in the parent ApplicationContext"
+ ", check the context created within classpath*:beanRefContext.xml to ensure a JobLauncher"
+ " is declared");
if (!launcher.isRunning()) {
if (jobName == null) {
status = launcher.run().getExitStatus();
} else {
status = launcher.run(jobName).getExitStatus();
}
}
} catch (NoSuchJobConfigurationException e) {
logger.fatal("Could not locate JobConfiguration \"" + jobName
+ "\"", e);
status = new ExitStatus(false,
ExitCodeMapper.NO_SUCH_JOB_CONFIGURATION);
} catch (Throwable t) {
logger.fatal(t);
status = exceptionClassifier.classifyForExitCode(t);
} finally {
if (context != null) {
try {
context.stop();
} finally {
context.close();
}
}
}
return exitCodeMapper.getExitCode(status.getExitCode());
}
/**
* Launch a batch job using a {@link BatchCommandLineLauncher}. 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
* <ul>
* <li>-Djob.configuration.path: the classpath location of the
* JobConfiguration to use
* <li>-Djob.name: job name to be passed to the
* {@link JobLauncher}
* <li>-Dbatch.execution.environment.key: the key in
* beanRefContext.xml used to load the execution envrionment.
* <li>-Dbean.ref.context: an altrernative location for
* beanRefContext.xml.</li>
* </ul>
*/
public static void main(String[] args) {
String path = System.getProperty(JOB_CONFIGURATION_PATH_KEY,
DEFAULT_JOB_CONFIGURATION_PATH);
String name = System.getProperty(JOB_NAME_KEY);
String beanRefContextPath = System.getProperty(BEAN_REF_CONTEXT_KEY,
DEFAULT_BEAN_REF_CONTEXT_PATH);
String parentKey = System.getProperty(BATCH_EXECUTION_ENVIRONMENT_KEY,
DEFAULT_PARENT_KEY);
BatchCommandLineLauncher command = new BatchCommandLineLauncher(
beanRefContextPath);
int result = command.start(path, name, parentKey);
command.exit(result);
}
}

View File

@@ -0,0 +1,30 @@
package org.springframework.batch.execution.bootstrap.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_CONFIGURATION_ERROR = 2;
public static final String NO_SUCH_JOB_CONFIGURATION = "NO_SUCH_JOB_CONFIGURATION";
public static final String JOB_CONFIGURATION_NOT_PROVIDED = "JOB_CONFIGURATION_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,80 @@
/*
* 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.bootstrap.support;
import java.util.Properties;
import org.springframework.batch.execution.launch.JobLauncher;
import org.springframework.batch.repeat.ExitStatus;
/**
* Interface to expose for remote management of jobs. Similar to
* {@link JobLauncher}, but replaces {@link ExitStatus} with String in return
* types, 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 and get back a representation of the {@link ExitStatus}
* returned by a {@link JobLauncher}. Normally the launch will be
* asynchronous, so the possible values of the return type are constrained
* (it will never be {@link ExitStatus#CONTINUABLE}).
*
* @return a representation of the {@link ExitStatus} returned by a
* {@link JobLauncher}.
*/
String run();
/**
* Launch a job configuration with the given name.
*
* @param name the name of the job to launch
* @return a representation of the {@link ExitStatus} returned by a
* {@link JobLauncher}.
*
* @see #run()
*/
String run(String name);
/**
* Stop all running jobs.
*
* @see JobLauncher#stop()
*/
void stop();
/**
* Enquire if any jobs are still running.
*
* @return true if any jobs are running.
*
* @see JobLauncher#isRunning()
*/
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.bootstrap.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.bootstrap.SystemExiter#exit(int)
*/
public void exit(int status) {
System.exit(status);
}
}

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.bootstrap.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_CONFIGURATION_NOT_PROVIDED,
new Integer(JVM_EXITCODE_JOB_CONFIGURATION_ERROR));
mapping.put(ExitCodeMapper.NO_SUCH_JOB_CONFIGURATION,
new Integer(JVM_EXITCODE_JOB_CONFIGURATION_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.bootstrap.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,80 @@
/*
* 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.bootstrap.support;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.execution.launch.JobExecutionListener;
import org.springframework.batch.execution.launch.JobExecutionListenerSupport;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.util.Assert;
/**
* {@link JobExecutionListener} that will interrupt the Thread that the job was
* started in when the stop signal comes. Use only for a standalone process, not
* in an application server container.
*
* @author Dave Syer
*
*/
public class ThreadInterruptJobExecutionListener extends
JobExecutionListenerSupport {
private volatile Thread processingThread;
private int running = 0;
/**
* Save the current thread so it can be interrupted later. This may seem odd
* at first, however, a simple bootstrap requires that only one thread can
* kick off a container, and that the first thread that calls start is the
* 'processing thread'. If the container has already been started, no
* exception will be thrown.
*
* @see org.springframework.batch.execution.launch.JobExecutionListenerSupport#before(org.springframework.batch.core.domain.JobExecution)
*/
public void before(JobExecution execution) {
Assert.isTrue(running == 0,
"This listener only supports one job at at time.");
running++;
/*
* There is no reason to kick off a new thread, since only one thread
* should be processing at once. However, a handle to the thread is
* maintained to allow for interrupt
*/
processingThread = Thread.currentThread();
}
/**
* Interrupt the thread that is running the job if the {@link ExitStatus}
* indicates that it is still running.
*
* @see org.springframework.batch.execution.launch.JobExecutionListenerSupport#onStop(org.springframework.batch.core.domain.JobExecution)
*/
public void onStop(JobExecution execution) {
if (execution==null || execution.getExitStatus().isRunning()) {
processingThread.interrupt();
}
}
/**
* internal housekeeping.
*
* @see org.springframework.batch.execution.launch.JobExecutionListenerSupport#after(org.springframework.batch.core.domain.JobExecution)
*/
public void after(JobExecution execution) {
running--;
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.bootstrap.support;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* A {@link MethodInterceptor} that can mask a mismatch between the target and
* proxy interfaces by converting the returned value to the correct type.
*
* @author Dave Syer
*
*/
public class TypeConverterMethodInterceptor implements MethodInterceptor {
// Get the default PropertyEditorRegistry free.
private TypeConverter typeConverter = new SimpleTypeConverter();
/**
* Public setter for the {@link TypeConverter} property. Defaults to a
* {@link SimpleTypeConverter}.
*
* @param typeConverter
* the typeConverter to set
*/
public void setTypeConverter(TypeConverter typeConverter) {
this.typeConverter = typeConverter;
}
/**
* Invoke the method with the same name and arguments on the target, but
* possibly with a different return type. If the return type doesn't match
* attempt to convert it.
*
* @return an object that satisfies the signature of the proxy method.
*
* @throws TypeMismatchException
* if the target method returns an object that cannot be
* converted to the desired type.
*
* @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation)
*/
public Object invoke(MethodInvocation invocation) throws Throwable {
// The method called on the proxy
Method invoked = invocation.getMethod();
// The corresponding method on the target if there is one...
Method method = ReflectionUtils.findMethod(invocation.getThis()
.getClass(), invoked.getName(), invoked.getParameterTypes());
// If there was no such method do nothing... TODO: throw Exception?
if (method == null) {
return null;
}
// Invoke the target method
Object result = ReflectionUtils.invokeMethod(method, invocation
.getThis(), invocation.getArguments());
if (result == null) {
return null;
}
// If the return type doesn't match, try and convert it
if (!ClassUtils.isAssignableValue(invoked.getReturnType(), result)) {
result = convert(result, invoked.getReturnType());
}
return result;
}
private Object convert(Object result, Class returnType) {
if (returnType.isAssignableFrom(String.class)) {
return result.toString();
}
return typeConverter.convertIfNecessary(result, returnType);
}
}

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,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.configuration.DuplicateJobConfigurationException;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.JobConfigurationLocator;
import org.springframework.batch.core.configuration.JobConfigurationRegistry;
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 JobConfiguration} beans
* with a {@link JobConfigurationRegistry}. Include a bean of this type along
* with your job configuration, and use the same
* {@link JobConfigurationRegistry} as a {@link JobConfigurationLocator} when
* you need to locate a {@link JobConfigurationLocator} to launch.
*
* @author Dave Syer
*
*/
public class JobConfigurationRegistryBeanPostProcessor implements BeanPostProcessor, InitializingBean, DisposableBean {
// It doesn't make sense for this to have a default value...
private JobConfigurationRegistry jobConfigurationRegistry = null;
private Collection jobConfigurations = new HashSet();
/**
* Injection setter for {@link JobConfigurationRegistry}.
*
* @param jobConfigurationRegistry the jobConfigurationRegistry to set
*/
public void setJobConfigurationRegistry(JobConfigurationRegistry jobConfigurationRegistry) {
this.jobConfigurationRegistry = jobConfigurationRegistry;
}
/**
* 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 JobConfiguration} instances that were
* regsistered by this post processor.
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
for (Iterator iter = jobConfigurations.iterator(); iter.hasNext();) {
JobConfiguration jobConfiguration = (JobConfiguration) iter.next();
jobConfigurationRegistry.unregister(jobConfiguration);
}
jobConfigurations.clear();
}
/**
* If the bean is an instance of {@link JobConfiguration} then register it.
* @throws FatalBeanException if there is a
* {@link DuplicateJobConfigurationException}.
*
* @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 JobConfiguration) {
JobConfiguration jobConfiguration = (JobConfiguration) bean;
try {
jobConfigurationRegistry.register(jobConfiguration);
jobConfigurations.add(jobConfiguration);
}
catch (DuplicateJobConfigurationException 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,97 @@
/*
* 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.configuration.DuplicateJobConfigurationException;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.JobConfigurationRegistry;
import org.springframework.batch.core.configuration.ListableJobConfigurationRegistry;
import org.springframework.batch.core.configuration.NoSuchJobConfigurationException;
import org.springframework.util.Assert;
/**
* Simple map-based implementation of {@link JobConfigurationRegistry}. Access
* to the map is synchronized, guarded by an internal lock.
*
* @author Dave Syer
*
*/
public class MapJobConfigurationRegistry implements ListableJobConfigurationRegistry {
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(JobConfiguration jobConfiguration) throws DuplicateJobConfigurationException {
Assert.notNull(jobConfiguration);
String name = jobConfiguration.getName();
Assert.notNull(name, "Job configuration must have a name.");
synchronized (map) {
if (map.containsKey(name) && jobConfiguration.equals(map.get(name))) {
throw new DuplicateJobConfigurationException("A job configuration with this name [" + name
+ "] was already registered");
}
// allow replacing job configuration with new instance
map.put(name, jobConfiguration);
}
}
/*
* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.JobConfigurationRegistry#unregister(org.springframework.batch.container.common.configuration.JobConfiguration)
*/
public void unregister(JobConfiguration jobConfiguration) {
String name = jobConfiguration.getName();
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 JobConfiguration getJobConfiguration(String name) throws NoSuchJobConfigurationException {
synchronized (map) {
if (!map.containsKey(name)) {
throw new NoSuchJobConfigurationException("No job configuration with the name [" + name
+ "] was registered");
}
return (JobConfiguration) map.get(name);
}
}
/* (non-Javadoc)
* @see org.springframework.batch.container.common.configuration.ListableJobConfigurationRegistry#getJobConfigurations()
*/
public Collection getJobConfigurations() {
synchronized (map) {
return Collections.unmodifiableCollection(new HashSet(map.keySet()));
}
}
}

View File

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

View File

@@ -0,0 +1,213 @@
/*
* 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.sql.Timestamp;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.common.ExceptionClassifier;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.StepConfiguration;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.core.executor.JobExecutor;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.core.executor.StepExecutorFactory;
import org.springframework.batch.core.executor.StepInterruptedException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.step.simple.SimpleExitCodeExceptionClassifier;
import org.springframework.batch.execution.step.simple.SimpleStepExecutorFactory;
import org.springframework.batch.io.exception.BatchCriticalException;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatContext;
/**
* Default implementation of (@link JobExecutor} interface. Sequentially
* executes a job by iterating it's life of steps.
*
* @author Lucas Ward
* @author Dave Syer
*/
public class DefaultJobExecutor implements JobExecutor {
private static final SimpleStepExecutorFactory DEFAULT_STEP_EXECUTOR_FACTORY = new SimpleStepExecutorFactory();
private JobRepository jobRepository;
private StepExecutorFactory stepExecutorFactory = DEFAULT_STEP_EXECUTOR_FACTORY;
private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier();
/**
* Run the specified job by looping through the steps and delegating to the
* {@link StepExecutor}.
*
* @see org.springframework.batch.core.executor.JobExecutor#run(org.springframework.batch.core.configuration.JobConfiguration,
* org.springframework.batch.core.domain.JobExecution)
*/
public ExitStatus run(JobConfiguration configuration, JobExecution execution)
throws BatchCriticalException {
JobInstance job = execution.getJob();
updateStatus(execution, BatchStatus.STARTING);
List steps = job.getSteps();
ExitStatus status = ExitStatus.FAILED;
try {
int startedCount = 0;
List stepConfigurations = configuration
.getStepConfigurations();
for (Iterator i = steps.iterator(), j = stepConfigurations.iterator(); i.hasNext()
&& j.hasNext();) {
StepInstance step = (StepInstance) i.next();
StepConfiguration stepConfiguration = (StepConfiguration) j
.next();
if (shouldStart(step, stepConfiguration)) {
startedCount++;
updateStatus(execution, BatchStatus.STARTED);
StepExecutor stepExecutor = stepExecutorFactory
.getExecutor(stepConfiguration);
StepExecution stepExecution = new StepExecution(step,
execution);
status = stepExecutor.process(stepConfiguration,
stepExecution);
}
}
if (startedCount==0) {
if (stepConfigurations.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.");
}
}
updateStatus(execution, BatchStatus.COMPLETED);
} catch (StepInterruptedException e) {
updateStatus(execution, BatchStatus.STOPPED);
status = exceptionClassifier.classifyForExitCode(e);
rethrow(e);
} catch (Throwable t) {
updateStatus(execution, BatchStatus.FAILED);
status = exceptionClassifier.classifyForExitCode(t);
rethrow(t);
} finally {
execution.setEndTime(new Timestamp(System.currentTimeMillis()));
execution.setExitStatus(status);
jobRepository.saveOrUpdate(execution);
}
return status;
}
private void updateStatus(JobExecution jobExecution, BatchStatus status) {
JobInstance job = jobExecution.getJob();
jobExecution.setStatus(status);
job.setStatus(status);
jobRepository.update(job);
jobRepository.saveOrUpdate(jobExecution);
for (Iterator iter = jobExecution.getStepContexts().iterator(); iter
.hasNext();) {
RepeatContext context = (RepeatContext) iter.next();
context.setAttribute("JOB_STATUS", status);
}
}
/*
* 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(StepInstance step,
StepConfiguration stepConfiguration) {
if (step.getStatus() == BatchStatus.COMPLETED
&& stepConfiguration.isAllowStartIfComplete() == false) {
// step is complete, false should be returned, indicated that the
// step should
// not be started
return false;
}
if (step.getStepExecutionCount() < stepConfiguration.getStartLimit()) {
// step start count is less than start max, return true
return true;
} else {
// start max has been exceeded, throw an exception.
throw new BatchCriticalException(
"Maximum start limit exceeded for step: " + step.getName()
+ "StartMax: " + stepConfiguration.getStartLimit());
}
}
/**
* @param t
*/
private static void rethrow(Throwable t) throws RuntimeException {
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new BatchCriticalException(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;
DEFAULT_STEP_EXECUTOR_FACTORY.setJobRepository(jobRepository);
}
/**
* Setter for injecting a {@link StepExecutorFactory}. The factory is
* responsible for providing a {@link StepExecutor} to execute each step in
* turn. The values returned from the factory are not cached or re-used by
* this implementation.
*
* @param stepExecutorFactory
*/
public void setStepExecutorFactory(StepExecutorFactory stepExecutorFactory) {
this.stepExecutorFactory = stepExecutorFactory;
}
/**
* Public setter for injecting an {@link ExceptionClassifier} that can
* translate exceptions to {@link ExitStatus}.
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(
ExitCodeExceptionClassifier exceptionClassifier) {
this.exceptionClassifier = exceptionClassifier;
}
}

View File

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

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.launch;
import org.springframework.batch.core.executor.JobExecutionException;
/**
* @author Dave Syer
*
*/
public class JobExecutionAlreadyRunningException extends JobExecutionException {
/**
* @param msg
*/
public JobExecutionAlreadyRunningException(String msg) {
super(msg);
}
/**
* @param msg
* @param cause
*/
public JobExecutionAlreadyRunningException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.launch;
import org.springframework.batch.core.domain.JobExecution;
/**
* Listener interface for the job execution lifecycle.
*
* @author Dave Syer
*
*/
public interface JobExecutionListener {
/**
* Callback for the start of a job, before any steps are processed.
*
* @param execution
* the current {@link JobExecution}
*/
void before(JobExecution execution);
/**
* Callback for the start of a job, after all steps are processed, or on an
* error.
*
* @param execution
*/
void after(JobExecution execution);
/**
* Callback for a job that has been stopped, or asked to stop.
*
* @param execution
*/
void onStop(JobExecution execution);
}

View File

@@ -0,0 +1,58 @@
/*
* 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.domain.JobExecution;
/**
* Simple no-op implementation of {@link JobExecutionListener} which does
* nothing.
*
* @author Dave Syer
*
*/
public class JobExecutionListenerSupport implements JobExecutionListener {
/**
* No-op for subclasses to extend.
*
* @see org.springframework.batch.execution.launch.JobExecutionListener#after(org.springframework.batch.core.domain.JobExecution)
*/
public void after(JobExecution execution) {
// no-op
}
/**
* No-op for subclasses to extend.
*
* @see org.springframework.batch.execution.launch.JobExecutionListener#before(org.springframework.batch.core.domain.JobExecution)
*/
public void before(JobExecution execution) {
// no-op
}
/**
* No-op for subclasses to extend.
*
* @see org.springframework.batch.execution.launch.JobExecutionListener#onStop(org.springframework.batch.core.domain.JobExecution)
*/
public void onStop(JobExecution execution) {
// no-op
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.configuration.NoSuchJobConfigurationException;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
/**
* Interface which defines a facade for running jobs. The interface is
* intentionally minimal and package private. It is convenient to be able to
* test a {@link JobLauncher} with stub implementations of this interface.
*
* @author Lucas Ward
* @author Dave Syer
*/
interface JobExecutorFacade {
/**
* Prepare a job execution identifiable by the {@link JobIdentifier}. THis
* can then be used to run the job with the {@link #start(JobExecution)}
* method. Implementations normally require a job configuration to be
* locatable corresponding to the {@link JobIdentifier}, matching them at
* least by name.
*
* @param jobIdentifier
* the identifier of the job to start
*
* @throws NoSuchJobConfigurationException
* @throws JobExecutionAlreadyRunningException
*/
JobExecution createExecutionFrom(JobIdentifier jobIdentifier)
throws NoSuchJobConfigurationException, JobExecutionAlreadyRunningException;
/**
* Start a job execution.
*
* @param execution
* the execution of the job to start
* @throws NoSuchJobConfigurationException
*/
void start(JobExecution execution) throws NoSuchJobConfigurationException;
/**
* Stop the job execution that was started with this runtime information.
*
* @param jobIdentifier
* the {@link JobIdentifier}.
* @throws NoSuchJobExecutionException
* if a job with this runtime information is not running
*/
void stop(JobExecution execution) throws NoSuchJobExecutionException;
/**
* Simple check for whether or not there are jobs in progress. Can be used
* by clients to wait for all jobs to finish. Finer grained monitoring and
* reporting can be implemented using the persistent execution details
* (normally in a database), provided they are maintained by the
* implementation.
*
* @return true if any jobs are active.
*/
boolean isRunning();
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.launch;
import org.springframework.batch.core.configuration.NoSuchJobConfigurationException;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
/**
* Simple interface for controlling jobs, including possible ad-hoc executions,
* based on different runtime identifiers.
*
* @author Lucas Ward
* @author Dave Syer
*/
public interface JobLauncher {
/**
* Start a job execution with default name and other runtime information
* generated on the fly.<br/>
*
* @return the exit code from the job if it returns synchronously. If the
* implementation is asynchronous, the status might well be unknown.
* @throws JobExecutionAlreadyRunningException
*
*/
public JobExecution run() throws NoSuchJobConfigurationException, JobExecutionAlreadyRunningException;
/**
* Start a job execution with the given name and other runtime information
* generated on the fly. The name is used to locate a job configuration, and
* the other runtime information is used to identify the job instance.
*
* @param name
* the name to assign to the job configuration
* @return the exit code from the job if it returns synchronously. If the
* implementation is asynchronous, the status might well be unknown.
*
* @throws NoSuchJobConfigurationException
* @throws JobExecutionAlreadyRunningException
*/
public JobExecution run(String jobName)
throws NoSuchJobConfigurationException, JobExecutionAlreadyRunningException;
/**
* Start a job execution with the given runtime information.
*
* @return the exit code from the job if it returns synchronously. If the
* implementation is asynchronous, the status might well be unknown.
*
* @throws NoSuchJobConfigurationException
*/
public JobExecution run(JobIdentifier jobIdentifier)
throws NoSuchJobConfigurationException, JobExecutionAlreadyRunningException;
/**
* Stop the current job executions if there are any. If not, no action will
* be taken.
*
* @see org.springframework.context.Lifecycle#stop()
*/
public void stop();
/**
* Check whether or not any job execution is currently running.
*
* @return true if this launcher started a job or jobs and one can be
* determined to be in an active state.
*/
public boolean isRunning();
}

View File

@@ -0,0 +1,32 @@
/*
* 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;
/**
* @author Dave Syer
*
*/
public class NoSuchJobExecutionException extends Exception {
/**
* @param message
*/
public NoSuchJobExecutionException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,328 @@
/*
* 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 java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.JobConfigurationLocator;
import org.springframework.batch.core.configuration.NoSuchJobConfigurationException;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.executor.JobExecutor;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.job.DefaultJobExecutor;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* <p>
* Simple implementation of (@link {@link JobExecutorFacade}).
*
* <p>
* A {@link JobIdentifier} will be used to uniquely identify the job by the
* repository. Once the job is obtained, the {@link JobExecutor} will be used to
* run the job.
* </p>
*
* <p>
* Listeners can be registered for callbacks at the start and end of a job.
* </p>
*
* @author Lucas Ward
* @author Dave Syer
*
*/
class SimpleJobExecutorFacade implements JobExecutorFacade,
JobExecutionListener, StatisticsProvider, InitializingBean {
private Map jobExecutionRegistry = new HashMap();
private JobExecutor jobExecutor = new DefaultJobExecutor();
private JobRepository jobRepository;
// there is no sensible default for this
private JobConfigurationLocator jobConfigurationLocator;
private List listeners = new ArrayList();
private int running = 0;
private Object mutex = new Object();
/**
* Public setter for the listeners property.
*
* @param listeners
* the listeners to set - a list of {@link JobExecutionListener}.
*/
public void setJobExecutionListeners(List listeners) {
this.listeners = listeners;
}
/**
* Check mandatory properties (jobConfigurationLocator, jobRepository).
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jobRepository, "JobRepository must be provided.");
Assert.notNull(jobConfigurationLocator,
"JobConfigurationLocator must be provided.");
}
/**
* Public accessor for the running property.
*
* @return the running
*/
public boolean isRunning() {
synchronized (mutex) {
return running > 0;
}
}
/**
* Setter for injection of {@link JobConfigurationLocator}.
*
* @param jobConfigurationLocator
* the jobConfigurationLocator to set
*/
public void setJobConfigurationLocator(
JobConfigurationLocator jobConfigurationLocator) {
this.jobConfigurationLocator = jobConfigurationLocator;
}
/**
* Setter for {@link JobExecutor}.
*
* @param jobExecutor
*/
public void setJobExecutor(JobExecutor jobExecutor) {
this.jobExecutor = jobExecutor;
}
/**
* Setter for {@link JobRepository}.
*
* @param jobRepository
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Locates a {@link JobConfiguration} by using the name of the provided
* {@link JobIdentifier} and the {@link JobConfigurationLocator}.
*
* @param jobIdentifier
* the identifier of the job that is being prepared.
*
* @throws IllegalArgumentException
* if the {@link JobIdentifier} is null or its name is null
* @throws NoSuchJobConfigurationException
* if the {@link JobConfigurationLocator} does not contain a
* {@link JobConfiguration} with the name provided.
*
* @see org.springframework.batch.execution.launch.JobExecutorFacade#createExecutionFrom(org.springframework.batch.core.domain.JobIdentifier)
*/
public JobExecution createExecutionFrom(JobIdentifier jobIdentifier)
throws NoSuchJobConfigurationException, JobExecutionAlreadyRunningException {
Assert.notNull(jobIdentifier, "JobIdentifier must not be null.");
Assert.notNull(jobIdentifier.getName(),
"JobIdentifier name must not be null.");
if (jobExecutionRegistry.containsKey(jobIdentifier)) {
throw new JobExecutionAlreadyRunningException(
"A job with this JobIdentifier is already executing in this container: "+jobIdentifier);
};
JobConfiguration jobConfiguration = jobConfigurationLocator
.getJobConfiguration(jobIdentifier.getName());
JobInstance job = jobRepository.findOrCreateJob(jobConfiguration,
jobIdentifier);
JobExecution execution = job.createNewJobExecution();
// Save the JobExecution so that it picks up an ID (useful for clients
// monitoring asynchronous executions):
jobRepository.saveOrUpdate(execution);
return execution;
}
/**
* Starts a job execution that was previously acquired from the
* {@link #createExecutionFrom(JobIdentifier)} method.
*
* @see org.springframework.batch.execution.launch.JobExecutorFacade#start(JobExecution)
*
* @throws NoSuchJobConfigurationException
* if the {@link JobConfigurationLocator} does not contain a
* {@link JobConfiguration} with the name provided by the
* enclosed {@link JobIdentifier}.
*
*/
public void start(JobExecution execution)
throws NoSuchJobConfigurationException {
JobConfiguration jobConfiguration = jobConfigurationLocator
.getJobConfiguration(execution.getJob().getIdentifier()
.getName());
this.before(execution);
try {
jobExecutor.run(jobConfiguration, execution);
} finally {
this.after(execution);
}
}
/**
* Internal accounting for the job execution. Callback at start of job,
* dealing with internal housekeeping before delegating to listeners in the
* order that they were given.
*
* @param execution
*
* @see JobExecutionListener#before(JobExecution)
*/
public void before(JobExecution execution) {
synchronized (mutex) {
running++;
jobExecutionRegistry.put(execution.getJob().getIdentifier(),
execution);
}
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
JobExecutionListener listener = (JobExecutionListener) iterator
.next();
listener.before(execution);
}
}
/**
* Broadcast stop signal to all the registered listeners.
*
* @param execution
*
* @see JobExecutionListener#onStop(JobExecution)
*/
public void onStop(JobExecution execution) {
for (Iterator iterator = listeners.iterator(); iterator.hasNext();) {
JobExecutionListener listener = (JobExecutionListener) iterator
.next();
listener.onStop(execution);
}
}
/**
* Internal accounting for the job execution. Callback at end of job
* delegating first to listeners, in reverse order to the list supplied, and
* then finally dealing with internal housekeeping.
*
* @param execution
*
* @see JobExecutionListener#after(JobExecution)
*/
public void after(JobExecution execution) {
ArrayList reversed = new ArrayList(listeners);
Collections.reverse(reversed);
for (Iterator iterator = reversed.iterator(); iterator.hasNext();) {
JobExecutionListener listener = (JobExecutionListener) iterator
.next();
listener.after(execution);
}
synchronized (mutex) {
// assume execution is synchronous so when we get to here we are
// not running any more
jobExecutionRegistry.remove(execution.getJob().getIdentifier());
running--;
}
}
/**
* Send a stop signal to the running execution by setting all their
* {@link RepeatContext} to terminate only. Then call the
* {@link JobExecutionListener#onStop(JobExecution)} method.
*
* @see org.springframework.batch.container.BatchContainer#onStop(org.springframework.batch.container.common.runtime.JobRuntimeInformation)
*/
public void stop(JobExecution execution) throws NoSuchJobExecutionException {
if (!jobExecutionRegistry.containsValue(execution)) {
throw new NoSuchJobExecutionException(
"The job is not executing in this executor: [" + execution
+ "]");
}
for (Iterator iter = execution.getStepContexts().iterator(); iter
.hasNext();) {
RepeatContext context = (RepeatContext) iter.next();
context.setTerminateOnly();
}
for (Iterator iter = execution.getChunkContexts().iterator(); iter
.hasNext();) {
RepeatContext context = (RepeatContext) iter.next();
context.setTerminateOnly();
}
this.onStop(execution);
}
/**
* Provides a snapshot of properties from running jobs (the ones that were
* launched from this {@link JobExecutorFacade).
*
* @return a read-only view of the state of the running jobs.
*/
public Properties getStatistics() {
int i = 0;
Properties props = new Properties();
for (Iterator iter = jobExecutionRegistry.values().iterator(); iter
.hasNext();) {
JobExecution element = (JobExecution) iter.next();
i++;
String runtime = "job" + i;
props.setProperty(runtime, "" + element.getJob().getIdentifier());
int j = 0;
for (Iterator iterator = element.getStepContexts().iterator(); iterator
.hasNext();) {
RepeatContext context = (RepeatContext) iterator.next();
j++;
props.setProperty(runtime + ".step" + j, "" + context);
}
j = 0;
for (Iterator iterator = element.getChunkContexts().iterator(); iterator
.hasNext();) {
RepeatContext context = (RepeatContext) iterator.next();
j++;
props.setProperty(runtime + ".chunk" + j, "" + context);
}
}
return props;
}
}

View File

@@ -0,0 +1,581 @@
/*
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.JobConfigurationLocator;
import org.springframework.batch.core.configuration.NoSuchJobConfigurationException;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.executor.JobExecutor;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.runtime.JobIdentifierFactory;
import org.springframework.batch.execution.job.DefaultJobExecutor;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifierFactory;
import org.springframework.batch.io.exception.BatchConfigurationException;
import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.Assert;
/**
* Generic {@link JobLauncher} allowing choice of strategy for concurrent
* execution and .
*
* @see JobLauncher
* @author Dave Syer
*/
public class SimpleJobLauncher implements JobLauncher, InitializingBean,
ApplicationListener, ApplicationEventPublisherAware, StatisticsProvider {
protected static final Log logger = LogFactory
.getLog(SimpleJobLauncher.class);
private JobExecutor jobExecutor = new DefaultJobExecutor();
// there is no sensible default for this
private JobRepository jobRepository;
// there is no sensible default for this
private JobConfigurationLocator jobConfigurationLocator;
// this can be defaulted from some other properties (see
// afterPropertiesSet())
private JobExecutorFacade jobExecutorFacade;
private TaskExecutor taskExecutor = new SyncTaskExecutor();
private List listeners = new ArrayList();
private String jobConfigurationName;
// Do not autostart by default - allow user to set job configuration
// later and then manually start:
private volatile boolean autoStart = false;
private JobIdentifierFactory jobIdentifierFactory = new ScheduledJobIdentifierFactory();
private final Object monitor = new Object();
// A private registry for keeping track of running jobs.
private volatile Map registry = new HashMap();
private ApplicationEventPublisher applicationEventPublisher;
/**
* Setter for {@link JobIdentifierFactory}.
*
* @param jobIdentifierFactory
* the {@link JobIdentifierFactory} to set
*/
public void setJobIdentifierFactory(
JobIdentifierFactory jobIdentifierFactory) {
this.jobIdentifierFactory = jobIdentifierFactory;
}
/**
* Setter for the {@link JobConfiguration} that this launcher will run.
*
* @param jobConfiguration
* the jobConfiguration to set
*/
public void setJobConfigurationName(String jobConfiguration) {
this.jobConfigurationName = jobConfiguration;
}
/**
* Setter for autostart flag. If this is true then the container will be
* started when the Spring context is refreshed. Defaults to false.
*
* @param autoStart
*/
public void setAutoStart(boolean autoStart) {
this.autoStart = autoStart;
}
/**
* Public setter for the listeners property.
*
* @param listeners
* the listeners to set - a list of {@link JobExecutionListener}.
*/
public void setJobExecutionListeners(List listeners) {
this.listeners = listeners;
}
/**
* Setter for injection of {@link JobConfigurationLocator}. Mandatory with
* no default.
*
* @param jobConfigurationLocator
* the jobConfigurationLocator to set
*/
public void setJobConfigurationLocator(
JobConfigurationLocator jobConfigurationLocator) {
this.jobConfigurationLocator = jobConfigurationLocator;
}
/**
* Setter for {@link JobExecutor}. Defaults to a {@link DefaultJobExecutor}.
*
* @param jobExecutor
*/
public void setJobExecutor(JobExecutor jobExecutor) {
this.jobExecutor = jobExecutor;
}
/**
* Setter for {@link JobRepository}. Mandatory with no default.
*
* @param jobRepository
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Setter for {@link JobExecutorFacade}. Package private because it is only
* used for testing purposes.
*/
void setJobExecutorFacade(JobExecutorFacade jobExecutorFacade) {
this.jobExecutorFacade = jobExecutorFacade;
}
/**
* Check that mandatory properties are set and create a {@link JobExecutor}
* if one wasn't provided.
*
* @see #setJobExecutorFacade(JobExecutorFacade)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
if (jobExecutorFacade == null) {
logger.debug("Using SimpleJobExecutorFacade");
Assert.notNull(jobConfigurationLocator);
Assert.notNull(jobExecutor);
Assert.notNull(jobRepository);
SimpleJobExecutorFacade jobExecutorFacade = new SimpleJobExecutorFacade();
jobExecutorFacade
.setJobConfigurationLocator(jobConfigurationLocator);
jobExecutorFacade.setJobExecutionListeners(listeners);
jobExecutorFacade.setJobExecutor(jobExecutor);
jobExecutorFacade.setJobRepository(jobRepository);
this.jobExecutorFacade = jobExecutorFacade;
}
}
/**
* If autostart flag is on, initialise on context start-up and call
* {@link #run()}.
*
* @throws BatchConfigurationException
* if the job tries to but cannot start because of a
* {@link NoSuchJobConfigurationException}.
*
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
*
*/
public void onApplicationEvent(ApplicationEvent event) {
if ((event instanceof ContextRefreshedEvent) && this.autoStart
&& !isRunning()) {
try {
run();
} catch (NoSuchJobConfigurationException e) {
throw new BatchConfigurationException(
"Cannot start job on context refresh because it does not exist",
e);
} catch (JobExecutionAlreadyRunningException e) {
throw new BatchConfigurationException(
"Cannot start job on context refresh because it is already running",
e);
}
}
}
/**
* This method is wrapped in a Runnable by {@link #run(JobIdentifier)}, so
* that the internal housekeeping is done consistently. Subclasses should be
* careful to do the same.
*
* @param jobIdentifier
* @return
* @throws NoSuchJobConfigurationException
*/
protected final void runInternal(JobExecution execution)
throws NoSuchJobConfigurationException {
JobIdentifier jobIdentifier = execution.getJob().getIdentifier();
if (getJobExecution(jobIdentifier)==null) {
logger.info("Job already stopped (not launching): "+jobIdentifier);
return;
}
try {
logger.info("Launching: "+jobIdentifier);
jobExecutorFacade.start(execution);
logger.info("Completed successfully: "+jobIdentifier);
} finally {
unregister(jobIdentifier);
}
}
/**
* Start the job using the task executor provided.
*
* @throws NoSuchJobConfigurationException
* if the identifier cannot be used to locate a
* {@link JobConfiguration}.
*
* @see org.springframework.batch.execution.launch.SimpleJobLauncher#run(org.springframework.batch.core.domain.JobIdentifier)
*/
public JobExecution run(final JobIdentifier jobIdentifier)
throws NoSuchJobConfigurationException,
JobExecutionAlreadyRunningException {
if (getJobExecution(jobIdentifier) != null) {
throw new JobExecutionAlreadyRunningException(
"A job is already executing with this identifier: ["
+ jobIdentifier + "]");
}
final JobExecution execution = jobExecutorFacade
.createExecutionFrom(jobIdentifier);
// TODO: throw JobExecutionAlreadyRunningException if it is in a running
// state (someone else launched it)
final JobExecutionHolder holder = register(execution);
taskExecutor.execute(new Runnable() {
public void run() {
try {
synchronized (monitor) {
if (isInternalRunning(jobIdentifier)) {
logger.info("This job is already running, so not re-launched: "+jobIdentifier);
return;
}
}
holder.start();
runInternal(execution);
} catch (NoSuchJobConfigurationException e) {
applicationEventPublisher
.publishEvent(new RepeatOperationsApplicationEvent(
jobIdentifier, "No such job",
RepeatOperationsApplicationEvent.ERROR));
logger.error(
"JobConfiguration could not be located inside Runnable for identifier: ["
+ jobIdentifier + "]", e);
} finally {
holder.stop();
}
}
});
return execution;
}
/**
* Start a job execution with the given name. If a job is already running
* has no effect.
*
* @param name
* the name to assign to the job
* @throws NoSuchJobConfigurationException
* @throws JobExecutionAlreadyRunningException
*/
public JobExecution run(String name)
throws NoSuchJobConfigurationException,
JobExecutionAlreadyRunningException {
if (name == null) {
throw new NoSuchJobConfigurationException(
"Null job name cannot be located.");
}
JobIdentifier runtimeInformation = jobIdentifierFactory
.getJobIdentifier(name);
return this.run(runtimeInformation);
}
/**
* Start a job execution with default name and other runtime information
* provided by the factory. If a job is already running has no effect. The
* default name is taken from the enclosed {@link JobConfiguration}.
*
* @throws NoSuchJobConfigurationException
*
* @throws NoSuchJobConfigurationException
* if the job configuration cannot be located
* @throws JobExecutionAlreadyRunningException
*
* @see #setJobIdentifierFactory(JobIdentifierFactory)
* @see org.springframework.context.Lifecycle#start()
*/
public JobExecution run() throws NoSuchJobConfigurationException,
JobExecutionAlreadyRunningException {
if (jobConfigurationName != null) {
return this.run(jobConfigurationName);
}
throw new NoSuchJobConfigurationException(
"Null default job name cannot be located.");
}
/**
* Extension point for subclasses to stop a specific job.
*
* @throws NoSuchJobExecutionException
*/
protected void doStop(JobIdentifier jobIdentifier)
throws NoSuchJobExecutionException {
JobExecution execution = getJobExecution(jobIdentifier);
logger.info("Stopping job: "+jobIdentifier);
if (execution != null) {
jobExecutorFacade.stop(execution);
}
unregister(jobIdentifier);
}
/**
* Stop all jobs if any are running. If not, no action will be taken.
* Delegates to the {@link #doStop()} method.
*
* @throws NoSuchJobExecutionException
* @see org.springframework.context.Lifecycle#stop()
* @see org.springframework.batch.execution.launch.JobLauncher#stop()
*/
final public void stop() {
for (Iterator iter = new HashSet(registry.keySet()).iterator(); iter
.hasNext();) {
JobIdentifier context = (JobIdentifier) iter.next();
try {
stop(context);
} catch (NoSuchJobExecutionException e) {
logger.error(e);
}
}
}
/**
* Stop a job with this {@link JobIdentifier}. Delegates to the
* {@link #doStop(JobIdentifier)} method.
*
* @throws NoSuchJobExecutionException
*
* @see org.springframework.batch.execution.launch.JobLauncher#stop(org.springframework.batch.core.domain.JobIdentifier)
* @see BatchContainer#stop(JobRuntimeInformation))
*/
final public void stop(JobIdentifier runtimeInformation)
throws NoSuchJobExecutionException {
synchronized (monitor) {
doStop(runtimeInformation);
}
}
/**
* Stop all jobs with {@link JobIdentifier} having this name. Delegates to
* the {@link #stop(JobIdentifier)}.
*
* @throws NoSuchJobExecutionException
*
* @see org.springframework.batch.execution.launch.JobLauncher#stop(java.lang.String)
*/
final public void stop(String name) throws NoSuchJobExecutionException {
this.stop(jobIdentifierFactory.getJobIdentifier(name));
}
/**
* Check each registered {@link JobIdentifier} to see if it is running (@see
* {@link #isRunning(JobIdentifier)}), and if any are, then return true.
*
* @see org.springframework.batch.container.bootstrap.BatchContainerLauncher#isRunning()
*/
final public boolean isRunning() {
Collection jobs = new HashSet(registry.keySet());
for (Iterator iter = jobs.iterator(); iter.hasNext();) {
JobIdentifier jobIdentifier = (JobIdentifier) iter.next();
if (isInternalRunning(jobIdentifier)) {
return true;
}
}
return !jobs.isEmpty();
}
private boolean isInternalRunning(JobIdentifier jobIdentifier) {
synchronized (registry) {
JobExecutionHolder jobExecutionHolder = getJobExecutionHolder(jobIdentifier);
return isRunning(jobIdentifier)
&& jobExecutionHolder!=null && jobExecutionHolder.isRunning();
}
}
/**
* Extension point for subclasses to check an individual
* {@link JobIdentifier} to see if it is running. As long as at least one
* job is running the launcher is deemed to be running.
*
* @param jobIdentifier
* a {@link JobIdentifier}
* @return always true. Subclasses can override and provide more accurate
* information.
*/
protected boolean isRunning(JobIdentifier jobIdentifier) {
return true;
}
/**
* Convenient synchronized accessor for the registry.
*
* @param jobIdentifier
* @return TODO
*/
private JobExecutionHolder register(JobExecution execution) {
JobExecutionHolder jobExecutionHolder = new JobExecutionHolder(execution);
synchronized (registry) {
registry.put(execution.getJob().getIdentifier(), jobExecutionHolder);
}
return jobExecutionHolder;
}
/**
* Convenient synchronized accessor for the registry.
*
* @param jobIdentifier
*/
private JobExecution getJobExecution(JobIdentifier jobIdentifier) {
synchronized (registry) {
if (registry.containsKey(jobIdentifier)) {
return ((JobExecutionHolder) registry.get(jobIdentifier)).getExecution();
}
}
return null;
}
/**
* Convenient synchronized accessor for the registry.
*
* @param jobIdentifier
*/
private JobExecutionHolder getJobExecutionHolder(JobIdentifier jobIdentifier) {
synchronized (registry) {
if (registry.containsKey(jobIdentifier)) {
return (JobExecutionHolder) registry.get(jobIdentifier);
}
}
return null;
}
/**
* Convenient synchronized accessor for the registry. Must be used by
* subclasses to release the {@link JobIdentifier} when a job is finished
* (or stopped).
*
* @param jobIdentifier
*/
private void unregister(JobIdentifier jobIdentifier) {
synchronized (registry) {
registry.remove(jobIdentifier);
}
}
/**
* Setter for the {@link TaskExecutor}. Defaults to a
* {@link SyncTaskExecutor}.
*
* @param taskExecutor
* the taskExecutor to set
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
/**
* Accessor for the job executions currently in progress (and having been
* started from this launcher). If you launch a job synchronously then it
* will have finished when the {@link #run()} method returns, so there will
* be no statistics. Because the request is potentially fulfilled
* asynchronously, and only on demand, the data might be out of date by the
* time this method is called, so it should be used for information purposes
* only.
*
* @return Properties representing the {@link JobExecution} objects passed
* up from the underlying execution. If there are no jobs running it
* will be empty.
*/
public Properties getStatistics() {
if (jobExecutorFacade instanceof StatisticsProvider) {
return ((StatisticsProvider) jobExecutorFacade).getStatistics();
} else {
return new Properties();
}
}
public void setApplicationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
private class JobExecutionHolder {
private static final int NEW = 0;
private static final int STARTED = 1;
private static final int STOPPED = 2;
private JobExecution execution;
private int status = NEW;
public JobExecutionHolder(JobExecution execution) {
this.execution = execution;
}
JobExecution getExecution() {
return execution;
}
boolean isRunning() {
return status==STARTED;
}
void start() {
status = STARTED;
}
void stop() {
status = STOPPED;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,262 @@
/*
* 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 java.util.Properties;
import org.springframework.batch.core.configuration.JobConfiguration;
import org.springframework.batch.core.configuration.StepConfiguration;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.core.repository.BatchRestartException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.repository.dao.JobDao;
import org.springframework.batch.execution.repository.dao.StepDao;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.util.Assert;
/**
*
* <p>
* Simple Job Repository that stores Jobs, JobExecutions, Steps, and
* StepExecutions using the provided JobDao and StepDao.
* <p>
*
* @author Lucas Ward
* @author Dave Syer
* @see JobRepository
* @see StepDao
* @see JobDao
*
*/
public class SimpleJobRepository implements JobRepository {
private JobDao jobDao;
private StepDao stepDao;
public SimpleJobRepository(JobDao jobDao, StepDao stepDao) {
super();
this.jobDao = jobDao;
this.stepDao = stepDao;
}
/**
* <p>
* Find or Create a JobInstance(@link JobInstance) based on the passed in
* RuntimeInformation and Configuration. JobRuntimeInformation contains the
* following fields which logically identify a job: JobName, JobStream,
* JobRun, and Schedule Date. However, unique identification of a job can
* only come from the database, and therefore must come from JobDao by
* either creating a new job or finding an existing one, which will ensure
* that the id field of the job 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 Job's restartPolicy will be checked first. If it is
* not restartable, a new job will be created, regardless of whether or not
* one exists. If it is restartable, the JobDao 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 it will be returned. If no job is found, a
* new one will be created based on the configuration.
* </p>
*
* @see JobRepository#findOrCreateJob(JobConfiguration, JobIdentifier)
* @throws BatchRestartException if more than one JobInstance if found
* or if JobInstance.getJobExecutionCount() is greater than JobConfiguration.getStartLimit()
*/
public JobInstance findOrCreateJob(JobConfiguration jobConfiguration, JobIdentifier runtimeInformation) {
List jobs;
// Check if a job is restartable, if not, create and return a new job
if (jobConfiguration.isRestartable() == false) {
return createJob(jobConfiguration, runtimeInformation);
}
else {
// find all jobs matching the runtime information.
jobs = jobDao.findJobs(runtimeInformation);
}
if (jobs.size() == 1) {
// One job was found
JobInstance job = (JobInstance) jobs.get(0);
job.setSteps(findSteps(jobConfiguration.getStepConfigurations(), job));
job.setJobExecutionCount(jobDao.getJobExecutionCount(job.getId()));
if (job.getJobExecutionCount() > jobConfiguration.getStartLimit()) {
throw new BatchRestartException("Restart Max exceeded for Job: " + job.toString());
}
return job;
}
else if (jobs.size() == 0) {
// no job found, create one
return createJob(jobConfiguration, runtimeInformation);
}
else {
// More than one job found, throw exception
throw new BatchRestartException("Error restarting job, more than one JobInstance found for: "
+ jobConfiguration.toString());
}
}
/**
* 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
jobDao.save(jobExecution);
}
else {
// new execution
jobDao.update(jobExecution);
}
}
/**
* Update an existing job. A job must have been obtained from the
* findOrCreateJob method, otherwise it is likely that the id is incorrect
* or non-existant.
*
* @param job to be updated.
* @throws IllegalArgumentException if Job or it's Id is null.
*/
public void update(JobInstance job) {
Assert.notNull(job, "Job cannot be null.");
Assert.notNull(job.getId(), "Job cannot be updated if it's ID is null. It must be obtained"
+ "from SimpleJobRepository.findOrCreateJob to be considered valid.");
jobDao.update(job);
}
/**
* 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.getStepId(), "StepExecution's Step Id cannot be null.");
if (stepExecution.getId() == null) {
// new execution, obtain id and insert
stepDao.save(stepExecution);
}
else {
// existing execution, update
stepDao.update(stepExecution);
}
}
/**
* Update the given step.
*
* @param StepInstance to be updated.
* @throws IllegalArgumentException if step or it's id is null.
*/
public void update(StepInstance step) {
Assert.notNull(step, "Step cannot be null.");
Assert.notNull(step.getId(), "Step cannot be updated if it's ID is null. It must be obtained"
+ "from SimpleJobRepository.findOrCreateJob to be considered valid.");
stepDao.update(step);
}
/*
* Convenience method for creating a new job. A new job is created by
* calling {@link JobDao#createJob(JobRuntimeInformation)} and then it's
* list of StepConfigurations is passed to the createSteps method.
*/
private JobInstance createJob(JobConfiguration jobConfiguration, JobIdentifier runtimeInformation) {
JobInstance job = jobDao.createJob(runtimeInformation);
job.setSteps(createSteps(job, jobConfiguration.getStepConfigurations()));
return job;
}
/*
* Create steps based on the given Job and list of StepConfigurations.
*/
private List createSteps(JobInstance job, List stepConfigurations) {
List steps = new ArrayList();
Iterator i = stepConfigurations.iterator();
while (i.hasNext()) {
StepConfiguration stepConfiguration = (StepConfiguration) i.next();
StepInstance step = stepDao.createStep(job, stepConfiguration.getName());
//Ensure valid restart data is being returned.
if(step.getRestartData() == null || step.getRestartData().getProperties() == null){
step.setRestartData(new GenericRestartData(new Properties()));
}
steps.add(step);
}
return steps;
}
/*
* Find Steps for the given list of StepConfiguration's with a given JobId
*/
protected List findSteps(List stepConfigurations, JobInstance job) {
List steps = new ArrayList();
Iterator i = stepConfigurations.iterator();
while (i.hasNext()) {
StepConfiguration stepConfiguration = (StepConfiguration) i.next();
StepInstance step = stepDao.findStep(job, stepConfiguration.getName());
if (step != null) {
step.setStepExecutionCount(stepDao.getStepExecutionCount(step.getId()));
//Ensure valid restart data is being returned.
if(step.getRestartData() == null || step.getRestartData().getProperties() == null){
step.setRestartData(new GenericRestartData(new Properties()));
}
steps.add(step);
}
}
return steps;
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.List;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
/**
* Data Access Object for jobs.
*
* @author Lucas Ward
*
*/
public interface JobDao {
/**
* Create a job using the provided JobIdentifier as the natural key.
*
* PostConditions: A valid job will be returned which contains an unique Id.
*
* @param jobIdentifier
* @return Job
*/
public JobInstance createJob(JobIdentifier jobIdentifier);
/**
* Find all jobs that match the given JobIdentifier. If no jobs matching the
* Identifier are found, then a list of size 0 will be returned.
*
* @param jobIdentifier
* @return List of {@link JobInstance} objects matching {@link JobIdentifier}
*/
public List findJobs(JobIdentifier jobIdentifier);
/**
* Update an existing Job.
*
* Preconditions: Job must have an ID.
*
* @param job
*/
public void update(JobInstance job);
/**
* Save a new JobExecution.
*
* Preconditions: JobExecution must have a JobId.
*
* @param jobExecution
*/
public void save(JobExecution jobExecution);
/**
* Update and existing JobExecution.
*
* Preconditions: JobExecution must have an Id (which can be obtained by the
* save method) and a JobId.
*
* @param jobExecution
*/
public void update(JobExecution jobExecution);
/**
* Return the number of JobExecutions with the given Job Id
*
* Preconditions: Job must have an id.
*
* @param job
*/
public int getJobExecutionCount(Long jobId);
/**
* Return list of JobExecutions for given job.
*
* @param job
* @return list of jobExecutions.
*/
public List findJobExecutions(JobInstance job);
}

View File

@@ -0,0 +1,98 @@
/*
* 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.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
public class MapJobDao implements JobDao {
private static Map jobsById;
private static Map executionsById;
private long currentId = 0;
static {
jobsById = TransactionAwareProxyFactory.createTransactionalMap();
executionsById = TransactionAwareProxyFactory.createTransactionalMap();
}
public static void clear() {
jobsById.clear();
executionsById.clear();
}
public JobInstance createJob(JobIdentifier jobIdentifier) {
JobInstance job = new JobInstance(jobIdentifier, new Long(currentId++));
jobsById.put(job.getId(), job);
return job;
}
public List findJobs(JobIdentifier jobRuntimeInformation) {
List list = new ArrayList();
for (Iterator iter = jobsById.values().iterator(); iter.hasNext();) {
JobInstance job = (JobInstance) iter.next();
if (job.getName().equals(jobRuntimeInformation.getName())) {
list.add(job);
}
}
return list;
}
public int getJobExecutionCount(Long jobId) {
Set executions = (Set) executionsById.get(jobId);
if (executions==null) return 0;
return executions.size(); }
public void save(JobExecution jobExecution) {
Set executions = (Set) executionsById.get(jobExecution.getJobId());
if (executions==null) {
executions = TransactionAwareProxyFactory.createTransactionalSet();
executionsById.put(jobExecution.getJobId(), executions);
}
executions.add(jobExecution);
jobExecution.setId(new Long(currentId++));
}
public List findJobExecutions(JobInstance job) {
Set executions = (Set) executionsById.get(job.getId());
if( executions == null ){
return new ArrayList();
}
else{
return new ArrayList(executions);
}
}
public void update(JobInstance job) {
// no-op
}
public void update(JobExecution jobExecution) {
// no-op
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.support.transaction.TransactionAwareProxyFactory;
public class MapStepDao implements StepDao {
private static Map stepsByJobId;
private static Map executionsById;
private static Map restartsById;
private static long currentId = 0;
static {
stepsByJobId = TransactionAwareProxyFactory.createTransactionalMap();
executionsById = TransactionAwareProxyFactory.createTransactionalMap();
restartsById = TransactionAwareProxyFactory.createTransactionalMap();
}
public static void clear() {
stepsByJobId.clear();
executionsById.clear();
restartsById.clear();
}
public StepInstance createStep(JobInstance job, String stepName) {
StepInstance step = new StepInstance(job, stepName, new Long(currentId++));
Set steps = (Set) stepsByJobId.get(job.getId());
if (steps==null) {
steps = TransactionAwareProxyFactory.createTransactionalSet();
stepsByJobId.put(job.getId(), steps);
}
steps.add(step);
//System.err.println(steps);
return step;
}
public StepInstance findStep(JobInstance job, String stepName) {
for (Iterator iter = stepsByJobId.values().iterator(); iter.hasNext();) {
Set steps = (Set) iter.next();
for (Iterator iterator = steps.iterator(); iterator.hasNext();) {
StepInstance step = (StepInstance) iterator.next();
if (step.getName().equals(stepName)) {
return step;
}
}
}
return null;
}
public List findSteps(JobInstance job) {
Set steps = (Set) stepsByJobId.get(job.getId());
if (steps==null) {
return new ArrayList();
}
return new ArrayList(steps);
}
public RestartData getRestartData(Long stepId) {
return (RestartData) restartsById.get(stepId);
}
public int getStepExecutionCount(Long jobId) {
Set executions = (Set) executionsById.get(jobId);
if (executions==null) return 0;
return executions.size(); }
public void save(StepExecution stepExecution) {
Set executions = (Set) executionsById.get(stepExecution.getStepId());
if (executions==null) {
executions = TransactionAwareProxyFactory.createTransactionalSet();
executionsById.put(stepExecution.getStepId(), executions);
}
stepExecution.setId(new Long(currentId++));
executions.add(stepExecution);
}
public void saveRestartData(Long stepId, RestartData restartData) {
restartsById.put(stepId, restartData);
}
public List findStepExecutions(StepInstance step) {
Set executions = (Set) executionsById.get(step.getId());
if(executions == null){
//no step executions, return empty array list.
return new ArrayList();
}
else{
return new ArrayList(executions);
}
}
public void update(StepInstance step) {
// no-op
}
public void update(StepExecution stepExecution) {
// no-op
}
}

View File

@@ -0,0 +1,512 @@
/*
* 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.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.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException;
import org.springframework.batch.execution.runtime.DefaultJobIdentifier;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifier;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* SQL implementation of {@link JobDao}. 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
*/
public class SqlJobDao implements JobDao, InitializingBean {
private static final String CHECK_JOB_EXECUTION_EXISTS = "SELECT COUNT(*) FROM %PREFIX%JOB_EXECUTION WHERE ID=?";
// Job SQL statements
private static final String CREATE_JOB = "INSERT into %PREFIX%JOB(ID, JOB_NAME, JOB_KEY, SCHEDULE_DATE)"
+ " values (?, ?, ?, ?)";
/**
* Default value for the table prefix property.
*/
public static final String DEFAULT_TABLE_PREFIX = "BATCH_";
private static final int EXIT_MESSAGE_LENGTH = 250;
private static final String FIND_JOBS = "SELECT ID, STATUS from %PREFIX%JOB where JOB_NAME = ? and "
+ "JOB_KEY = ? and SCHEDULE_DATE = ?";
private static final String GET_JOB_EXECUTION_COUNT = "SELECT count(ID) from %PREFIX%JOB_EXECUTION "
+ "where JOB_ID = ?";
protected static final Log logger = LogFactory.getLog(SqlJobDao.class);
private static final String SAVE_JOB_EXECUTION = "INSERT into %PREFIX%JOB_EXECUTION(ID, JOB_ID, START_TIME, "
+ "END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE) values (?, ?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_JOB = "UPDATE %PREFIX%JOB set STATUS = ? where ID = ?";
// Job Execution SqlStatements
private static final String UPDATE_JOB_EXECUTION = "UPDATE %PREFIX%JOB_EXECUTION set START_TIME = ?, END_TIME = ?, "
+ " STATUS = ?, CONTINUABLE = ?, EXIT_CODE = ?, EXIT_MESSAGE = ? where ID = ?";
private String checkJobExecutionExistsQuery;
private String findJobsQuery;
private JdbcTemplate jdbcTemplate;
private String jobExecutionCountQuery;
private DataFieldMaxValueIncrementer jobExecutionIncrementer;
private DataFieldMaxValueIncrementer jobIncrementer;
private String saveJobExecutionQuery;
private String tablePrefix = DEFAULT_TABLE_PREFIX;
private String updateJobExecutionQuery;
private String updateJobQuery;
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*
* Ensure jdbcTemplate and incrementers have been provided.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jdbcTemplate, "JdbcTemplate cannot be null");
Assert.notNull(jobIncrementer, "JobIncrementor cannot be null");
Assert.notNull(jobExecutionIncrementer,
"JobExecutionIncrementer cannot be null");
}
/**
* In this sql implementation a job id is obtained by asking the
* jobIncrementer (which is likely a sequence) for the nextLong, and then
* passing the Id and identifier values (job name, jobKey, schedule date)
* into an INSERT statement.
*
* @see JobDao#createJob(JobIdentifier)
* @throws IllegalArgumentException
* if any {@link JobIdentifier} fields are null.
*/
public JobInstance createJob(JobIdentifier jobIdentifier) {
validateJobIdentifier(jobIdentifier);
ScheduledJobIdentifier defaultJobId = getScheduledJobIdentifier(jobIdentifier);
Long jobId = new Long(jobIncrementer.nextLongValue());
Object[] parameters = new Object[] { jobId, defaultJobId.getName(),
defaultJobId.getJobKey(), defaultJobId.getScheduleDate() };
jdbcTemplate.update(getCreateJobQuery(), parameters);
JobInstance job = new JobInstance(jobIdentifier, jobId);
return job;
}
public List findJobExecutions(final JobInstance job) {
Assert.notNull(job, "Job cannot be null.");
Assert.notNull(job.getId(), "Job Id cannot be null.");
return jdbcTemplate.query(
getQuery(JobExecutionRowMapper.FIND_JOB_EXECUTIONS),
new Object[] { job.getId() }, new JobExecutionRowMapper(job));
}
/**
* 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#findJobs(JobIdentifier)
* @throws IllegalArgumentException
* if any {@link JobIdentifier} fields are null.
*/
public List findJobs(final JobIdentifier jobIdentifier) {
validateJobIdentifier(jobIdentifier);
ScheduledJobIdentifier defaultJobId = getScheduledJobIdentifier(jobIdentifier);
Object[] parameters = new Object[] { defaultJobId.getName(),
defaultJobId.getJobKey(), defaultJobId.getScheduleDate() };
RowMapper rowMapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
JobInstance job = new JobInstance(jobIdentifier, new Long(rs
.getLong(1)));
job.setStatus(BatchStatus.getStatus(rs.getString(2)));
return job;
}
};
return jdbcTemplate.query(getFindJobsQuery(), parameters, rowMapper);
}
public String getCheckJobExecutionExistsQuery() {
if (checkJobExecutionExistsQuery != null) {
return checkJobExecutionExistsQuery;
}
return getQuery(CHECK_JOB_EXECUTION_EXISTS);
}
public String getCreateJobQuery() {
return getQuery(CREATE_JOB);
}
public String getFindJobsQuery() {
if (findJobsQuery != null) {
return findJobsQuery;
}
return getQuery(FIND_JOBS);
}
/**
* @see JobDao#getJobExecutionCount(JobInstance)
* @throws IllegalArgumentException
* if jobId is null.
*/
public int getJobExecutionCount(Long jobId) {
Assert.notNull(jobId, "JobId cannot be null");
Object[] parameters = new Object[] { jobId };
return jdbcTemplate
.queryForInt(getJobExecutionCountQuery(), parameters);
}
public String getJobExecutionCountQuery() {
if (jobExecutionCountQuery != null) {
return jobExecutionCountQuery;
}
return getQuery(GET_JOB_EXECUTION_COUNT);
}
private String getQuery(String base) {
return StringUtils.replace(base, "%PREFIX%", tablePrefix);
}
public String getSaveJobExecutionQuery() {
if (saveJobExecutionQuery != null) {
return saveJobExecutionQuery;
}
return getQuery(SAVE_JOB_EXECUTION);
}
/**
* Convert a {@link JobIdentifier} to a {@link ScheduledJobIdentifier} by
* supplying additional fields with null values, as necessary.
*
* @param jobIdentifier
* a {@link JobIdentifier}
* @return a {@link ScheduledJobIdentifier} with the same name
*/
private ScheduledJobIdentifier getScheduledJobIdentifier(
JobIdentifier jobIdentifier) {
if (jobIdentifier instanceof ScheduledJobIdentifier) {
return (ScheduledJobIdentifier) jobIdentifier;
}
if (jobIdentifier instanceof DefaultJobIdentifier) {
return new ScheduledJobIdentifier(jobIdentifier.getName(),
((DefaultJobIdentifier) jobIdentifier).getJobKey());
}
return new ScheduledJobIdentifier(jobIdentifier.getName());
}
public String getUpdateJobExecutionQuery() {
if (updateJobExecutionQuery != null) {
return updateJobExecutionQuery;
}
return getQuery(UPDATE_JOB_EXECUTION);
}
public String getUpdateJobQuery() {
if (updateJobQuery != null) {
return updateJobQuery;
}
return getQuery(UPDATE_JOB);
}
/**
*
* 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#save(JobExecution)
* @throws IllegalArgumentException
* if jobExecution is null, as well as any of it's fields to be
* persisted.
*/
public void save(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() };
jdbcTemplate.update(getSaveJobExecutionQuery(), parameters, new int[] {
Types.INTEGER, Types.INTEGER, Types.TIMESTAMP, Types.TIMESTAMP,
Types.VARCHAR, Types.CHAR, Types.VARCHAR, Types.VARCHAR });
}
/**
* Public setter for the checkJobExecutionExistsQuery property.
*
* @param checkJobExecutionExistsQuery the checkJobExecutionExistsQuery to set
*/
public void setCheckJobExecutionExistsQuery(String checkJobExecutionExistsQuery) {
this.checkJobExecutionExistsQuery = checkJobExecutionExistsQuery;
}
/**
* Public setter for the findJobsQuery property.
*
* @param findJobsQuery the findJobsQuery to set
*/
public void setFindJobsQuery(String findJobsQuery) {
this.findJobsQuery = findJobsQuery;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* Public setter for the jobExecutionCountQuery property.
*
* @param jobExecutionCountQuery the jobExecutionCountQuery to set
*/
public void setJobExecutionCountQuery(String jobExecutionCountQuery) {
this.jobExecutionCountQuery = jobExecutionCountQuery;
}
public void setJobExecutionIncrementer(
DataFieldMaxValueIncrementer jobExecutionIncrementer) {
this.jobExecutionIncrementer = jobExecutionIncrementer;
}
public void setJobIncrementer(DataFieldMaxValueIncrementer jobIncrementer) {
this.jobIncrementer = jobIncrementer;
}
/**
* Public setter for the saveJobExecutionQuery property.
*
* @param saveJobExecutionQuery the saveJobExecutionQuery to set
*/
public void setSaveJobExecutionQuery(String saveJobExecutionQuery) {
this.saveJobExecutionQuery = saveJobExecutionQuery;
}
/**
* 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 setter for the updateJobExecutionQuery property.
*
* @param updateJobExecutionQuery the updateJobExecutionQuery to set
*/
public void setUpdateJobExecutionQuery(String updateJobExecutionQuery) {
this.updateJobExecutionQuery = updateJobExecutionQuery;
}
/**
* Public setter for the updateJobQuery property.
*
* @param updateJobQuery the updateJobQuery to set
*/
public void setUpdateJobQuery(String updateJobQuery) {
this.updateJobQuery = updateJobQuery;
}
/**
* 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#update(JobExecution)
*/
public void update(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 (jdbcTemplate.queryForInt(getCheckJobExecutionExistsQuery(),
new Object[] { jobExecution.getId() }) != 1) {
throw new NoSuchBatchDomainObjectException(
"Invalid JobExecution, ID " + jobExecution.getId()
+ " not found.");
}
jdbcTemplate
.update(getUpdateJobExecutionQuery(), parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP,
Types.VARCHAR, Types.CHAR, Types.VARCHAR,
Types.VARCHAR, Types.INTEGER });
}
/**
* @see JobDao#update(JobInstance)
* @throws IllegalArgumentException
* if Job, Job.status, or job.id is null
*/
public void update(JobInstance job) {
Assert.notNull(job, "Job Cannot be Null");
Assert.notNull(job.getStatus(), "Job Status cannot be Null");
Assert.notNull(job.getId(), "Job ID cannot be null");
Object[] parameters = new Object[] { job.getStatus().toString(),
job.getId() };
jdbcTemplate.update(getUpdateJobQuery(), parameters);
}
/*
* 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.getStartTime(),
"JobExecution start time cannot be null.");
Assert.notNull(jobExecution.getStatus(),
"JobExecution status cannot be null.");
}
/**
* Validate {@link JobIdentifier}. Due to differing requirements, it is
* acceptable for any field to be blank, however null fields may cause odd
* and vague exception reports from the database driver.
*/
private void validateJobIdentifier(JobIdentifier jobIdentifier) {
Assert.notNull(jobIdentifier, "JobIdentifier cannot be null.");
Assert.notNull(jobIdentifier.getName(),
"JobIdentifier name cannot be null.");
if (jobIdentifier instanceof ScheduledJobIdentifier) {
ScheduledJobIdentifier identifier = (ScheduledJobIdentifier) jobIdentifier;
Assert.notNull(identifier.getJobKey(),
"JobIdentifier JobKey cannot be null.");
Assert.notNull(identifier.getScheduleDate(),
"JobIdentifier ScheduleDate cannot be null.");
}
}
/**
* Re-usable mapper for {@link JobExecution} instances.
*
* @author Dave Syer
*
*/
public static class JobExecutionRowMapper implements RowMapper {
public static final String FIND_JOB_EXECUTIONS = "SELECT ID, START_TIME, END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%JOB_EXECUTION"
+ " where JOB_ID = ?";
public static final String GET_JOB_EXECUTION = "SELECT ID, START_TIME, END_TIME, STATUS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%JOB_EXECUTION"
+ " where ID = ?";
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;
}
}
}

View File

@@ -0,0 +1,607 @@
/*
* 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.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.JobExecution;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.execution.repository.dao.SqlJobDao.JobExecutionRowMapper;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.support.PropertiesConverter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Sql implementation of {@link StepDao}. Uses Sequences (via Spring's
*
* @link DataFieldMaxValueIncrementer abstraction) to create all Step and
* StepExecution 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.
*
* TODO: JavaDoc should be geared more towards usability, the comments above are
* useful information, and should be there, but needs usability stuff. Depends
* on the step dao java docs as well.
*
* @author Lucas Ward
* @author Dave Syer
* @see StepDao
*/
public class SqlStepDao implements StepDao, InitializingBean {
private static final String CREATE_STEP = "INSERT into %PREFIX%STEP(ID, JOB_ID, STEP_NAME) values (?, ?, ?)";
private static final int EXIT_MESSAGE_LENGTH = 250;
private static final String FIND_STEP = "SELECT ID, STATUS, RESTART_DATA from %PREFIX%STEP where JOB_ID = ? "
+ "and STEP_NAME = ?";
private static final String FIND_STEP_EXECUTIONS = "SELECT ID, JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, COMMIT_COUNT,"
+ " TASK_COUNT, TASK_STATISTICS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE from %PREFIX%STEP_EXECUTION where STEP_ID = ?";
// Step SQL statements
private static final String FIND_STEPS = "SELECT ID, STEP_NAME, STATUS, RESTART_DATA from %PREFIX%STEP where JOB_ID = ?";
private static final String GET_STEP_EXECUTION_COUNT = "SELECT count(ID) from %PREFIX%STEP_EXECUTION where "
+ "STEP_ID = ?";
protected static final Log logger = LogFactory.getLog(SqlStepDao.class);
// StepExecution statements
private static final String SAVE_STEP_EXECUTION = "INSERT into %PREFIX%STEP_EXECUTION(ID, VERSION, STEP_ID, JOB_EXECUTION_ID, START_TIME, "
+ "END_TIME, STATUS, COMMIT_COUNT, TASK_COUNT, TASK_STATISTICS, CONTINUABLE, EXIT_CODE, EXIT_MESSAGE) "
+ "values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_STEP = "UPDATE %PREFIX%STEP set STATUS = ?, RESTART_DATA = ? where ID = ?";
private static final String UPDATE_STEP_EXECUTION = "UPDATE %PREFIX%STEP_EXECUTION set START_TIME = ?, END_TIME = ?, "
+ "STATUS = ?, COMMIT_COUNT = ?, TASK_COUNT = ?, TASK_STATISTICS = ?, CONTINUABLE = ? , EXIT_CODE = ?, "
+ "EXIT_MESSAGE = ? where ID = ?";
private String createStepQuery;
private String findStepExecutionsQuery;
private String findStepQuery;
private String findStepsQuery;
private JdbcOperations jdbcTemplate;
private JobDao jobDao;
private String saveStepExecutionQuery;
private String stepExecutionCountQuery;
private DataFieldMaxValueIncrementer stepExecutionIncrementer;
private DataFieldMaxValueIncrementer stepIncrementer;
private String tablePrefix = SqlJobDao.DEFAULT_TABLE_PREFIX;
private String updateStepExecutionQuery;
private String updateStepQuery;
public void afterPropertiesSet() throws Exception {
Assert.notNull(jdbcTemplate, "JdbcTemplate cannot be null.");
Assert.notNull(stepIncrementer, "StepIncrementer cannot be null.");
Assert.notNull(stepExecutionIncrementer,
"StepExecutionIncrementer canot be null.");
}
private void cascadeJobExecution(JobExecution jobExecution) {
if (jobExecution.getId() != null) {
// assume already saved...
return;
}
jobDao.save(jobExecution);
}
/**
* Create a step with the given job's id, and the provided step name. A
* unique id is created for the step using an incrementer. (@link
* DataFieldMaxValueIncrementer)
*
* @see StepDao#createStep(JobInstance, String)
* @throws IllegalArgumentException
* if job or stepName is null.
*/
public StepInstance createStep(JobInstance job, String stepName) {
Assert.notNull(job, "Job cannot be null.");
Assert.notNull(stepName, "StepName cannot be null.");
Long stepId = new Long(stepIncrementer.nextLongValue());
Object[] parameters = new Object[] { stepId, job.getId(), stepName };
jdbcTemplate.update(getCreateStepQuery(), parameters);
StepInstance step = new StepInstance(job, stepName, stepId);
return step;
}
/**
* Find one step for given job and stepName. A RowMapper is used to map each
* row returned to a step object. If none are found, the list will be empty
* and null will be returned. If one step is found, it will be returned. If
* anymore than one step is found, an exception is thrown.
*
* @see StepDao#findStep(Long, String)
* @throws IllegalArgumentException
* if job, stepName, or job.id is null.
* @throws IncorrectResultSizeDataAccessException
* if more than one step is found.
*/
public StepInstance findStep(JobInstance job, String stepName) {
Assert.notNull(job, "Job cannot be null.");
Assert.notNull(job.getId(), "Job ID cannot be null");
Assert.notNull(stepName, "StepName cannot be null");
Object[] parameters = new Object[] { job.getId(), stepName };
RowMapper rowMapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
StepInstance step = new StepInstance(new Long(rs.getLong(1)));
step.setStatus(BatchStatus.getStatus(rs.getString(2)));
step.setRestartData(new GenericRestartData(PropertiesConverter
.stringToProperties(rs.getString(3))));
return step;
}
};
List steps = jdbcTemplate.query(getFindStepQuery(), parameters,
rowMapper);
if (steps.size() == 0) {
// No step found
return null;
} else if (steps.size() == 1) {
StepInstance step = (StepInstance) steps.get(0);
return step;
} else {
// This error will likely never be thrown, because there should
// never be two steps with the same name and Job_ID due to database
// constraints.
throw new IncorrectResultSizeDataAccessException(
"Step Invalid, multiple steps found for StepName:"
+ stepName + " and JobId:" + job.getId(), 1, steps
.size());
}
}
/**
* Get StepExecution for the given step. Due to the nature of statistics,
* they will not be returned with reconstituted object.
*
* @see StepDao#getStepExecution(Long)
* @throws IllegalArgumentException
* if id is null.
*/
public List findStepExecutions(final StepInstance step) {
Assert.notNull(step, "Step cannot be null.");
Assert.notNull(step.getId(), "Step id cannot be null.");
RowMapper rowMapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
JobExecution jobExecution = (JobExecution) jdbcTemplate
.queryForObject(
getQuery(JobExecutionRowMapper.GET_JOB_EXECUTION),
new Object[] { new Long(rs.getLong(2)) },
new JobExecutionRowMapper(step.getJob()));
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.setStatistics(PropertiesConverter
.stringToProperties(rs.getString(8)));
stepExecution.setExitStatus(new ExitStatus("Y".equals(rs
.getString(9)), rs.getString(10), rs.getString(11)));
return stepExecution;
}
};
return jdbcTemplate.query(getFindStepExecutionsQuery(),
new Object[] { step.getId() }, rowMapper);
}
/**
* @see StepDao#findSteps(JobInstance)
*
* Sql implementation which uses a RowMapper to populate a list of all rows
* in the step table with the same JOB_ID.
*
* @throws IllegalArgumentException
* if jobId is null.
*/
public List findSteps(final JobInstance job) {
Assert.notNull(job, "Job cannot be null.");
Object[] parameters = new Object[] { job.getId() };
RowMapper rowMapper = new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
StepInstance step = new StepInstance(job, rs.getString(2),
new Long(rs.getLong(1)));
String status = rs.getString(3);
step.setStatus(BatchStatus.getStatus(status));
step.setRestartData(new GenericRestartData(PropertiesConverter
.stringToProperties(rs.getString(3))));
return step;
}
};
return jdbcTemplate.query(getFindStepsQuery(), parameters, rowMapper);
}
public String getCreateStepQuery() {
if (createStepQuery != null) {
return createStepQuery;
}
return getQuery(CREATE_STEP);
}
public String getFindStepExecutionsQuery() {
if (findStepExecutionsQuery != null) {
return findStepExecutionsQuery;
}
return getQuery(FIND_STEP_EXECUTIONS);
}
public String getFindStepQuery() {
if (findStepQuery != null) {
return findStepQuery;
}
return getQuery(FIND_STEP);
}
public String getFindStepsQuery() {
if (findStepsQuery != null) {
return findStepsQuery;
}
return getQuery(FIND_STEPS);
}
private String getQuery(String base) {
return StringUtils.replace(base, "%PREFIX%", tablePrefix);
}
public String getSaveStepExecutionQuery() {
if (saveStepExecutionQuery != null) {
return saveStepExecutionQuery;
}
return getQuery(SAVE_STEP_EXECUTION);
}
public int getStepExecutionCount(Long stepId) {
Object[] parameters = new Object[] { stepId };
return jdbcTemplate.queryForInt(getStepExecutionCountQuery(),
parameters);
}
public String getStepExecutionCountQuery() {
if (stepExecutionCountQuery != null) {
return stepExecutionCountQuery;
}
return getQuery(GET_STEP_EXECUTION_COUNT);
}
public String getUpdateStepExecutionQuery() {
if (updateStepExecutionQuery != null) {
return updateStepExecutionQuery;
}
return getQuery(UPDATE_STEP_EXECUTION);
}
public String getUpdateStepQuery() {
if (updateStepQuery != null) {
return updateStepQuery;
}
return getQuery(UPDATE_STEP);
}
/**
* 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#save(StepExecution)
*/
public void save(StepExecution stepExecution) {
validateStepExecution(stepExecution);
cascadeJobExecution(stepExecution.getJobExecution());
stepExecution.setId(new Long(stepExecutionIncrementer.nextLongValue()));
Object[] parameters = new Object[] {
stepExecution.getId(),
new Long(0),
stepExecution.getStepId(),
stepExecution.getJobExecutionId(),
stepExecution.getStartTime(),
stepExecution.getEndTime(),
stepExecution.getStatus().toString(),
stepExecution.getCommitCount(),
stepExecution.getTaskCount(),
PropertiesConverter.propertiesToString(stepExecution
.getStatistics()),
stepExecution.getExitStatus().isContinuable() ? "Y" : "N",
stepExecution.getExitStatus().getExitCode(),
stepExecution.getExitStatus().getExitDescription() };
jdbcTemplate.update(getSaveStepExecutionQuery(), parameters, new int[] {
Types.INTEGER, Types.INTEGER, Types.INTEGER, Types.INTEGER,
Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER,
Types.INTEGER, Types.VARCHAR, Types.CHAR, Types.VARCHAR,
Types.VARCHAR });
}
/**
* Public setter for the createStepQuery property.
*
* @param createStepQuery
* the createStepQuery to set
*/
public void setCreateStepQuery(String createStepQuery) {
this.createStepQuery = createStepQuery;
}
/**
* Public setter for the findStepExecutionsQuery property.
*
* @param findStepExecutionsQuery
* the findStepExecutionsQuery to set
*/
public void setFindStepExecutionsQuery(String findStepExecutionsQuery) {
this.findStepExecutionsQuery = findStepExecutionsQuery;
}
/**
* Public setter for the findStepQuery property.
*
* @param findStepQuery
* the findStepQuery to set
*/
public void setFindStepQuery(String findStepQuery) {
this.findStepQuery = findStepQuery;
}
/**
* Public setter for the findStepQuery property.
*
* @param findStepsQuery
* the findStepsQuery to set
*/
public void setFindStepsQuery(String findStepsQuery) {
this.findStepsQuery = findStepsQuery;
}
public void setJdbcTemplate(JdbcOperations jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
/**
* Injection setter for job dao. Used to save {@link JobExecution}
* instances.
*
* @param jobDao
* a {@link JobDao}
*/
public void setJobDao(JobDao jobDao) {
this.jobDao = jobDao;
}
/**
* Public setter for the findStepQuery property.
*
* @param saveStepExecutionQuery
* the saveStepExecutionQuery to set
*/
public void setSaveStepExecutionQuery(String saveStepExecutionQuery) {
this.saveStepExecutionQuery = saveStepExecutionQuery;
}
/**
* Public setter for the stepExecutionCountQuery property.
*
* @param stepExecutionCountQuery
* the stepExecutionCountQuery to set
*/
public void setStepExecutionCountQuery(String stepExecutionCountQuery) {
this.stepExecutionCountQuery = stepExecutionCountQuery;
}
/**
* Set the {@link DataFieldMaxValueIncrementer} that will be used to
* increment the primary keys used for {@link StepExecution} instances.
*
* @param stepExecutionIncrementer a {@link DataFieldMaxValueIncrementer}
*/
public void setStepExecutionIncrementer(
DataFieldMaxValueIncrementer stepExecutionIncrementer) {
this.stepExecutionIncrementer = stepExecutionIncrementer;
}
/**
* Set the {@link DataFieldMaxValueIncrementer} that will be used to
* increment the primary keys used for {@link StepInstance} instances.
*
* @param stepExecutionIncrementer a {@link DataFieldMaxValueIncrementer}
*/
public void setStepIncrementer(DataFieldMaxValueIncrementer stepIncrementer) {
this.stepIncrementer = stepIncrementer;
}
/**
* Public setter for the table prefix property. This will be prefixed to all
* the table names before queries are executed (unless individual queries
* are overridden with the set*Query methods). Defaults to
* {@value #DEFAULT_TABLE_PREFIX}.
*
* @param tablePrefix
* the tablePrefix to set
*/
public void setTablePrefix(String tablePrefix) {
this.tablePrefix = tablePrefix;
}
/**
* Public setter for the {@link String} property.
*
* @param updateStepExecutionQuery
* the updateStepExecutionQuery to set
*/
public void setUpdateStepExecutionQuery(String updateStepExecutionQuery) {
this.updateStepExecutionQuery = updateStepExecutionQuery;
}
/**
* Public setter for the {@link String} property.
*
* @param updateStepQuery
* the updateStepQuery to set
*/
public void setUpdateStepQuery(String updateStepQuery) {
this.updateStepQuery = updateStepQuery;
}
/**
* @see StepDao#update(StepExecution)
*/
public void update(StepExecution stepExecution) {
validateStepExecution(stepExecution);
Assert.notNull(stepExecution.getId(),
"StepExecution Id cannot be null. StepExecution must saved"
+ " before it can be updated.");
// TODO: Not sure if this is a good idea on step execution considering
// it is saved at every commit
// point.
// if (jdbcTemplate.queryForInt(CHECK_STEP_EXECUTION_EXISTS, new
// Object[] { stepExecution.getId() }) != 1) {
// return; // throw exception?
// }
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);
}
Object[] parameters = new Object[] {
stepExecution.getStartTime(),
stepExecution.getEndTime(),
stepExecution.getStatus().toString(),
stepExecution.getCommitCount(),
stepExecution.getTaskCount(),
PropertiesConverter.propertiesToString(stepExecution
.getStatistics()),
stepExecution.getExitStatus().isContinuable() ? "Y" : "N",
stepExecution.getExitStatus().getExitCode(), exitDescription,
stepExecution.getId() };
jdbcTemplate
.update(getUpdateStepExecutionQuery(), parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP,
Types.VARCHAR, Types.INTEGER, Types.INTEGER,
Types.VARCHAR, Types.CHAR, Types.VARCHAR,
Types.VARCHAR, Types.INTEGER });
}
/**
* @see StepDao#update(StepInstance)
* @throws IllegalArgumentException
* if step, or it's status and id is null.
*/
public void update(final StepInstance step) {
Assert.notNull(step, "Step cannot be null.");
Assert.notNull(step.getStatus(), "Step status cannot be null.");
Assert.notNull(step.getId(), "Step Id cannot be null.");
Properties restartProps = null;
RestartData restartData = step.getRestartData();
if (restartData != null) {
restartProps = restartData.getProperties();
}
Object[] parameters = new Object[] { step.getStatus().toString(),
PropertiesConverter.propertiesToString(restartProps),
step.getId() };
jdbcTemplate.update(getUpdateStepQuery(), parameters);
}
/*
* 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.getStepId(),
"StepExecution Step-Id cannot be null.");
Assert.notNull(stepExecution.getStartTime(),
"StepExecution start time cannot be null.");
Assert.notNull(stepExecution.getStatus(),
"StepExecution status cannot be null.");
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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.List;
import org.springframework.batch.core.domain.JobInstance;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
/**
* Data access object for steps.
*
* TODO: Add java doc.
*
* @author Lucas Ward
*
*/
public interface StepDao {
/**
* Find a step with the given JobId and Step Name. Return null if none are
* found.
*
* @param jobId
* @param stepName
* @return Step
*/
public StepInstance findStep(JobInstance job, String stepName);
/**
* Find all steps with the given Job.
*
* @param job
* the job to use as a search key
* @return list of {@link StepInstance}
*/
public List findSteps(JobInstance job);
/**
* Create a step for the given Step Name and Job Id.
*
* @param job
* @param stepName
*
* @return
*/
public StepInstance createStep(JobInstance job, String stepName);
/**
* Update an existing Step.
*
* Preconditions: Step must have an ID.
*
* @param job
*/
public void update(StepInstance step);
/**
* Save the given StepExecution.
*
* Preconditions: Id must be null. Postconditions: Id will be set to a
* Unique Long.
*
* @param stepExecution
*/
public void save(StepExecution stepExecution);
/**
* Update the given StepExecution
*
* Preconditions: Id must not be null.
*
* @param stepExecution
*/
public void update(StepExecution stepExecution);
/**
* Return the count of StepExecutions with the given StepId.
*
* @param stepId
* @return the number of step executions for this step TODO: change
* signature to search by {@link StepInstance}
*/
public int getStepExecutionCount(Long stepId);
/**
* Return all StepExecutions for the given step.
*
* @param step
* the step to use as a search key
* @return list of stepExecutions
*/
public List findStepExecutions(StepInstance 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,201 @@
/*
* 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 org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.execution.scope.StepContext;
import org.springframework.batch.execution.scope.StepContextAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.AbstractFactoryBean;
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, the same file handle will be returned. A unique step is defined as
* having the same job identifier and step name. An external file mover (such as
* an EAI solution) should rename and move any input files to conform to the
* patter defined by the file pattern.<br/>
*
* If no pattern is passed in, then following default is used:
*
* <pre>
* /%BATCH_ROOT%/job_data/%JOB_NAME%/%JOB_IDENTIFIER%-%STEP_NAME%.txt
* </pre>
*
* The %% variables are replaced with the corresponding bean property at run
* time, when the factory method is executed. Note that the default pattern
* starts with a forward slash "/", which means the root directory will be
* interpreted as an absolute path if it too starts with "/" (because of the
* implementation of the Spring Core Resource abstractions).<br/>
*
* It doesn't make much sense to use this factory unless it is step scoped, but
* note that it is thread safe only if it is step scoped and its mutators are
* not used except for configuration.
*
* @author Tomas Slanina
* @author Lucas Ward
* @author Dave Syer
*
* @see FactoryBean
*/
public class BatchResourceFactoryBean extends AbstractFactoryBean implements
ResourceLoaderAware, StepContextAware {
private static final String BATCH_ROOT_PATTERN = "%BATCH_ROOT%";
private static final String JOB_IDENTIFIER_PATTERN = "%JOB_IDENTIFIER%";
private static final String JOB_NAME_PATTERN = "%JOB_NAME%";
private static final String STEP_NAME_PATTERN = "%STEP_NAME%";
private static final String DEFAULT_PATTERN = "/%BATCH_ROOT%/data/%JOB_NAME%/"
+ "%JOB_IDENTIFIER%-%STEP_NAME%.txt";
private String filePattern = DEFAULT_PATTERN;
private String jobName = null;
private String rootDirectory = "";
private String stepName = "";
private ResourceLoader resourceLoader = new FileSystemResourceLoader();
private JobIdentifier jobIdentifier;
private JobIdentifierLabelGenerator jobIdentifierLabelGenerator = new DefaultJobIdentifierLabelGenerator();
/**
* 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;
}
/**
* Public setter for the {@link JobIdentifierLabelGenerator} property.
*
* @param jobIdentifierLabelGenerator
* the {@link JobIdentifierLabelGenerator} to set
*/
public void setJobIdentifierLabelGenerator(
JobIdentifierLabelGenerator jobIdentifierLabelGenerator) {
this.jobIdentifierLabelGenerator = jobIdentifierLabelGenerator;
}
/**
* Collect the properties of the enclosing {@link StepExecution} that will
* be needed to create a file name.
*
* @see org.springframework.batch.execution.scope.StepContextAware#setStepScopeContext(org.springframework.core.AttributeAccessor)
*/
public void setStepContext(StepContext context) {
Assert.state(context.getStepExecution() != null,
"The StepContext does not have an execution.");
StepExecution execution = context.getStepExecution();
stepName = execution.getStep().getName();
jobName = execution.getStep().getJob().getName();
jobIdentifier = execution.getJobExecution().getJob().getIdentifier();
}
/**
* Returns the Resource representing the file defined by the file pattern.
*
* @see FactoryBean#getObject()
* @return a resource representing the file on the file system.
*/
protected Object createInstance() {
return resourceLoader.getResource(createFileName());
}
public Class getObjectType() {
return Resource.class;
}
/**
* 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
*/
private String createFileName() {
Assert.notNull(filePattern, "filename pattern is null");
String fileName = filePattern;
// TODO consider refactoring to void replacePattern() method and
// collecting variable fileName
fileName = replacePattern(fileName, BATCH_ROOT_PATTERN, rootDirectory);
fileName = replacePattern(fileName, JOB_NAME_PATTERN,
jobName == null ? "job" : jobName);
fileName = replacePattern(fileName, STEP_NAME_PATTERN, stepName);
fileName = replacePattern(fileName, JOB_IDENTIFIER_PATTERN,
jobIdentifierLabelGenerator.getLabel(jobIdentifier));
return fileName;
}
public void setFilePattern(String filePattern) {
this.filePattern = replacePattern(filePattern, "\\", File.separator);
}
public void setRootDirectory(String rootDirectory) {
this.rootDirectory = replacePattern(rootDirectory, "\\", File.separator);
if (rootDirectory != null && rootDirectory.endsWith(File.separator)) {
this.rootDirectory = rootDirectory.substring(0, rootDirectory
.lastIndexOf(File.separator));
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.text.DateFormat;
import java.text.SimpleDateFormat;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.runtime.SimpleJobIdentifier;
import org.springframework.batch.execution.runtime.ScheduledJobIdentifier;
/**
* {@link JobIdentifierLabelGenerator} that knows about
* {@link SimpleJobIdentifier} and {@link ScheduledJobIdentifier} and provides a
* fixed format label for each.
*
*
* @author Dave Syer
*
*/
public class DefaultJobIdentifierLabelGenerator implements
JobIdentifierLabelGenerator {
private static final DateFormat dateFormat = new SimpleDateFormat(
"yyyyMMdd");
/**
* Concatenate the properties of the {@link JobIdentifier}. From a
* {@link SimpleJobIdentifier} we just get the name, and from a
* {@link ScheduledJobIdentifier} we get the name, stream, run and schedule
* date (yyyyMMdd) joined by hyphens.
*
* @see org.springframework.batch.execution.resource.JobIdentifierLabelGenerator#getLabel(org.springframework.batch.core.domain.JobIdentifier)
*/
public String getLabel(JobIdentifier jobIdentifier) {
if (jobIdentifier == null) {
return null;
}
if (jobIdentifier instanceof ScheduledJobIdentifier) {
ScheduledJobIdentifier id = (ScheduledJobIdentifier) jobIdentifier;
return jobIdentifier.getName() + "-" + id.getJobKey() + "-"
+ dateFormat.format(id.getScheduleDate());
}
return jobIdentifier.getName();
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.resource;
import org.springframework.batch.core.domain.JobIdentifier;
/**
* Strategy for generating a label (e.g. for a file name) from a
* {@link JobIdentifier}. A label is a short string with no special characters
* that can be used to recognise the {@link JobIdentifier}.
*
* @author Dave Syer
*
*/
public interface JobIdentifierLabelGenerator {
/**
* Create a label from the {@link JobIdentifier}.
*
* @param jobIdentifier
* a {@link JobIdentifier}
* @return a short string describing the identifier with no whitespace or
* special characters. Return null if the identifier is null.
*/
String getLabel(JobIdentifier jobIdentifier);
}

View File

@@ -0,0 +1,85 @@
/*
* 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.runtime;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.runtime.SimpleJobIdentifier;
/**
* @author Dave Syer
*
*/
public class DefaultJobIdentifier extends SimpleJobIdentifier implements
JobIdentifier {
private String key = "";
/**
* Default constructor package access only.
*/
DefaultJobIdentifier() {
this(null);
}
/**
* @param name the name for the job
*/
public DefaultJobIdentifier(String name) {
super(name);
}
/**
* @param name the name for the job
*/
public DefaultJobIdentifier(String name, String key) {
this(name);
this.key = key;
}
public String getJobKey() {
return key;
}
public void setJobKey(String key) {
this.key = key;
}
/**
* Adds the key data to the base class.
*
* @see org.springframework.batch.core.runtime.SimpleJobIdentifier#toString()
*/
public String toString() {
return super.toString() + ",key=" + key;
}
/**
* Returns true if the provided JobIdentifier equals this JobIdentifier. Two
* Identifiers are considered to be equal if they have the same name,
* stream, run, and schedule date.
*/
public boolean equals(Object other) {
return EqualsBuilder.reflectionEquals(this, other) || EqualsBuilder.reflectionEquals(other, this);
}
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
}

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.runtime;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.runtime.JobIdentifierFactory;
/**
* {@link JobIdentifierFactory} for creating {@link DefaultJobIdentifierFactory}
* instances.
*
* @author Dave Syer
*
*/
public class DefaultJobIdentifierFactory implements JobIdentifierFactory {
protected String key = "key";
public JobIdentifier getJobIdentifier(String name) {
DefaultJobIdentifier runtimeInformation = new DefaultJobIdentifier(name);
runtimeInformation.setJobKey(key);
return runtimeInformation;
}
public void setJobKey(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.runtime;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.springframework.batch.core.domain.JobIdentifier;
public class ScheduledJobIdentifier extends DefaultJobIdentifier implements JobIdentifier {
private static final DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
private Date scheduleDate;
ScheduledJobIdentifier() {
this(null);
}
public ScheduledJobIdentifier(String name) {
super(name);
initDate();
}
/**
* @param name
* @param key
*/
public ScheduledJobIdentifier(String name, String key) {
super(name, key);
initDate();
}
private void initDate() {
try {
scheduleDate = dateFormat.parse("19700101");
} catch (ParseException e) {
throw new IllegalStateException("Could not parse trivial date 19700101");
}
}
public Date getScheduleDate() {
return scheduleDate;
}
public void setScheduleDate(Date scheduleDate) {
this.scheduleDate = scheduleDate;
}
public String toString() {
return super.toString() + ",scheduleDate="
+ scheduleDate;
}
/**
* Returns true if the provided JobIdentifier equals this JobIdentifier. Two
* Identifiers are considered to be equal if they have the same name,
* stream, run, and schedule date.
*/
public boolean equals(Object other) {
return EqualsBuilder.reflectionEquals(this, other) || EqualsBuilder.reflectionEquals(other, this);
}
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.runtime;
import java.util.Date;
import org.springframework.batch.core.domain.JobIdentifier;
import org.springframework.batch.core.runtime.JobIdentifierFactory;
/**
* {@link JobIdentifierFactory} for creating {@link ScheduledJobIdentifier}
* instances.
*
* @author Dave Syer
*
*/
public class ScheduledJobIdentifierFactory extends DefaultJobIdentifierFactory implements JobIdentifierFactory {
private Date scheduleDate = new Date();
public JobIdentifier getJobIdentifier(String name) {
ScheduledJobIdentifier identifier = new ScheduledJobIdentifier(name);
identifier.setJobKey(key);
identifier.setScheduleDate(scheduleDate);
return identifier;
}
public void setScheduleDate(Date scheduleDate) {
this.scheduleDate = scheduleDate;
}
}

View File

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

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor;
/**
* Simple implementation of {@link StepContext}.
*
* @author Dave Syer
*
*/
public class SimpleStepContext extends SynchronizedAttributeAccessor implements
StepContext {
private Map callbacks = new HashMap();
private StepContext parent;
private StepExecution stepExecution;
/**
* Default constructor.
*/
public SimpleStepContext() {
this(null);
}
/**
* @param object
*/
public SimpleStepContext(StepContext parent) {
super();
this.parent = parent;
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.execution.scope.StepContext#getParent()
*/
public StepContext getParent() {
return parent;
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.repeat.RepeatContext#registerDestructionCallback(java.lang.String,
* java.lang.Runnable)
*/
/*
* (non-Javadoc)
*
* @see org.springframework.batch.execution.scope.StepContext#registerDestructionCallback(java.lang.String,
* java.lang.Runnable)
*/
public void registerDestructionCallback(String name, Runnable callback) {
synchronized (callbacks) {
Set set = (Set) callbacks.get(name);
if (set == null) {
set = new HashSet();
callbacks.put(name, set);
}
set.add(callback);
}
}
/*
* Package access because only needed internally.
*/
void close() {
List errors = new ArrayList();
Set copy;
synchronized (callbacks) {
copy = new HashSet(callbacks.entrySet());
}
for (Iterator iter = copy.iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
Set set = (Set) entry.getValue();
for (Iterator iterator = set.iterator(); iterator.hasNext();) {
Runnable callback = (Runnable) iterator.next();
/*
* There used to be a check here to make sure there was an
* attribute with the given name, but an inner bean is not
* registered with the bean factory, so the destroy method is
* only called in inner bean if we make the callback
* unconditionally.
*/
if (callback != null) {
/*
* The documentation of the interface says that these
* callbacks must not throw exceptions, but we don't trust
* them necessarily...
*/
try {
callback.run();
} catch (RuntimeException t) {
errors.add(t);
}
}
}
}
if (errors.isEmpty()) {
return;
}
throw (RuntimeException) errors.get(0);
}
/**
* @param stepExecution
*/
public void setStepExecution(StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.execution.scope.StepContext#getJobIdentifier()
*/
public StepExecution getStepExecution() {
return stepExecution;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.core.AttributeAccessor;
/**
* Interface for step-scoped context object and step-scoped services.
*
* @author Dave Syer
*
*/
public interface StepContext extends AttributeAccessor {
/**
* Accessor for the {@link StepExecution} associated with the currently
* executing step.
*
* @return the {@link StepExecution} associated with the current step
*/
StepExecution getStepExecution();
/**
* Accessor for the parent context.
*
* @return the parent of this context (or null if there isn't one)
*/
StepContext getParent();
/**
* Register a destruction callback for the end of life of the scope.
*/
void registerDestructionCallback(String name, Runnable callback);
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
/**
* Marker interface for beans to be injected with a {@link StepContext}. Useful
* for business logic implementations that want to store some state in the
* context, to communicate between iterations, or with an enclosing executor.<br/>
*
* A bean which is step scoped which also implements this interface will be
* injected with the context at the start of the bean lifecycle.
*
* @author Dave Syer
*
*/
public interface StepContextAware {
/**
* Callback for injection of {@link StepContext}.
*
* @param context
* the current context supplied by framework.
*/
void setStepContext(StepContext context);
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.core.Ordered;
/**
* Scope for step context. Objects in this scope with &lt;aop:scoped-proxy/&gt;
* use the Spring container as an object factory, so there is only one instance
* of such a bean per executing step.
*
* @author Dave Syer
*
*/
public class StepScope implements Scope,
BeanFactoryPostProcessor, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE;
public void setOrder(int order) {
this.order = order;
}
public int getOrder() {
return order;
}
/**
* Context key for clients to use for conversation identifier.
*/
public static final String ID_KEY = "JOB_IDENTIFIER";
private String name = "step";
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#get(java.lang.String,
* org.springframework.beans.factory.ObjectFactory)
*/
public Object get(String name, ObjectFactory objectFactory) {
SimpleStepContext context = getContext();
Object scopedObject = context.getAttribute(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
context.setAttribute(name, scopedObject);
if (scopedObject instanceof StepContextAware) {
((StepContextAware) scopedObject).setStepContext(context);
}
}
return scopedObject;
}
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#getConversationId()
*/
public String getConversationId() {
SimpleStepContext context = getContext();
Object id = context.getAttribute(ID_KEY);
return "" + id;
}
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#registerDestructionCallback(java.lang.String,
* java.lang.Runnable)
*/
public void registerDestructionCallback(String name, Runnable callback) {
StepContext context = getContext();
context.registerDestructionCallback(name, callback);
}
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.config.Scope#remove(java.lang.String)
*/
public Object remove(String name) {
SimpleStepContext context = getContext();
return context.removeAttribute(name);
}
/**
* Get an attribute accessor in the form of a {@link SimpleStepContext} that
* can be used to store scoped bean instances.
*
* @return the current step context which we can use as a scope storage
* medium
*/
private SimpleStepContext getContext() {
SimpleStepContext context = StepSynchronizationManager.getContext();
if (context == null) {
throw new IllegalStateException(
"No context holder available for step scope");
}
return context;
}
/**
* Register this scope with the enclosing BeanFactory.
*
* @param beanFactory
* the BeanFactory to register with
* @throws BeansException
* if there is a problem.
*/
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
beanFactory.registerScope(name, this);
}
/**
* Public setter for the name property. This can then be used as a bean
* definition attribute, e.g. scope="step". Defaults to "step".
*
* @param name
* the name to set for this scope.
*/
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.scope;
/**
* @author Dave Syer
*
*/
public class StepSynchronizationManager {
private static final ThreadLocal contextHolder = new ThreadLocal();
/**
* Getter for the current context..
*
* @return the current {@link SimpleStepContext} or null if there is none (if
* we are not in a step).
*/
public static SimpleStepContext getContext() {
return (SimpleStepContext) contextHolder.get();
}
/**
* Method for registering a context - should only be used by
* {@link StepExecutor} implementations to ensure that {@link #getContext()}
* always returns the correct value.
*
* @return a new context at the start of a batch.
*/
public static SimpleStepContext open() {
StepContext oldSession = getContext();
SimpleStepContext context = new SimpleStepContext(oldSession);
StepSynchronizationManager.contextHolder.set(context);
return context;
}
/**
* Method for de-registering the current context - should only be used by
* {@link StepExecutor} implementations to ensure that {@link #getContext()}
* always returns the correct value.
*
* @return the old value if there was one.
*/
public static StepContext close() {
SimpleStepContext oldSession = getContext();
if (oldSession==null) {
return null;
}
oldSession.close();
StepContext context = oldSession.getParent();
StepSynchronizationManager.contextHolder.set(context);
return context;
}
/**
* Used internally by {@link StepExecutor} implementations to clear the
* current context at the end of a batch.
*
* @return the old value if there was one.
*/
public static StepContext clear() {
StepContext context = getContext();
StepSynchronizationManager.contextHolder.set(null);
return context;
}
}

View File

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

View File

@@ -0,0 +1,69 @@
/*
* 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.configuration.StepConfiguration;
import org.springframework.batch.core.configuration.StepConfigurationSupport;
import org.springframework.batch.repeat.exception.handler.ExceptionHandler;
import org.springframework.beans.factory.BeanNameAware;
/**
* A {@link StepConfiguration} implementation that provides common behaviour to
* subclasses. Implements {@link BeanNameAware} so that if no name is provided
* explicitly it will be inferred from the bean definition in Spring
* configuration.
*
* @author Dave Syer
*
*/
public class AbstractStepConfiguration extends StepConfigurationSupport {
private int skipLimit = 0;
private ExceptionHandler exceptionHandler;
/**
* Default constructor.
*/
public AbstractStepConfiguration() {
super();
}
/**
* Convenient constructor for setting only the name property.
* @param name
*/
public AbstractStepConfiguration(String name) {
super(name);
}
public ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
public void setExceptionHandler(ExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
public void setSkipLimit(int skipLimit) {
this.skipLimit = skipLimit;
}
public int getSkipLimit() {
return skipLimit;
}
}

View File

@@ -0,0 +1,135 @@
/*
* 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.configuration.StepConfiguration;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.core.executor.StepExecutorFactory;
import org.springframework.batch.execution.step.simple.SimpleStepExecutor;
import org.springframework.batch.repeat.RepeatOperations;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* A {@link StepExecutorFactory} that uses a prototype bean in the application
* context to satisfy the factory contract. If the prototype bean and
* {@link StepConfiguration} are of known (simple) type, they can be combined to
* add the commit interval information from the configuration.<br/>
*
* The nominated bean has to be a prototype because its state may be changed
* before it is used, applying values for things like commit interval from the
* {@link StepConfiguration}.
*
* @author Dave Syer
*
*/
public class PrototypeBeanStepExecutorFactory implements StepExecutorFactory,
BeanFactoryAware, InitializingBean {
private String stepExecutorName = null;
private BeanFactory beanFactory;
/**
* Setter for injected {@link BeanFactory}.
*
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
/**
* Assert that if the step executor name is provided, then it is valid and
* is of prototype scope.
*
* @see InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
// Make an assertion that the bean exists and is of the correct type
Assert
.notNull(beanFactory.getBean(stepExecutorName,
StepExecutor.class),
"Step executor name must correspond to a StepExecutor instance.");
Assert.state(beanFactory.isPrototype(stepExecutorName),
"StepExecutor must be a prototype. Change the scope of the bean named '"
+ stepExecutorName + "' to prototype.");
}
/**
* Locate a {@link StepExecutor} for this configuration, allowing different
* strategies for configuring the inner loop (chunk operations). Try the
* following in this order, until one succeeds. In each case first obtain
* the {@link StepExecutor} referred to by the {@link #stepExecutorName},
* then:
* <ul>
*
* <li>If the {@link StepExecutor} refers to a {@link SimpleStepExecutor},
* and {@link StepConfiguration} is an instance of
* {@link RepeatOperationsHolder}, then the {@link RepeatOperations} for
* the chunk will be pulled from there directly. This gives maximum
* flexibility for clients to control the properties of the iteration. For
* simple use cases where clients only need to control a few aspects of the
* execution, like the commit interval, this is not necessary.</li>
*
* <li>If the {@link StepExecutor} is a {@link SimpleStepExecutor} and the
* configuration is a {@link SimpleStepConfiguration} then this
* implementation modifies the state of the {@link StepExecutor} to set the
* completion policy of the chunk operations. In this case the chunk
* operations cannot be set by the client of this factory.</li>
*
* <li> Use the {@link StepExecutor} directly. </li>
*
* </ul>
* <br/>
*
* @throws IllegalStateException
* if no {@link StepExecutor} can be located.
*
* @see StepExecutorFactory#getExecutor(StepConfiguration)
*/
public StepExecutor getExecutor(StepConfiguration configuration) {
StepExecutor executor = getStepExecutor();
executor.applyConfiguration(configuration);
return executor;
}
/**
* Setter for the bean name of the {@link StepExecutor} to use. The
* corresponding bean must be prototype scoped, so that its properties can
* be overridden per execution by the {@link StepConfiguration}.
*
* @param stepExecutor
* the stepExecutor to set
*/
public void setStepExecutorName(String stepExecutorName) {
this.stepExecutorName = stepExecutorName;
}
/**
* Internal convenience method to get a step executor instance.
*
* @return the step executor instance to use.
*/
private StepExecutor getStepExecutor() {
return (StepExecutor) beanFactory.getBean(stepExecutorName,
StepExecutor.class);
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2006-2007 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.batch.execution.step;
import org.springframework.batch.core.configuration.StepConfiguration;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.repeat.RepeatOperations;
/**
* Marker interface for indicating that a {@link RepeatOperations} instance is
* available for the inner loop (chunk operations) and outer loop (step
* operations) in a {@link StepExecutor}. The inner loop is normally going to
* be in-process and thread-bound so it makes sense for
* {@link StepConfiguration} implementations to be able to override the
* strategies that control that loop.
*
* @author Dave Syer
*
*/
public interface RepeatOperationsHolder {
/**
* Principal method in the {@link RepeatOperationsHolder} interface.
*
* @return a {@link RepeatOperations} which can be used to iterate over an
* inner loop (chunk).
*/
RepeatOperations getChunkOperations();
/**
* Additional method in the {@link RepeatOperationsHolder} interface.
*
* @return a {@link RepeatOperations} which can be used to iterate over an
* outer loop (step).
*/
RepeatOperations getStepOperations();
}

View File

@@ -0,0 +1,73 @@
/*
* 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.configuration.StepConfiguration;
import org.springframework.batch.repeat.RepeatOperations;
/**
* {@link StepConfiguration} implementation that allows full configuration of
* the {@link RepeatOperations} that will be used in the chunk (inner loop).
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class RepeatOperationsStepConfiguration extends AbstractStepConfiguration implements RepeatOperationsHolder {
// default chunkOperations is null
private RepeatOperations chunkOperations;
// default stepOperations is null
private RepeatOperations stepOperations;
/**
* Public accessor for the chunkOperations property.
*
* @return the executor
*/
public RepeatOperations getChunkOperations() {
return chunkOperations;
}
/**
* Public setter for the chunkOperations.
*
* @param chunkOperations the repeatOperations to set
*/
public void setChunkOperations(RepeatOperations chunkOperations) {
this.chunkOperations = chunkOperations;
}
/**
* Public accessor for the stepOperations property.
*
* @return the stepOperations
*/
public RepeatOperations getStepOperations() {
return stepOperations;
}
/**
* Public setter for the {@link RepeatOperations} property.
*
* @param stepOperations the stepOperations to set
*/
public void setStepOperations(RepeatOperations stepOperations) {
this.stepOperations = stepOperations;
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.configuration.StepConfiguration;
import org.springframework.batch.core.tasklet.Tasklet;
/**
* Simple {@link StepConfiguration} good enough for most purposes and easy to
* configure simple properties, principally the commit interval.
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class SimpleStepConfiguration extends AbstractStepConfiguration {
// default commit interval is one
private int commitInterval = 1;
public SimpleStepConfiguration() {
super();
}
public SimpleStepConfiguration(String name) {
super(name);
}
public SimpleStepConfiguration(Tasklet module) {
this();
setTasklet(module);
}
public void setCommitInterval(int commitInterval) {
this.commitInterval = commitInterval;
}
public int getCommitInterval() {
return commitInterval;
}
}

View File

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

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.step.simple;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.core.tasklet.Recoverable;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
/**
* Adds some recovery behaviour to {@link SimpleStepExecutor}.
*
* @author Dave Syer
*
*/
public class DefaultStepExecutor extends SimpleStepExecutor {
/**
* Extends {@link SimpleStepExecutor#doTaskletProcessing(Tasklet, StepInstance)} to
* add some basic recovery behaviour. If the {@link Tasklet} implements
* {@link Recoverable} <em>and</em> {@link Skippable} then the recovery
* and skip methods are called. The recovery is done in a new transaction,
* started with propagation
* {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW} so that the
* inevitable rollback on the main processing loop does not cause the
* recovery to roll back as well.
*
* @throws Exception whenever {@link SimpleStepExecutor} would, but takes
* the recovery path first.
*
* @see org.springframework.batch.execution.step.simple.SimpleStepExecutor#doTaskletProcessing(org.springframework.batch.core.tasklet.Tasklet,
* org.springframework.batch.core.domain.StepInstance)
*/
protected ExitStatus doTaskletProcessing(Tasklet module, final StepExecution step) throws Exception {
ExitStatus exitStatus = ExitStatus.CONTINUABLE;
try {
exitStatus = super.doTaskletProcessing(module, step);
}
catch (final Exception e) {
if (module instanceof Recoverable && module instanceof Skippable) {
final Recoverable recoverable = (Recoverable) module;
new TransactionTemplate(transactionManager, new DefaultTransactionDefinition(
TransactionDefinition.PROPAGATION_REQUIRES_NEW)).execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
recoverable.recover(e);
return null;
}
});
}
if (module instanceof Skippable) {
((Skippable) module).skip();
}
// Rethrow so that outer transaction is rolled back properly
throw e;
}
return exitStatus;
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.simple;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.core.executor.StepInterruptedException;
import org.springframework.batch.repeat.ExitStatus;
/**
* <p>
* Simple implementation of {@link ExitCodeExceptionClassifier} 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 SimpleExitCodeExceptionClassifier implements
ExitCodeExceptionClassifier {
/*
* (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 StepInterruptedException) {
exitStatus = new ExitStatus(false, STEP_INTERRUPTED,
StepInterruptedException.class.getName());
} else {
String message = "";
if (throwable!=null) {
message = throwable.getClass().getName();
if (throwable.getMessage()!=null) {
message += ": " + throwable.getMessage();
}
}
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,488 @@
/*
* 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.simple;
import java.sql.Timestamp;
import java.util.Iterator;
import java.util.Properties;
import org.springframework.batch.core.configuration.StepConfiguration;
import org.springframework.batch.core.domain.BatchStatus;
import org.springframework.batch.core.domain.StepExecution;
import org.springframework.batch.core.domain.StepInstance;
import org.springframework.batch.core.executor.ExitCodeExceptionClassifier;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.core.executor.StepInterruptedException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.execution.scope.SimpleStepContext;
import org.springframework.batch.execution.scope.StepScope;
import org.springframework.batch.execution.scope.StepSynchronizationManager;
import org.springframework.batch.execution.step.RepeatOperationsHolder;
import org.springframework.batch.execution.step.SimpleStepConfiguration;
import org.springframework.batch.io.exception.BatchCriticalException;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatCallback;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.RepeatOperations;
import org.springframework.batch.repeat.exception.handler.ExceptionHandler;
import org.springframework.batch.repeat.exception.handler.SimpleLimitExceptionHandler;
import org.springframework.batch.repeat.policy.SimpleCompletionPolicy;
import org.springframework.batch.repeat.support.RepeatTemplate;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
/**
* Simple implementation of {@link StepExecutor} 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
*
*/
public class SimpleStepExecutor implements StepExecutor {
/**
* Context attribute key for step execution. Used by monitoring and managing
* clients to inspect current step execution.
*/
private static final String STEP_EXECUTION_KEY = "STEP_EXECUTION";
private RepeatOperations chunkOperations = new RepeatTemplate();
private RepeatOperations stepOperations = new RepeatTemplate();
private JobRepository jobRepository;
private ExitCodeExceptionClassifier exceptionClassifier = new SimpleExitCodeExceptionClassifier();
// default to checking current thread for interruption.
private StepInterruptionPolicy interruptionPolicy = new ThreadStepInterruptionPolicy();
// Not for production use...
protected PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
public void setTransactionManager(
PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Injected strategy for storage and retrieval of persistent step
* information. Mandatory property.
*
* @param jobRepository
*/
public void setRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* 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;
}
/**
* 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 StepInterruptedException
* if the step or a chunk is interrupted
* @throws RuntimeException
* if there is an exception during a chunk execution
* @see StepExecutor#process(StepConfiguration, StepExecution)
*/
public ExitStatus process(final StepConfiguration configuration,
final StepExecution stepExecution) throws BatchCriticalException,
StepInterruptedException {
final StepInstance step = stepExecution.getStep();
boolean isRestart = step.getStepExecutionCount() > 0 ? true : false;
Assert.notNull(step);
final Tasklet module = configuration.getTasklet();
ExitStatus status = ExitStatus.FAILED;
final SimpleStepContext stepScopeContext = StepSynchronizationManager
.open();
stepScopeContext.setStepExecution(stepExecution);
// Add the job identifier so that it can be used to identify
// the conversation in StepScope
stepScopeContext.setAttribute(StepScope.ID_KEY, stepExecution
.getJobExecution().getJob().getIdentifier());
try {
stepExecution
.setStartTime(new Timestamp(System.currentTimeMillis()));
updateStatus(stepExecution, BatchStatus.STARTED);
final boolean saveRestartData = configuration.isSaveRestartData();
if (saveRestartData && isRestart) {
restoreFromRestartData(module, step.getRestartData());
}
status = stepOperations.iterate(new RepeatCallback() {
public ExitStatus doInIteration(final RepeatContext context)
throws Exception {
stepExecution.getJobExecution()
.registerStepContext(context);
context.registerDestructionCallback(
"STEP_EXECUTION_CONTEXT_CALLBACK", new Runnable() {
public void run() {
stepExecution.getJobExecution()
.unregisterStepContext(context);
}
});
// Add the step execution as an attribute so monitoring
// clients can see it.
context.setAttribute(STEP_EXECUTION_KEY, stepExecution);
// Before starting a new transaction, check for
// interruption.
interruptionPolicy.checkInterrupted(context);
ExitStatus result;
try {
result = (ExitStatus) new TransactionTemplate(
transactionManager)
.execute(new TransactionCallback() {
public Object doInTransaction(
TransactionStatus status) {
// New transaction obtained,
// resynchronize
// TransactionSyncrhonization objects
BatchTransactionSynchronizationManager
.resynchronize();
ExitStatus result;
result = processChunk(configuration,
stepExecution);
if (saveRestartData) {
step
.setRestartData(getRestartData(module));
jobRepository.update(step);
}
Properties statistics = getStatistics(module);
stepExecution.setStatistics(statistics);
stepExecution.incrementCommitCount();
jobRepository
.saveOrUpdate(stepExecution);
return result;
}
});
} 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.incrementRollbackCount();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RuntimeException(t);
}
}
// Check for interruption after transaction as well, so that
// the interrupted exception is correctly propagated up to
// caller
interruptionPolicy.checkInterrupted(context);
return result;
}
});
updateStatus(stepExecution, BatchStatus.COMPLETED);
return status;
} catch (RuntimeException e) {
// classify exception so an exit code can be stored.
status = exceptionClassifier.classifyForExitCode(e);
if (e.getCause() instanceof StepInterruptedException) {
updateStatus(stepExecution, BatchStatus.STOPPED);
throw (StepInterruptedException) e.getCause();
} else {
updateStatus(stepExecution, BatchStatus.FAILED);
throw e;
}
} finally {
stepExecution.setExitStatus(status);
stepExecution.setEndTime(new Timestamp(System.currentTimeMillis()));
try {
jobRepository.saveOrUpdate(stepExecution);
} finally {
// clear any registered synchronizations
try {
StepSynchronizationManager.close();
} finally {
BatchTransactionSynchronizationManager
.clearSynchronizations();
}
}
}
}
/**
* Convenience method to update the status in all relevant places.
*
* @param step
* the current step
* @param stepExecution
* the current stepExecution
* @param status
* the status to set
*/
private void updateStatus(StepExecution stepExecution, BatchStatus status) {
StepInstance step = stepExecution.getStep();
stepExecution.setStatus(status);
step.setStatus(status);
jobRepository.update(step);
jobRepository.saveOrUpdate(stepExecution);
for (Iterator iter = stepExecution.getJobExecution().getStepContexts()
.iterator(); iter.hasNext();) {
RepeatContext context = (RepeatContext) iter.next();
context.setAttribute("JOB_STATUS", status);
}
}
/**
* 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 configuration
* the current step configuration
* @param stepExecution
* the current step, containing the {@link Tasklet} with the
* business logic.
* @return true if there is more data to process.
*/
protected final ExitStatus processChunk(
final StepConfiguration configuration,
final StepExecution stepExecution) {
return chunkOperations.iterate(new RepeatCallback() {
public ExitStatus doInIteration(final RepeatContext context)
throws Exception {
stepExecution.getJobExecution().registerChunkContext(context);
context.registerDestructionCallback(
"CHUNK_EXECUTION_CONTEXT_CALLBACK", new Runnable() {
public void run() {
stepExecution.getJobExecution()
.unregisterStepContext(context);
}
});
// check for interruption before each item as well
interruptionPolicy.checkInterrupted(context);
ExitStatus exitStatus = doTaskletProcessing(configuration
.getTasklet(), stepExecution);
stepExecution.incrementTaskCount();
// check for interruption after each item as well
interruptionPolicy.checkInterrupted(context);
return exitStatus;
}
});
}
/**
* Execute the business logic, delegating to the given {@link Tasklet}.
* Subclasses could extend the behaviour as long as they always return the
* value of this method call in their superclass.
*
* @param tasklet
* the unit of business logic to execute
* @param stepExecution
* the current step
* @return boolean if there is more processing to do
* @throws Exception
* if there is an error
*/
protected ExitStatus doTaskletProcessing(Tasklet tasklet,
StepExecution stepExecution) throws Exception {
return tasklet.execute();
}
/**
* @param tasklet
* @return restart data from the {@link Tasklet} if it is
* {@link Restartable}
*/
private RestartData getRestartData(Tasklet tasklet) {
if (tasklet instanceof Restartable) {
return ((Restartable) tasklet).getRestartData();
} else {
return null;
}
}
private void restoreFromRestartData(Tasklet tasklet, RestartData restartData) {
if (tasklet instanceof Restartable && restartData != null) {
((Restartable) tasklet).restoreFrom(restartData);
}
}
private Properties getStatistics(Tasklet tasklet) {
if (tasklet instanceof StatisticsProvider) {
return ((StatisticsProvider) tasklet).getStatistics();
} else {
return null;
}
}
/**
* Setter for the {@link StepInterruptionPolicy}. The policy is used to
* check whether an external request has been made to interrupt the job
* execution.
*
* @param interruptionPolicy
* a {@link StepInterruptionPolicy}
*/
public void setInterruptionPolicy(StepInterruptionPolicy interruptionPolicy) {
this.interruptionPolicy = interruptionPolicy;
}
/**
* Setter for the {@link ExitCodeExceptionClassifier} that will be used to
* classify any exception that causes a job to fail.
*
* @param exceptionClassifier
*/
public void setExceptionClassifier(
ExitCodeExceptionClassifier exceptionClassifier) {
this.exceptionClassifier = exceptionClassifier;
}
/**
* Apply the configuration by inspecting it to see if it has any relevant
* policy information.
* <ul>
* <li> If the configuration is a {@link RepeatOperationsHolder} then we use
* the provided {@link RepeatOperations} instances for chunk and step. </li>
* <li> If the configuration is a {@link SimpleStepConfiguration} then we
* apply the commit interval at the chunk level and the exception handler at
* the step level, provided the existing repeat operations are instances of
* {@link RepeatTemplate}. In addition if there is a non-zero skip limit
* and no {@link ExceptionHandler} then we inject a
* {@link SimpleLimitExceptionHandler} with that limit.</li>
* </ul>
*
* @param configuration
* a step configuration
*/
public void applyConfiguration(StepConfiguration configuration) {
if (configuration instanceof RepeatOperationsHolder) {
RepeatOperationsHolder holder = (RepeatOperationsHolder) configuration;
RepeatOperations chunkOperations = holder.getChunkOperations();
RepeatOperations stepOperations = holder.getStepOperations();
Assert
.state(chunkOperations != null,
"Chunk operations obtained from step configuration must be non-null.");
if (chunkOperations != null) {
setChunkOperations(chunkOperations);
}
if (stepOperations != null) {
setStepOperations(stepOperations);
}
} else if (configuration instanceof SimpleStepConfiguration) {
SimpleStepConfiguration simpleConfiguation = (SimpleStepConfiguration) configuration;
if (this.chunkOperations instanceof RepeatTemplate) {
RepeatTemplate template = (RepeatTemplate) this.chunkOperations;
template.setCompletionPolicy(new SimpleCompletionPolicy(
simpleConfiguation.getCommitInterval()));
}
ExceptionHandler exceptionHandler = simpleConfiguation
.getExceptionHandler();
if (simpleConfiguation.getSkipLimit() > 0
&& exceptionHandler == null) {
SimpleLimitExceptionHandler handler = new SimpleLimitExceptionHandler();
handler.setLimit(simpleConfiguation.getSkipLimit());
exceptionHandler = handler;
}
if (this.stepOperations instanceof RepeatTemplate
&& exceptionHandler != null) {
RepeatTemplate template = (RepeatTemplate) this.stepOperations;
template.setExceptionHandler(exceptionHandler);
}
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.simple;
import org.springframework.batch.core.configuration.StepConfiguration;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.core.executor.StepExecutorFactory;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.execution.step.SimpleStepConfiguration;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* A {@link StepExecutorFactory} that only knows how to create
* {@link SimpleStepExecutor} instances.
*
* @author Dave Syer
*
*/
public class SimpleStepExecutorFactory implements StepExecutorFactory,
InitializingBean {
private JobRepository jobRepository;
/**
* Create a {@link SimpleStepExecutor} for this configuration. If the
* configuration is a {@link SimpleStepConfiguration} then a
* {@link StepExecutor} is created with policies matching the commit
* interval of the configuration. <br/>
*
* @throws IllegalStateException
* if the configuration is not a {@link SimpleStepConfiguration}.
* @throws IllegalStateException
* if the {@link JobRepository} is null.
*
* @see StepExecutorFactory#getExecutor(StepConfiguration)
*/
public StepExecutor getExecutor(StepConfiguration configuration) {
Assert.notNull(jobRepository, "JobRepository cannot be null");
Assert.state(configuration instanceof SimpleStepConfiguration,
"StepConfiguration must be instance of SimpleStepConfiguration - found: ["
+ (configuration == null ? null : configuration
.getClass()) + "]");
SimpleStepExecutor executor = new SimpleStepExecutor();
executor.setRepository(jobRepository);
executor.applyConfiguration(configuration);
return executor;
}
/**
* Public setter for {@link JobRepository}.
*
* @param jobRepository
* is a mandatory dependence (no default).
*/
public void setJobRepository(JobRepository jobRepository) {
this.jobRepository = jobRepository;
}
/**
* Assert that all mandatory properties are set (the {@link JobRepository}).
*
* @throws Exception
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jobRepository);
}
}

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.simple;
import org.springframework.batch.core.executor.StepExecutor;
import org.springframework.batch.core.executor.StepInterruptedException;
import org.springframework.batch.repeat.RepeatContext;
/**
* Strategy interface for an interruption policy. This policy allows
* {@link StepExecutor} 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 StepInterruptedException}.
* @param context the current context of the running step.
*
* @throws StepInterruptedException when the job has been interrupted.
*/
void checkInterrupted(RepeatContext context) throws StepInterruptedException;
}

View File

@@ -0,0 +1,52 @@
/*
* 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.simple;
import org.springframework.batch.core.executor.StepInterruptedException;
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 {
/**
* Returns if the current job lifecycle has been interrupted by checking if
* the current thread is interrupted.
*/
public void checkInterrupted(RepeatContext context) throws StepInterruptedException {
if (isInterrupted(context)) {
throw new StepInterruptedException("Job interrupted status detected.");
}
}
/**
* TODO: add more interruption policies: they should all check context.isTerminateOnly()
* @param context the current context
* @return true if the job has been interrupted
*/
private boolean isInterrupted(RepeatContext context) {
return Thread.currentThread().isInterrupted() || context.isTerminateOnly();
}
}

View File

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

View File

@@ -0,0 +1,283 @@
/*
* 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 java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.batch.core.tasklet.Recoverable;
import org.springframework.batch.core.tasklet.Tasklet;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemProvider;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.synch.RepeatSynchronizationManager;
import org.springframework.batch.retry.RetryOperations;
import org.springframework.batch.retry.RetryPolicy;
import org.springframework.batch.retry.callback.ItemProviderRetryCallback;
import org.springframework.batch.retry.policy.ItemProviderRetryPolicy;
import org.springframework.batch.retry.support.RetryTemplate;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* A concrete implementation of the {@link Tasklet} interface that provides
* functionality for 'split processing'. This type of processing is
* characterized by separating the reading and processing of batch data into two
* separate classes: ItemProvider and ItemProcessor. The ItemProvider class
* provides a solid means for re-usability and enforces good architecture
* practices. Because an object *must* be returned by the {@link ItemProvider}
* to continue processing, (returning null indicates processing should end) a
* developer is forced to read in all relevant data, place it into a domain
* object, and return that object. The {@link ItemProcessor} will then use this
* object for calculations and output.<br/>
*
* If a {@link RetryPolicy} is provided it will be used to construct a stateful
* retry around the {@link ItemProcessor}, delegating recover and identity
* concerns to the {@link ItemProvider}. In this case clients of this class do
* not need to take any additional action at runtime to take advantage of the
* retry and recovery, provided the {@link #execute()} method is called again
* with the {@link ItemProvider} in the same state (normally this would be the
* case because a transaction would have rolled back and the item would be
* represented).<br/>
*
* If neither a {@link RetryPolicy} nor a {@link RetryOperations} is provided
* then the {@link Recoverable} interface can be used to attempt to recover
* immediately (with no retry) from a processing error. Clients of this class
* must call {@link Recoverable#recover(Throwable)} directly, which is simply
* delegated to {@link ItemProvider#recover(Object, Throwable)}.
*
* @see ItemProvider
* @see ItemProcessor
* @see RetryPolicy
* @see Recoverable
*
* @author Lucas Ward
* @author Dave Syer
* @author Robert Kasanicky
*
*/
public class ItemProviderProcessTasklet implements Tasklet, Recoverable, Skippable, StatisticsProvider,
InitializingBean {
/**
* Prefix added to statistics keys from processor if needed to avoid
* ambiguity between provider and processor.
*/
public static final String PROCESSOR_STATISTICS_PREFIX = "processor.";
/**
* Prefix added to statistics keys from provider if needed to avoid
* ambiguity between provider and processor.
*/
public static final String PROVIDER_STATISTICS_PREFIX = "provider.";
/**
* Attribute key in the surrounding {@link RepeatContext} for the current
* item being processed. Needed to provide recoverable behavior if
* {@link RetryOperations} are not provided.
*/
private static final String ITEM_KEY = ItemProviderProcessTasklet.class.getName() + ".ITEM";
private RetryPolicy retryPolicy = null;
private RetryOperations retryOperations = null;
protected ItemProvider itemProvider;
protected ItemProcessor itemProcessor;
/**
* Check mandatory properties (provider and processor), and ensure that only
* one (or neither) of {@link RetryPolicy} or {@link RetryOperations} is
* provided.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(itemProvider, "ItemProvider must be provided");
Assert.notNull(itemProcessor, "ItemProcessor must be provided");
Assert.state(!(retryPolicy != null && retryOperations != null),
"Either RetryOperations or RetryPolicy can be provided, but not both.");
if (retryPolicy != null) {
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(new ItemProviderRetryPolicy(retryPolicy));
retryOperations = template;
}
}
/**
* Read from the {@link ItemProvider} and process (if not null) with the
* {@link ItemProcessor}. The call to {@link ItemProcessor} is wrapped in a
* retry, if either a {@link RetryPolicy} or a {@link RetryOperations} is
* provided.
*
* @see org.springframework.batch.core.tasklet.Tasklet#execute()
*/
public ExitStatus execute() throws Exception {
if (retryOperations != null) {
return new ExitStatus(retryOperations.execute(new ItemProviderRetryCallback(itemProvider, itemProcessor)) != null);
}
else {
Object data = itemProvider.next();
if (data == null) {
return ExitStatus.FINISHED;
}
RepeatContext context = RepeatSynchronizationManager.getContext();
Assert.state(context != null,
"No context available: you probably need to use this class inside a batch operation.");
context.setAttribute(ITEM_KEY, data);
itemProcessor.process(data);
// No exception so clear context (we can't recover directly because
// the current transaction is going to roll back)
context.removeAttribute(ITEM_KEY);
return ExitStatus.CONTINUABLE;
}
}
/**
* Call out to the provider for recovery step.
*
* @see org.springframework.batch.core.tasklet.Recoverable#recover(java.lang.Throwable)
*/
public void recover(Throwable cause) {
RepeatContext context = RepeatSynchronizationManager.getContext();
Assert.state(context != null,
"No context available: you probably need to use this class inside a batch operation.");
try {
Object data = context.getAttribute(ITEM_KEY);
itemProvider.recover(data, cause);
}
finally {
context.removeAttribute(ITEM_KEY);
}
}
/**
* @param itemProvider
*/
public void setItemProvider(ItemProvider itemProvider) {
this.itemProvider = itemProvider;
}
/**
* @param moduleProcessor
*/
public void setItemProcessor(ItemProcessor moduleProcessor) {
this.itemProcessor = moduleProcessor;
}
/**
* If the provider and / or processor are {@link Skippable} then delegate to
* them in that order.
*
* @see org.springframework.batch.io.Skippable#skip()
*/
public void skip() {
if (this.itemProvider instanceof Skippable) {
((Skippable) this.itemProvider).skip();
}
if (this.itemProcessor instanceof Skippable) {
((Skippable) this.itemProcessor).skip();
}
}
/**
* If the provider and / or processor are {@link StatisticsProvider} then
* delegate to them in that order. If they both implement
* {@link StatisticsProvider} then the property keys are prepended with
* special prefixes to avoid potential ambiguity. The prefixes are only
* prepended in the case of a duplicate key shared between provider and
* processor.
*
* @see org.springframework.batch.io.Skippable#skip()
*/
public Properties getStatistics() {
Properties stats = new Properties();
if (this.itemProvider instanceof StatisticsProvider) {
stats = ((StatisticsProvider) this.itemProvider).getStatistics();
}
if (this.itemProcessor instanceof StatisticsProvider) {
Properties props = ((StatisticsProvider) this.itemProcessor).getStatistics();
if (!stats.isEmpty()) {
stats = prependKeys(stats, props, PROVIDER_STATISTICS_PREFIX, PROCESSOR_STATISTICS_PREFIX);
} else {
stats.putAll(props);
}
}
return stats;
}
/**
* @param props1
* @param string
* @return
*/
private Properties prependKeys(Properties props1, Properties props2, String prefix1, String prefix2) {
Properties result = new Properties();
Set duplicates = new HashSet();
for (Iterator iterator = props1.entrySet().iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (props2.containsKey(key)) {
duplicates.add(key);
continue;
}
result.setProperty(key, value);
}
for (Iterator iterator = props2.entrySet().iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
if (duplicates.contains(key)) {
continue;
}
result.setProperty(key, value);
}
for (Iterator iterator = duplicates.iterator(); iterator.hasNext();) {
String key = (String) iterator.next();
result.setProperty(prefix1+key, props1.getProperty(key));
result.setProperty(prefix2+key, props2.getProperty(key));
}
return result;
}
/**
* Public setter for the retryPolicy.
*
* @param retyPolicy the retryPolicy to set
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
this.retryPolicy = retryPolicy;
}
/**
* Public setter for the retryOperations.
*
* @param retryOperations the retryOperations to set
*/
public void setRetryOperations(RetryOperations retryOperations) {
this.retryOperations = retryOperations;
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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 java.util.Properties;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemProvider;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.support.PropertiesConverter;
/**
* An extension of {@link ItemProviderProcessTasklet} that delegates calls to
* {@link Restartable} to the provider and processor.
*
* @see ItemProvider
* @see ItemProcessor
* @see Restartable
*
* @author Lucas Ward
* @author Dave Syer
*
*/
public class RestartableItemProviderTasklet extends ItemProviderProcessTasklet implements Restartable {
/**
* @see Restartable#getRestartData()
*/
public RestartData getRestartData() {
RestartData itemProviderRestartData = null;
RestartData itemProcessorRestartData = null;
if (itemProvider instanceof Restartable) {
itemProviderRestartData = ((Restartable) itemProvider).getRestartData();
}
if (itemProcessor instanceof Restartable) {
itemProcessorRestartData = ((Restartable) itemProcessor).getRestartData();
}
RestartableItemProviderTaskletRestartData restartData = new RestartableItemProviderTaskletRestartData(itemProviderRestartData, itemProcessorRestartData);
return restartData;
}
/**
* @see Restartable#restoreFrom(RestartData)
*/
public void restoreFrom(RestartData data) {
if (data == null || data.getProperties() == null)
return;
RestartableItemProviderTaskletRestartData moduleRestartData;
if (data instanceof RestartableItemProviderTaskletRestartData) {
moduleRestartData = (RestartableItemProviderTaskletRestartData) data;
}
else {
moduleRestartData = new RestartableItemProviderTaskletRestartData(data.getProperties());
}
if (itemProvider instanceof Restartable) {
((Restartable) itemProvider).restoreFrom(moduleRestartData.providerData);
}
if (itemProcessor instanceof Restartable) {
((Restartable) itemProcessor).restoreFrom(moduleRestartData.processorData);
}
}
private class RestartableItemProviderTaskletRestartData implements RestartData {
private static final String PROVIDER_KEY = "DATA_PROVIDER";
private static final String PROCESSOR_KEY = "DATA_PROCESSOR";
RestartData providerData;
RestartData processorData;
public RestartableItemProviderTaskletRestartData(RestartData providerData, RestartData processorData) {
this.providerData = providerData;
this.processorData = processorData;
}
public RestartableItemProviderTaskletRestartData(Properties data) {
providerData = new GenericRestartData(PropertiesConverter
.stringToProperties(data.getProperty(PROVIDER_KEY)));
processorData = new GenericRestartData(PropertiesConverter.stringToProperties(data
.getProperty(PROCESSOR_KEY)));
}
public Properties getProperties() {
Properties props = new Properties();
if (providerData != null) {
props.setProperty(PROVIDER_KEY, PropertiesConverter.propertiesToString(providerData.getProperties()));
}
if (processorData != null) {
props.setProperty(PROCESSOR_KEY, PropertiesConverter.propertiesToString(processorData.getProperties()));
}
return props;
}
}
}

View File

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

View File

@@ -0,0 +1,9 @@
<html>
<body>
<p>
Reference implementations of the Core API. Provides basic support for
runtime environments like JMX monitors and command-line launchers for
batch processes.
</p>
</body>
</html>

View File

@@ -0,0 +1,9 @@
batch.jdbc.driver=
batch.jdbc.url=
batch.jdbc.user=
batch.jdbc.password=
batch.schema=
batch.jndi.name=
batch.naming.factory.initial=
batch.naming.provider.url=
batch.database.vendor=

View File

@@ -0,0 +1,58 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION ;
DROP TABLE BATCH_JOB_EXECUTION ;
DROP TABLE BATCH_STEP ;
DROP TABLE BATCH_JOB ;
DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_STEP_SEQ ;
DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_JOB_SEQ ;
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_ID BIGINT NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT BIGINT ,
TASK_COUNT BIGINT ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_STEP_SEQ;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_SEQ;

View File

@@ -0,0 +1,58 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION ;
DROP TABLE BATCH_JOB_EXECUTION ;
DROP TABLE BATCH_STEP ;
DROP TABLE BATCH_JOB ;
DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_STEP_SEQ ;
DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_JOB_SEQ ;
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
VERSION BIGINT,
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
VERSION BIGINT NOT NULL,
STEP_ID BIGINT NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT BIGINT ,
TASK_COUNT BIGINT ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_STEP_SEQ;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_SEQ;

View File

@@ -0,0 +1,66 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION IF EXISTS;
DROP TABLE BATCH_JOB_EXECUTION IF EXISTS;
DROP TABLE BATCH_STEP IF EXISTS;
DROP TABLE BATCH_JOB IF EXISTS;
DROP TABLE BATCH_STEP_EXECUTION_SEQ IF EXISTS;
DROP TABLE BATCH_STEP_SEQ IF EXISTS;
DROP TABLE BATCH_JOB_EXECUTION_SEQ IF EXISTS;
DROP TABLE BATCH_JOB_SEQ IF EXISTS;
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID BIGINT IDENTITY PRIMARY KEY ,
VERSION BIGINT,
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID BIGINT IDENTITY PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID BIGINT IDENTITY PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID BIGINT IDENTITY PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_ID BIGINT NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT BIGINT ,
TASK_COUNT BIGINT ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP_EXECUTION_SEQ (
ID BIGINT IDENTITY
);
CREATE TABLE BATCH_STEP_SEQ (
ID BIGINT IDENTITY
);
CREATE TABLE BATCH_JOB_EXECUTION_SEQ (
ID BIGINT IDENTITY
);
CREATE TABLE BATCH_JOB_SEQ (
ID BIGINT IDENTITY
);

View File

@@ -0,0 +1,58 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION ;
DROP TABLE BATCH_JOB_EXECUTION ;
DROP TABLE BATCH_STEP ;
DROP TABLE BATCH_JOB ;
DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_STEP_SEQ ;
DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_JOB_SEQ ;
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID NUMBER(38) PRIMARY KEY ,
VERSION NUMBER(38),
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID NUMBER(38) PRIMARY KEY ,
VERSION NUMBER(38),
JOB_ID NUMBER(38) NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID NUMBER(38) PRIMARY KEY ,
VERSION NUMBER(38),
JOB_ID NUMBER(38) NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID NUMBER(38) PRIMARY KEY ,
VERSION NUMBER(38) NOT NULL,
STEP_ID NUMBER(38) NOT NULL,
JOB_EXECUTION_ID NUMBER(38) NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT NUMBER(38) ,
TASK_COUNT NUMBER(38) ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_STEP_SEQ;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_SEQ;

View File

@@ -0,0 +1,58 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION ;
DROP TABLE BATCH_JOB_EXECUTION ;
DROP TABLE BATCH_STEP ;
DROP TABLE BATCH_JOB ;
DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_STEP_SEQ ;
DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ;
DROP SEQUENCE BATCH_JOB_SEQ ;
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT,
JOB_ID BIGINT NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID BIGINT PRIMARY KEY ,
VERSION BIGINT NOT NULL,
STEP_ID BIGINT NOT NULL,
JOB_EXECUTION_ID BIGINT NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT BIGINT ,
TASK_COUNT BIGINT ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_STEP_SEQ;
CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ;
CREATE SEQUENCE BATCH_JOB_SEQ;

View File

@@ -0,0 +1,6 @@
platform=db2
# SQL language oddities
BIGINT = BIGINT
IDENTITY =
# for generating drop statements...
SEQUENCE = SEQUENCE

View File

@@ -0,0 +1,2 @@
#macro (sequence $name)CREATE SEQUENCE ${name};
#end

View File

@@ -0,0 +1,7 @@
platform=db2
# SQL language oddities
BIGINT = BIGINT
IDENTITY =
GENERATED = GENERATED BY DEFAULT AS IDENTITY
# for generating drop statements...
SEQUENCE = SEQUENCE

View File

@@ -0,0 +1,2 @@
#macro (sequence $name)CREATE SEQUENCE ${name};
#end

View File

@@ -0,0 +1,10 @@
-- Autogenerated: do not edit this file
DROP TABLE BATCH_STEP_EXECUTION $!{IFEXISTS};
DROP TABLE BATCH_JOB_EXECUTION $!{IFEXISTS};
DROP TABLE BATCH_STEP $!{IFEXISTS};
DROP TABLE BATCH_JOB $!{IFEXISTS};
DROP ${SEQUENCE} BATCH_STEP_EXECUTION_SEQ $!{IFEXISTS};
DROP ${SEQUENCE} BATCH_STEP_SEQ $!{IFEXISTS};
DROP ${SEQUENCE} BATCH_JOB_EXECUTION_SEQ $!{IFEXISTS};
DROP ${SEQUENCE} BATCH_JOB_SEQ $!{IFEXISTS};

View File

@@ -0,0 +1,7 @@
platform=hsqldb
# SQL language oddities
BIGINT = BIGINT
IDENTITY = IDENTITY
IFEXISTS = IF EXISTS
# for generating drop statements...
SEQUENCE = TABLE

View File

@@ -0,0 +1,4 @@
#macro (sequence $name)CREATE TABLE ${name} (
ID BIGINT IDENTITY
);
#end

View File

@@ -0,0 +1,47 @@
-- Autogenerated: do not edit this file
CREATE TABLE BATCH_JOB (
ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED},
VERSION ${BIGINT},
JOB_NAME VARCHAR(100) NOT NULL ,
JOB_KEY VARCHAR(250) ,
SCHEDULE_DATE DATE ,
STATUS VARCHAR(10) );
CREATE TABLE BATCH_JOB_EXECUTION (
ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED},
VERSION ${BIGINT},
JOB_ID ${BIGINT} NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
CREATE TABLE BATCH_STEP (
ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED},
VERSION ${BIGINT},
JOB_ID ${BIGINT} NOT NULL,
STEP_NAME VARCHAR(100) NOT NULL,
STATUS VARCHAR(10),
RESTART_DATA VARCHAR(200));
CREATE TABLE BATCH_STEP_EXECUTION (
ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED},
VERSION ${BIGINT} NOT NULL,
STEP_ID ${BIGINT} NOT NULL,
JOB_EXECUTION_ID ${BIGINT} NOT NULL,
START_TIME TIMESTAMP NOT NULL ,
END_TIME TIMESTAMP ,
STATUS VARCHAR(10),
COMMIT_COUNT ${BIGINT} ,
TASK_COUNT ${BIGINT} ,
TASK_STATISTICS VARCHAR(250),
CONTINUABLE CHAR(1),
EXIT_CODE VARCHAR(20),
EXIT_MESSAGE VARCHAR(250));
#sequence( "BATCH_STEP_EXECUTION_SEQ" )
#sequence( "BATCH_STEP_SEQ" )
#sequence( "BATCH_JOB_EXECUTION_SEQ" )
#sequence( "BATCH_JOB_SEQ" )

View File

@@ -0,0 +1,7 @@
platform=oracle10g
# SQL language oddities
BIGINT = NUMBER(38)
IDENTITY =
GENERATED =
# for generating drop statements...
SEQUENCE = SEQUENCE

View File

@@ -0,0 +1,2 @@
#macro (sequence $name)CREATE SEQUENCE ${name};
#end

View File

@@ -0,0 +1,7 @@
platform=postgresql
# SQL language oddities
BIGINT = BIGINT
IDENTITY =
GENERATED =
# for generating drop statements...
SEQUENCE = SEQUENCE

View File

@@ -0,0 +1,2 @@
#macro (sequence $name)CREATE SEQUENCE ${name};
#end

View File

@@ -0,0 +1,4 @@
#parse("${includes}/destroy.sql.vpp")
#parse("${includes}/init.sql.vpp")

View File

@@ -0,0 +1,7 @@
Changelog: Spring Batch Execution
* 1.0-M2
** 2007/07/12
* No-one uses this file: we should just switch to auto-generated changelogs?

View File

@@ -0,0 +1,38 @@
Tried to create an executable jar with this:
+---
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>org.springframework.batch.container.bootstrap.BatchCommandLineLauncher</mainClass>
<addClasspath>true</addClasspath>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
+---
But the resulting MANIFEST.MF is rubbish. Look at the classpath
(where did that come from)?
+---
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: dsyer
Build-Jdk: 1.5.0_09
Main-Class: org.springframework.batch.container.bootstrap.BatchCommand
LineLauncher
Class-Path: spring-2.1-m2.jar commons-logging-1.1.jar log4j-1.2.12.jar
dom4j-1.6.1.jar commons-lang-2.1.jar spring-batch-infrastructure-1.0
-m2-SNAPSHOT.jar antlr-2.7.6.jar commons-collections-2.1.1.jar hibern
ate-3.2.3.ga.jar spring-mock-2.1-m2.jar ehcache-1.2.3.jar
+---

View File

@@ -0,0 +1,46 @@
------
Glossary
------
Wayne Lund
------
May 2007
[[1]]<<Batch>>: An accumulation of business transactions over time.
[[2]]<<Batch Application Style>>: Term used to designate batch as an application style in its own right similar to online, Web or SOA. It has standard elements of input, validation, transformation of information to business model, business processing and output. In addition, it requires monitoring at a macro level.
[[3]]<<Batch Processing>>: The handling of a batch of many business transactions that have accumulated over a period of time (e.g. an hour, day, week, month, or year). It is the application of a process, or set of processes, to many data entities or objects in a repetitive and predictable fashion with either no manual element, or a separate manual element for error processing.
[[4]]<<Batch Window>>: The time frame within which a batch job must complete. This can be constrained by other systems coming online, other dependent jobs needing to execute or other factors specific to the batch environment.
[[5]]<<Step Controller>>: It is the main batch task or Unit of Work controller. It initializes the module, and controls the transaction environment based on commit interval setting, etc.
[[6]]<<Tasklet>>: The main application program created by application developer to process the business logic for each LUW.
[[7]]<<Batch Job Type>>: Job Types describe application of jobs for particular type of processing. Common areas are interface processing (typically flat files), forms processing (either for online pdf generation or print formats), report processing. s
[[8]]<<Driving Query>>: A driving query identifies the set of work for a job to do; the job then breaks that work into individual units of work. For instance, identify all financial transactions that have a status of "pending transmission" and send them to our partner system. The driving query returns a set of record IDs to process; each record ID then becomes a unit of work. A driving query may involve a join (if the criteria for selection falls across two or more tables) or it may work with a single table.
[[9]]<<Logicial Unit of Work (LUW)>>: A batch job iterates through a driving query (or another input source such as a file) to perform the set of work that the job must accomplish. Each iteration of work performed is a unit of work.
[[10]]<<Commit Interval>>: A set of LUWs constitute a commit interval.
[[11]]<<Partitioning>>: Splitting a job into multiple threads where each thread is responsible for a subset of the overall data to be processed. The threads of execution may be within the same JVM or they may span JVMs in a clustered environment that supports workload balancing.
[[12]]<<Staging Table>>: A table that holds temporary data while it is being processed.
[[13]]<<Restartable>>: - a job that can be executed again and will assume the same identity as when run initially. In othewords, it is has the same job instance id.
[[14]]Rerunnable - a job that is restartable and manages it's own state in terms of previous run's record processing. Note>>: Rerunnable is tied to the driving query. If the query can be formed so that it will limit the processed rows when the job is restarted than re-runnable = true. Often times a condition is added to the where statement to limit the rows returned by the driving query with something like "and processedFlag != true".
---------------------------------------------------------------------
Note: If its false the architecture assumes responsibility for tracking which rows have been processed. There is a default strategy for tracking the last record processed by partition. Most batch jobs only have one partition. The option is only valid for a restartable job. The reason being is that we have to persist the restart data which is only available on a restartable job.
In DSL it is the following:
StartOver ::= restartable = false. Restartable ::= true | false
If (Restartable)
re-runnable ::= true | false
We don't persist restart information for a non-restartable job. As you can see, it doesn't make sense. Rerunnable has always confused the best of us.
----------------------------------------------------------------------------

View File

@@ -0,0 +1,61 @@
------
Simple Batch Execution Container Overview
------
Scott Wintermute
------
May 2007
Overview of the Spring Batch Simple Batch Execution Container
The diagram below provides an overview of the high level components, technical services, and basic operations required by a batch architecture. This architecture framework is a blueprint that has been proven through decades of implementations on the last several generations of platforms (COBOL/Mainframe, C++/Unix, and now Java/anywhere). The Simple Batch Execution Container provides a physical implementation of the layers, components and technical services commonly found in robust, maintainable systems used to address the creation of simple to complex batch applications, with the infrastructure and extensions to address very complex processing needs. The materials below will walk through the details of the diagram.
[images/simple-batch-execution-container.jpg] Simple Batch Execution Container high level flow and interaction of the architecture.
Tiers
The application style is organized into four logical tiers, which include Run, Job, Application, and Data tiers. The primary goal for organizing an application according to the tiers is to embed what is known as "separation of concerns" within the system. Effective separation of concerns results in reducing the impact of change to the system.
* <<Run Tier:>> The Run Tier is concerned with the scheduling and launching of the application. A vendor product is typically used in this tier to allow time-based and interdependent scheduling of batch jobs as well as providing parallel processing capabilities.
* <<Job Tier:>> The Job Tier is responsible for the overall execution of a batch job. It sequentially executes batch steps, ensuring that all steps are in the correct state and all appropriate policies are enforced.
* <<Application Tier:>> The Application Tier contains components required to execute the program. It contains specific modules that address the required batch functionality and enforces policies around a module execution (e.g., commit intervals, capture of statistics, etc.)
* <<Data Tier:>> The Data Tier provides the integration with the physical data sources that might include databases, files, or queues. <<Note>>: In some cases the Job tier can be completely missing and in other cases one Job Script can start several Batch Job instances.
High Level Processing Flow
The diagram above illustrates the flow and architecture components in a typical batch run execution.
Standard interaction is described as follows:
<<1.>> In the Run tier, a Scheduler starts a batch application by invoking a Job Script. The Scheduler identifies what batch process it wants to run by passing the name of the batch process and any required additional parameters to the Job Script.
<<2.>> The Job Script initializes the program and executes any job specific scripts prior to calling the Batch Launcher.
<<3.>> The Batch Launcher starts the Batch Execution Container based upon any environment settings established in the script. (NOTE: A Batch Execution Container is not a Java EE container)
<<3.1>> The Batch Container starts and controls the batch execution. It initializes the Job execution environment with static configuration items such as database settings, logging levels and creates a Job based on the Job Configuration created by a Batch Developer.
<<4>> Based on configuration provided by a Batch Developer, the Job sequentially executes steps after checking policies to ensure that each step should be started. The status of the job and step (start time, end time, status such as "started" or "completed") is stored at various points during the process.
<<5.1>> In order to maintain data integrity, at the application tier, the Step acts as a controller to ensure that either an entire group of actions completes successfully or that none of the actions completes. This group of actions is referred to as a logical unit of work (LUW). The Step controls the overall execution of the Tasklet, ensuring that transaction are committed at the appropriate time, and restart and statistics information is stored appropriately. The first thing the Step is responsible for is the initialization of the data required to begin processing. The Step will interact with other architecture components, such as the Input Source, to setup the data required to be processed.
<<5.1.1>> The Input Source provides services to access various data sources. It provides location transparency to the Batch Tasklet and hides the physical location details of the data.
<<5.2>> Once the data is initialized by the Input Source, the Step will call into the Tasklet to begin processing. The Tasklet contains the business logic to define the LUW and the Step repeatedly calls the Tasklets LUW to finish the business function. The Step does this by first invoking the execute method on the Tasklet in order to acquire a single record/set of data for processing.
<<5.2.1>> Before a record is returned to the Tasklet, it may be validated by any number of validation Frameworks that can be provided to an input source. A single record/set of data is gathered by interacting with the Input Source.
<<5.3>> Once a record/set has been obtained, the step calls the module to begin processing.
<<5.3.1>> The Tasklet executes its internal business logic by calling other Business Logic components as necessary. Based on the business service, it can requests or persists objects from the data access components.
<<5.3.3>> Data Access components can be leveraged retrieve or persist domain objects.
<<5.3.4>> Once the business logic has been executed, the resulting output record is written out by utilizing the Output Source interface. The Step will repeatedly call steps 4.2 \-> 4.4 for every record provided by the Input Source.
<<5.4>> Once all of the records are processed, the Step calls the Tasklet to perform any clean up activities such as closing connections, exporting files, etc.
<<5.4.1>> The Step is responsible for committing data associated with the remaining logical units of work as well as performing any finalization and administrative functions (e.g. closing database connections).
Once the Step has completed finalization the control is passed back to the Job, where any necessary logging or clean up is executed for application termination and wrap-up -- provided there are no additional Steps to execute.

View File

@@ -0,0 +1,201 @@
------
Batch Processing Strategy
------
Scott Wintermute
------
May 2007
Batch Processing Strategy
To help design and implement batch systems, basic batch application building blocks and patterns should be provided to the designers and programmers in form of sample structure charts and code shells. When starting to design a batch job, the business logic should be decomposed into a series of steps which can be implemented using the following standard building blocks:
* Conversion Applications: For each type of file supplied by or generated to an external system, a conversion application will need to be created to convert the transaction records supplied into a standard format required for processing. This type of batch application can partly or entirely consist of translation utility modules (see Basic Batch Services).
* Validation Applications: Validation applications ensure that all input/output records are correct and consistent. Validation is typically based on file headers and trailers, checksums and validation algorithms as well as record level cross-checks.
* Extract Applications: An application that reads a set of records from a database or input file, selects records based on predefined rules, and writes the records to an output file.
* Extract/Update Applications: An application that reads records from a database or an input file, and makes changes to a database or an output file driven by the data found in each input record.
* Processing and Updating Applications: An application that performs processing on input transactions from an extract or a validation application. The processing will usually involve reading a database to obtain data required for processing, potentially updating the database and creating records for output processing.
* Output/Format Applications: Applications reading an input file, restructures data from this record according to a standard format, and produces an output file for printing or transmission to another program or system.
<<Pre-processing Capabilities>>
Additionally a basic application shell should be provided for business logic that cannot be built using the previously mentioned building blocks.
In addition to the main building blocks, each application may use one or more of standard utility steps, such as:
* Sort - A Program that reads an input file and produces an output file where records have been re-sequenced according to a sort key field in the records. Sorts are usually performed by standard system utilities.
* Split - A program that reads a single input file, and writes each record to one of several output files based on a field value. Splits can be tailored or performed by parameter-driven standard system utilities.
* Merge - A program that reads records from multiple input files and produces one output file with combined data from the input files. Merges can be tailored or performed by parameter-driven standard system utilities.
Batch applications can additionally be categorized by their input source:
* Database-driven applications are driven by rows or values retrieved from the database.
* File-driven applications are driven by records or values retrieved from a file
The foundation of any batch system is the processing strategy. Factors affecting the selection of the strategy include estimated batch system volume, concurrency with on-line or with another batch systems, available batch windows etc. Also with more enterprises wanting to be up and running 24x7, leaving no obvious batch windows.
Typical processing options for batch are:
* Normal processing in a batch window during off-line
* Concurrent batch / on-line processing
* Parallel processing of many different batch runs or jobs at the same time
* Streaming i.e. processing of many instances of the same job at the same time
* A combination of these
The order in the list above reflects the implementation complexity, processing in a batch window being the easiest and streaming the most complex to implement.
Some or all of these options may be supported by a commercial scheduler.
In the following section these processing options will be discussed in more detail. It is important to notice that the commit and locking strategy adopted by batch processes will be dependent on the type of processing performed and as a rule of thumb, the on-line locking should use the same principles. Therefore a batch architecture cannot be simply an afterthought when designing an overall architecture.
The locking strategy can use only normal database locks, or an additional custom locking service can be implemented in the architecture. The locking service would track database locking (for example by storing the necessary information in a dedicated db-table) and give or deny permissions to the application programs requesting a db operation. Retry logic could also be implemented by this architecture to avoid aborting a batch job in case of a lock situation.
<<1. Normal processing in a batch window>>
For simple batch processes running in a separate batch window, where the data being updated is not required by on-line users or other batch processes, concurrency is not an issue and a single commit can be done at the end of the batch run.
In most cases a more robust approach is more appropriate. A thing to keep in mind is that batch systems have a tendency to grow as time goes by, both in terms of complexity and the data volumes they will handle. If no locking strategy is in place and the system still relies on a single commit point, modifying the batch programs can be painful. Therefore, even with the simplest batch systems, consider the need for commit logic depicted in the [Restart/Recovery section|Restart & Recovery] as well as the information concerning the more complex cases below.
<<2. Concurrent batch / on-line processing>>
Batch applications processing data that can simultaneously be updated by on-line users, should not lock any data (either in the database or in files) which could be required by on-line users for more than a few seconds. Also updates should be committed to the database at the end of every few transaction. This minimizes the portion of data that is unavailable to other processes and the elapsed time the data is unavailable.
Another option to minimize physical locking is to have a logical row-level locking implemented using either an Optimistic Locking Pattern or a Pessimistic Locking Pattern.
* Optimistic locking assumes a low likelihood of record contention. It typically means inserting a timestamp column in each database table used concurrently by both batch and on-line processing. When an application fetches a row for processing, it also fetches the timestamp. As the application then tries to update the processed row, the update uses the original timestamp in the WHERE clause. If the timestamp matches, the data and the timestamp will be updated successfully. If the timestamp does not match, this indicates that another application has updated the same row between the fetch and the update attempt and therefore the update cannot be performed.
* Pessimistic locking is any locking strategy that assumes there is a high likelihood of record contention and therefore either a physical or logical lock needs to be obtained at retrieval time. One type of pessimistic logical locking uses a dedicated lock-column in the database table. When an application retrieves the row for update, it sets a flag in the lock column. With the flag in place, other applications attempting to retrieve the same row will logically fail. When the application that set the flag updates the row, it also clears the flag, enabling the row to be retrieved by other applications. Please note, that the integrity of data must be maintained also between the initial fetch and the setting of the flag, for example by using db locks (e.g.,SELECT FOR UPDATE). Note also that this method suffers from the same downside as physical locking except that it is somewhat easier to manage building a time-out mechanism that will get the lock released if the user goes to lunch while the record is locked.
These patterns are not necessarily suitable for batch processing, but they might be used for concurrent batch and on-line processing for example in cases where the database doesn't support row-level locking. As a general rule, optimistic locking is more suitable for on-line applications, while pessimistic locking is more suitable for batch applications. Whenever logical locking is used, the same scheme must be used for all applications accessing data entities protected by logical locks.
Note that both of these solutions only address locking a single record. Often we may need to lock a logically related group of records. With physical locks, you have to manage these very carefully in order to avoid potential deadlocks. With logical locks, it is usually best to build a logical lock manager that understands the logical record groups you want to protect and can ensure that locks are coherent and non-deadlocking. This logical lock manager usually uses its own tables for lock management, contention reporting, time-out mechanism, etc.
<<3. Parallel Processing>>
Parallel processing allows multiple batch runs / jobs to run in parallel to minimize the total elapsed batch processing time. This is not a problem as long as the jobs are not sharing the same files, db-tables or index spaces. If they do, this service should be implemented using partitioned data. Another option is to build an architecture module for maintaining interdependencies using a control table. A control table should contain a row for each shared resource and whether it is in use by an application or not. The batch architecture (Control Program Tasklet) or the application in a parallel job would then retrieve information from that table to determine if it can get access to the resource it needs or not.
If the data access is not a problem, parallel processing can be implemented in a mainframe environment using parallel job classes, in order to ensure adequate CPU time for all the processes. In an environment other than the mainframe, a similar solution can be put in place with for example threads. The solution has to be robust enough to ensure time slices for all the running processes.
Other key issues in parallel processing include load balancing and the availability of general system resources such as files, database buffer pools etc. Also note that the control table itself can easily become a critical resource.
<<4. Partitioning>>
Using partitioning allows multiple versions of large batch applications to run in concurrent. The purpose of this is to reduce the elapsed time required to process long batch jobs. Processes which can be successfully partitioned are those where the input file can be split and/or the main database tables partitioned to allow the application to run against different sets of data.
In addition, processes which are partitioned must be designed to only process their assigned data set. A partitioning architecture has to be closely tied to the database design and the database partitioning strategy. Please note, that the database partitioning doesn't necessarily mean physical partitioning of the database, although in most cases this is advisable. The following picture illustrates the partitioning approach:!app_style_batch_processing.png|align=center!
The architecture should be flexible enough to allow dynamic configuration of the number of partitions. Both automatic and user controlled configuration should be considered. Automatic configuration may be based on parameters such as the input file size and/or the number of input records.
<<4.1 Streaming Approaches>>
The following lists some of the possible streaming approaches. Selecting a streaming approach has to be done on a case-by-case basis.
<1. Fixed and Even Break-Up of Record Set>
This involves breaking the input record set into an even number of portions (e.g. 10, where each portion will have exactly 1/10th of the entire record set). Each portion is then processed by one instance of the batch/extract application.
In order to use this approach, preprocessing will be required to split the recordset up. The result of this split will be a lower and upper bound placement number which can be used as input to the batch/extract application in order to restrict its processing to its portion alone.
Preprocessing could be a large overhead as it has to calculate and determine the bounds of each portion of the record set.
<2. Breakup by a Key Column>
This involves breaking up the input record set by a key column such as a location code, and assigning data from each key to a batch instance. In order to achieve this, column values can either be
<3. Assigned to a batch instance via a streaming table (see below for details).>
<4. Assigned to a batch instance by a portion of the value (e.g. values 0000-0999, 1000 - 1999, etc.)>
Under option 1, addition of new values will mean a manual reconfiguration of the batch/extract to ensure that the new value is added to a particular instance.
Under option 2, this will ensure that all values are covered via an instance of the batch job. However, the number of values processed by one instance is dependent on the distribution of column values (i.e. there may be a large number of locations in the 0000-0999 range, and few in the 1000-1999 range). Under this option, the data range should be designed with streaming in mind.
Under both options, the optimal even distribution of records to batch instances cannot be realized. There is no dynamic configuration of the number of batch instances used.
<5. Breakup by Views>
This approach is basically breakup by a key column, but on the database level. It involves breaking up the recordset into views. These views will be used by each instance of the batch application during its processing. The breakup will be done by grouping the data.
With this option, each instance of a batch application will have to be configured to hit a particular view (instead of the master table). Also, with the addition of new data values, this new group of data will have to be included into a view. There is no dynamic configuration capability, as a change in the number of instances will result in a change to the views.
<6. Addition of a Processing Indicator>
This involves the addition of a new column to the input table, which acts as an indicator. As a preprocessing step, all indicators would be marked to non-processed. During the record fetch stage of the batch application, records are read on the condition that that record is marked non-processed, and once they are read (with lock), they are marked processing. When that record is completed, the indicator is updated to either complete or error. Many instances of a batch application can be started without an change, as the additional column ensures that a record is only processed once.
With this option, I/O on the table increased dynamically. In the case of a updating batch application, this impact is reduced, as a write will have to occur anyway.
<7. Extract Table to a Flat File>
This involves the extraction of the table into a file. This file can then be split into multiple segments and used as input to the batch instances.
With this option, the additional overhead of extracting the table into a file, and splitting it, may cancel out the effect of multi-streaming. Dynamic configuration can be achieved via changing the file splitting script.
<8. Use of a Hashing Column>
This scheme involves the addition of a hash column (key/index) to the database tables used to retrieve the driver record. This hash column will have an indicator to determine which instance of the batch application will process this particular row. For example, if there are three batch instances to be started, then an indicator of 'A' will mark that row for processing by instance 1, an indicator of 'B' will mark that row for processing by instance 2, etc.
The procedure used to retrieve the records would then have an additional WHERE clause to select all rows marked by a particular indicator. The inserts in this table would involve the addition of the marker field, which would be defaulted to one of the instances (e.g. 'A').
A simple batch application would be used to update the indicators such as to redistribute the load between the different instances. When a sufficiently large number of new rows have been added, this batch can be run (anytime, except in the batch window) to redistribute the new rows to other instances.
Additional instances of the batch application only require the running of the batch application as above to redistribute the indicators to cater for a new number of instances.
4.2 Database and Application design Principles
An architecture that supports multi-streamed applications which run against partitioned database tables using the key column approach, should include a central streaming repository for storing streaming parameters. This provides flexibility and ensures maintainability. The repository will generally consist of a single table known as the streaming table.
Information stored in the streaming table will be static and in general should be maintained by the DBA. The table should consist of one row of information for each stream of a multi-streamed application. The table should have a similar layout to the following table:
{center}
|| Streaming Table ||
| Program ID Code
Stream Number (Logical ID of the stream)
Low Value of the db key column for this stream
High Value of the db key column for this stream |
{center}
On program start-up the program id and stream number should be passed to the application from the architecture (Control Processing Tasklet). These variables are used to read the streaming table, to determine what range of data the application is to process (if a key column approach is used). In addition the stream number must be used throughout the processing to:
* Add to the output files/database updates in order for the merge process to work properly
* Report normal processing to the batch log and any errors that occur during execution to the architecture error handler
4.3 Minimizing Deadlocks
When applications run in parallel or streamed, contention in database resources and deadlocks may occur. It is critical that the database design team eliminates potential contention situations as far as possible as part of the database design.
Also ensure that the database index tables are designed with deadlock prevention and performance in mind.
Deadlocks or hot spots often occur in administration or architecture tables such as log tables, control tables, lock tables etc.. The implications of these should be taken into account as well. A realistic stress test is crucial for identifying the possible bottlenecks in the architecture.
To minimize the impact of conflicts on data, the architecture should provide services such as wait-and-retry intervals when attaching to a database or when encountering a deadlock. This means a built-in mechanism to react to certain database return codes and instead of issuing an immediate error handling, waiting a predetermined amount of time and retrying the database operation.
4.4 Parameter Passing and Validation
The streaming architecture should be relatively transparent to application developers. The architecture should perform all tasks associated with running the application in a streamed mode i.e.
* Retrieve streaming parameters before application start-up
* Validate streaming parameters before application start-up
* Pass parameters to application at start-up
The validation should include checks to ensure that:
* the application has sufficient streams to cover the whole data range
* there are no gaps between streams
If the database is partitioned, some additional validation may be necessary to ensure that a single stream does not span database partitions.
Also the architecture should take into consideration the consolidation of streams. Key questions include:
* Must all the streams be finished before going into the next job step?
* What happens if one of the streams aborts?

View File

@@ -0,0 +1,124 @@
------------------------------------------
The Spring Batch - Reference Documentation
----------------------------------------
Wayne Lund, Waseem Malik, Lucas Ward, Scott Wintermute,
Kerry O'Brien, Tomi Vanek
-------------------------------------------
May 2007
Preface
*1. {{{introduction.html}Spring Container Batch Processing}}
*1.1. Overview
*1.2 Usage Scenarios
*2. {{{overview.html}Architecture Overview}}
**2.1. Introduction to Architecture Layers
**2.2. Batch Applications
**2.3 Container Application layer
**2.4 Container Support Layer
**2.5 The Container Core Layer
**2.6 Using the Spring-batch infrastructure
*2.6.1 Infrastructure Provided I/O Support
*2.6.2 Infrastucture Provided Base Services
*2.7. Batch Execution Container Configurations
*2.7.1. Single VM Simple Batch Execution Container - One Job, One Step, One Partition
*2.7.2. Single VM Multi-threaded Batch Execution Container Configuration - One Job, One Step, Multiple Partitions
*2.7.3 Batch Execution Container Hosted in J2EE Container - managed environment
*3. Core Batch Services
*3.1. Launching Batch Jobs
*3.2. Mapping Batch Error Codes to Launch Client Error Codes
*3.2.1. Returning error codes to Enterprise Schedulers with command line interfaces
*3.2.2. Web Request - returning error codes to "On-Demand Batch Requests"
*3.3. Job Services
*3.3.1. Step Configuration
*3.3.2. Job Status Service
*3.3.3. Job Status
*3.3.4. Job Statistics
*3.4. Step Services
*3.4.1. Step Execution
*3.4.2. Restart Services
*3.4.3. Skip Services
*3.4.5. Step Statistics
*3.5. Data Providers - Operations, Templates and Convenience Callbacks
*3.5.1. Wrapping input sources
*3.5.2. Validation of input
*3.5.3. Delimited File Data Providers
*3.5.4. Fixed Position File Data Providers
*3.5.5. XML File Data Providers
*3.5.6. SQL Input Source Data Provider
*3.6. Transaction Management
*3.6.1. Transaction Synchronization with non-transactional resources
*3.7. Partitioning Batch Jobs
*3.7.1. Partitioning Strategies
*3.7.2. Partitioning Job Steps
*3.7.3. Partition Status
*3.7.4. Partition Statistics
*3.7.5. Handling Exceptions within partitions
*4. [Batch in a J2EE Container]
*5. [Testing Batch Jobs]
*5.1. Unit Testing & Mock Objects Provided by the Framework
*5.2. Integration Testing
*5.3. Performance Testing Batch Jobs
*6. {{{samples.html}Practical Examples for Spring Batch}}
*6.1. Sample Applications|Spring Reference-Application Job-Map
*6.2. Running the Sample Batch Applications
*7. [INCUB:Batch XML Schema]
*8. {{{glossary.html}Glossary}}

View File

@@ -0,0 +1,145 @@
------
Architecture Overview
------
Wayne Lund
------
May 2007
2. Architecture Overview
*2.1 Introduction
This chapter covers the overall spring batch architecture. The Spring Container Archtiecture is made up of five logical layers; 1) the Batch Application, 2) the Batch Application Layer, 3) the batch core layer, and 4) the batch infrastucture layer.
*----------*----------------*------------+
|Provided By | Layer | Description
*----------*----------------*------------*
| Application Developer | Batch Application | This is where the application writes their batch jobs and modules. |
*----------*----------------*------------*
| Spring Batch Execution Container | Container Application Layer | Allows for extending and overwriting of the batch support layer for custom requirements. Facilities implemented in this layer could migrate down to Batch Support Layer. This is also the layer to add the project specific jars required by job types (e.g. reporting jars like Crystal, Brio, etc, form generation jars like Central Pro or Adobe, etc). |
*----------*----------------*------------*
| Spring Batch Execution Container | Container Support Layer | Provides default implementations of batch core services including I/O, Restart, Partitioning, Statistics, and configurations |
*----------*----------------*------------*
| Spring Batch Execution Container | Container Core Layer | Enables configuration, Common Services & Interfaces, management |
*----------*----------------*------------*
| Spring Batch Infrastructure | Batch-Infrastructure | Provides IO support, Batch style transactions, advanced exception handling, batch-template, batch-retry |
*----------*----------------*------------*
[Figure 2.0] - Batch Architecture Layers
The batch architecture is modeled after a container architecture, meaning that there are managed resources essential to high performance batch architectures that are configured through a spring context. The following sections will provide a quick review of each layer and their role in the batch architecture.
*2.2 Batch Applications
*2.3 Container Application Layer
*2.4 Container Support Layer
The batch support layer provides default implementations for all interfaces, interceptors, advice and other core batch services. Figure 2.3.1 illustrates the following logical packages. !Batch Support.png!
Although physically they break out into many more than depicted, logically you can think of the groupings in the following manner:
* I/O Support packages
* Restart Support
* Lifecycle Support packages
* DAO support layer
**2.4.1 I/O Support Packages
The I/O related packages are currently the richest packages in the batch architecture. They are modeled after Spring Patterns of Operations and Templates. For example, you'll see FlatFileInputOperations accompanied with a FlatFileInputTemplate. The FlatFileInputTemplate is wired up with a File Descriptor, which contains a Record Descriptor along with various other properties. With the File and Record Descriptors the InputTemplate supports a callback method that allows for the mapping of a record into an object. This support applies to fixed length records, delimited records and XML records. To further simplify this a DefaultFlatFileDataProvider is supplied an input template, which contains the field and record descriptions, along with a line mapper that knows how to map the line to an object. The next() operation on a record simply needs to readAndMap(lineMapper) a record. This pattern is used over again for XML and SQL input for simple mapping of input records to objects.
In addition to declarative descriptions of the records that can be re-used by multiple batch jobs, the I/O facilities also support configurable validation strategies. The two currently supported are Apache Commons Validator and Spring's VALang.
**2.4.2 Restart Support
The Restart Support provides implementations for a few common restart strategies that will be discussed further in the respective section. The following are provided out-of-the-box:
* IDList Restart Strategy - a strategy that supports a batch application where the application does not have a "process" flag and needs the batch architecture to track which records have been processed. This is not the ideal scenario.
* Last Processed Restart Strategy - when the record can be identified through a where and order by only the last record(s) processed needs to be saved for restart.
* No Restart Strategy - some batch jobs simply can't support restart. When they are re-run they are considered to be a new instance of a batch job.
* Sql Restart Strategy - \[need some additional javadoc for this strategy\].
**2.4.3 Lifecycle Support
*2.5. The Container Core Layer
The Batch Core interfaces and services are illiustrated in a simplied view of a package diagram. There are roughly seven logical packages:
* Core Spring Extensions
* Core Batch Advice
* Core Batch Configuration
* Core Batch Repository
* Core Batch Tasklet
\\ !Batch Core.png!
[Figure 2.5] Batch Core Layer
In the actual physical packaging there are a few more packages but the above illistration serves as an overview of the logical services that the batch container provides. The following sections will provide an introduction into each set of core batch facilities.
**2.5.1 Core Spring Extensions
The Core Spring extensions provide the scaffolding for a batch container. This includes facilities for managing the batch architecture in terms of launching, suspending and stopping batch jobs. There is house keeping that goes on, especially in concurrent batch jobs, related to ensuring that batch jobs quiese properly. The lifecycle management provides services for the proper initialization and subsequent shutdown of batch resources and services. The batch architecture is flexible in terms of how batch jobs may be launched. For example, batch jobs can be started via JMX facilities, scripts from the command line that launch a Java VM. It can also support launching batch jobs through web services or http. There are no restrictions. Finally, there are standard batch error codes. These error codes can be exposed to external utilities, like Schedulers, to ensure that batch jobs expose the status of jobs to an operational environment. This is especially important in the batch context where the modus operandi is headless, meaning unattended operation.
**2.5.2 Core Batch Advice
Core Batch Advice is an inventory of the type of advice that batch architectures will inject during the runtime of a batch application. These are defined as a set of extensible interfaces, with a number of default implementations in the support layer that provide some of the most common types of advice. Partition Advice is helpful with large datasets that need to be "chunked" up and run concurrently for better through put. Resource Advice is helpful for registering interest in transactional information so that file locations can be kept in sync with information processed within a transaction. In addition, the resource is associated with the correct step context and its associated configuration properties. Skip advice is applied for records that the module is unable to process. Restart Advice is helpful for Restartable jobs where for advising the job on how to restart. There is considerable variability on how restart can occur. For example, a job may be marking records as "processed" and the restart advice will advise the process with query that restarts the job at the last successfully processed record. Finally, Statistics are vital in operational environments to report on records processed, records skipped and total number of records read. In addition, certain batch jobs lend themselves to custom reporting to expose additional business level information like the number of trades processed or cases opened, etc.
**2.5.3 Core Batch Configuration
Batch configuration is considerably different from online web applications or SOA based applications. The Core Batch Configuration provides a place for configuring runtime properties related to the batch application style. This includes the ability to add Commit Policy. In a batch style application it is often advantageous to keep the commit interval as high as possible when processing Logical Units of Work. Whereas in an online web application with declarative transaction the transaction scope would be at the entrance to a business service, a batch transaction scope may include many logical units of work before a transaction commit is executed. A Start Policy allows a configuration to tell the batch job whether it is Restartable, and if so, what type of restart to initiate. Some jobs are not restartable and care should be taken to ensure that information is not applied multiple times when the business rules do not allow for it. Exception policies deal with what to do when exceptions occur. This impacts logging policies and exception handling. The architecture defines a common set of exceptions that projects can apply handlers to like processing errors, validation errors, parsing errors, missing configuration parameters, etc.
**2.5.4 Container Repository
This is an internal package for storing the state of a batch job and any associated partition and step status.
**2.5.5 Core Batch Tasklet
The core batch module is where control is handed off to the application. There are a number of patterns that have been observed in processing batch data. Spring Core Batch Tasklet implements the most common patterns and provides and extension point for additional Tasklet processing implementations. The basic idea of module provides the facilities for reading and processing data. The simplest implementation of Tasklet, the ReadProcessTasklet, handles both the input and output of data within one class. An alternative implementation, the DataProviderProcessTasklet, provides functionality for 'split processing'. This type of processing is characterized by separating the reading and processing of batch data into two seperate classes: DataProvider and TaskletProcessor. The DataProvider class provides a solid means for reusablility and enforces good architecture practices. Because an object \*must\* be returned by the DataProider to continue processing, (Returning null indicates processing should end) a developer is forced to read in all relevant data, place it into domain or value objects, and return the object. The TaskletProcessor will then use this object within the business logic and final output.
2.6 Container's Use of batch infrastructure
**2.6.1 Infrastructure Provided I/O
The I/O core interfaces and implementations provide facilities for simplifying the extraction of data from I/O sources like files and database tables. The key concepts are FieldDescriptors and FieldSets along with appropriate CallBack Handlers. These are modeled after common spring operations and templates like JdbcTemplate. Through the use of LineMappers a developer needs only to describe a record format and write the appropriate callback method that maps the parsed record into an object of their choice. These can either be true POJO objects or Value Objects (structures) that are subsequently available for the module to processs. The interface for Field Descriptors also allows for a level of validation through the use of Spring's VALang or Apache's Common Validator.
**2.6.2 Core Batch Interceptors & Interceptor Services
* Batch Operations & Batch Template
Interceptors and the associated services are the key to how advise is applied in the batch architecture. The interceptors are Point Cuts in the batch lifecycle that allow the injection of advise. The shared lifecycle behavior abstracted through the BatchLifeCycleInterceptor defineds three methods; init, onError and finalize. All subclasses of LifeCycleInterceptor define default behavior for these three methods. The JobLifecycleInterceptor further exposes the methods beforeJob(), beforeStep(), afterJob(), and afterStep() allowing hooks into the lifecycle for specific advise. The Batch Architecture provides default implementations for all lifecycle point cuts, or interception points. The Tasklet Interceptor, in addition to the standard lifecycle methods, implements logic around beforeLuw(), afterLuw(), commitIntervalStarted() and commitIntervalCompleted(). Having well defined lifecycle interception points allows for the easy insertion of custom advice into the batch runtime environment.
2.7 Batch Esecution Container Configurations
In addition to core facilities for configuring or wiring together jobs and steps with required resources, policies, and interceptors, spring batch allows considerable flexibility in how scalability is achieved. More options for scalability will be available in the future. The important key for scalability in Java is the recognition that there is a limit to what one JVM may scale up to in terms of number of threads, managed resources, memory configuration, etc. The spring batch architecture allows for the configuration of simple batch jobs where one VM and one process is sufficient to do perform the work within a batch window all the way through many threads distributed within a cluster of JEE servers. The figure below illistrates the scalability spectrum.
This is not to be understood as the only way to scale batch jobs as there are many factors. For example, other federated java architectures hold potential like Teracotta or Gigaspaces although there is no current implementation for these distributed models in the current batch architecture.
!scalability-model.png!
[Figure 2.3.1] - Scalability Model
*2.7.1. Single VM Simple Batch Execution Container - One Job, One Step, One Partition
The simplest configuration is one job with with step and hence, one implied partition. Implied means that there is nothing for the developer to consider because the default number of partitions is one. There is typically one input source and one output source in this simple configuration. See the Simple Tasklet Job for an example of what this configuration looks like. A simple configuration still typically configures a datasource context, the batch configuration for describing the Job, Step, along with the associated configured policies, field descriptors, and line mappers. !SimpleTradeConfiguration.jpg!
[Figure 2.3.1] Simple Container Configuration
The details of this configuration will be covered thoroughly in subsequent sections of the document but for now it should be understood that Job, the Step, the input template, the file descriptor with its associated line mapper, and the output (e.g. the TradeWriter).
*2.3.2. Single VM Multi-threaded Batch Execution Container Configuration - One Job, One Step, Multiple Partitions
In a Single JVM using partitioning a multi-threaded execution is supported. \[This is still work in progress\]
*2.3.3. Batch Execution Container Hosted in J2EE Container - managed environment
The J2EE container model has fallen under fire over the past few years for many valid reasons. There are some things that the J2EE container do very well though that projects should consider when planning for scalability with batch architectures. Commercial and open source containers like WebSphere, BEA and JBOSS typically:
* manage datasources effectively along with attendent services like prepared statement caching.
* manage transactions effectively including many configurable properties for long lived transactions.
* manage thread pools more effectively.
* supply robust implementations of JTA, a requirement when batch jobs output to multiple XA resources like JMS and JDBC.
* manage distribution effectively including domains, clusters and cells
* provide robust JMX management for configuring, managing and administering distributed applications.
* workload management facilities (clusters) provided by J2EE vendors
Projects are encouraged to deploy batch applications with the simplest configuration possible, but when federated JVMs are a requirement to process volumes of data within a batch window, batch-in-container provides an effective way of distributing the processing. Spring Batch supports this through a simple change in configuration. \[Work in progress on the exact implementation - being released as part of M2\].

View File

@@ -0,0 +1,266 @@
------
Sample Container Applications
------
Wayne Lund
------
May 2007
Overview of Batch Reference Applications
There is considerable variability in the types of input and output formats in batch jobs. There is also a number of options to consider in terms of how the types of strategies that will be used to handle skips, recovery, and statistics. However, when approaching a new batch job there are a few standard questions to answer to help determine how the job will be written and how to utilize the services offered by the spring batch framework. Consider the following:
* How do I configure this batch job? In the reference applications the pattern is to follow the convention of <nameOf>Job.xml. Each section with identify the XML definition.
* What is the input source? Each sample batch job will identify its input source.
* What is my output source? Each sample batch job will identify its output source.
* How are records read and validated from the input source? This refers to the input type and its format (e.g. flat file with fixed position, comma separated or XML, etc.)
* What is the policy of the job if a input record fails the validation step? The most important aspect is whether the record can be skipped so that processing can be continued.
* How will I process the data and write to the output source? How and what business logic is being applied to the processing of a record.
* How do I recover from an exception while operating on the output source? There are numerous recovery strategies that can be applied to handling errors on transactional targets. The reference applications will provide a feeling for some of the choices.
* Can I restart the job and if so which strategy will I use to restart the job? The reference applications will show some of the options available to jobs and what the decision criteria is for the respective choices.
[Samples] Reference Applications Table of Features
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| Job / Feature | delimited input | fixed-length input | xml input | db driving query input | db cursor input | delimited output | fixed-length output | db output | skip | restart | quartz scheduling |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| simpleTaskletJob | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| fixedLengthImport | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| multi-line order | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| quartzBatch | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| simple skip sample | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| Skip And Restart Sample | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| SQL Cursor Trade Job | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| Trade Job | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
| XML Job | | | | | | | | | | | |
*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------*
{Simple Tasklet Job}
The goal is to show the simplest use of the batch framework with a single job with a single step&nbsp;where the module processes&nbsp;one input source to one output source.
<<Description:>> This job is defined by simpleTaskletJob.xml file. Job itself is defined by element simpleTaskletJob. Each job consists of several steps, these steps are defined in steps property. In this example we have only one step. Each step defines module that is responsible for . In this case processing will be handled by SimpleTradeTasklet class. Each module must implement execute() method. All processing of business data should be handled by this method. In this example execute() method tries to read the data from defined input source using read() method and if the data exists, it is processed using process() method. If there is no data to read, method returns false to signal, that there is nothing for further processing.
<<Method read()>> gets the data from the input template defined and maps it to an object using mapper defined in XML definition. This sample uses FlatFileInputTemplate class as input template. This template reads the whole line from the file and pass it to tokenizer which knows the structure of the line. Location of the file is defined by fileLocatorStrategy property, structurte of the line is defined by fixedFileDescriptor. Result of parsing the line is stored in FieldSet, which is used by mapper to create value object. In our example we use DefaultLineMapper which creates an instance of Trade class.
<<Method process()>> is quite simple - just writes trade object using DbTradeWriter class. This class writes values obtained from an object to the database.
<<Specific information:>> This job has whole logic implemented in Tasklet. It is not using Data provider as well as Tasklet processor, which is typical way how to handle data.
<<XML definition:>> simpleTaskletJob.xml
\[Note: we need to document Spring IDE in setup and installation so we can use to describe the project. Also, if we could also publish we can provide links to the graphics from docs. This is a sample only\].
Visualization of the spring configuration through Spring-IDE exposes the structure of a job configuration. The following is the visualization of the Simple Tasklet Job configuration. See {{{http://springide.org/blog/}Spring IDE}}.
[images/simple-module-job-configuration.jpg]Spring IDE Graph of Simple Tasklet Job Configuration.
[Figure:]\ Simple Tasklet Job Configuration
For simplicity we are only displaying the job configuration itself and leaving out the details of the supporting container configuration. The source view of the configuration is as follows:
[]
--------------------------------------------------------------------------------------
<import resource="BatchArchConfig.xml" />
<bean id="simpleTaskletJob" parent="Job">
<property name="name" value="fixedLengthImportJob" />
<property name="steps">
<list>
<bean id="tradeStep" parent="Step">
<property name="name" value="ImportTradeDataStep" />
<property name="module">
<bean class="com.accenture.adsj.refapp.batch.module.SimpleTradeTasklet">
<property name="inputTemplate" ref="fileInputTemplate" />
<property name="tradeDbWriter" ref="tradeWriter" />
</bean>
</property>
<property name="commitFrequency" value="5" />
<property name="startPolicy">
<bean class="org.springframework.batch.container.conf.StartPolicy">
<property name="ignoreComplete" value="true" />
<property name="restartEnabled" value="true" />
<property name="startlimit" value="12" />
</bean>
</property>
<property name="exceptionPolicy">
<bean class="org.springframework.batch.container.conf.ExceptionPolicy">
<property name="totalExceptionLimit" value="20" />
<property name="transactionInvalidExceptionLimit" value="20" />
<property name="transactionValidExceptionLimit" value="5" />
</bean>
</property>
</bean>
</list>
</property>
</bean>
<bean id="tradeWriter" class="com.accenture.adsj.refapp.batch.dao.DbTradeWriter">
<property name="jdbcTemplate" ref="jdbcTemplate" />
<property name="incrementer">
<bean parent="incrementerParent">
<property name="incrementerName" value="TRADE_SEQ" />
</bean>
</property>
</bean>
<bean id="fileInputTemplate" class="org.springframework.batch.container.io.file.support.FlatFileInputTemplate">
<property name="name" value="FileInputSource" />
<property name="fileLocatorStrategy" ref="fileLocator" />
<property name="tokenizer">
<bean class="org.springframework.batch.container.io.file.support.FixedLineTokenizer">
<property name="fileDescriptor" ref="fixedFileDescriptor" />
</bean>
</property>
</bean>
<bean id="fixedFileDescriptor" class="org.springframework.batch.container.io.support.DefaultFileDescriptor">
<property name="recordDescriptors">
<bean class="org.springframework.batch.container.io.support.DefaultRecordDescriptor">
<property name="fieldDescriptors">
<list>
<bean class="org.springframework.batch.container.io.support.DefaultFieldDescriptor">
<property name="name" value="ISIN" />
<property name="length" value="12" />
</bean>
<bean class="org.springframework.batch.container.io.support.DefaultFieldDescriptor">
<property name="name" value="Quantity" />
<property name="length" value="3" />
</bean>
<bean class="org.springframework.batch.container.io.support.DefaultFieldDescriptor">
<property name="name" value="Price" />
<property name="length" value="5" />
</bean>
<bean class="org.springframework.batch.container.io.support.DefaultFieldDescriptor">
<property name="name" value="Customer" />
<property name="length" value="9" />
</bean>
</list>
</property>
</bean>
</property>
</bean>
<bean id="tradeLineMapper" class="com.accenture.adsj.refapp.batch.mapping.TradeRowMapper" />
<bean class="com.accenture.adsj.refapp.batch.advice.LogAdvice" id="logAdvice" />
<aop:config>
<aop:aspect id="logging" ref="logAdvice">
<aop:around pointcut-ref="pointcut" method="doBasicLogging" />
<aop:pointcut id="pointcut" expression="execution(* org.springframework.batch.container.dao.*.*(..))" />
</aop:aspect>
</aop:config>
</beans>
-----------------------------------------------------------------------------------------------------------------------
You should take the time to make sure you understand the relationship of the xml configuration with the visualization as provided by Spring IDE. \[Note: this will be updated when we use the namespace handler\].
<<Input source:>> file with fixed row structure
In this example we are using a simple fixed length record structure that can be found in the project at <REFAPP_INSTALL_HOME>/testBatchRoot/job_data/simpleTaskletJob/input/20070122.teststream.ImportTradeDataStep.txt. There's generally a considerable amount of thought that goes into architecting the folder structures for batch file management. See [provide a link to DefaultFileStrategy]. The only point to note here is the ImportTradeDataStep matches the name of the step in the configuration and the fixed length records look like:
[]
------------------------------------------------------------------------------------
20070122.teststream.ImportTradeDataStep.txt
UK21341EAH4597898.34customer1
UK21341EAH4611218.12customer2
UK21341EAH4724512.78customer2
UK21341EAH48108109.25customer3
UK21341EAH49854123.39customer4
------------------------------------------------------------------------------------
Looking back to the configuration file you will see where this is documented in the propery of the DefaultRecordDescriptor. You can see the following:
*--------------*-----------------*
|| FieldName | Length ||
*--------------*-----------------*
| ISIN | 12 |
*--------------*-----------------*
| Quantity | 3 |
*--------------*-----------------*
| Price | 5 |
*--------------*-----------------*
| Customer | 9 |
*--------------*-----------------*
<<Output target:>> database
<<Data Provider:>> data provider is not used, all functionality is implemented directly in Tasklet.
<<Tasklet processor:>> module processor is not used, all functionality is implemented directly in Tasklet.
Fixed Length Import Job
The goal is to demonstrate a typical scenarion of importing data from a fixed-length file to database
<<Description:>> This job shows a more typical scenario, when reading input data and processing the data is cleanly separated. The data provider is responsible for reading input and mapping each record to a domain object, which is then passed to the module processor. The module processor handles the processing of the domain objects, in this case it only writes them to database.
<<XML definition:>> fixedLengthImportJob.xml
<<Input source:>> file with fixed row structure
<<Output target:>> database
<<Data Provider:>> DefaultFlatFileDataProvider which uses the injected FlatFileInputTemplate to read input and the DefaultLineMapper to map each line to an object according to the file descriptor.
<<Tasklet processor:>> module processor does not do any special processing, it just writes the data to database using a DAO object (called OutputSource in this case, because it is specialized for writing to database, it has no methods for reading data).
Multiline Order Job
The goal is to demostrate how to handle a more complex file input format, where a record meant for processing inludes nested records and spans multiple lines
<<XML definition:>> multilineOrderJob.xml
<<Input source:>> file with multiline records
<<Output target:>> file with multiline records
<<Data Provider:>> OrderDataProvider is an example of a non-default programmatic data provider. It reads input until it detects that the multiline record has finished and encapsulates the record in a single domain object.
<<Tasklet processor:>> module processor passes the object to a an injected 'report service' which in this case writes the output to a file do demonstrate how to use the FlatFileOutputTemplate for writing multiline output according to a file descriptor.
Quartz Batch
The goal is to demonstrate how to schedule job execution using Quartz scheduler
<<XML definition:>> quartzBatch.xml
<<Description:>> First, declares launcher beans. Each launcher bean is able to launch a job using injected arguments. Second, triggers are declared saying when the launchers should be run. Last, there is the scheduler bean, where the triggers are registered.
Simple Skip Sample
Skip And Restart Sample
SQL Cursor Trade Job
Trade Job
The goal is to show a reasonably complex scenario, that would resemble the real-life usage of the framework.
<<Description:>> This job has 3 steps. First, data about trades is imported from a file to database. Second, the data about trades is read from the database and credit on customer accounts is decreased appropriately. Last, a report about customers is exported to a file.
<<XML definition:>> tradeJob.xml - the job definition, tradeJobIo.xml - input and output configuration, tradeJobAop.xml - optional AOP logging
<<Description:>> This job has 3 steps. First, data about trades is imported from a file to database. Second, the data about trades is read from the database and credit on customer accounts is decreased appropriately. Last, a report about customers is exported to a file.
XML Job

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="Spring Batch: ${project.name}">
<bannerLeft>
<name>Spring Batch: ${project.name}</name>
</bannerLeft>
<bannerRight>
<src>images/shim.gif</src>
</bannerRight>
<poweredBy>
<logo name="" href="" img="images/shim.gif"/>
</poweredBy>
<skin>
<groupId>org.springframework.maven.skins</groupId>
<artifactId>maven-spring-skin</artifactId>
<version>1.0.4</version>
</skin>
<body>
<links>
<item name="Home" href="../index.html"/>
<item name="${project.name}" href="index.html"/>
</links>
<menu name="Spring Batch Execution">
<item name="${project.name}" href="index.html"/>
</menu>
<menu ref="reports"/>
</body>
</project>

View File

@@ -0,0 +1,37 @@
/*
* 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;
import junit.framework.TestCase;
public abstract class AbstractExceptionTests extends TestCase {
public void testExceptionString() throws Exception {
Exception exception = getException("foo");
assertEquals("foo", exception.getMessage());
}
public void testExceptionStringThrowable() throws Exception {
Exception exception = getException("foo", new IllegalStateException());
assertEquals("foo", exception.getMessage().substring(0, 3));
}
public abstract Exception getException(String msg) throws Exception;
public abstract Exception getException(String msg, Throwable t) throws Exception;
}

View File

@@ -0,0 +1,36 @@
/*
* 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.bootstrap;
import junit.framework.TestCase;
import org.springframework.context.ApplicationEvent;
/**
* @author Dave Syer
*
*/
public class BatchExecutionRequestEventTests extends TestCase {
/**
* Test method for {@link org.springframework.batch.execution.bootstrap.BatchExecutionRequestEvent#BatchContainerRequestEvent(java.lang.Object)}.
*/
public void testBatchContainerRequestEvent() {
ApplicationEvent event = new BatchExecutionRequestEvent(this);
assertEquals(this, event.getSource());
}
}

View File

@@ -0,0 +1,66 @@
/*
* 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.bootstrap;
import java.util.ArrayList;
import java.util.List;
import javax.management.Notification;
import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent;
import org.springframework.jmx.export.notification.NotificationPublisher;
import org.springframework.jmx.export.notification.UnableToSendNotificationException;
import junit.framework.TestCase;
/**
* @author Dave Syer
*
*/
public class JobExecutionNotificationPublisherTests extends TestCase {
JobExecutionNotificationPublisher publisher = new JobExecutionNotificationPublisher();
public void testRepeatOperationsBeforeNotUsed() throws Exception {
final List list = new ArrayList();
publisher.setNotificationPublisher(new NotificationPublisher() {
public void sendNotification(Notification notification)
throws UnableToSendNotificationException {
list.add(notification);
}
});
publisher.onApplicationEvent(new RepeatOperationsApplicationEvent(this,
"foo", RepeatOperationsApplicationEvent.BEFORE) {
});
assertEquals(0, list.size());
}
public void testRepeatOperationsOpenUsed() throws Exception {
final List list = new ArrayList();
publisher.setNotificationPublisher(new NotificationPublisher() {
public void sendNotification(Notification notification)
throws UnableToSendNotificationException {
list.add(notification);
}
});
publisher.onApplicationEvent(new RepeatOperationsApplicationEvent(this,
"foo", RepeatOperationsApplicationEvent.OPEN));
assertEquals(1, list.size());
assertEquals("foo", ((Notification) list.get(0)).getMessage()
.substring(0, 3));
}
}

Some files were not shown because too many files have changed in this diff Show More