diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java index 107212df4..1543002c0 100644 --- a/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java @@ -73,7 +73,7 @@ public abstract class AbstractJobLauncher implements JobLauncher, * @param jobRuntimeInformationFactory * the jobRuntimeInformationFactory to set */ - public void setJobRuntimeInformationFactory( + public void setJobIdentifierFactory( JobIdentifierFactory jobRuntimeInformationFactory) { this.jobRuntimeInformationFactory = jobRuntimeInformationFactory; } @@ -103,14 +103,14 @@ public abstract class AbstractJobLauncher implements JobLauncher, * * @param batchContainer */ - public void setBatchContainer(JobExecutorFacade batchContainer) { + public void setJobExecutorFacade(JobExecutorFacade batchContainer) { this.batchContainer = batchContainer; } /** * Check that mandatory properties are set. * - * @see #setBatchContainer(JobExecutorFacade) + * @see #setJobExecutorFacade(JobExecutorFacade) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { @@ -203,7 +203,7 @@ public abstract class AbstractJobLauncher implements JobLauncher, * @throws NoSuchJobConfigurationException * if the job configuration cannot be located * - * @see #setJobRuntimeInformationFactory(JobIdentifierFactory) + * @see #setJobIdentifierFactory(JobIdentifierFactory) * @see org.springframework.context.Lifecycle#start() */ public ExitStatus run() throws NoSuchJobConfigurationException { diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java index 73325c2ee..cff593965 100644 --- a/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java @@ -58,7 +58,7 @@ public class SimpleJobLauncher implements JobLauncher { /** * Check that mandatory properties are set. * - * @see #setBatchContainer(JobExecutorFacade) + * @see #setJobExecutorFacade(JobExecutorFacade) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { @@ -124,7 +124,7 @@ public class SimpleJobLauncher implements JobLauncher { * default name is taken from the enclosed {@link JobConfiguration}. * @throws NoSuchJobConfigurationException if the job configuration cannot be located * - * @see #setJobRuntimeInformationFactory(JobIdentifierFactory) + * @see #setJobIdentifierFactory(JobIdentifierFactory) * @see org.springframework.context.Lifecycle#start() */ public ExitStatus run(){ diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java index 767783ae2..a45304c57 100644 --- a/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java @@ -53,10 +53,12 @@ import org.springframework.util.Assert; * @author Dave Syer * @since 2.1 */ -public class TaskExecutorJobLauncher extends AbstractJobLauncher implements ApplicationListener, - NotificationPublisherAware, ApplicationEventPublisherAware { +public class TaskExecutorJobLauncher extends AbstractJobLauncher implements + ApplicationListener, NotificationPublisherAware, + ApplicationEventPublisherAware { - private static final Log logger = LogFactory.getLog(TaskExecutorJobLauncher.class); + private static final Log logger = LogFactory + .getLog(TaskExecutorJobLauncher.class); private TaskExecutor taskExecutor = new SyncTaskExecutor(); @@ -68,9 +70,11 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl /* * (non-Javadoc) + * * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) */ - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + public void setApplicationEventPublisher( + ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; } @@ -78,7 +82,8 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl * Setter for the {@link TaskExecutor}. Defaults to a * {@link SyncTaskExecutor}. * - * @param taskExecutor the taskExecutor to set + * @param taskExecutor + * the taskExecutor to set */ public void setTaskExecutor(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; @@ -86,16 +91,19 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl /* * (non-Javadoc) + * * @see org.springframework.jmx.export.notification.NotificationPublisherAware#setNotificationPublisher(org.springframework.jmx.export.notification.NotificationPublisher) */ - public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + public void setNotificationPublisher( + NotificationPublisher notificationPublisher) { this.notificationPublisher = notificationPublisher; } /** * Start the provided container using the task executor provided. * - * @throws IllegalStateException if JobConfiguration is null. + * @throws IllegalStateException + * if JobConfiguration is null. */ protected ExitStatus doRun(final JobIdentifier runtimeInformation) { @@ -105,14 +113,16 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl public void run() { try { batchContainer.start(runtimeInformation); - } - catch (NoSuchJobConfigurationException e) { - applicationEventPublisher.publishEvent(new RepeatOperationsApplicationEvent(runtimeInformation, - "No such job", RepeatOperationsApplicationEvent.ERROR)); - logger.error("JobConfiguration could not be located inside Runnable for runtime information: [" - + runtimeInformation + "]", e); - } - finally { + } catch (NoSuchJobConfigurationException e) { + applicationEventPublisher + .publishEvent(new RepeatOperationsApplicationEvent( + runtimeInformation, "No such job", + RepeatOperationsApplicationEvent.ERROR)); + logger + .error( + "JobConfiguration could not be located inside Runnable for runtime information: [" + + runtimeInformation + "]", e); + } finally { unregister(runtimeInformation); } } @@ -125,11 +135,13 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl /** * Delegates to the underlying {@link JobExecutorFacade}. Does not wait for * the jobs to stop (probably therefore returns immediately). - * @throws NoSuchJobExecutionException + * + * @throws NoSuchJobExecutionException * * @see org.springframework.context.Lifecycle#stop() */ - protected void doStop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException { + protected void doStop(JobIdentifier runtimeInformation) + throws NoSuchJobExecutionException { batchContainer.stop(runtimeInformation); // TODO: wait for the jobs to stop? } @@ -146,9 +158,11 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl if (applicationEvent instanceof RepeatOperationsApplicationEvent) { RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) applicationEvent; int type = event.getType(); - if (type == RepeatOperationsApplicationEvent.OPEN || type == RepeatOperationsApplicationEvent.CLOSE + if (type == RepeatOperationsApplicationEvent.OPEN + || type == RepeatOperationsApplicationEvent.CLOSE || type == RepeatOperationsApplicationEvent.ERROR) { - String message = event.getMessage() + "; source=" + event.getSource(); + String message = event.getMessage() + "; source=" + + event.getSource(); logger.info(message); publish(message); } @@ -164,8 +178,8 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl * for information purposes only. * * @return Properties representing the last {@link JobExecutionContext} - * objects passed up from the underlying execution. If there are no jobs - * running it will be empty. + * objects passed up from the underlying execution. If there are no + * jobs running it will be empty. */ public Properties getStatistics() { if (batchContainer instanceof StatisticsProvider) { @@ -180,8 +194,17 @@ public class TaskExecutorJobLauncher extends AbstractJobLauncher implements Appl */ private void publish(String message) { if (notificationPublisher != null) { - notificationPublisher.sendNotification(new Notification("RepeatOperationsApplicationEvent", this, - notificationCount++, message)); + 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); } } diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExportedJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExportedJobLauncher.java new file mode 100644 index 000000000..20ff58461 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/ExportedJobLauncher.java @@ -0,0 +1,55 @@ +package org.springframework.batch.execution.bootstrap.support; + +import org.springframework.batch.execution.bootstrap.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(); + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptor.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptor.java new file mode 100644 index 000000000..198520b5e --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptor.java @@ -0,0 +1,79 @@ +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. TODO: make this + // configurable. + 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. + * + * @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); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/package.html b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/package.html new file mode 100644 index 000000000..7f6a1eae1 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/support/package.html @@ -0,0 +1,7 @@ + +
++Support classes for use in bootstrap implementations or configurations. +
+ + diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java index 543ba7fba..307f5ea9e 100644 --- a/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java @@ -48,7 +48,7 @@ public class TaskExecutorJobLauncherTests extends TestCase { protected void setUp() throws Exception { super.setUp(); launcher - .setJobRuntimeInformationFactory(new SimpleJobIdentifierFactory()); + .setJobIdentifierFactory(new SimpleJobIdentifierFactory()); } public void testStopContainer() throws Exception { @@ -57,7 +57,7 @@ public class TaskExecutorJobLauncherTests extends TestCase { launcher.setTaskExecutor(new SimpleAsyncTaskExecutor()); InterruptibleContainer container = new InterruptibleContainer(); - launcher.setBatchContainer(container); + launcher.setJobExecutorFacade(container); launcher.setJobConfigurationName(new JobConfiguration("foo").getName()); launcher.run(); @@ -110,7 +110,7 @@ public class TaskExecutorJobLauncherTests extends TestCase { .createControl(JobExecutorFacadeWithStatistics.class); JobExecutorFacadeWithStatistics batchContainer = (JobExecutorFacadeWithStatistics) control .getMock(); - launcher.setBatchContainer(batchContainer); + launcher.setJobExecutorFacade(batchContainer); Properties properties = PropertiesConverter.stringToProperties("a=b"); control.expectAndReturn(batchContainer.getStatistics(), properties); @@ -125,7 +125,7 @@ public class TaskExecutorJobLauncherTests extends TestCase { .createControl(JobExecutorFacade.class); JobExecutorFacade batchContainer = (JobExecutorFacade) control .getMock(); - launcher.setBatchContainer(batchContainer); + launcher.setJobExecutorFacade(batchContainer); Properties properties = new Properties(); control.replay(); @@ -176,7 +176,7 @@ public class TaskExecutorJobLauncherTests extends TestCase { .createControl(JobExecutorFacade.class); JobExecutorFacade batchContainer = (JobExecutorFacade) control .getMock(); - launcher.setBatchContainer(batchContainer); + launcher.setJobExecutorFacade(batchContainer); SimpleJobIdentifier jobRuntimeInformation = new SimpleJobIdentifier( "spam"); batchContainer.start(jobRuntimeInformation); diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptorTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptorTests.java new file mode 100644 index 000000000..4626ca89a --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/support/TypeConverterMethodInterceptorTests.java @@ -0,0 +1,144 @@ +package org.springframework.batch.execution.bootstrap.support; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.beans.TypeMismatchException; + +public class TypeConverterMethodInterceptorTests extends TestCase { + + TypeConverterMethodInterceptor interceptor = new TypeConverterMethodInterceptor(); + + private List list = new ArrayList(); + + /** + * Even though TestBean does not implement Test, the proxy will invoke a + * method is called with the same signature. + * + * @throws Exception + */ + public void testInvokeWithNull() { + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + assertEquals(0, list.size()); + proxy.operate(); + assertEquals(1, list.size()); + } + + /** + * Even though TestBean does not implement Test, the proxy will return + * something if a method is called with the same signature. + * + * @throws Exception + */ + public void testInvokeWithBoolean() { + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + assertEquals(true, proxy.isTest()); + } + + /** + * Even though TestBean does not implement Test, the proxy will return + * something if a method is called with the same signature. + * + * @throws Exception + */ + public void testInvokeWithConversionToInt() { + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + assertEquals(123, proxy.getValue()); + } + + /** + * Even though TestBean does not implement Test, the proxy will return a + * converted value if a method is called with the same name and arguments, + * but different return type. + * + * @throws Exception + */ + public void testInvokeWithComplex() throws Exception { + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + assertEquals("FOO:true", proxy.getBean()); + } + + public void testInvalidConversion() throws Exception { + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + try { + proxy.getInvalid(); + fail("Expected TypeMismatchException"); + } catch (TypeMismatchException e) { + // expected + } + } + + public void testTyepConverter() throws Exception { + final TestCase testCase = this; + interceptor.setTypeConverter(new SimpleTypeConverter() { + public Object convertIfNecessary(Object value, Class requiredType) { + return testCase; + } + }); + ProxyFactory factory = new ProxyFactory(Test.class, interceptor); + factory.setTarget(new TestBean(true)); + Test proxy = (Test) factory.getProxy(); + assertEquals(testCase, proxy.getInvalid()); + } + + public interface Test { + boolean isTest(); + + String getBean(); + + TestCase getInvalid(); + + int getValue(); + + void operate(); + } + + // N.B. TestBean intentionally does not implement Test! + public class TestBean { + private boolean test; + + public TestBean(boolean test) { + super(); + this.test = test; + } + + public boolean isTest() { + return test; + } + + public TestBean getBean() { + return this; + } + + public TestBean getInvalid() { + return this; + } + + public String getValue() { + return "123"; + } + + public void operate() { + list.add("FOO"); + } + + public String toString() { + return "FOO:" + test; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java b/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java index 7a209b28e..a98014fca 100644 --- a/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java @@ -15,6 +15,8 @@ */ package org.springframework.batch.repeat; +import java.io.Serializable; + /** * Value object used to carry information about the status of a * {@link RepeatOperations}. @@ -22,7 +24,7 @@ package org.springframework.batch.repeat; * @author Dave Syer * */ -public class ExitStatus { +public class ExitStatus implements Serializable { /** * Convenient constant value for when we detect that processing is underway. @@ -120,4 +122,14 @@ public class ExitStatus { + ";exitDescription=" + exitDescription; } + /** + * Add an exit code to an existing {@link ExitStatus}. + * + * @param code the code to add + * @return a new {@link ExitStatus} with the same properties but a new exit code. + */ + public ExitStatus addExitCode(String code) { + return new ExitStatus(continuable, code, exitDescription); + } + } diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java index c2141950a..1f58ee3f3 100644 --- a/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java @@ -15,6 +15,8 @@ */ package org.springframework.batch.repeat; +import org.apache.commons.lang.SerializationUtils; + import junit.framework.TestCase; /** @@ -66,4 +68,26 @@ public class ExitStatusTests extends TestCase { assertFalse(ExitStatus.CONTINUABLE.and(ExitStatus.FINISHED.isContinuable()).isContinuable()); } + public void testAddExitCode() throws Exception { + ExitStatus status = ExitStatus.CONTINUABLE.addExitCode("FOO"); + assertTrue(ExitStatus.CONTINUABLE!=status); + assertTrue(status.isContinuable()); + assertEquals("FOO", status.getExitCode()); + } + + public void testAddExitCodeWithDescription() throws Exception { + ExitStatus status = new ExitStatus(true, "BAR", "Bar").addExitCode("FOO"); + assertEquals("FOO", status.getExitCode()); + assertEquals("Bar", status.getExitDescription()); + } + + public void testSerializable() throws Exception { + ExitStatus status = ExitStatus.CONTINUABLE.addExitCode("FOO"); + byte[] bytes = SerializationUtils.serialize(status); + Object object = SerializationUtils.deserialize(bytes); + assertTrue(object instanceof ExitStatus); + ExitStatus restored = (ExitStatus) object; + assertTrue(restored.isContinuable()); + assertEquals("FOO", restored.getExitCode()); + } } diff --git a/samples/src/main/resources/jobs/adhocLoopJob.xml b/samples/src/main/resources/jobs/adhocLoopJob.xml index 319288806..781a73c74 100644 --- a/samples/src/main/resources/jobs/adhocLoopJob.xml +++ b/samples/src/main/resources/jobs/adhocLoopJob.xml @@ -1,86 +1,121 @@ - -