+ * {@link ChunkListener} shouldn't throw exceptions and expect continued processing, they
+ * must be handled in the implementation or the step will terminate.
*
* @author Lucas Ward
* @author Michael Minella
* @author Mahmoud Ben Hassine
* @author Parikshit Dutta
+ * @author Injae Kim
*/
public interface ChunkListener extends StepListener {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobKeyGenerator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobKeyGenerator.java
index 589434b97f..147a26a37c 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/JobKeyGenerator.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobKeyGenerator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2022 the original author or authors.
+ * Copyright 2013-2024 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.
@@ -21,9 +21,11 @@
*
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Taeik Lim
* @param The type of the source data used to calculate the key.
* @since 2.2
*/
+@FunctionalInterface
public interface JobKeyGenerator {
/**
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobParameters.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParameters.java
index 36cc3a1d44..a5e54b0c65 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/JobParameters.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParameters.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -26,8 +26,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -141,7 +139,7 @@ public String getString(String key, @Nullable String defaultValue) {
}
/**
- * Typesafe getter for the {@link Long} represented by the provided key.
+ * Typesafe getter for the {@link Double} represented by the provided key.
* @param key The key for which to get a value.
* @return The {@link Double} value or {@code null} if the key is absent.
*/
@@ -378,24 +376,4 @@ public String toString() {
return new StringBuilder("{").append(String.join(",", parameters)).append("}").toString();
}
- /**
- * @return The {@link Properties} that contain the key and values for the
- * {@link JobParameter} objects.
- * @deprecated since 5.0, scheduled for removal in 5.2. Use
- * {@link org.springframework.batch.core.converter.JobParametersConverter#getProperties(JobParameters)}
- *
- */
- @Deprecated(since = "5.0", forRemoval = true)
- public Properties toProperties() {
- Properties props = new Properties();
-
- for (Map.Entry> param : parameters.entrySet()) {
- if (param.getValue() != null) {
- props.put(param.getKey(), Objects.toString(param.getValue().toString(), ""));
- }
- }
-
- return props;
- }
-
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersBuilder.java
index 3450f4894a..a12ad7bc67 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersBuilder.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/JobParametersBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -105,6 +105,7 @@ public JobParametersBuilder addString(String key, @NonNull String parameter) {
* @return a reference to this object.
*/
public JobParametersBuilder addString(String key, @NonNull String parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, String.class, identifying));
return this;
}
@@ -128,6 +129,7 @@ public JobParametersBuilder addDate(String key, @NonNull Date parameter) {
* @return a reference to this object.
*/
public JobParametersBuilder addDate(String key, @NonNull Date parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, Date.class, identifying));
return this;
}
@@ -151,6 +153,7 @@ public JobParametersBuilder addLocalDate(String key, @NonNull LocalDate paramete
* @return a reference to this object.
*/
public JobParametersBuilder addLocalDate(String key, @NonNull LocalDate parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, LocalDate.class, identifying));
return this;
}
@@ -174,6 +177,7 @@ public JobParametersBuilder addLocalTime(String key, @NonNull LocalTime paramete
* @return a reference to this object.
*/
public JobParametersBuilder addLocalTime(String key, @NonNull LocalTime parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, LocalTime.class, identifying));
return this;
}
@@ -197,6 +201,7 @@ public JobParametersBuilder addLocalDateTime(String key, @NonNull LocalDateTime
* @return a reference to this object.
*/
public JobParametersBuilder addLocalDateTime(String key, @NonNull LocalDateTime parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, LocalDateTime.class, identifying));
return this;
}
@@ -220,6 +225,7 @@ public JobParametersBuilder addLong(String key, @NonNull Long parameter) {
* @return a reference to this object.
*/
public JobParametersBuilder addLong(String key, @NonNull Long parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, Long.class, identifying));
return this;
}
@@ -243,6 +249,7 @@ public JobParametersBuilder addDouble(String key, @NonNull Double parameter) {
* @return a reference to this object.
*/
public JobParametersBuilder addDouble(String key, @NonNull Double parameter, boolean identifying) {
+ Assert.notNull(parameter, "Value for parameter '" + key + "' must not be null");
this.parameterMap.put(key, new JobParameter<>(parameter, Double.class, identifying));
return this;
}
@@ -256,20 +263,6 @@ public JobParameters toJobParameters() {
return new JobParameters(this.parameterMap);
}
- /**
- * Add a new {@link JobParameter} for the given key.
- * @param key The parameter accessor.
- * @param jobParameter The runtime parameter.
- * @return a reference to this object.
- * @deprecated since 5.0, scheduled for removal in 5.2. Use {@link #addJobParameter}.
- */
- @Deprecated(since = "5.0", forRemoval = true)
- public JobParametersBuilder addParameter(String key, JobParameter> jobParameter) {
- Assert.notNull(jobParameter, "JobParameter must not be null");
- this.parameterMap.put(key, jobParameter);
- return this;
- }
-
/**
* Add a new {@link JobParameter} for the given key.
* @param key The parameter accessor.
@@ -285,27 +278,28 @@ public JobParametersBuilder addJobParameter(String key, JobParameter> jobParam
/**
* Add a job parameter.
* @param name the name of the parameter
- * @param value the value of the parameter
+ * @param value the value of the parameter. Must not be {@code null}.
* @param type the type of the parameter
* @param identifying true if the parameter is identifying. false otherwise
* @return a reference to this object.
* @param the type of the parameter
* @since 5.0
*/
- public JobParametersBuilder addJobParameter(String name, T value, Class type, boolean identifying) {
+ public JobParametersBuilder addJobParameter(String name, @NonNull T value, Class type, boolean identifying) {
+ Assert.notNull(value, "Value for parameter '" + name + "' must not be null");
return addJobParameter(name, new JobParameter<>(value, type, identifying));
}
/**
* Add an identifying job parameter.
* @param name the name of the parameter
- * @param value the value of the parameter
+ * @param value the value of the parameter. Must not be {@code null}.
* @param type the type of the parameter
* @return a reference to this object.
* @param the type of the parameter
* @since 5.0
*/
- public JobParametersBuilder addJobParameter(String name, T value, Class type) {
+ public JobParametersBuilder addJobParameter(String name, @NonNull T value, Class type) {
return addJobParameter(name, value, type, true);
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java
index 25d31a319e..d261384ef0 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -22,7 +22,7 @@
import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar;
import org.springframework.batch.core.configuration.support.DefaultJobLoader;
-import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
+import org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton;
import org.springframework.batch.core.configuration.support.MapJobRegistry;
import org.springframework.batch.core.explore.support.JobExplorerFactoryBean;
import org.springframework.batch.core.launch.support.JobOperatorFactoryBean;
@@ -63,7 +63,7 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
registerJobExplorer(registry, batchAnnotation);
registerJobLauncher(registry, batchAnnotation);
registerJobRegistry(registry);
- registerJobRegistryBeanPostProcessor(registry);
+ registerJobRegistrySmartInitializingSingleton(registry);
registerJobOperator(registry, batchAnnotation);
registerAutomaticJobRegistrar(registry, batchAnnotation);
watch.stop();
@@ -225,17 +225,19 @@ private void registerJobRegistry(BeanDefinitionRegistry registry) {
registry.registerBeanDefinition("jobRegistry", beanDefinition);
}
- private void registerJobRegistryBeanPostProcessor(BeanDefinitionRegistry registry) {
- if (registry.containsBeanDefinition("jobRegistryBeanPostProcessor")) {
- LOGGER.info("Bean jobRegistryBeanPostProcessor already defined in the application context, skipping"
- + " the registration of a jobRegistryBeanPostProcessor");
+ private void registerJobRegistrySmartInitializingSingleton(BeanDefinitionRegistry registry) {
+ if (registry.containsBeanDefinition("jobRegistrySmartInitializingSingleton")) {
+ LOGGER
+ .info("Bean jobRegistrySmartInitializingSingleton already defined in the application context, skipping"
+ + " the registration of a jobRegistrySmartInitializingSingleton");
return;
}
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
- .genericBeanDefinition(JobRegistryBeanPostProcessor.class);
+ .genericBeanDefinition(JobRegistrySmartInitializingSingleton.class);
beanDefinitionBuilder.addPropertyReference("jobRegistry", "jobRegistry");
- registry.registerBeanDefinition("jobRegistryBeanPostProcessor", beanDefinitionBuilder.getBeanDefinition());
+ registry.registerBeanDefinition("jobRegistrySmartInitializingSingleton",
+ beanDefinitionBuilder.getBeanDefinition());
}
private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) {
@@ -255,6 +257,12 @@ private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchPro
beanDefinitionBuilder.addPropertyReference("jobExplorer", "jobExplorer");
beanDefinitionBuilder.addPropertyReference("jobRegistry", "jobRegistry");
+ // set optional properties
+ String jobParametersConverterRef = batchAnnotation.jobParametersConverterRef();
+ if (registry.containsBeanDefinition(jobParametersConverterRef)) {
+ beanDefinitionBuilder.addPropertyReference("jobParametersConverter", jobParametersConverterRef);
+ }
+
registry.registerBeanDefinition("jobOperator", beanDefinitionBuilder.getBeanDefinition());
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java
index 301d160b62..27239d36c0 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -28,6 +28,7 @@
import org.springframework.batch.core.configuration.support.ApplicationContextFactory;
import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar;
import org.springframework.batch.core.configuration.support.ScopeConfiguration;
+import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.TaskExecutorJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
@@ -93,9 +94,9 @@
* "jobOperator" of type
* {@link org.springframework.batch.core.launch.support.SimpleJobOperator})
*
a
- * {@link org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor}
- * (bean name "jobRegistryBeanPostProcessor" of type
- * {@link org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor})
+ * {@link org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton}
+ * (bean name "jobRegistrySmartInitializingSingleton" of type
+ * {@link org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton})
*
*
* If the configuration is specified as modular=true, the context also
@@ -240,7 +241,9 @@
/**
* The large object handler to use in job repository and job explorer.
* @return the bean name of the lob handler to use. Defaults to {@literal lobHandler}.
+ * @deprecated Since 5.2 with no replacement. Scheduled for removal in v6
*/
+ @Deprecated(since = "5.2.0", forRemoval = true)
String lobHandlerRef() default "lobHandler";
/**
@@ -272,4 +275,11 @@
*/
String conversionServiceRef() default "conversionService";
+ /**
+ * Set the {@link JobParametersConverter} to use in the job operator.
+ * @return the bean name of the job parameters converter to use. Defaults to
+ * {@literal jobParametersConverter}
+ */
+ String jobParametersConverterRef() default "jobParametersConverter";
+
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/JobBuilderFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/JobBuilderFactory.java
deleted file mode 100644
index df3b411326..0000000000
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/JobBuilderFactory.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2012-2023 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
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.batch.core.configuration.annotation;
-
-import org.springframework.batch.core.job.builder.JobBuilder;
-import org.springframework.batch.core.repository.JobRepository;
-import org.springframework.util.Assert;
-
-/**
- * Convenient factory for a {@link JobBuilder} that sets the {@link JobRepository}
- * automatically.
- *
- * @author Dave Syer
- * @author Mahmoud Ben Hassine
- * @author Jinho Han
- * @deprecated Deprecated as of v5.0 and scheduled for removal in v5.2 in favor of using
- * the {@link JobBuilder}.
- *
- */
-@Deprecated(since = "5.0.0", forRemoval = true)
-public class JobBuilderFactory {
-
- private final JobRepository jobRepository;
-
- /**
- * @param jobRepository The {@link JobRepository} to be used by the builder factory.
- * Must not be {@code null}.
- */
- public JobBuilderFactory(JobRepository jobRepository) {
- Assert.notNull(jobRepository, "JobRepository must not be null");
- this.jobRepository = jobRepository;
- }
-
- /**
- * Creates a job builder and initializes its job repository. Note that, if the builder
- * is used to create a @Bean definition, the name of the job and the bean name
- * might be different.
- * @param name the name of the job
- * @return a job builder
- */
- public JobBuilder get(String name) {
- return new JobBuilder(name, this.jobRepository);
- }
-
-}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/StepBuilderFactory.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/StepBuilderFactory.java
deleted file mode 100644
index 2354f154ee..0000000000
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/StepBuilderFactory.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2012-2023 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
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.batch.core.configuration.annotation;
-
-import org.springframework.batch.core.repository.JobRepository;
-import org.springframework.batch.core.step.builder.StepBuilder;
-import org.springframework.util.Assert;
-
-/**
- * Convenient factory for a {@link StepBuilder} which sets the {@link JobRepository}
- * automatically.
- *
- * @author Dave Syer
- * @author Mahmoud Ben Hassine
- * @author Jinho Han
- * @deprecated Deprecated as of v5.0 and scheduled for removal in v5.2 in favor of using
- * the {@link StepBuilder}.
- *
- */
-@Deprecated(since = "5.0.0", forRemoval = true)
-public class StepBuilderFactory {
-
- private final JobRepository jobRepository;
-
- /**
- * Constructor for the {@link StepBuilderFactory}.
- * @param jobRepository The {@link JobRepository} to be used by the builder factory.
- * Must not be {@code null}.
- */
- public StepBuilderFactory(JobRepository jobRepository) {
- Assert.notNull(jobRepository, "JobRepository must not be null");
- this.jobRepository = jobRepository;
- }
-
- /**
- * Creates a step builder and initializes its job repository. Note that, if the
- * builder is used to create a @Bean definition, the name of the step and the bean
- * name might be different.
- * @param name the name of the step
- * @return a step builder
- */
- public StepBuilder get(String name) {
- return new StepBuilder(name, this.jobRepository);
- }
-
-}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java
index eee6738096..67df9fd41f 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -27,6 +27,8 @@
import org.springframework.batch.core.configuration.BatchConfigurationException;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.converter.DateToStringConverter;
+import org.springframework.batch.core.converter.DefaultJobParametersConverter;
+import org.springframework.batch.core.converter.JobParametersConverter;
import org.springframework.batch.core.converter.LocalDateTimeToStringConverter;
import org.springframework.batch.core.converter.LocalDateToStringConverter;
import org.springframework.batch.core.converter.LocalTimeToStringConverter;
@@ -52,7 +54,6 @@
import org.springframework.batch.item.database.support.DefaultDataFieldMaxValueIncrementerFactory;
import org.springframework.batch.support.DatabaseType;
import org.springframework.beans.BeansException;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
@@ -115,11 +116,8 @@
@Import(ScopeConfiguration.class)
public class DefaultBatchConfiguration implements ApplicationContextAware {
- @Autowired
protected ApplicationContext applicationContext;
- private final JobRegistry jobRegistry = new MapJobRegistry();
-
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
@@ -152,10 +150,28 @@ public JobRepository jobRepository() throws BatchConfigurationException {
}
}
- @Bean
+ /**
+ * Define a job launcher.
+ * @return a job launcher
+ * @throws BatchConfigurationException if unable to configure the default job launcher
+ * @deprecated Since 5.2. Use {@link #jobLauncher(JobRepository)} instead
+ */
+ @Deprecated(forRemoval = true)
public JobLauncher jobLauncher() throws BatchConfigurationException {
+ return jobLauncher(jobRepository());
+ }
+
+ /**
+ * Define a job launcher bean.
+ * @param jobRepository the job repository
+ * @return a job launcher
+ * @throws BatchConfigurationException if unable to configure the default job launcher
+ * @since 5.2
+ */
+ @Bean
+ public JobLauncher jobLauncher(JobRepository jobRepository) throws BatchConfigurationException {
TaskExecutorJobLauncher taskExecutorJobLauncher = new TaskExecutorJobLauncher();
- taskExecutorJobLauncher.setJobRepository(jobRepository());
+ taskExecutorJobLauncher.setJobRepository(jobRepository);
taskExecutorJobLauncher.setTaskExecutor(getTaskExecutor());
try {
taskExecutorJobLauncher.afterPropertiesSet();
@@ -189,17 +205,41 @@ public JobExplorer jobExplorer() throws BatchConfigurationException {
@Bean
public JobRegistry jobRegistry() throws BatchConfigurationException {
- return this.jobRegistry; // FIXME returning a new instance here does not work
+ return new MapJobRegistry();
}
- @Bean
+ /**
+ * Define a job operator.
+ * @return a job operator
+ * @throws BatchConfigurationException if unable to configure the default job operator
+ * @deprecated Since 5.2. Use
+ * {@link #jobOperator(JobRepository, JobExplorer, JobRegistry, JobLauncher)} instead
+ */
+ @Deprecated(forRemoval = true)
public JobOperator jobOperator() throws BatchConfigurationException {
+ return jobOperator(jobRepository(), jobExplorer(), jobRegistry(), jobLauncher());
+ }
+
+ /**
+ * Define a job operator bean.
+ * @param jobRepository a job repository
+ * @param jobExplorer a job explorer
+ * @param jobRegistry a job registry
+ * @param jobLauncher a job launcher
+ * @return a job operator
+ * @throws BatchConfigurationException if unable to configure the default job operator
+ * @since 5.2
+ */
+ @Bean
+ public JobOperator jobOperator(JobRepository jobRepository, JobExplorer jobExplorer, JobRegistry jobRegistry,
+ JobLauncher jobLauncher) throws BatchConfigurationException {
JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean();
jobOperatorFactoryBean.setTransactionManager(getTransactionManager());
- jobOperatorFactoryBean.setJobRepository(jobRepository());
- jobOperatorFactoryBean.setJobExplorer(jobExplorer());
- jobOperatorFactoryBean.setJobRegistry(jobRegistry());
- jobOperatorFactoryBean.setJobLauncher(jobLauncher());
+ jobOperatorFactoryBean.setJobRepository(jobRepository);
+ jobOperatorFactoryBean.setJobExplorer(jobExplorer);
+ jobOperatorFactoryBean.setJobRegistry(jobRegistry);
+ jobOperatorFactoryBean.setJobLauncher(jobLauncher);
+ jobOperatorFactoryBean.setJobParametersConverter(getJobParametersConverter());
try {
jobOperatorFactoryBean.afterPropertiesSet();
return jobOperatorFactoryBean.getObject();
@@ -210,12 +250,13 @@ public JobOperator jobOperator() throws BatchConfigurationException {
}
/**
- * Defines a {@link JobRegistryBeanPostProcessor} bean.
- * @return a {@link JobRegistryBeanPostProcessor} bean
+ * Defines a {@link JobRegistryBeanPostProcessor}.
+ * @return a {@link JobRegistryBeanPostProcessor}
* @throws BatchConfigurationException if unable to register the bean
* @since 5.1
+ * @deprecated Use {@link #jobRegistrySmartInitializingSingleton(JobRegistry)} instead
*/
- @Bean
+ @Deprecated(forRemoval = true)
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() throws BatchConfigurationException {
JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry());
@@ -228,6 +269,28 @@ public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() throws BatchC
}
}
+ /**
+ * Define a {@link JobRegistrySmartInitializingSingleton} bean.
+ * @param jobRegistry the job registry to populate
+ * @throws BatchConfigurationException if unable to register the bean
+ * @return a bean of type {@link JobRegistrySmartInitializingSingleton}
+ * @since 5.2
+ */
+ @Bean
+ public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry)
+ throws BatchConfigurationException {
+ JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = new JobRegistrySmartInitializingSingleton();
+ jobRegistrySmartInitializingSingleton.setJobRegistry(jobRegistry);
+ try {
+ jobRegistrySmartInitializingSingleton.afterPropertiesSet();
+ return jobRegistrySmartInitializingSingleton;
+ }
+ catch (Exception e) {
+ throw new BatchConfigurationException(
+ "Unable to configure the default job registry SmartInitializingSingleton", e);
+ }
+ }
+
/*
* Getters to customize the configuration of infrastructure beans
*/
@@ -329,8 +392,9 @@ protected Charset getCharset() {
* A special handler for large objects. The default is usually fine, except for some
* (usually older) versions of Oracle.
* @return the {@link LobHandler} to use
- *
+ * @deprecated Since 5.2 with no replacement. Scheduled for removal in v6
*/
+ @Deprecated(since = "5.2.0", forRemoval = true)
protected LobHandler getLobHandler() {
return new DefaultLobHandler();
}
@@ -397,14 +461,23 @@ protected String getDatabaseType() throws MetaDataAccessException {
}
/**
- * Return the {@link TaskExecutor} to use in the the job launcher. Defaults to
+ * Return the {@link TaskExecutor} to use in the job launcher. Defaults to
* {@link SyncTaskExecutor}.
- * @return the {@link TaskExecutor} to use in the the job launcher.
+ * @return the {@link TaskExecutor} to use in the job launcher.
*/
protected TaskExecutor getTaskExecutor() {
return new SyncTaskExecutor();
}
+ /**
+ * Return the {@link JobParametersConverter} to use in the job operator. Defaults to
+ * {@link DefaultJobParametersConverter}
+ * @return the {@link JobParametersConverter} to use in the job operator.
+ */
+ protected JobParametersConverter getJobParametersConverter() {
+ return new DefaultJobParametersConverter();
+ }
+
/**
* Return the conversion service to use in the job repository and job explorer. This
* service is used to convert job parameters from String literal to typed values and
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistryBeanPostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistryBeanPostProcessor.java
index 35625920f7..1f6ba7acfa 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistryBeanPostProcessor.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistryBeanPostProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -40,11 +40,17 @@
* {@link JobRegistry}. Include a bean of this type along with your job configuration and
* use the same {@link JobRegistry} as a {@link JobLocator} when you need to locate a
* {@link Job} to launch.
+ *
+ * An alternative to this class is {@link JobRegistrySmartInitializingSingleton}, which is
+ * recommended in cases where this class may cause early bean initializations. You must
+ * include at most one of either of them as a bean.
*
+ * @deprecated since 5.2 in favor of {@link JobRegistrySmartInitializingSingleton}.
* @author Dave Syer
* @author Mahmoud Ben Hassine
*
*/
+@Deprecated(since = "5.2")
public class JobRegistryBeanPostProcessor
implements BeanPostProcessor, BeanFactoryAware, InitializingBean, DisposableBean {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingleton.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingleton.java
new file mode 100644
index 0000000000..ede418cf23
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingleton.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.configuration.support;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.DuplicateJobException;
+import org.springframework.batch.core.configuration.JobLocator;
+import org.springframework.batch.core.configuration.JobRegistry;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.FatalBeanException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.SmartInitializingSingleton;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link SmartInitializingSingleton} that registers {@link Job} beans with a
+ * {@link JobRegistry}. Include a bean of this type along with your job configuration and
+ * use the same {@link JobRegistry} as a {@link JobLocator} when you need to locate a
+ * {@link Job} to launch.
+ *
+ * This class is an alternative to {@link JobRegistryBeanPostProcessor} and prevents early
+ * bean initializations. You must include at most one of either of them as a bean.
+ *
+ * @author Henning Pƶttker
+ * @since 5.1.1
+ */
+public class JobRegistrySmartInitializingSingleton
+ implements SmartInitializingSingleton, BeanFactoryAware, InitializingBean, DisposableBean {
+
+ private static final Log logger = LogFactory.getLog(JobRegistrySmartInitializingSingleton.class);
+
+ // It doesn't make sense for this to have a default value...
+ private JobRegistry jobRegistry = null;
+
+ private final Collection jobNames = new HashSet<>();
+
+ private String groupName = null;
+
+ private ListableBeanFactory beanFactory;
+
+ /**
+ * Default constructor.
+ */
+ public JobRegistrySmartInitializingSingleton() {
+ }
+
+ /**
+ * Convenience constructor for setting the {@link JobRegistry}.
+ * @param jobRegistry the {@link JobRegistry} to register the {@link Job}s with
+ */
+ public JobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
+ this.jobRegistry = jobRegistry;
+ }
+
+ /**
+ * The group name for jobs registered by this component. Optional (defaults to null,
+ * which means that jobs are registered with their bean names). Useful where there is
+ * a hierarchy of application contexts all contributing to the same
+ * {@link JobRegistry}: child contexts can then define an instance with a unique group
+ * name to avoid clashes between job names.
+ * @param groupName the groupName to set
+ */
+ public void setGroupName(String groupName) {
+ this.groupName = groupName;
+ }
+
+ /**
+ * Injection setter for {@link JobRegistry}.
+ * @param jobRegistry the {@link JobRegistry} to register the {@link Job}s with
+ */
+ public void setJobRegistry(JobRegistry jobRegistry) {
+ this.jobRegistry = jobRegistry;
+ }
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
+ this.beanFactory = listableBeanFactory;
+ }
+ }
+
+ /**
+ * Make sure the registry is set before use.
+ */
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ Assert.state(jobRegistry != null, "JobRegistry must not be null");
+ }
+
+ /**
+ * Unregister all the {@link Job} instances that were registered by this smart
+ * initializing singleton.
+ */
+ @Override
+ public void destroy() throws Exception {
+ for (String name : jobNames) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Unregistering job: " + name);
+ }
+ jobRegistry.unregister(name);
+ }
+ jobNames.clear();
+ }
+
+ @Override
+ public void afterSingletonsInstantiated() {
+ if (beanFactory == null) {
+ return;
+ }
+ Map jobs = beanFactory.getBeansOfType(Job.class, false, false);
+ for (var entry : jobs.entrySet()) {
+ postProcessAfterInitialization(entry.getValue(), entry.getKey());
+ }
+ }
+
+ private void postProcessAfterInitialization(Job job, String beanName) {
+ try {
+ String groupName = this.groupName;
+ if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory
+ && beanFactory.containsBean(beanName)) {
+ groupName = getGroupName(defaultListableBeanFactory.getBeanDefinition(beanName), job);
+ }
+ job = groupName == null ? job : new GroupAwareJob(groupName, job);
+ ReferenceJobFactory jobFactory = new ReferenceJobFactory(job);
+ String name = jobFactory.getJobName();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Registering job: " + name);
+ }
+ jobRegistry.register(jobFactory);
+ jobNames.add(name);
+ }
+ catch (DuplicateJobException e) {
+ throw new FatalBeanException("Cannot register job configuration", e);
+ }
+ }
+
+ /**
+ * Determine a group name for the job to be registered. The default implementation
+ * returns the {@link #setGroupName(String) groupName} configured. Provides an
+ * extension point for specialised subclasses.
+ * @param beanDefinition the bean definition for the job
+ * @param job the job
+ * @return a group name for the job (or null if not needed)
+ */
+ protected String getGroupName(BeanDefinition beanDefinition, Job job) {
+ return groupName;
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java
index 03ec94b23e..876a9ed2ce 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/AbstractFlowParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -409,8 +409,7 @@ protected static Collection createTransition(FlowExecutionStatus
endBuilder.addConstructorArgValue(abandon);
- String nextOnEnd = exitCodeExists ? null : next;
- endState = getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), null, nextOnEnd);
+ endState = getStateTransitionReference(parserContext, endBuilder.getBeanDefinition(), null, next);
next = endName;
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/BeanDefinitionUtils.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/BeanDefinitionUtils.java
index 336bd43961..f2711b24b1 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/BeanDefinitionUtils.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/BeanDefinitionUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2007 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -21,9 +21,13 @@
/**
* @author Dan Garrette
+ * @author Taeik Lim
* @since 2.0.1
*/
-public class BeanDefinitionUtils {
+public abstract class BeanDefinitionUtils {
+
+ private BeanDefinitionUtils() {
+ }
/**
* @param beanName a bean definition name
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
index 7b3f25581b..c538d17723 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespaceUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -37,8 +37,12 @@
* @author Thomas Risberg
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Taeik Lim
*/
-public class CoreNamespaceUtils {
+public abstract class CoreNamespaceUtils {
+
+ private CoreNamespaceUtils() {
+ }
private static final String STEP_SCOPE_PROCESSOR_BEAN_NAME = "org.springframework.batch.core.scope.internalStepScope";
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java
index cbad5b1cee..7b18458ee7 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/StepParserStepFactoryBean.java
@@ -893,7 +893,7 @@ public void setKeyGenerator(KeyGenerator keyGenerator) {
/**
*
* Public setter for the capacity of the cache in the retry policy. If there are more
- * items than the specified capacity, the the step fails without being skipped or
+ * items than the specified capacity, the step fails without being skipped or
* recovered, and an exception is thrown. This guards against inadvertent infinite
* loops generated by item identity problems.
*
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/converter/DefaultJobParametersConverter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/converter/DefaultJobParametersConverter.java
index 522175f62c..a9f671ca56 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/converter/DefaultJobParametersConverter.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/converter/DefaultJobParametersConverter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -169,7 +169,11 @@ protected JobParameter> decode(String encodedJobParameter) {
}
private String parseValue(String encodedJobParameter) {
- return StringUtils.commaDelimitedListToStringArray(encodedJobParameter)[0];
+ String[] tokens = StringUtils.commaDelimitedListToStringArray(encodedJobParameter);
+ if (tokens.length == 0) {
+ return "";
+ }
+ return tokens[0];
}
private Class> parseType(String encodedJobParameter) {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/JobExplorerFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/JobExplorerFactoryBean.java
index 4d99c15fa8..9d3e24dae5 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/JobExplorerFactoryBean.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/JobExplorerFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -143,7 +143,9 @@ public void setJobKeyGenerator(JobKeyGenerator jobKeyGenerator) {
* {@code null}, which works for most databases.
* @param lobHandler Large object handler for saving an
* {@link org.springframework.batch.item.ExecutionContext}.
+ * @deprecated Since 5.2 with no replacement. Scheduled for removal in v6
*/
+ @Deprecated(since = "5.2.0", forRemoval = true)
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/MongoJobExplorerFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/MongoJobExplorerFactoryBean.java
new file mode 100644
index 0000000000..c9e38e76f8
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/MongoJobExplorerFactoryBean.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.explore.support;
+
+import org.springframework.batch.core.repository.dao.ExecutionContextDao;
+import org.springframework.batch.core.repository.dao.JobExecutionDao;
+import org.springframework.batch.core.repository.dao.JobInstanceDao;
+import org.springframework.batch.core.repository.dao.StepExecutionDao;
+import org.springframework.batch.core.repository.dao.MongoExecutionContextDao;
+import org.springframework.batch.core.repository.dao.MongoJobExecutionDao;
+import org.springframework.batch.core.repository.dao.MongoJobInstanceDao;
+import org.springframework.batch.core.repository.dao.MongoStepExecutionDao;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.util.Assert;
+
+/**
+ * This factory bean creates a job explorer backed by MongoDB. It requires a mongo
+ * template and a mongo transaction manager. The mongo template must be configured
+ * with a {@link MappingMongoConverter} having a {@code MapKeyDotReplacement} set to a non
+ * null value. See {@code MongoDBJobRepositoryIntegrationTests} for an example. This is
+ * required to support execution context keys containing dots (like "step.type" or
+ * "batch.version")
+ *
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoJobExplorerFactoryBean extends AbstractJobExplorerFactoryBean implements InitializingBean {
+
+ private MongoOperations mongoOperations;
+
+ public void setMongoOperations(MongoOperations mongoOperations) {
+ this.mongoOperations = mongoOperations;
+ }
+
+ @Override
+ protected JobInstanceDao createJobInstanceDao() {
+ return new MongoJobInstanceDao(this.mongoOperations);
+ }
+
+ @Override
+ protected JobExecutionDao createJobExecutionDao() {
+ return new MongoJobExecutionDao(this.mongoOperations);
+ }
+
+ @Override
+ protected StepExecutionDao createStepExecutionDao() {
+ return new MongoStepExecutionDao(this.mongoOperations);
+ }
+
+ @Override
+ protected ExecutionContextDao createExecutionContextDao() {
+ return new MongoExecutionContextDao(this.mongoOperations);
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ super.afterPropertiesSet();
+ Assert.notNull(this.mongoOperations, "MongoOperations must not be null.");
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
index 34d6d19f58..a5f6b20122 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java
@@ -17,8 +17,7 @@
package org.springframework.batch.core.job;
import java.time.LocalDateTime;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
import java.util.stream.Collectors;
import io.micrometer.core.instrument.LongTaskTimer;
@@ -43,6 +42,7 @@
import org.springframework.batch.core.StartLimitExceededException;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.job.builder.AlreadyUsedStepNameException;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.batch.core.launch.support.ExitCodeMapper;
import org.springframework.batch.core.listener.CompositeJobExecutionListener;
@@ -300,6 +300,7 @@ public final void execute(JobExecution execution) {
execution.setStartTime(LocalDateTime.now());
updateStatus(execution, BatchStatus.STARTED);
+ checkStepNamesUnicity();
listener.beforeJob(execution);
@@ -368,11 +369,11 @@ public final void execute(JobExecution execution) {
finally {
JobSynchronizationManager.release();
}
-
}
-
}
+ protected abstract void checkStepNamesUnicity() throws AlreadyUsedStepNameException;
+
private void stopObservation(JobExecution execution, Observation observation) {
List throwables = execution.getFailureExceptions();
if (!throwables.isEmpty()) {
@@ -430,6 +431,16 @@ else if (ex instanceof NoSuchJobException || ex.getCause() instanceof NoSuchJobE
return exitStatus;
}
+ protected static void addToMapCheckingUnicity(Map map, Step step, String name)
+ throws AlreadyUsedStepNameException {
+ map.merge(name, step, (old, value) -> {
+ if (!old.equals(value)) {
+ throw new AlreadyUsedStepNameException(name);
+ }
+ return old;
+ });
+ }
+
private void updateStatus(JobExecution jobExecution, BatchStatus status) {
jobExecution.setStatus(status);
jobRepository.update(jobExecution);
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java
index b22317ef28..e45176e199 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/SimpleJob.java
@@ -16,9 +16,7 @@
package org.springframework.batch.core.job;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
@@ -145,4 +143,9 @@ protected void doExecute(JobExecution execution)
}
}
+ @Override
+ protected void checkStepNamesUnicity() {
+ // noop : steps of SimpleJob can share the same name
+ }
+
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/AlreadyUsedStepNameException.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/AlreadyUsedStepNameException.java
new file mode 100644
index 0000000000..fdedfabf6a
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/AlreadyUsedStepNameException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2006-2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.core.job.builder;
+
+/**
+ * Exception to indicate the name of a step is already used by a different step in the
+ * same flow. Step names must be unique within a flow definition because the search of the
+ * next step to find relies on the step name
+ *
+ * @author Fabrice Bibonne
+ *
+ */
+public class AlreadyUsedStepNameException extends RuntimeException {
+
+ public AlreadyUsedStepNameException(String name) {
+ super("the name " + name + " is already used");
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/SimpleJobBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/SimpleJobBuilder.java
index a8be4c6b31..b714d484ea 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/SimpleJobBuilder.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/builder/SimpleJobBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -83,7 +83,7 @@ public SimpleJobBuilder start(Step step) {
* @return a builder for fluent chaining
*/
public FlowBuilder.TransitionBuilder on(String pattern) {
- Assert.state(steps.size() > 0, "You have to start a job with a step");
+ Assert.state(!steps.isEmpty(), "You have to start a job with a step");
for (Step step : steps) {
if (builder == null) {
builder = new JobFlowBuilder(new FlowJobBuilder(this), step);
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/FlowJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/FlowJob.java
index 33e2f491fe..9655d62a4f 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/FlowJob.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/FlowJob.java
@@ -15,19 +15,20 @@
*/
package org.springframework.batch.core.job.flow;
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionException;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.AbstractJob;
import org.springframework.batch.core.job.SimpleStepHandler;
+import org.springframework.batch.core.job.builder.AlreadyUsedStepNameException;
import org.springframework.batch.core.step.StepHolder;
import org.springframework.batch.core.step.StepLocator;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
/**
* Implementation of the {@link Job} interface that allows for complex flows of steps,
* rather than requiring sequential execution. In general, this job implementation was
@@ -74,9 +75,7 @@ public void setFlow(Flow flow) {
*/
@Override
public Step getStep(String stepName) {
- if (!initialized) {
- init();
- }
+ init();
return stepMap.get(stepName);
}
@@ -84,30 +83,34 @@ public Step getStep(String stepName) {
* Initialize the step names
*/
private void init() {
- findSteps(flow, stepMap);
- initialized = true;
+ if (!initialized) {
+ findStepsThrowingIfNameNotUnique(flow);
+ initialized = true;
+ }
}
- private void findSteps(Flow flow, Map map) {
+ private void findStepsThrowingIfNameNotUnique(Flow flow) {
for (State state : flow.getStates()) {
if (state instanceof StepLocator locator) {
for (String name : locator.getStepNames()) {
- map.put(name, locator.getStep(name));
+ addToMapCheckingUnicity(this.stepMap, locator.getStep(name), name);
}
}
- else if (state instanceof StepHolder) {
- Step step = ((StepHolder) state).getStep();
- String name = step.getName();
- stepMap.put(name, step);
+ // TODO remove this else bock ? not executed during tests : the only State
+ // which implements StepHolder is StepState which already implements
+ // StepLocator
+ // within tests coverage `state instanceof StepHolder` is false 30 times/30
+ else if (state instanceof StepHolder stepHolder) {
+ Step step = stepHolder.getStep();
+ addToMapCheckingUnicity(this.stepMap, step, step.getName());
}
- else if (state instanceof FlowHolder) {
- for (Flow subflow : ((FlowHolder) state).getFlows()) {
- findSteps(subflow, map);
+ else if (state instanceof FlowHolder flowHolder) {
+ for (Flow subflow : flowHolder.getFlows()) {
+ findStepsThrowingIfNameNotUnique(subflow);
}
}
}
-
}
/**
@@ -115,9 +118,7 @@ else if (state instanceof FlowHolder) {
*/
@Override
public Collection getStepNames() {
- if (!initialized) {
- init();
- }
+ init();
return stepMap.keySet();
}
@@ -139,4 +140,9 @@ protected void doExecute(final JobExecution execution) throws JobExecutionExcept
}
}
+ @Override
+ protected void checkStepNamesUnicity() throws AlreadyUsedStepNameException {
+ init();
+ }
+
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/JobFlowExecutor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/JobFlowExecutor.java
index 9827040573..c1583e25f1 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/JobFlowExecutor.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/JobFlowExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -35,6 +35,7 @@
* @author Dave Syer
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Seungrae Kim
*
*/
public class JobFlowExecutor implements FlowExecutor {
@@ -58,7 +59,6 @@ public JobFlowExecutor(JobRepository jobRepository, StepHandler stepHandler, Job
this.jobRepository = jobRepository;
this.stepHandler = stepHandler;
this.execution = execution;
- stepExecutionHolder.set(null);
}
@Override
@@ -118,7 +118,7 @@ public StepExecution getStepExecution() {
@Override
public void close(FlowExecution result) {
- stepExecutionHolder.set(null);
+ stepExecutionHolder.remove();
}
@Override
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparator.java
index c49d569422..53015ae8ce 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparator.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2023 the original author or authors.
+ * Copyright 2013-2024 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.
@@ -20,8 +20,10 @@
import java.util.Comparator;
/**
- * Sorts by ascending specificity of pattern, based on counting wildcards (with * taking
- * precedence over ?). Hence * > foo* > ??? > fo? > foo.
+ * Sorts by descending specificity of pattern, based on counting wildcards (with ? being
+ * considered more specific than *). This means that more specific patterns will be
+ * considered greater than less specific patterns. Hence foo > fo? > ??? > foo*
+ * > *
*
* For more complex comparisons, any string containing at least one * token will be
* considered more generic than any string that has no * token. If both strings have at
@@ -39,8 +41,8 @@
*
* If the strings contain neither * nor ? tokens then alphabetic comparison will be used.
*
- * Hence * > foo* > *f* > *foo* > ??? > ?o? > foo?? > bar?? > fo?
- * > foo > bar
+ * Hence bar > foo > fo? > bar?? > foo?? > ?0? > ??? > *foo* > *f*
+ * > foo* > *
*
* @see Comparator
* @author Michael Minella
@@ -61,31 +63,31 @@ public int compare(StateTransition arg0, StateTransition arg1) {
int arg0AsteriskCount = StringUtils.countOccurrencesOf(arg0Pattern, "*");
int arg1AsteriskCount = StringUtils.countOccurrencesOf(arg1Pattern, "*");
if (arg0AsteriskCount > 0 && arg1AsteriskCount == 0) {
- return 1;
+ return -1;
}
if (arg0AsteriskCount == 0 && arg1AsteriskCount > 0) {
- return -1;
+ return 1;
}
if (arg0AsteriskCount > 0 && arg1AsteriskCount > 0) {
if (arg0AsteriskCount < arg1AsteriskCount) {
- return 1;
+ return -1;
}
if (arg0AsteriskCount > arg1AsteriskCount) {
- return -1;
+ return 1;
}
}
int arg0WildcardCount = StringUtils.countOccurrencesOf(arg0Pattern, "?");
int arg1WildcardCount = StringUtils.countOccurrencesOf(arg1Pattern, "?");
if (arg0WildcardCount > arg1WildcardCount) {
- return 1;
+ return -1;
}
if (arg0WildcardCount < arg1WildcardCount) {
- return -1;
+ return 1;
}
if (arg0Pattern.length() != arg1Pattern.length() && (arg0AsteriskCount > 0 || arg0WildcardCount > 0)) {
- return Integer.compare(arg1Pattern.length(), arg0Pattern.length());
+ return Integer.compare(arg0Pattern.length(), arg1Pattern.length());
}
- return arg0.getPattern().compareTo(arg1Pattern);
+ return arg1.getPattern().compareTo(arg0Pattern);
}
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java
index f66ce004c2..1818d017fd 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/SimpleFlow.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -313,7 +313,7 @@ private void initializeTransitions() {
set = new LinkedHashSet<>();
}
else {
- set = new TreeSet<>(stateTransitionComparator);
+ set = new TreeSet<>(stateTransitionComparator).descendingSet();
}
transitionMap.put(name, set);
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/StateTransition.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/StateTransition.java
index ca2f762b1a..6757f9cc69 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/StateTransition.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/flow/support/StateTransition.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -22,6 +22,8 @@
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
+import java.util.Objects;
+
/**
* Value object representing a potential transition from one {@link State} to another. The
* originating State name and the next {@link State} to execute are linked by a pattern
@@ -31,6 +33,7 @@
* @author Dave Syer
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Kim Youngwoong
* @since 2.0
*/
public final class StateTransition {
@@ -159,6 +162,22 @@ public boolean isEnd() {
return next == null;
}
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ StateTransition that = (StateTransition) o;
+ return Objects.equals(state, that.state) && Objects.equals(pattern, that.pattern)
+ && Objects.equals(next, that.next);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(state, pattern, next);
+ }
+
@Override
public String toString() {
return String.format("StateTransition: [state=%s, pattern=%s, next=%s]", state == null ? null : state.getName(),
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java
index 768947859d..a56947412b 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/JobOperator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -101,24 +101,6 @@ default JobInstance getJobInstance(String jobName, JobParameters jobParameters)
*/
String getParameters(long executionId) throws NoSuchJobExecutionException;
- /**
- * Start a new instance of a job with the parameters specified.
- * @param jobName the name of the {@link Job} to launch
- * @param parameters the parameters to launch it with (new line separated key=value
- * pairs)
- * @return the id of the {@link JobExecution} that is launched
- * @throws NoSuchJobException if there is no {@link Job} with the specified name
- * @throws JobInstanceAlreadyExistsException if a job instance with this name and
- * parameters already exists
- * @throws JobParametersInvalidException thrown if any of the job parameters are
- * invalid.
- * @deprecated use {@link #start(String, Properties)} instead. Will be removed in
- * v5.2.
- */
- @Deprecated(since = "5.0.1", forRemoval = true)
- Long start(String jobName, String parameters)
- throws NoSuchJobException, JobInstanceAlreadyExistsException, JobParametersInvalidException;
-
/**
* Start a new instance of a job with the parameters specified.
* @param jobName the name of the {@link Job} to launch
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
index 469ff15622..34bdf928b0 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
@@ -53,6 +53,7 @@
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -405,7 +406,7 @@ private List getJobExecutionsWithStatusGreaterThan(String jobIdent
for (JobInstance jobInstance : lastInstances) {
List jobExecutions = jobExplorer.getJobExecutions(jobInstance);
- if (jobExecutions == null || jobExecutions.isEmpty()) {
+ if (CollectionUtils.isEmpty(jobExecutions)) {
continue;
}
for (JobExecution jobExecution : jobExecutions) {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/DataFieldMaxValueJobParametersIncrementer.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/DataFieldMaxValueJobParametersIncrementer.java
index c01e511c1e..5cce9c53f9 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/DataFieldMaxValueJobParametersIncrementer.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/DataFieldMaxValueJobParametersIncrementer.java
@@ -19,6 +19,7 @@
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersIncrementer;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -50,7 +51,7 @@ public DataFieldMaxValueJobParametersIncrementer(DataFieldMaxValueIncrementer da
}
@Override
- public JobParameters getNext(JobParameters jobParameters) {
+ public JobParameters getNext(@Nullable JobParameters jobParameters) {
return new JobParametersBuilder(jobParameters == null ? new JobParameters() : jobParameters)
.addLong(this.key, this.dataFieldMaxValueIncrementer.nextLongValue())
.toJobParameters();
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java
deleted file mode 100644
index 6369c092b3..0000000000
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobLauncher.java
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright 2006-2023 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
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.batch.core.launch.support;
-
-import java.time.Duration;
-
-import io.micrometer.core.instrument.Counter;
-import io.micrometer.core.instrument.MeterRegistry;
-import io.micrometer.core.instrument.Metrics;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import org.springframework.batch.core.BatchStatus;
-import org.springframework.batch.core.ExitStatus;
-import org.springframework.batch.core.Job;
-import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.JobInstance;
-import org.springframework.batch.core.JobParameters;
-import org.springframework.batch.core.JobParametersInvalidException;
-import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.launch.JobLauncher;
-import org.springframework.batch.core.observability.BatchMetrics;
-import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
-import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
-import org.springframework.batch.core.repository.JobRepository;
-import org.springframework.batch.core.repository.JobRestartException;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.core.task.SyncTaskExecutor;
-import org.springframework.core.task.TaskExecutor;
-import org.springframework.core.task.TaskRejectedException;
-import org.springframework.util.Assert;
-
-/**
- * Simple implementation of the {@link JobLauncher} interface. The Spring Core
- * {@link TaskExecutor} interface is used to launch a {@link Job}. This means that the
- * type of executor set is very important. If a {@link SyncTaskExecutor} is used, then the
- * job will be processed within the same thread that called the launcher.
- * Care should be taken to ensure any users of this class understand fully whether or not
- * the implementation of TaskExecutor used will start tasks synchronously or
- * asynchronously. The default setting uses a synchronous task executor.
- *
- * There is only one required dependency of this Launcher, a {@link JobRepository}. The
- * JobRepository is used to obtain a valid JobExecution. The Repository must be used
- * because the provided {@link Job} could be a restart of an existing {@link JobInstance},
- * and only the Repository can reliably recreate it.
- *
- * @author Lucas Ward
- * @author Dave Syer
- * @author Will Schipp
- * @author Michael Minella
- * @author Mahmoud Ben Hassine
- * @since 1.0
- * @see JobRepository
- * @see TaskExecutor
- * @deprecated Since v5.0.0 for removal in v5.2.0. Use {@link TaskExecutorJobLauncher}.
- */
-@Deprecated(since = "5.0.0", forRemoval = true)
-public class SimpleJobLauncher implements JobLauncher, InitializingBean {
-
- protected static final Log logger = LogFactory.getLog(SimpleJobLauncher.class);
-
- private JobRepository jobRepository;
-
- private TaskExecutor taskExecutor;
-
- private MeterRegistry meterRegistry = Metrics.globalRegistry;
-
- private Counter jobLaunchCount; // NoopCounter is still incubating
-
- /**
- * Run the provided job with the given {@link JobParameters}. The
- * {@link JobParameters} will be used to determine if this is an execution of an
- * existing job instance, or if a new one should be created.
- * @param job the job to be run.
- * @param jobParameters the {@link JobParameters} for this particular execution.
- * @return the {@link JobExecution} if it returns synchronously. If the implementation
- * is asynchronous, the status might well be unknown.
- * @throws JobExecutionAlreadyRunningException if the JobInstance already exists and
- * has an execution already running.
- * @throws JobRestartException if the execution would be a re-start, but a re-start is
- * either not allowed or not needed.
- * @throws JobInstanceAlreadyCompleteException if this instance has already completed
- * successfully
- * @throws JobParametersInvalidException thrown if jobParameters is invalid.
- */
- @Override
- public JobExecution run(final Job job, final JobParameters jobParameters)
- throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
- JobParametersInvalidException {
-
- Assert.notNull(job, "The Job must not be null.");
- Assert.notNull(jobParameters, "The JobParameters must not be null.");
- if (this.jobLaunchCount != null) {
- this.jobLaunchCount.increment();
- }
-
- final JobExecution jobExecution;
- JobExecution lastExecution = jobRepository.getLastJobExecution(job.getName(), jobParameters);
- if (lastExecution != null) {
- if (!job.isRestartable()) {
- throw new JobRestartException("JobInstance already exists and is not restartable");
- }
- /*
- * validate here if it has stepExecutions that are UNKNOWN, STARTING, STARTED
- * and STOPPING retrieve the previous execution and check
- */
- for (StepExecution execution : lastExecution.getStepExecutions()) {
- BatchStatus status = execution.getStatus();
- if (status.isRunning()) {
- throw new JobExecutionAlreadyRunningException(
- "A job execution for this job is already running: " + lastExecution);
- }
- else if (status == BatchStatus.UNKNOWN) {
- throw new JobRestartException(
- "Cannot restart step [" + execution.getStepName() + "] from UNKNOWN status. "
- + "The last execution ended with a failure that could not be rolled back, "
- + "so it may be dangerous to proceed. Manual intervention is probably necessary.");
- }
- }
- }
-
- // Check the validity of the parameters before doing creating anything
- // in the repository...
- job.getJobParametersValidator().validate(jobParameters);
-
- /*
- * There is a very small probability that a non-restartable job can be restarted,
- * but only if another process or thread manages to launch and fail a job
- * execution for this instance between the last assertion and the next method
- * returning successfully.
- */
- jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
-
- try {
- taskExecutor.execute(new Runnable() {
-
- @Override
- public void run() {
- try {
- if (logger.isInfoEnabled()) {
- logger.info("Job: [" + job + "] launched with the following parameters: [" + jobParameters
- + "]");
- }
- job.execute(jobExecution);
- if (logger.isInfoEnabled()) {
- Duration jobExecutionDuration = BatchMetrics.calculateDuration(jobExecution.getStartTime(),
- jobExecution.getEndTime());
- logger.info("Job: [" + job + "] completed with the following parameters: [" + jobParameters
- + "] and the following status: [" + jobExecution.getStatus() + "]"
- + (jobExecutionDuration == null ? ""
- : " in " + BatchMetrics.formatDuration(jobExecutionDuration)));
- }
- }
- catch (Throwable t) {
- if (logger.isInfoEnabled()) {
- logger.info("Job: [" + job
- + "] failed unexpectedly and fatally with the following parameters: ["
- + jobParameters + "]", t);
- }
- rethrow(t);
- }
- }
-
- private void rethrow(Throwable t) {
- if (t instanceof RuntimeException) {
- throw (RuntimeException) t;
- }
- else if (t instanceof Error) {
- throw (Error) t;
- }
- throw new IllegalStateException(t);
- }
- });
- }
- catch (TaskRejectedException e) {
- jobExecution.upgradeStatus(BatchStatus.FAILED);
- if (jobExecution.getExitStatus().equals(ExitStatus.UNKNOWN)) {
- jobExecution.setExitStatus(ExitStatus.FAILED.addExitDescription(e));
- }
- jobRepository.update(jobExecution);
- }
-
- return jobExecution;
- }
-
- /**
- * Set the JobRepository.
- * @param jobRepository instance of {@link JobRepository}.
- */
- public void setJobRepository(JobRepository jobRepository) {
- this.jobRepository = jobRepository;
- }
-
- /**
- * Set the TaskExecutor. (Optional)
- * @param taskExecutor instance of {@link TaskExecutor}.
- */
- public void setTaskExecutor(TaskExecutor taskExecutor) {
- this.taskExecutor = taskExecutor;
- }
-
- /**
- * Set the meter registry to use for metrics. Defaults to
- * {@link Metrics#globalRegistry}.
- * @param meterRegistry the meter registry
- * @since 5.0
- */
- public void setMeterRegistry(MeterRegistry meterRegistry) {
- this.meterRegistry = meterRegistry;
- }
-
- /**
- * Ensure the required dependencies of a {@link JobRepository} have been set.
- */
- @Override
- public void afterPropertiesSet() throws Exception {
- Assert.state(jobRepository != null, "A JobRepository has not been set.");
- if (taskExecutor == null) {
- logger.info("No TaskExecutor has been set, defaulting to synchronous executor.");
- taskExecutor = new SyncTaskExecutor();
- }
- this.jobLaunchCount = BatchMetrics.createCounter(this.meterRegistry, "job.launch.count", "Job launch count");
- }
-
-}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java
index f700e35e50..059e769960 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -253,14 +253,6 @@ public Long restart(long executionId) throws JobInstanceAlreadyCompleteException
}
- @Override
- @Deprecated(since = "5.0.1", forRemoval = true)
- public Long start(String jobName, String parameters)
- throws NoSuchJobException, JobInstanceAlreadyExistsException, JobParametersInvalidException {
- Properties properties = PropertiesConverter.stringToProperties(parameters);
- return start(jobName, properties);
- }
-
@Override
public Long start(String jobName, Properties parameters)
throws NoSuchJobException, JobInstanceAlreadyExistsException, JobParametersInvalidException {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobLauncher.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobLauncher.java
index fe003d3d44..aab3443cd5 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobLauncher.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobLauncher.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -15,18 +15,33 @@
*/
package org.springframework.batch.core.launch.support;
+import java.time.Duration;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Metrics;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.batch.core.BatchStatus;
+import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobInstance;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersInvalidException;
+import org.springframework.batch.core.StepExecution;
import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.observability.BatchMetrics;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
+import org.springframework.core.task.TaskRejectedException;
+import org.springframework.util.Assert;
/**
* Implementation of the {@link JobLauncher} interface based on a {@link TaskExecutor}.
@@ -51,27 +66,171 @@
* @see JobRepository
* @see TaskExecutor
*/
-public class TaskExecutorJobLauncher extends SimpleJobLauncher {
+public class TaskExecutorJobLauncher implements JobLauncher, InitializingBean {
+
+ protected static final Log logger = LogFactory.getLog(TaskExecutorJobLauncher.class);
+
+ private JobRepository jobRepository;
+
+ private TaskExecutor taskExecutor;
+ private MeterRegistry meterRegistry = Metrics.globalRegistry;
+
+ private Counter jobLaunchCount; // NoopCounter is still incubating
+
+ /**
+ * Run the provided job with the given {@link JobParameters}. The
+ * {@link JobParameters} will be used to determine if this is an execution of an
+ * existing job instance, or if a new one should be created.
+ * @param job the job to be run.
+ * @param jobParameters the {@link JobParameters} for this particular execution.
+ * @return the {@link JobExecution} if it returns synchronously. If the implementation
+ * is asynchronous, the status might well be unknown.
+ * @throws JobExecutionAlreadyRunningException if the JobInstance already exists and
+ * has an execution already running.
+ * @throws JobRestartException if the execution would be a re-start, but a re-start is
+ * either not allowed or not needed.
+ * @throws JobInstanceAlreadyCompleteException if this instance has already completed
+ * successfully
+ * @throws JobParametersInvalidException thrown if jobParameters is invalid.
+ */
@Override
- public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
- JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException {
- return super.run(job, jobParameters);
+ public JobExecution run(final Job job, final JobParameters jobParameters)
+ throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
+ JobParametersInvalidException {
+
+ Assert.notNull(job, "The Job must not be null.");
+ Assert.notNull(jobParameters, "The JobParameters must not be null.");
+ if (this.jobLaunchCount != null) {
+ this.jobLaunchCount.increment();
+ }
+
+ final JobExecution jobExecution;
+ JobExecution lastExecution = jobRepository.getLastJobExecution(job.getName(), jobParameters);
+ if (lastExecution != null) {
+ if (!job.isRestartable()) {
+ throw new JobRestartException("JobInstance already exists and is not restartable");
+ }
+ /*
+ * validate here if it has stepExecutions that are UNKNOWN, STARTING, STARTED
+ * and STOPPING retrieve the previous execution and check
+ */
+ for (StepExecution execution : lastExecution.getStepExecutions()) {
+ BatchStatus status = execution.getStatus();
+ if (status.isRunning()) {
+ throw new JobExecutionAlreadyRunningException(
+ "A job execution for this job is already running: " + lastExecution);
+ }
+ else if (status == BatchStatus.UNKNOWN) {
+ throw new JobRestartException(
+ "Cannot restart step [" + execution.getStepName() + "] from UNKNOWN status. "
+ + "The last execution ended with a failure that could not be rolled back, "
+ + "so it may be dangerous to proceed. Manual intervention is probably necessary.");
+ }
+ }
+ }
+
+ // Check the validity of the parameters before doing creating anything
+ // in the repository...
+ job.getJobParametersValidator().validate(jobParameters);
+
+ /*
+ * There is a very small probability that a non-restartable job can be restarted,
+ * but only if another process or thread manages to launch and fail a job
+ * execution for this instance between the last assertion and the next method
+ * returning successfully.
+ */
+ jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
+
+ try {
+ taskExecutor.execute(new Runnable() {
+
+ @Override
+ public void run() {
+ try {
+ if (logger.isInfoEnabled()) {
+ logger.info("Job: [" + job + "] launched with the following parameters: [" + jobParameters
+ + "]");
+ }
+ job.execute(jobExecution);
+ if (logger.isInfoEnabled()) {
+ Duration jobExecutionDuration = BatchMetrics.calculateDuration(jobExecution.getStartTime(),
+ jobExecution.getEndTime());
+ logger.info("Job: [" + job + "] completed with the following parameters: [" + jobParameters
+ + "] and the following status: [" + jobExecution.getStatus() + "]"
+ + (jobExecutionDuration == null ? ""
+ : " in " + BatchMetrics.formatDuration(jobExecutionDuration)));
+ }
+ }
+ catch (Throwable t) {
+ if (logger.isInfoEnabled()) {
+ logger.info("Job: [" + job
+ + "] failed unexpectedly and fatally with the following parameters: ["
+ + jobParameters + "]", t);
+ }
+ rethrow(t);
+ }
+ }
+
+ private void rethrow(Throwable t) {
+ if (t instanceof RuntimeException) {
+ throw (RuntimeException) t;
+ }
+ else if (t instanceof Error) {
+ throw (Error) t;
+ }
+ throw new IllegalStateException(t);
+ }
+ });
+ }
+ catch (TaskRejectedException e) {
+ jobExecution.upgradeStatus(BatchStatus.FAILED);
+ if (jobExecution.getExitStatus().equals(ExitStatus.UNKNOWN)) {
+ jobExecution.setExitStatus(ExitStatus.FAILED.addExitDescription(e));
+ }
+ jobRepository.update(jobExecution);
+ }
+
+ return jobExecution;
}
- @Override
+ /**
+ * Set the JobRepository.
+ * @param jobRepository instance of {@link JobRepository}.
+ */
public void setJobRepository(JobRepository jobRepository) {
- super.setJobRepository(jobRepository);
+ this.jobRepository = jobRepository;
}
- @Override
+ /**
+ * Set the TaskExecutor. (Optional)
+ * @param taskExecutor instance of {@link TaskExecutor}.
+ */
public void setTaskExecutor(TaskExecutor taskExecutor) {
- super.setTaskExecutor(taskExecutor);
+ this.taskExecutor = taskExecutor;
+ }
+
+ /**
+ * Set the meter registry to use for metrics. Defaults to
+ * {@link Metrics#globalRegistry}.
+ * @param meterRegistry the meter registry
+ * @since 5.0
+ */
+ public void setMeterRegistry(MeterRegistry meterRegistry) {
+ this.meterRegistry = meterRegistry;
}
+ /**
+ * Ensure the required dependencies of a {@link JobRepository} have been set.
+ */
@Override
public void afterPropertiesSet() throws Exception {
- super.afterPropertiesSet();
+ Assert.state(jobRepository != null, "A JobRepository has not been set.");
+ if (taskExecutor == null) {
+ logger.info("No TaskExecutor has been set, defaulting to synchronous executor.");
+ taskExecutor = new SyncTaskExecutor();
+ }
+ this.jobLaunchCount = BatchMetrics.createCounter(this.meterRegistry, "job.launch.count", "Job launch count");
}
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/AbstractListenerFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/AbstractListenerFactoryBean.java
index c4b6bd9f7c..18c9e4cf2e 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/AbstractListenerFactoryBean.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/AbstractListenerFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -35,9 +35,6 @@
import org.springframework.core.Ordered;
import org.springframework.util.Assert;
-import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerByAnnotation;
-import static org.springframework.batch.support.MethodInvokerUtils.getMethodInvokerForInterface;
-
/**
* {@link FactoryBean} implementation that builds a listener based on the various
* lifecycle methods or annotations that are provided. There are three possible ways of
@@ -61,6 +58,7 @@
*
* @author Lucas Ward
* @author Dan Garrette
+ * @author Taeik Lim
* @since 2.0
* @see ListenerMetaData
*/
@@ -98,8 +96,8 @@ public Object getObject() {
Set invokers = new HashSet<>();
MethodInvoker invoker;
- invoker = getMethodInvokerForInterface(metaData.getListenerInterface(), metaData.getMethodName(), delegate,
- metaData.getParamTypes());
+ invoker = MethodInvokerUtils.getMethodInvokerForInterface(metaData.getListenerInterface(),
+ metaData.getMethodName(), delegate, metaData.getParamTypes());
if (invoker != null) {
invokers.add(invoker);
}
@@ -111,7 +109,8 @@ public Object getObject() {
}
if (metaData.getAnnotation() != null) {
- invoker = getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate, metaData.getParamTypes());
+ invoker = MethodInvokerUtils.getMethodInvokerByAnnotation(metaData.getAnnotation(), delegate,
+ metaData.getParamTypes());
if (invoker != null) {
invokers.add(invoker);
synthetic = true;
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
index 14fd39e02d..07915965c0 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -156,7 +156,7 @@ public ExecutionContext getExecutionContext(JobExecution jobExecution) {
List results = getJdbcTemplate().query(getQuery(FIND_JOB_EXECUTION_CONTEXT),
new ExecutionContextRowMapper(), executionId);
- if (results.size() > 0) {
+ if (!results.isEmpty()) {
return results.get(0);
}
else {
@@ -268,6 +268,11 @@ public void deleteExecutionContext(StepExecution stepExecution) {
getJdbcTemplate().update(getQuery(DELETE_STEP_EXECUTION_CONTEXT), stepExecution.getId());
}
+ /**
+ * @deprecated Since 5.2 with no replacement. Scheduled for removal in v6
+ * @param lobHandler the lob handler to use
+ */
+ @Deprecated(since = "5.2.0", forRemoval = true)
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
index 795d8b1b4b..b1e46e0c23 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -24,6 +24,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.Lock;
@@ -97,7 +98,6 @@ public class JdbcStepExecutionDao extends AbstractJdbcBatchMetadataDao implement
FROM %PREFIX%JOB_EXECUTION JE
JOIN %PREFIX%STEP_EXECUTION SE ON SE.JOB_EXECUTION_ID = JE.JOB_EXECUTION_ID
WHERE JE.JOB_INSTANCE_ID = ? AND SE.STEP_NAME = ?
- ORDER BY SE.CREATE_TIME DESC, SE.STEP_EXECUTION_ID DESC
""";
private static final String CURRENT_VERSION_STEP_EXECUTION = """
@@ -117,6 +117,10 @@ SELECT COUNT(*)
WHERE STEP_EXECUTION_ID = ?
""";
+ private static final Comparator BY_CREATE_TIME_DESC_ID_DESC = Comparator
+ .comparing(StepExecution::getCreateTime, Comparator.reverseOrder())
+ .thenComparing(StepExecution::getId, Comparator.reverseOrder());
+
private int exitMessageLength = DEFAULT_EXIT_MESSAGE_LENGTH;
private DataFieldMaxValueIncrementer stepExecutionIncrementer;
@@ -277,10 +281,9 @@ public void updateStepExecution(StepExecution stepExecution) {
stepExecution.getWriteSkipCount(), stepExecution.getRollbackCount(), lastUpdated,
stepExecution.getId(), stepExecution.getVersion() };
int count = getJdbcTemplate().update(getQuery(UPDATE_STEP_EXECUTION), parameters,
- new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.INTEGER, Types.INTEGER,
- Types.INTEGER, Types.INTEGER, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.INTEGER,
- Types.INTEGER, Types.INTEGER, Types.INTEGER, Types.TIMESTAMP, Types.BIGINT,
- Types.INTEGER });
+ new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.BIGINT, Types.BIGINT,
+ Types.BIGINT, Types.BIGINT, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.BIGINT,
+ Types.BIGINT, Types.BIGINT, Types.BIGINT, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER });
// Avoid concurrent modifications...
if (count == 0) {
@@ -348,6 +351,7 @@ public StepExecution getLastStepExecution(JobInstance jobInstance, String stepNa
jobExecution.setVersion(rs.getInt(27));
return new StepExecutionRowMapper(jobExecution).mapRow(rs, rowNum);
}, jobInstance.getInstanceId(), stepName);
+ executions.sort(BY_CREATE_TIME_DESC_ID_DESC);
if (executions.isEmpty()) {
return null;
}
@@ -391,15 +395,15 @@ public StepExecution mapRow(ResultSet rs, int rowNum) throws SQLException {
stepExecution.setStartTime(rs.getTimestamp(3) == null ? null : rs.getTimestamp(3).toLocalDateTime());
stepExecution.setEndTime(rs.getTimestamp(4) == null ? null : rs.getTimestamp(4).toLocalDateTime());
stepExecution.setStatus(BatchStatus.valueOf(rs.getString(5)));
- stepExecution.setCommitCount(rs.getInt(6));
- stepExecution.setReadCount(rs.getInt(7));
- stepExecution.setFilterCount(rs.getInt(8));
- stepExecution.setWriteCount(rs.getInt(9));
+ stepExecution.setCommitCount(rs.getLong(6));
+ stepExecution.setReadCount(rs.getLong(7));
+ stepExecution.setFilterCount(rs.getLong(8));
+ stepExecution.setWriteCount(rs.getLong(9));
stepExecution.setExitStatus(new ExitStatus(rs.getString(10), rs.getString(11)));
- stepExecution.setReadSkipCount(rs.getInt(12));
- stepExecution.setWriteSkipCount(rs.getInt(13));
- stepExecution.setProcessSkipCount(rs.getInt(14));
- stepExecution.setRollbackCount(rs.getInt(15));
+ stepExecution.setReadSkipCount(rs.getLong(12));
+ stepExecution.setWriteSkipCount(rs.getLong(13));
+ stepExecution.setProcessSkipCount(rs.getLong(14));
+ stepExecution.setRollbackCount(rs.getLong(15));
stepExecution.setLastUpdated(rs.getTimestamp(16) == null ? null : rs.getTimestamp(16).toLocalDateTime());
stepExecution.setVersion(rs.getInt(17));
stepExecution.setCreateTime(rs.getTimestamp(18) == null ? null : rs.getTimestamp(18).toLocalDateTime());
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoExecutionContextDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoExecutionContextDao.java
new file mode 100644
index 0000000000..7b3e80294b
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoExecutionContextDao.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.dao;
+
+import java.util.Collection;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoExecutionContextDao implements ExecutionContextDao {
+
+ private static final String STEP_EXECUTIONS_COLLECTION_NAME = "BATCH_STEP_EXECUTION";
+
+ private static final String JOB_EXECUTIONS_COLLECTION_NAME = "BATCH_JOB_EXECUTION";
+
+ private final MongoOperations mongoOperations;
+
+ public MongoExecutionContextDao(MongoOperations mongoOperations) {
+ this.mongoOperations = mongoOperations;
+ }
+
+ @Override
+ public ExecutionContext getExecutionContext(JobExecution jobExecution) {
+ Query query = query(where("jobExecutionId").is(jobExecution.getId()));
+ org.springframework.batch.core.repository.persistence.JobExecution execution = this.mongoOperations.findOne(
+ query, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ if (execution == null) {
+ return new ExecutionContext();
+ }
+ return new ExecutionContext(execution.getExecutionContext().map());
+ }
+
+ @Override
+ public ExecutionContext getExecutionContext(StepExecution stepExecution) {
+ Query query = query(where("stepExecutionId").is(stepExecution.getId()));
+ org.springframework.batch.core.repository.persistence.StepExecution execution = this.mongoOperations.findOne(
+ query, org.springframework.batch.core.repository.persistence.StepExecution.class,
+ STEP_EXECUTIONS_COLLECTION_NAME);
+ if (execution == null) {
+ return new ExecutionContext();
+ }
+ return new ExecutionContext(execution.getExecutionContext().map());
+ }
+
+ @Override
+ public void saveExecutionContext(JobExecution jobExecution) {
+ ExecutionContext executionContext = jobExecution.getExecutionContext();
+ Query query = query(where("jobExecutionId").is(jobExecution.getId()));
+
+ Update update = Update.update("executionContext",
+ new org.springframework.batch.core.repository.persistence.ExecutionContext(executionContext.toMap(),
+ executionContext.isDirty()));
+ this.mongoOperations.updateFirst(query, update,
+ org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ }
+
+ @Override
+ public void saveExecutionContext(StepExecution stepExecution) {
+ ExecutionContext executionContext = stepExecution.getExecutionContext();
+ Query query = query(where("stepExecutionId").is(stepExecution.getId()));
+
+ Update update = Update.update("executionContext",
+ new org.springframework.batch.core.repository.persistence.ExecutionContext(executionContext.toMap(),
+ executionContext.isDirty()));
+ this.mongoOperations.updateFirst(query, update,
+ org.springframework.batch.core.repository.persistence.StepExecution.class,
+ STEP_EXECUTIONS_COLLECTION_NAME);
+
+ }
+
+ @Override
+ public void saveExecutionContexts(Collection stepExecutions) {
+ for (StepExecution stepExecution : stepExecutions) {
+ saveExecutionContext(stepExecution);
+ }
+ }
+
+ @Override
+ public void updateExecutionContext(JobExecution jobExecution) {
+ saveExecutionContext(jobExecution);
+ }
+
+ @Override
+ public void updateExecutionContext(StepExecution stepExecution) {
+ saveExecutionContext(stepExecution);
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java
new file mode 100644
index 0000000000..90d3326a9a
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.dao;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.repository.persistence.converter.JobExecutionConverter;
+import org.springframework.batch.core.repository.persistence.converter.JobInstanceConverter;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
+
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoJobExecutionDao implements JobExecutionDao {
+
+ private static final String JOB_EXECUTIONS_COLLECTION_NAME = "BATCH_JOB_EXECUTION";
+
+ private static final String JOB_EXECUTIONS_SEQUENCE_NAME = "BATCH_JOB_EXECUTION_SEQ";
+
+ private static final String JOB_INSTANCES_COLLECTION_NAME = "BATCH_JOB_INSTANCE";
+
+ private final MongoOperations mongoOperations;
+
+ private final JobExecutionConverter jobExecutionConverter = new JobExecutionConverter();
+
+ private final JobInstanceConverter jobInstanceConverter = new JobInstanceConverter();
+
+ private DataFieldMaxValueIncrementer jobExecutionIncrementer;
+
+ public MongoJobExecutionDao(MongoOperations mongoOperations) {
+ this.mongoOperations = mongoOperations;
+ this.jobExecutionIncrementer = new MongoSequenceIncrementer(mongoOperations, JOB_EXECUTIONS_SEQUENCE_NAME);
+ }
+
+ public void setJobExecutionIncrementer(DataFieldMaxValueIncrementer jobExecutionIncrementer) {
+ this.jobExecutionIncrementer = jobExecutionIncrementer;
+ }
+
+ @Override
+ public void saveJobExecution(JobExecution jobExecution) {
+ org.springframework.batch.core.repository.persistence.JobExecution jobExecutionToSave = this.jobExecutionConverter
+ .fromJobExecution(jobExecution);
+ long jobExecutionId = this.jobExecutionIncrementer.nextLongValue();
+ jobExecutionToSave.setJobExecutionId(jobExecutionId);
+ this.mongoOperations.insert(jobExecutionToSave, JOB_EXECUTIONS_COLLECTION_NAME);
+ jobExecution.setId(jobExecutionId);
+ }
+
+ @Override
+ public void updateJobExecution(JobExecution jobExecution) {
+ Query query = query(where("jobExecutionId").is(jobExecution.getId()));
+ org.springframework.batch.core.repository.persistence.JobExecution jobExecutionToUpdate = this.jobExecutionConverter
+ .fromJobExecution(jobExecution);
+ this.mongoOperations.findAndReplace(query, jobExecutionToUpdate, JOB_EXECUTIONS_COLLECTION_NAME);
+ }
+
+ @Override
+ public List findJobExecutions(JobInstance jobInstance) {
+ Query query = query(where("jobInstanceId").is(jobInstance.getId()));
+ List jobExecutions = this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ return jobExecutions.stream()
+ .map(jobExecution -> this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance))
+ .toList();
+ }
+
+ @Override
+ public JobExecution getLastJobExecution(JobInstance jobInstance) {
+ Query query = query(where("jobInstanceId").is(jobInstance.getId()));
+ Sort.Order sortOrder = Sort.Order.desc("jobExecutionId");
+ org.springframework.batch.core.repository.persistence.JobExecution jobExecution = this.mongoOperations.findOne(
+ query.with(Sort.by(sortOrder)),
+ org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ return jobExecution != null ? this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance) : null;
+ }
+
+ @Override
+ public Set findRunningJobExecutions(String jobName) {
+ Query query = query(where("jobName").is(jobName));
+ List jobInstances = this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.JobInstance.class,
+ JOB_INSTANCES_COLLECTION_NAME)
+ .stream()
+ .map(this.jobInstanceConverter::toJobInstance)
+ .toList();
+ Set runningJobExecutions = new HashSet<>();
+ for (JobInstance jobInstance : jobInstances) {
+ query = query(
+ where("jobInstanceId").is(jobInstance.getId()).and("status").in("STARTING", "STARTED", "STOPPING"));
+ this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME)
+ .stream()
+ .map(jobExecution -> this.jobExecutionConverter.toJobExecution(jobExecution, jobInstance))
+ .forEach(runningJobExecutions::add);
+ }
+ return runningJobExecutions;
+ }
+
+ @Override
+ public JobExecution getJobExecution(Long executionId) {
+ Query jobExecutionQuery = query(where("jobExecutionId").is(executionId));
+ org.springframework.batch.core.repository.persistence.JobExecution jobExecution = this.mongoOperations.findOne(
+ jobExecutionQuery, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ if (jobExecution == null) {
+ return null;
+ }
+ Query jobInstanceQuery = query(where("jobInstanceId").is(jobExecution.getJobInstanceId()));
+ org.springframework.batch.core.repository.persistence.JobInstance jobInstance = this.mongoOperations.findOne(
+ jobInstanceQuery, org.springframework.batch.core.repository.persistence.JobInstance.class,
+ JOB_INSTANCES_COLLECTION_NAME);
+ return this.jobExecutionConverter.toJobExecution(jobExecution,
+ this.jobInstanceConverter.toJobInstance(jobInstance));
+ }
+
+ @Override
+ public void synchronizeStatus(JobExecution jobExecution) {
+ Query query = query(where("jobExecutionId").is(jobExecution.getId()));
+ Update update = Update.update("status", jobExecution.getStatus());
+ // TODO the contract mentions to update the version as well. Double check if this
+ // is needed as the version is not used in the tests following the call sites of
+ // synchronizeStatus
+ this.mongoOperations.updateFirst(query, update,
+ org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobInstanceDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobInstanceDao.java
new file mode 100644
index 0000000000..b967e35f77
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobInstanceDao.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.dao;
+
+import java.util.List;
+
+import org.springframework.batch.core.DefaultJobKeyGenerator;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobKeyGenerator;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.launch.NoSuchJobException;
+import org.springframework.batch.core.repository.persistence.converter.JobInstanceConverter;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
+import org.springframework.util.Assert;
+
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoJobInstanceDao implements JobInstanceDao {
+
+ private static final String COLLECTION_NAME = "BATCH_JOB_INSTANCE";
+
+ private static final String SEQUENCE_NAME = "BATCH_JOB_INSTANCE_SEQ";
+
+ private final MongoOperations mongoOperations;
+
+ private DataFieldMaxValueIncrementer jobInstanceIncrementer;
+
+ private JobKeyGenerator jobKeyGenerator = new DefaultJobKeyGenerator();
+
+ private final JobInstanceConverter jobInstanceConverter = new JobInstanceConverter();
+
+ public MongoJobInstanceDao(MongoOperations mongoOperations) {
+ Assert.notNull(mongoOperations, "mongoOperations must not be null.");
+ this.mongoOperations = mongoOperations;
+ this.jobInstanceIncrementer = new MongoSequenceIncrementer(mongoOperations, SEQUENCE_NAME);
+ }
+
+ public void setJobKeyGenerator(JobKeyGenerator jobKeyGenerator) {
+ this.jobKeyGenerator = jobKeyGenerator;
+ }
+
+ public void setJobInstanceIncrementer(DataFieldMaxValueIncrementer jobInstanceIncrementer) {
+ this.jobInstanceIncrementer = jobInstanceIncrementer;
+ }
+
+ @Override
+ public JobInstance createJobInstance(String jobName, JobParameters jobParameters) {
+ Assert.notNull(jobName, "Job name must not be null.");
+ Assert.notNull(jobParameters, "JobParameters must not be null.");
+
+ Assert.state(getJobInstance(jobName, jobParameters) == null, "JobInstance must not already exist");
+
+ org.springframework.batch.core.repository.persistence.JobInstance jobInstanceToSave = new org.springframework.batch.core.repository.persistence.JobInstance();
+ jobInstanceToSave.setJobName(jobName);
+ String key = this.jobKeyGenerator.generateKey(jobParameters);
+ jobInstanceToSave.setJobKey(key);
+ long instanceId = jobInstanceIncrementer.nextLongValue();
+ jobInstanceToSave.setJobInstanceId(instanceId);
+ this.mongoOperations.insert(jobInstanceToSave, COLLECTION_NAME);
+
+ JobInstance jobInstance = new JobInstance(instanceId, jobName);
+ jobInstance.incrementVersion(); // TODO is this needed?
+ return jobInstance;
+ }
+
+ @Override
+ public JobInstance getJobInstance(String jobName, JobParameters jobParameters) {
+ String key = this.jobKeyGenerator.generateKey(jobParameters);
+ Query query = query(where("jobName").is(jobName).and("jobKey").is(key));
+ org.springframework.batch.core.repository.persistence.JobInstance jobInstance = this.mongoOperations
+ .findOne(query, org.springframework.batch.core.repository.persistence.JobInstance.class, COLLECTION_NAME);
+ return jobInstance != null ? this.jobInstanceConverter.toJobInstance(jobInstance) : null;
+ }
+
+ @Override
+ public JobInstance getJobInstance(Long instanceId) {
+ Query query = query(where("jobInstanceId").is(instanceId));
+ org.springframework.batch.core.repository.persistence.JobInstance jobInstance = this.mongoOperations
+ .findOne(query, org.springframework.batch.core.repository.persistence.JobInstance.class, COLLECTION_NAME);
+ return jobInstance != null ? this.jobInstanceConverter.toJobInstance(jobInstance) : null;
+ }
+
+ @Override
+ public JobInstance getJobInstance(JobExecution jobExecution) {
+ return getJobInstance(jobExecution.getJobId());
+ }
+
+ @Override
+ public List getJobInstances(String jobName, int start, int count) {
+ Query query = query(where("jobName").is(jobName));
+ Sort.Order sortOrder = Sort.Order.desc("jobInstanceId");
+ List jobInstances = this.mongoOperations
+ .find(query.with(Sort.by(sortOrder)),
+ org.springframework.batch.core.repository.persistence.JobInstance.class, COLLECTION_NAME)
+ .stream()
+ .toList();
+ return jobInstances.subList(start, jobInstances.size())
+ .stream()
+ .map(this.jobInstanceConverter::toJobInstance)
+ .limit(count)
+ .toList();
+ }
+
+ @Override
+ public JobInstance getLastJobInstance(String jobName) {
+ Query query = query(where("jobName").is(jobName));
+ Sort.Order sortOrder = Sort.Order.desc("jobInstanceId");
+ org.springframework.batch.core.repository.persistence.JobInstance jobInstance = this.mongoOperations.findOne(
+ query.with(Sort.by(sortOrder)), org.springframework.batch.core.repository.persistence.JobInstance.class,
+ COLLECTION_NAME);
+ return jobInstance != null ? this.jobInstanceConverter.toJobInstance(jobInstance) : null;
+ }
+
+ @Override
+ public List getJobNames() {
+ return this.mongoOperations
+ .findAll(org.springframework.batch.core.repository.persistence.JobInstance.class, COLLECTION_NAME)
+ .stream()
+ .map(org.springframework.batch.core.repository.persistence.JobInstance::getJobName)
+ .toList();
+ }
+
+ @Override
+ public List findJobInstancesByName(String jobName, int start, int count) {
+ Query query = query(where("jobName").alike(Example.of(jobName)));
+ Sort.Order sortOrder = Sort.Order.desc("jobInstanceId");
+ List jobInstances = this.mongoOperations
+ .find(query.with(Sort.by(sortOrder)),
+ org.springframework.batch.core.repository.persistence.JobInstance.class, COLLECTION_NAME)
+ .stream()
+ .toList();
+ return jobInstances.subList(start, jobInstances.size())
+ .stream()
+ .map(this.jobInstanceConverter::toJobInstance)
+ .limit(count)
+ .toList();
+ }
+
+ @Override
+ public long getJobInstanceCount(String jobName) throws NoSuchJobException {
+ if (!getJobNames().contains(jobName)) {
+ throw new NoSuchJobException("Job not found " + jobName);
+ }
+ Query query = query(where("jobName").is(jobName));
+ return this.mongoOperations.count(query, COLLECTION_NAME);
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoSequenceIncrementer.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoSequenceIncrementer.java
new file mode 100644
index 0000000000..db78dc343a
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoSequenceIncrementer.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.dao;
+
+import com.mongodb.client.model.FindOneAndUpdateOptions;
+import com.mongodb.client.model.ReturnDocument;
+import org.bson.Document;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
+
+// Based on https://www.mongodb.com/blog/post/generating-globally-unique-identifiers-for-use-with-mongodb
+// Section: Use a single counter document to generate unique identifiers one at a time
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @author Christoph Strobl
+ * @since 5.2.0
+ */
+public class MongoSequenceIncrementer implements DataFieldMaxValueIncrementer {
+
+ private final MongoOperations mongoTemplate;
+
+ private final String sequenceName;
+
+ public MongoSequenceIncrementer(MongoOperations mongoTemplate, String sequenceName) {
+ this.mongoTemplate = mongoTemplate;
+ this.sequenceName = sequenceName;
+ }
+
+ @Override
+ public long nextLongValue() throws DataAccessException {
+ return mongoTemplate.execute("BATCH_SEQUENCES",
+ collection -> collection
+ .findOneAndUpdate(new Document("_id", sequenceName), new Document("$inc", new Document("count", 1)),
+ new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER))
+ .getLong("count"));
+ }
+
+ @Override
+ public int nextIntValue() throws DataAccessException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String nextStringValue() throws DataAccessException {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoStepExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoStepExecutionDao.java
new file mode 100644
index 0000000000..ec9067fe61
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoStepExecutionDao.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.dao;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.repository.persistence.converter.JobExecutionConverter;
+import org.springframework.batch.core.repository.persistence.converter.StepExecutionConverter;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
+
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+import static org.springframework.data.mongodb.core.query.Query.query;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoStepExecutionDao implements StepExecutionDao {
+
+ private static final String STEP_EXECUTIONS_COLLECTION_NAME = "BATCH_STEP_EXECUTION";
+
+ private static final String STEP_EXECUTIONS_SEQUENCE_NAME = "BATCH_STEP_EXECUTION_SEQ";
+
+ private static final String JOB_EXECUTIONS_COLLECTION_NAME = "BATCH_JOB_EXECUTION";
+
+ private final StepExecutionConverter stepExecutionConverter = new StepExecutionConverter();
+
+ private final JobExecutionConverter jobExecutionConverter = new JobExecutionConverter();
+
+ private final MongoOperations mongoOperations;
+
+ private DataFieldMaxValueIncrementer stepExecutionIncrementer;
+
+ public MongoStepExecutionDao(MongoOperations mongoOperations) {
+ this.mongoOperations = mongoOperations;
+ this.stepExecutionIncrementer = new MongoSequenceIncrementer(mongoOperations, STEP_EXECUTIONS_SEQUENCE_NAME);
+ }
+
+ public void setStepExecutionIncrementer(DataFieldMaxValueIncrementer stepExecutionIncrementer) {
+ this.stepExecutionIncrementer = stepExecutionIncrementer;
+ }
+
+ @Override
+ public void saveStepExecution(StepExecution stepExecution) {
+ org.springframework.batch.core.repository.persistence.StepExecution stepExecutionToSave = this.stepExecutionConverter
+ .fromStepExecution(stepExecution);
+ long stepExecutionId = this.stepExecutionIncrementer.nextLongValue();
+ stepExecutionToSave.setStepExecutionId(stepExecutionId);
+ this.mongoOperations.insert(stepExecutionToSave, STEP_EXECUTIONS_COLLECTION_NAME);
+ stepExecution.setId(stepExecutionId);
+ }
+
+ @Override
+ public void saveStepExecutions(Collection stepExecutions) {
+ for (StepExecution stepExecution : stepExecutions) {
+ saveStepExecution(stepExecution);
+ }
+ }
+
+ @Override
+ public void updateStepExecution(StepExecution stepExecution) {
+ Query query = query(where("stepExecutionId").is(stepExecution.getId()));
+ org.springframework.batch.core.repository.persistence.StepExecution stepExecutionToUpdate = this.stepExecutionConverter
+ .fromStepExecution(stepExecution);
+ this.mongoOperations.findAndReplace(query, stepExecutionToUpdate, STEP_EXECUTIONS_COLLECTION_NAME);
+ }
+
+ @Override
+ public StepExecution getStepExecution(JobExecution jobExecution, Long stepExecutionId) {
+ Query query = query(where("stepExecutionId").is(stepExecutionId));
+ org.springframework.batch.core.repository.persistence.StepExecution stepExecution = this.mongoOperations
+ .findOne(query, org.springframework.batch.core.repository.persistence.StepExecution.class,
+ STEP_EXECUTIONS_COLLECTION_NAME);
+ return stepExecution != null ? this.stepExecutionConverter.toStepExecution(stepExecution, jobExecution) : null;
+ }
+
+ @Override
+ public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) {
+ // TODO optimize the query
+ // get all step executions
+ List stepExecutions = new ArrayList<>();
+ Query query = query(where("jobInstanceId").is(jobInstance.getId()));
+ List jobExecutions = this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ for (org.springframework.batch.core.repository.persistence.JobExecution jobExecution : jobExecutions) {
+ stepExecutions.addAll(jobExecution.getStepExecutions());
+ }
+ // sort step executions by creation date then id (see contract) and return the
+ // first one
+ Optional lastStepExecution = stepExecutions
+ .stream()
+ .filter(stepExecution -> stepExecution.getName().equals(stepName))
+ .min(Comparator
+ .comparing(org.springframework.batch.core.repository.persistence.StepExecution::getCreateTime)
+ .thenComparing(org.springframework.batch.core.repository.persistence.StepExecution::getId));
+ if (lastStepExecution.isPresent()) {
+ org.springframework.batch.core.repository.persistence.StepExecution stepExecution = lastStepExecution.get();
+ JobExecution jobExecution = this.jobExecutionConverter.toJobExecution(jobExecutions.stream()
+ .filter(execution -> execution.getJobExecutionId().equals(stepExecution.getJobExecutionId()))
+ .findFirst()
+ .get(), jobInstance);
+ return this.stepExecutionConverter.toStepExecution(stepExecution, jobExecution);
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ public void addStepExecutions(JobExecution jobExecution) {
+ Query query = query(where("jobExecutionId").is(jobExecution.getId()));
+ List stepExecutions = this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.StepExecution.class,
+ STEP_EXECUTIONS_COLLECTION_NAME)
+ .stream()
+ .map(stepExecution -> this.stepExecutionConverter.toStepExecution(stepExecution, jobExecution))
+ .toList();
+ jobExecution.addStepExecutions(stepExecutions);
+ }
+
+ @Override
+ public long countStepExecutions(JobInstance jobInstance, String stepName) {
+ long count = 0;
+ // TODO optimize the count query
+ Query query = query(where("jobInstanceId").is(jobInstance.getId()));
+ List jobExecutions = this.mongoOperations
+ .find(query, org.springframework.batch.core.repository.persistence.JobExecution.class,
+ JOB_EXECUTIONS_COLLECTION_NAME);
+ for (org.springframework.batch.core.repository.persistence.JobExecution jobExecution : jobExecutions) {
+ List stepExecutions = jobExecution
+ .getStepExecutions();
+ for (org.springframework.batch.core.repository.persistence.StepExecution stepExecution : stepExecutions) {
+ if (stepExecution.getName().equals(stepName)) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+}
diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/step/package-info.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExecutionContext.java
similarity index 66%
rename from spring-batch-integration/src/main/java/org/springframework/batch/integration/step/package-info.java
rename to spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExecutionContext.java
index 82e7319a2e..6c3f51b249 100644
--- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/step/package-info.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExecutionContext.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2018 the original author or authors.
+ * Copyright 2024 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
*
- * https://www.apache.org/licenses/LICENSE-2.0
+ * https://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,
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.springframework.batch.core.repository.persistence;
+
+import java.util.Map;
/**
- * Support classes related to steps when used with Spring Integration.
- *
* @author Mahmoud Ben Hassine
+ * @since 5.2.0
*/
-@NonNullApi
-package org.springframework.batch.integration.step;
-
-import org.springframework.lang.NonNullApi;
+public record ExecutionContext(Map map, boolean dirty) {
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulNamedQueryIntegrationTests.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExitStatus.java
similarity index 58%
rename from spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulNamedQueryIntegrationTests.java
rename to spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExitStatus.java
index a0b046c072..e149183cfc 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulNamedQueryIntegrationTests.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/ExitStatus.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2009 the original author or authors.
+ * Copyright 2024 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.
@@ -13,18 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.batch.item.database;
+package org.springframework.batch.core.repository.persistence;
/**
- * Tests {@link HibernateCursorItemReader} configured with stateful session and named
- * query.
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
*/
-public class HibernateCursorItemReaderStatefulNamedQueryIntegrationTests
- extends HibernateCursorItemReaderNamedQueryIntegrationTests {
-
- @Override
- protected boolean isUseStatelessSession() {
- return false;
- }
-
+public record ExitStatus(String exitCode, String exitDescription) {
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobExecution.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobExecution.java
new file mode 100644
index 0000000000..2a0577417d
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobExecution.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.batch.core.BatchStatus;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class JobExecution {
+
+ private String id;
+
+ private Long jobExecutionId;
+
+ private Long jobInstanceId;
+
+ private Map> jobParameters = new HashMap<>();
+
+ private List stepExecutions = new ArrayList<>();
+
+ private BatchStatus status;
+
+ private LocalDateTime startTime;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime endTime;
+
+ private LocalDateTime lastUpdated;
+
+ private ExitStatus exitStatus;
+
+ private ExecutionContext executionContext;
+
+ public JobExecution() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Long getJobInstanceId() {
+ return jobInstanceId;
+ }
+
+ public void setJobInstanceId(Long jobInstanceId) {
+ this.jobInstanceId = jobInstanceId;
+ }
+
+ public Long getJobExecutionId() {
+ return jobExecutionId;
+ }
+
+ public void setJobExecutionId(Long jobExecutionId) {
+ this.jobExecutionId = jobExecutionId;
+ }
+
+ public Map> getJobParameters() {
+ return jobParameters;
+ }
+
+ public void setJobParameters(Map> jobParameters) {
+ this.jobParameters = jobParameters;
+ }
+
+ public List getStepExecutions() {
+ return stepExecutions;
+ }
+
+ public void setStepExecutions(List stepExecutions) {
+ this.stepExecutions = stepExecutions;
+ }
+
+ public BatchStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(BatchStatus status) {
+ this.status = status;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(LocalDateTime createTime) {
+ this.createTime = createTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public LocalDateTime getLastUpdated() {
+ return lastUpdated;
+ }
+
+ public void setLastUpdated(LocalDateTime lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+
+ public ExitStatus getExitStatus() {
+ return exitStatus;
+ }
+
+ public void setExitStatus(ExitStatus exitStatus) {
+ this.exitStatus = exitStatus;
+ }
+
+ public ExecutionContext getExecutionContext() {
+ return executionContext;
+ }
+
+ public void setExecutionContext(ExecutionContext executionContext) {
+ this.executionContext = executionContext;
+ }
+
+ @Override
+ public String toString() {
+ return "JobExecution{" + "id='" + id + '\'' + ", jobExecutionId=" + jobExecutionId + ", jobInstanceId="
+ + jobInstanceId + ", jobParameters=" + jobParameters + ", stepExecutions=" + stepExecutions
+ + ", status=" + status + ", startTime=" + startTime + ", createTime=" + createTime + ", endTime="
+ + endTime + ", lastUpdated=" + lastUpdated + ", exitStatus=" + exitStatus + ", executionContext="
+ + executionContext + '}';
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobInstance.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobInstance.java
new file mode 100644
index 0000000000..a096be4b78
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobInstance.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class JobInstance {
+
+ private String id;
+
+ private Long jobInstanceId;
+
+ private String jobName;
+
+ private String jobKey;
+
+ public JobInstance() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Long getJobInstanceId() {
+ return jobInstanceId;
+ }
+
+ public void setJobInstanceId(Long jobInstanceId) {
+ this.jobInstanceId = jobInstanceId;
+ }
+
+ public String getJobName() {
+ return jobName;
+ }
+
+ public void setJobName(String jobName) {
+ this.jobName = jobName;
+ }
+
+ public String getJobKey() {
+ return jobKey;
+ }
+
+ public void setJobKey(String jobKey) {
+ this.jobKey = jobKey;
+ }
+
+ @Override
+ public String toString() {
+ return "JobInstance{" + "id='" + id + '\'' + ", jobInstanceId=" + jobInstanceId + ", jobName='" + jobName + '\''
+ + ", jobKey='" + jobKey + '\'' + '}';
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNamedQueryIntegrationTests.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobParameter.java
similarity index 55%
rename from spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNamedQueryIntegrationTests.java
rename to spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobParameter.java
index 37ecb8690d..af1c1f4673 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNamedQueryIntegrationTests.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/JobParameter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2009-2010 the original author or authors.
+ * Copyright 2024 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.
@@ -13,19 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.batch.item.database;
-
-import org.springframework.batch.item.sample.Foo;
+package org.springframework.batch.core.repository.persistence;
/**
- * Tests {@link HibernateCursorItemReader} configured with named query.
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
*/
-public class HibernateCursorItemReaderNamedQueryIntegrationTests
- extends AbstractHibernateCursorItemReaderIntegrationTests {
-
- @Override
- protected void setQuery(HibernateCursorItemReader reader) {
- reader.setQueryName("allFoos");
- }
-
+public record JobParameter(T value, String type, boolean identifying) {
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/StepExecution.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/StepExecution.java
new file mode 100644
index 0000000000..351fe34442
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/StepExecution.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence;
+
+import java.time.LocalDateTime;
+
+import org.springframework.batch.core.BatchStatus;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class StepExecution {
+
+ private String id;
+
+ private Long stepExecutionId;
+
+ private Long jobExecutionId;
+
+ private String name;
+
+ private BatchStatus status;
+
+ private long readCount;
+
+ private long writeCount;
+
+ private long commitCount;
+
+ private long rollbackCount;
+
+ private long readSkipCount;
+
+ private long processSkipCount;
+
+ private long writeSkipCount;
+
+ private long filterCount;
+
+ private LocalDateTime startTime;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime endTime;
+
+ private LocalDateTime lastUpdated;
+
+ private ExecutionContext executionContext;
+
+ private ExitStatus exitStatus;
+
+ private boolean terminateOnly;
+
+ public StepExecution() {
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Long getStepExecutionId() {
+ return stepExecutionId;
+ }
+
+ public void setStepExecutionId(Long stepExecutionId) {
+ this.stepExecutionId = stepExecutionId;
+ }
+
+ public Long getJobExecutionId() {
+ return jobExecutionId;
+ }
+
+ public void setJobExecutionId(Long jobExecutionId) {
+ this.jobExecutionId = jobExecutionId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public BatchStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(BatchStatus status) {
+ this.status = status;
+ }
+
+ public long getReadCount() {
+ return readCount;
+ }
+
+ public void setReadCount(long readCount) {
+ this.readCount = readCount;
+ }
+
+ public long getWriteCount() {
+ return writeCount;
+ }
+
+ public void setWriteCount(long writeCount) {
+ this.writeCount = writeCount;
+ }
+
+ public long getCommitCount() {
+ return commitCount;
+ }
+
+ public void setCommitCount(long commitCount) {
+ this.commitCount = commitCount;
+ }
+
+ public long getRollbackCount() {
+ return rollbackCount;
+ }
+
+ public void setRollbackCount(long rollbackCount) {
+ this.rollbackCount = rollbackCount;
+ }
+
+ public long getReadSkipCount() {
+ return readSkipCount;
+ }
+
+ public void setReadSkipCount(long readSkipCount) {
+ this.readSkipCount = readSkipCount;
+ }
+
+ public long getProcessSkipCount() {
+ return processSkipCount;
+ }
+
+ public void setProcessSkipCount(long processSkipCount) {
+ this.processSkipCount = processSkipCount;
+ }
+
+ public long getWriteSkipCount() {
+ return writeSkipCount;
+ }
+
+ public void setWriteSkipCount(long writeSkipCount) {
+ this.writeSkipCount = writeSkipCount;
+ }
+
+ public long getFilterCount() {
+ return filterCount;
+ }
+
+ public void setFilterCount(long filterCount) {
+ this.filterCount = filterCount;
+ }
+
+ public LocalDateTime getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(LocalDateTime startTime) {
+ this.startTime = startTime;
+ }
+
+ public LocalDateTime getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(LocalDateTime createTime) {
+ this.createTime = createTime;
+ }
+
+ public LocalDateTime getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(LocalDateTime endTime) {
+ this.endTime = endTime;
+ }
+
+ public LocalDateTime getLastUpdated() {
+ return lastUpdated;
+ }
+
+ public void setLastUpdated(LocalDateTime lastUpdated) {
+ this.lastUpdated = lastUpdated;
+ }
+
+ public ExecutionContext getExecutionContext() {
+ return executionContext;
+ }
+
+ public void setExecutionContext(ExecutionContext executionContext) {
+ this.executionContext = executionContext;
+ }
+
+ public ExitStatus getExitStatus() {
+ return exitStatus;
+ }
+
+ public void setExitStatus(ExitStatus exitStatus) {
+ this.exitStatus = exitStatus;
+ }
+
+ public boolean isTerminateOnly() {
+ return terminateOnly;
+ }
+
+ public void setTerminateOnly(boolean terminateOnly) {
+ this.terminateOnly = terminateOnly;
+ }
+
+ @Override
+ public String toString() {
+ return "StepExecution{" + "id='" + id + '\'' + ", stepExecutionId=" + stepExecutionId + ", jobExecutionId='"
+ + jobExecutionId + '\'' + ", name='" + name + '\'' + ", status=" + status + ", readCount=" + readCount
+ + ", writeCount=" + writeCount + ", commitCount=" + commitCount + ", rollbackCount=" + rollbackCount
+ + ", readSkipCount=" + readSkipCount + ", processSkipCount=" + processSkipCount + ", writeSkipCount="
+ + writeSkipCount + ", filterCount=" + filterCount + ", startTime=" + startTime + ", createTime="
+ + createTime + ", endTime=" + endTime + ", lastUpdated=" + lastUpdated + ", executionContext="
+ + executionContext + ", exitStatus=" + exitStatus + ", terminateOnly=" + terminateOnly + '}';
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobExecutionConverter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobExecutionConverter.java
new file mode 100644
index 0000000000..686c48464c
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobExecutionConverter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence.converter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.repository.persistence.ExecutionContext;
+import org.springframework.batch.core.repository.persistence.ExitStatus;
+import org.springframework.batch.core.repository.persistence.JobExecution;
+import org.springframework.batch.core.repository.persistence.JobParameter;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class JobExecutionConverter {
+
+ private final JobParameterConverter jobParameterConverter = new JobParameterConverter();
+
+ private final StepExecutionConverter stepExecutionConverter = new StepExecutionConverter();
+
+ public org.springframework.batch.core.JobExecution toJobExecution(JobExecution source, JobInstance jobInstance) {
+ Map> parameterMap = new HashMap<>();
+ source.getJobParameters()
+ .forEach((key, value) -> parameterMap.put(key, this.jobParameterConverter.toJobParameter(value)));
+ org.springframework.batch.core.JobExecution jobExecution = new org.springframework.batch.core.JobExecution(
+ jobInstance, source.getJobExecutionId(), new JobParameters(parameterMap));
+ jobExecution.addStepExecutions(source.getStepExecutions()
+ .stream()
+ .map(stepExecution -> this.stepExecutionConverter.toStepExecution(stepExecution, jobExecution))
+ .toList());
+ jobExecution.setStatus(source.getStatus());
+ jobExecution.setStartTime(source.getStartTime());
+ jobExecution.setCreateTime(source.getCreateTime());
+ jobExecution.setEndTime(source.getEndTime());
+ jobExecution.setLastUpdated(source.getLastUpdated());
+ jobExecution.setExitStatus(new org.springframework.batch.core.ExitStatus(source.getExitStatus().exitCode(),
+ source.getExitStatus().exitDescription()));
+ jobExecution.setExecutionContext(
+ new org.springframework.batch.item.ExecutionContext(source.getExecutionContext().map()));
+ return jobExecution;
+ }
+
+ public JobExecution fromJobExecution(org.springframework.batch.core.JobExecution source) {
+ JobExecution jobExecution = new JobExecution();
+ jobExecution.setJobExecutionId(source.getId());
+ jobExecution.setJobInstanceId(source.getJobInstance().getInstanceId());
+ Map> parameterMap = new HashMap<>();
+ source.getJobParameters()
+ .getParameters()
+ .forEach((key, value) -> parameterMap.put(key, this.jobParameterConverter.fromJobParameter(value)));
+ jobExecution.setJobParameters(parameterMap);
+ jobExecution.setStepExecutions(
+ source.getStepExecutions().stream().map(this.stepExecutionConverter::fromStepExecution).toList());
+ jobExecution.setStatus(source.getStatus());
+ jobExecution.setStartTime(source.getStartTime());
+ jobExecution.setCreateTime(source.getCreateTime());
+ jobExecution.setEndTime(source.getEndTime());
+ jobExecution.setLastUpdated(source.getLastUpdated());
+ jobExecution.setExitStatus(
+ new ExitStatus(source.getExitStatus().getExitCode(), source.getExitStatus().getExitDescription()));
+ org.springframework.batch.item.ExecutionContext executionContext = source.getExecutionContext();
+ jobExecution.setExecutionContext(new ExecutionContext(executionContext.toMap(), executionContext.isDirty()));
+ return jobExecution;
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobInstanceConverter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobInstanceConverter.java
new file mode 100644
index 0000000000..82b3a277de
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobInstanceConverter.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence.converter;
+
+import org.springframework.batch.core.repository.persistence.JobInstance;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class JobInstanceConverter {
+
+ public org.springframework.batch.core.JobInstance toJobInstance(JobInstance source) {
+ return new org.springframework.batch.core.JobInstance(source.getJobInstanceId(), source.getJobName());
+ }
+
+ public JobInstance fromJobInstance(org.springframework.batch.core.JobInstance source) {
+ JobInstance jobInstance = new JobInstance();
+ jobInstance.setJobName(source.getJobName());
+ jobInstance.setJobInstanceId(source.getInstanceId());
+ return jobInstance;
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobParameterConverter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobParameterConverter.java
new file mode 100644
index 0000000000..361c98c36b
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/JobParameterConverter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence.converter;
+
+import org.springframework.batch.core.repository.persistence.JobParameter;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class JobParameterConverter {
+
+ public org.springframework.batch.core.JobParameter toJobParameter(JobParameter source) {
+ try {
+ return new org.springframework.batch.core.JobParameter<>(source.value(),
+ (Class) Class.forName(source.type()), source.identifying());
+ }
+ catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public JobParameter fromJobParameter(org.springframework.batch.core.JobParameter source) {
+ return new JobParameter<>(source.getValue(), source.getType().getName(), source.isIdentifying());
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/StepExecutionConverter.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/StepExecutionConverter.java
new file mode 100644
index 0000000000..221e9c50cf
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/converter/StepExecutionConverter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.persistence.converter;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.repository.persistence.ExecutionContext;
+import org.springframework.batch.core.repository.persistence.ExitStatus;
+import org.springframework.batch.core.repository.persistence.StepExecution;
+
+/**
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class StepExecutionConverter {
+
+ public org.springframework.batch.core.StepExecution toStepExecution(StepExecution source,
+ JobExecution jobExecution) {
+ org.springframework.batch.core.StepExecution stepExecution = new org.springframework.batch.core.StepExecution(
+ source.getName(), jobExecution, source.getStepExecutionId());
+ stepExecution.setStatus(source.getStatus());
+ stepExecution.setReadCount(source.getReadCount());
+ stepExecution.setWriteCount(source.getWriteCount());
+ stepExecution.setCommitCount(source.getCommitCount());
+ stepExecution.setRollbackCount(source.getRollbackCount());
+ stepExecution.setReadSkipCount(source.getReadSkipCount());
+ stepExecution.setProcessSkipCount(source.getProcessSkipCount());
+ stepExecution.setWriteSkipCount(source.getWriteSkipCount());
+ stepExecution.setFilterCount(source.getFilterCount());
+ stepExecution.setStartTime(source.getStartTime());
+ stepExecution.setCreateTime(source.getCreateTime());
+ stepExecution.setEndTime(source.getEndTime());
+ stepExecution.setLastUpdated(source.getLastUpdated());
+ stepExecution.setExitStatus(new org.springframework.batch.core.ExitStatus(source.getExitStatus().exitCode(),
+ source.getExitStatus().exitDescription()));
+ stepExecution.setExecutionContext(
+ new org.springframework.batch.item.ExecutionContext(source.getExecutionContext().map()));
+ if (source.isTerminateOnly()) {
+ stepExecution.setTerminateOnly();
+ }
+ return stepExecution;
+ }
+
+ public StepExecution fromStepExecution(org.springframework.batch.core.StepExecution source) {
+ StepExecution stepExecution = new StepExecution();
+ stepExecution.setStepExecutionId(source.getId());
+ stepExecution.setJobExecutionId(source.getJobExecutionId());
+ stepExecution.setName(source.getStepName());
+ stepExecution.setJobExecutionId(source.getJobExecutionId());
+ stepExecution.setStatus(source.getStatus());
+ stepExecution.setReadCount(source.getReadCount());
+ stepExecution.setWriteCount(source.getWriteCount());
+ stepExecution.setCommitCount(source.getCommitCount());
+ stepExecution.setRollbackCount(source.getRollbackCount());
+ stepExecution.setReadSkipCount(source.getReadSkipCount());
+ stepExecution.setProcessSkipCount(source.getProcessSkipCount());
+ stepExecution.setWriteSkipCount(source.getWriteSkipCount());
+ stepExecution.setFilterCount(source.getFilterCount());
+ stepExecution.setStartTime(source.getStartTime());
+ stepExecution.setCreateTime(source.getCreateTime());
+ stepExecution.setEndTime(source.getEndTime());
+ stepExecution.setLastUpdated(source.getLastUpdated());
+ stepExecution.setExitStatus(
+ new ExitStatus(source.getExitStatus().getExitCode(), source.getExitStatus().getExitDescription()));
+ org.springframework.batch.item.ExecutionContext executionContext = source.getExecutionContext();
+ stepExecution.setExecutionContext(new ExecutionContext(executionContext.toMap(), executionContext.isDirty()));
+ stepExecution.setTerminateOnly(source.isTerminateOnly());
+ return stepExecution;
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/package-info.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/package-info.java
new file mode 100644
index 0000000000..2d1a93bd40
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/persistence/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * This package contains the classes of the persistence model.
+ */
+package org.springframework.batch.core.repository.persistence;
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBean.java
index 2d72203d6f..70b0b54436 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBean.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -129,9 +129,10 @@ public void setSerializer(ExecutionContextSerializer serializer) {
* (usually older) versions of Oracle. The default is determined from the data base
* type.
* @param lobHandler the {@link LobHandler} to set
- *
+ * @deprecated Since 5.2 with no replacement. Scheduled for removal in v6
* @see LobHandler
*/
+ @Deprecated(since = "5.2.0", forRemoval = true)
public void setLobHandler(LobHandler lobHandler) {
this.lobHandler = lobHandler;
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MongoJobRepositoryFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MongoJobRepositoryFactoryBean.java
new file mode 100644
index 0000000000..721272dde4
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/MongoJobRepositoryFactoryBean.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import org.springframework.batch.core.repository.dao.ExecutionContextDao;
+import org.springframework.batch.core.repository.dao.JobExecutionDao;
+import org.springframework.batch.core.repository.dao.JobInstanceDao;
+import org.springframework.batch.core.repository.dao.StepExecutionDao;
+import org.springframework.batch.core.repository.dao.MongoExecutionContextDao;
+import org.springframework.batch.core.repository.dao.MongoJobExecutionDao;
+import org.springframework.batch.core.repository.dao.MongoJobInstanceDao;
+import org.springframework.batch.core.repository.dao.MongoStepExecutionDao;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.util.Assert;
+
+/**
+ * This factory bean creates a job repository backed by MongoDB. It requires a mongo
+ * template and a mongo transaction manager. The mongo template must be configured
+ * with a {@link MappingMongoConverter} having a {@code MapKeyDotReplacement} set to a non
+ * null value. See {@code MongoDBJobRepositoryIntegrationTests} for an example. This is
+ * required to support execution context keys containing dots (like "step.type" or
+ * "batch.version")
+ *
+ * @author Mahmoud Ben Hassine
+ * @since 5.2.0
+ */
+public class MongoJobRepositoryFactoryBean extends AbstractJobRepositoryFactoryBean implements InitializingBean {
+
+ private MongoOperations mongoOperations;
+
+ public void setMongoOperations(MongoOperations mongoOperations) {
+ this.mongoOperations = mongoOperations;
+ }
+
+ @Override
+ protected JobInstanceDao createJobInstanceDao() {
+ return new MongoJobInstanceDao(this.mongoOperations);
+ }
+
+ @Override
+ protected JobExecutionDao createJobExecutionDao() {
+ return new MongoJobExecutionDao(this.mongoOperations);
+ }
+
+ @Override
+ protected StepExecutionDao createStepExecutionDao() {
+ return new MongoStepExecutionDao(this.mongoOperations);
+ }
+
+ @Override
+ protected ExecutionContextDao createExecutionContextDao() {
+ return new MongoExecutionContextDao(this.mongoOperations);
+ }
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ super.afterPropertiesSet();
+ Assert.notNull(this.mongoOperations, "MongoOperations must not be null.");
+ }
+
+}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java
new file mode 100644
index 0000000000..9fb6b33dd8
--- /dev/null
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
+
+/**
+ * A {@link JobRepository} implementation that does not use or store batch meta-data. It
+ * is intended for use-cases where restartability is not required and where the execution
+ * context is not involved in any way (like sharing data between steps through the
+ * execution context, or partitioned steps where partitions meta-data is shared between
+ * the manager and workers through the execution context, etc).
+ * This implementation holds a single job instance and a corresponding job execution that
+ * are suitable for one-time jobs executed in their own JVM. This job repository works
+ * with transactional steps as well as non-transactional steps (in which case, a
+ * {@link ResourcelessTransactionManager} can be used).
+ * This implementation is not thread-safe and should not be used in any concurrent
+ * environment.
+ *
+ * @since 5.2.0
+ * @author Mahmoud Ben Hassine
+ */
+public class ResourcelessJobRepository implements JobRepository {
+
+ private JobInstance jobInstance;
+
+ private JobExecution jobExecution;
+
+ @Override
+ public boolean isJobInstanceExists(String jobName, JobParameters jobParameters) {
+ return false;
+ }
+
+ @Override
+ public JobInstance createJobInstance(String jobName, JobParameters jobParameters) {
+ this.jobInstance = new JobInstance(1L, jobName);
+ return this.jobInstance;
+ }
+
+ @Override
+ public JobExecution createJobExecution(String jobName, JobParameters jobParameters) {
+ if (this.jobInstance == null) {
+ createJobInstance(jobName, jobParameters);
+ }
+ this.jobExecution = new JobExecution(this.jobInstance, 1L, jobParameters);
+ return this.jobExecution;
+ }
+
+ @Override
+ public void update(JobExecution jobExecution) {
+ jobExecution.setLastUpdated(LocalDateTime.now());
+ this.jobExecution = jobExecution;
+ }
+
+ @Override
+ public void add(StepExecution stepExecution) {
+ this.addAll(Collections.singletonList(stepExecution));
+ }
+
+ @Override
+ public void addAll(Collection stepExecutions) {
+ this.jobExecution.addStepExecutions(new ArrayList<>(stepExecutions));
+ }
+
+ @Override
+ public void update(StepExecution stepExecution) {
+ stepExecution.setLastUpdated(LocalDateTime.now());
+ if (this.jobExecution.isStopping()) {
+ stepExecution.setTerminateOnly();
+ }
+ }
+
+ @Override
+ public void updateExecutionContext(StepExecution stepExecution) {
+ stepExecution.setLastUpdated(LocalDateTime.now());
+ }
+
+ @Override
+ public void updateExecutionContext(JobExecution jobExecution) {
+ jobExecution.setLastUpdated(LocalDateTime.now());
+ }
+
+ @Override
+ public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) {
+ return this.jobExecution.getStepExecutions()
+ .stream()
+ .filter(stepExecution -> stepExecution.getStepName().equals(stepName))
+ .findFirst()
+ .orElse(null);
+ }
+
+ @Override
+ public long getStepExecutionCount(JobInstance jobInstance, String stepName) {
+ return this.jobExecution.getStepExecutions()
+ .stream()
+ .filter(stepExecution -> stepExecution.getStepName().equals(stepName))
+ .count();
+ }
+
+ @Override
+ public JobExecution getLastJobExecution(String jobName, JobParameters jobParameters) {
+ return this.jobExecution;
+ }
+
+}
\ No newline at end of file
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/SimpleJobRepository.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/SimpleJobRepository.java
index 44f8bf6eca..e98752c987 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/SimpleJobRepository.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/SimpleJobRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -328,7 +328,7 @@ public void deleteJobExecution(JobExecution jobExecution) {
@Override
public void deleteJobInstance(JobInstance jobInstance) {
- List jobExecutions = this.jobExecutionDao.findJobExecutions(jobInstance);
+ List jobExecutions = findJobExecutions(jobInstance);
for (JobExecution jobExecution : jobExecutions) {
deleteJobExecution(jobExecution);
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java
index 9ac094dc4d..c2339b95df 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -63,6 +63,7 @@
* @author Michael Minella
* @author Chris Schaefer
* @author Mahmoud Ben Hassine
+ * @author Jinwoo Bae
*/
public abstract class AbstractStep implements Step, InitializingBean, BeanNameAware {
@@ -261,7 +262,13 @@ public final void execute(StepExecution stepExecution)
}
}
finally {
-
+ stepExecution.setEndTime(LocalDateTime.now());
+ Duration stepExecutionDuration = BatchMetrics.calculateDuration(stepExecution.getStartTime(),
+ stepExecution.getEndTime());
+ if (logger.isInfoEnabled()) {
+ logger.info("Step: [" + stepExecution.getStepName() + "] executed in "
+ + BatchMetrics.formatDuration(stepExecutionDuration));
+ }
try {
// Update the step execution to the latest known value so the
// listeners can act on it
@@ -287,14 +294,8 @@ public final void execute(StepExecution stepExecution)
name, stepExecution.getJobExecution().getJobInstance().getJobName()), e);
}
stopObservation(stepExecution, observation);
- stepExecution.setEndTime(LocalDateTime.now());
stepExecution.setExitStatus(exitStatus);
- Duration stepExecutionDuration = BatchMetrics.calculateDuration(stepExecution.getStartTime(),
- stepExecution.getEndTime());
- if (logger.isInfoEnabled()) {
- logger.info("Step: [" + stepExecution.getStepName() + "] executed in "
- + BatchMetrics.formatDuration(stepExecutionDuration));
- }
+
try {
getJobRepository().update(stepExecution);
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilder.java
index 05d9f13906..55e6a0fdce 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilder.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -49,6 +49,7 @@
* @author Dave Syer
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Ilpyo Yang
* @since 2.2
* @param the type of builder represented
*/
@@ -74,6 +75,23 @@ public AbstractTaskletStepBuilder(StepBuilderHelper> parent) {
super(parent);
}
+ /**
+ * Create a new builder initialized with any properties in the parent. The parent is
+ * copied, so it can be re-used.
+ * @param parent a parent helper containing common step properties
+ */
+ public AbstractTaskletStepBuilder(AbstractTaskletStepBuilder> parent) {
+ super(parent);
+ this.chunkListeners = parent.chunkListeners;
+ this.stepOperations = parent.stepOperations;
+ this.transactionManager = parent.transactionManager;
+ this.transactionAttribute = parent.transactionAttribute;
+ this.streams.addAll(parent.streams);
+ this.exceptionHandler = parent.exceptionHandler;
+ this.throttleLimit = parent.throttleLimit;
+ this.taskExecutor = parent.taskExecutor;
+ }
+
protected abstract Tasklet createTasklet();
/**
@@ -162,7 +180,7 @@ public B listener(Object listener) {
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunk.class));
chunkListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), AfterChunkError.class));
- if (chunkListenerMethods.size() > 0) {
+ if (!chunkListenerMethods.isEmpty()) {
StepListenerFactoryBean factory = new StepListenerFactoryBean();
factory.setDelegate(listener);
this.listener((ChunkListener) factory.getObject());
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java
index 78c5fadc9b..e4c24fb3b0 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -90,6 +90,7 @@
* @author Chris Schaefer
* @author Michael Minella
* @author Mahmoud Ben Hassine
+ * @author Ian Choi
* @since 2.2
*/
public class FaultTolerantStepBuilder extends SimpleStepBuilder {
@@ -122,7 +123,7 @@ public class FaultTolerantStepBuilder extends SimpleStepBuilder {
private final Set> skipListeners = new LinkedHashSet<>();
- private int skipLimit = 0;
+ private int skipLimit = 10;
private SkipPolicy skipPolicy;
@@ -199,7 +200,7 @@ public FaultTolerantStepBuilder listener(Object listener) {
skipListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnSkipInProcess.class));
skipListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnSkipInWrite.class));
- if (skipListenerMethods.size() > 0) {
+ if (!skipListenerMethods.isEmpty()) {
StepListenerFactoryBean factory = new StepListenerFactoryBean();
factory.setDelegate(listener);
skipListeners.add((SkipListener) factory.getObject());
@@ -306,7 +307,7 @@ public FaultTolerantStepBuilder retryContextCache(RetryContextCache retryC
/**
* Sets the maximum number of failed items to skip before the step fails. Ignored if
* an explicit {@link #skipPolicy(SkipPolicy)} is provided.
- * @param skipLimit the skip limit to set
+ * @param skipLimit the skip limit to set. Default is 10.
* @return this for fluent chaining
*/
public FaultTolerantStepBuilder skipLimit(int skipLimit) {
@@ -554,8 +555,11 @@ protected SkipPolicy createSkipPolicy() {
map.put(ForceRollbackForWriteSkipException.class, true);
LimitCheckingItemSkipPolicy limitCheckingItemSkipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, map);
if (skipPolicy == null) {
- Assert.state(!(skippableExceptionClasses.isEmpty() && skipLimit > 0),
- "If a skip limit is provided then skippable exceptions must also be specified");
+ if (skippableExceptionClasses.isEmpty() && skipLimit > 0) {
+ logger.debug(String.format(
+ "A skip limit of %s is set but no skippable exceptions are defined. Consider defining skippable exceptions.",
+ skipLimit));
+ }
skipPolicy = limitCheckingItemSkipPolicy;
}
else if (limitCheckingItemSkipPolicy != null) {
@@ -689,10 +693,6 @@ private void addNonRetryableExceptionIfMissing(Class extends Throwable>... cls
/**
* ChunkListener that wraps exceptions thrown from the ChunkListener in
* {@link FatalStepExecutionException} to force termination of StepExecution
- *
- * ChunkListeners shoulnd't throw exceptions and expect continued processing, they
- * must be handled in the implementation or the step will terminate
- *
*/
private static class TerminateOnExceptionChunkListenerDelegate implements ChunkListener {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java
index aa51c34e54..fed10c44a1 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -114,7 +114,6 @@ protected SimpleStepBuilder(SimpleStepBuilder parent) {
this.itemListeners = parent.itemListeners;
this.readerTransactionalQueue = parent.readerTransactionalQueue;
this.meterRegistry = parent.meterRegistry;
- this.transactionManager(parent.getTransactionManager());
}
public FaultTolerantStepBuilder faultTolerant() {
@@ -270,7 +269,7 @@ public SimpleStepBuilder listener(Object listener) {
itemListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnProcessError.class));
itemListenerMethods.addAll(ReflectionUtils.findMethod(listener.getClass(), OnWriteError.class));
- if (itemListenerMethods.size() > 0) {
+ if (!itemListenerMethods.isEmpty()) {
StepListenerFactoryBean factory = new StepListenerFactoryBean();
factory.setDelegate(listener);
itemListeners.add((StepListener) factory.getObject());
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java
index 48aed1eae5..cec322ccd4 100755
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -47,6 +47,7 @@
* @author Dave Syer
* @author Robert Kasanicky
* @author Morten Andersen-Gott
+ * @author Ian Choi
*
*/
public class FaultTolerantStepFactoryBean extends SimpleStepFactoryBean {
@@ -61,7 +62,7 @@ public class FaultTolerantStepFactoryBean extends SimpleStepFactoryBean transform(StepContribution contribution, Chunk inputs) thr
iterator.remove();
}
}
+ if (inputs.isEnd()) {
+ outputs.setEnd();
+ }
return outputs;
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapper.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapper.java
index 1685914c1a..55b5684f3d 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapper.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -22,9 +22,10 @@
* Simple {@link SystemProcessExitCodeMapper} implementation that performs following
* mapping:
*
- * 0 -> ExitStatus.FINISHED else -> ExitStatus.FAILED
+ * 0 -> ExitStatus.COMPLETED else -> ExitStatus.FAILED
*
* @author Robert Kasanicky
+ * @author Mahmoud Ben Hassine
*/
public class SimpleSystemProcessExitCodeMapper implements SystemProcessExitCodeMapper {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java
index b35a228a4d..4499279e8e 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -60,6 +60,7 @@
* @author Robert Kasanicky
* @author Will Schipp
* @author Mahmoud Ben Hassine
+ * @author Injae Kim
*/
public class SystemCommandTasklet implements StepExecutionListener, StoppableTasklet, InitializingBean {
@@ -121,8 +122,15 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon
}
if (systemCommandTask.isDone()) {
- contribution.setExitStatus(systemProcessExitCodeMapper.getExitStatus(systemCommandTask.get()));
- return RepeatStatus.FINISHED;
+ Integer exitCode = systemCommandTask.get();
+ ExitStatus exitStatus = systemProcessExitCodeMapper.getExitStatus(exitCode);
+ contribution.setExitStatus(exitStatus);
+ if (ExitStatus.FAILED.equals(exitStatus)) {
+ throw new SystemCommandException("Execution of system command failed with exit code " + exitCode);
+ }
+ else {
+ return RepeatStatus.FINISHED;
+ }
}
else if (System.currentTimeMillis() - t0 > timeout) {
systemCommandTask.cancel(interruptOnCancel);
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java
index 8b2cf1e1b8..aaf192beb2 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java
@@ -425,7 +425,9 @@ public RepeatStatus doInTransaction(TransactionStatus status) {
try {
// Going to attempt a commit. If it fails this flag will
// stay false and we can use that later.
- getJobRepository().updateExecutionContext(stepExecution);
+ if (stepExecution.getExecutionContext().isDirty()) {
+ getJobRepository().updateExecutionContext(stepExecution);
+ }
stepExecution.incrementCommitCount();
if (logger.isDebugEnabled()) {
logger.debug("Saving step execution before commit: " + stepExecution);
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd
index 9f8241f3d1..07f613bed1 100644
--- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.0.xsd
@@ -182,7 +182,7 @@
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd
index 559c74a748..7f0b739f15 100644
--- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.1.xsd
@@ -230,7 +230,7 @@ ref" is not required, and only needs to be specified explicitly
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd
index df341d1b29..8871bfbb51 100644
--- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-2.2.xsd
@@ -230,7 +230,7 @@ ref" is not required, and only needs to be specified explicitly
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-3.0.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-3.0.xsd
index 3857e27962..2946e125cb 100644
--- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-3.0.xsd
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch-3.0.xsd
@@ -245,7 +245,7 @@
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch.xsd b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch.xsd
index 9879886658..1c5b20f37c 100644
--- a/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch.xsd
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/configuration/xml/spring-batch.xsd
@@ -246,7 +246,7 @@
@@ -270,6 +270,7 @@
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-mongodb.js b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-mongodb.js
new file mode 100644
index 0000000000..0213a39df0
--- /dev/null
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-mongodb.js
@@ -0,0 +1,5 @@
+// to execute in MongoShell after changing the database name `db.` as needed
+db.getCollection("BATCH_JOB_INSTANCE").drop();
+db.getCollection("BATCH_JOB_EXECUTION").drop();
+db.getCollection("BATCH_STEP_EXECUTION").drop();
+db.getCollection("BATCH_SEQUENCES").drop();
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js
new file mode 100644
index 0000000000..e3a971ad8a
--- /dev/null
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js
@@ -0,0 +1,18 @@
+// to execute in MongoShell after changing the database name `db.` as needed
+db.createCollection("BATCH_JOB_INSTANCE");
+db.createCollection("BATCH_JOB_EXECUTION");
+db.createCollection("BATCH_STEP_EXECUTION");
+
+// SEQUENCES
+db.createCollection("BATCH_SEQUENCES");
+db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_JOB_INSTANCE_SEQ", count: Long(0)});
+db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_JOB_EXECUTION_SEQ", count: Long(0)});
+db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_STEP_EXECUTION_SEQ", count: Long(0)});
+
+// INDICES
+db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_name_idx", {"jobName": 1}, {});
+db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_name_key_idx", {"jobName": 1, "jobKey": 1}, {});
+db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_instance_idx", {"jobInstanceId": -1}, {});
+db.getCollection("BATCH_JOB_EXECUTION").createIndex("job_instance_idx", {"jobInstanceId": 1}, {});
+db.getCollection("BATCH_JOB_EXECUTION").createIndex("job_instance_idx", {"jobInstanceId": 1, "status": 1}, {});
+db.getCollection("BATCH_STEP_EXECUTION").createIndex("step_execution_idx", {"stepExecutionId": 1}, {});
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java
index 74e280f1ef..f5e9983011 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2022 the original author or authors.
+ * Copyright 2013-2025 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.
@@ -65,4 +65,22 @@ void testCreateJobKeyOrdering() {
assertEquals(key1, key2);
}
+ @Test
+ public void testCreateJobKeyForEmptyParameters() {
+ JobParameters jobParameters1 = new JobParameters();
+ JobParameters jobParameters2 = new JobParameters();
+ String key1 = jobKeyGenerator.generateKey(jobParameters1);
+ String key2 = jobKeyGenerator.generateKey(jobParameters2);
+ assertEquals(key1, key2);
+ }
+
+ @Test
+ public void testCreateJobKeyForEmptyParametersAndNonIdentifying() {
+ JobParameters jobParameters1 = new JobParameters();
+ JobParameters jobParameters2 = new JobParametersBuilder().addString("name", "foo", false).toJobParameters();
+ String key1 = jobKeyGenerator.generateKey(jobParameters1);
+ String key2 = jobKeyGenerator.generateKey(jobParameters2);
+ assertEquals(key1, key2);
+ }
+
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
index 3a450aa993..907ea62ff8 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -15,14 +15,21 @@
*/
package org.springframework.batch.core;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import org.springframework.util.SerializationUtils;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
-import org.junit.jupiter.api.Test;
-import org.springframework.util.SerializationUtils;
-
/**
* @author Dave Syer
* @author Mahmoud Ben Hassine
@@ -186,4 +193,29 @@ void testSerializable() {
assertEquals(status.getExitCode(), clone.getExitCode());
}
+ @ParameterizedTest
+ @MethodSource("provideKnownExitStatuses")
+ public void testIsNonDefaultExitStatusShouldReturnTrue(ExitStatus status) {
+ boolean result = ExitStatus.isNonDefaultExitStatus(status);
+ assertTrue(result);
+ }
+
+ @ParameterizedTest
+ @MethodSource("provideCustomExitStatuses")
+ public void testIsNonDefaultExitStatusShouldReturnFalse(ExitStatus status) {
+ boolean result = ExitStatus.isNonDefaultExitStatus(status);
+ assertFalse(result);
+ }
+
+ private static Stream provideKnownExitStatuses() {
+ return Stream.of(Arguments.of((ExitStatus) null), Arguments.of(new ExitStatus(null)),
+ Arguments.of(ExitStatus.COMPLETED), Arguments.of(ExitStatus.EXECUTING), Arguments.of(ExitStatus.FAILED),
+ Arguments.of(ExitStatus.NOOP), Arguments.of(ExitStatus.STOPPED), Arguments.of(ExitStatus.UNKNOWN));
+ }
+
+ private static Stream provideCustomExitStatuses() {
+ return Stream.of(Arguments.of(new ExitStatus("CUSTOM")), Arguments.of(new ExitStatus("SUCCESS")),
+ Arguments.of(new ExitStatus("DONE")));
+ }
+
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/JobParametersBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/JobParametersBuilderTests.java
index 2f5cdb5d6e..220dfc4724 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/JobParametersBuilderTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/JobParametersBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2023 the original author or authors.
+ * Copyright 2008-2024 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.
@@ -22,6 +22,7 @@
import java.util.Map;
import java.util.Set;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -85,6 +86,13 @@ void testAddingExistingJobParameters() {
assertEquals(finalParams.getString("baz"), "quix");
}
+ @Test
+ void testAddingNullJobParameters() {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> new JobParametersBuilder().addString("foo", null).toJobParameters());
+ Assertions.assertEquals("Value for parameter 'foo' must not be null", exception.getMessage());
+ }
+
@Test
void testNonIdentifyingParameters() {
this.parametersBuilder.addDate("SCHEDULE_DATE", date, false);
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java
index f26b80d3e8..c8ce09889a 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -27,7 +27,10 @@
import org.springframework.batch.core.DefaultJobKeyGenerator;
import org.springframework.batch.core.JobKeyGenerator;
import org.springframework.batch.core.configuration.JobRegistry;
-import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
+import org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton;
+import org.springframework.batch.core.converter.DefaultJobParametersConverter;
+import org.springframework.batch.core.converter.JobParametersConverter;
+import org.springframework.batch.core.converter.JsonJobParametersConverter;
import org.springframework.batch.core.explore.JobExplorer;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.JobOperator;
@@ -79,7 +82,8 @@ void testConfigurationWithUserDefinedBeans() {
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobLauncher.class)).isMock());
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobRegistry.class)).isMock());
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobOperator.class)).isMock());
- Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobRegistryBeanPostProcessor.class)).isMock());
+ Assertions
+ .assertTrue(Mockito.mockingDetails(context.getBean(JobRegistrySmartInitializingSingleton.class)).isMock());
}
@Test
@@ -162,7 +166,8 @@ void testDefaultInfrastructureBeansRegistration() {
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
JobRegistry jobRegistry = context.getBean(JobRegistry.class);
JobOperator jobOperator = context.getBean(JobOperator.class);
- JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = context.getBean(JobRegistryBeanPostProcessor.class);
+ JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = context
+ .getBean(JobRegistrySmartInitializingSingleton.class);
// then
Assertions.assertNotNull(jobLauncher);
@@ -170,7 +175,7 @@ void testDefaultInfrastructureBeansRegistration() {
Assertions.assertNotNull(jobExplorer);
Assertions.assertNotNull(jobRegistry);
Assertions.assertNotNull(jobOperator);
- Assertions.assertNotNull(jobRegistryBeanPostProcessor);
+ Assertions.assertNotNull(jobRegistrySmartInitializingSingleton);
}
@Test
@@ -202,6 +207,31 @@ public void testCustomJobKeyGeneratorConfiguration() {
jobKeyGenerator.getClass());
}
+ @Test
+ @DisplayName("When no JobParametersConverter is provided the default implementation should be used")
+ public void testDefaultJobParametersConverterConfiguration() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JobConfiguration.class);
+
+ JobOperator jobOperator = context.getBean(JobOperator.class);
+ JobParametersConverter jobParametersConverter = (JobParametersConverter) ReflectionTestUtils
+ .getField(jobOperator, "jobParametersConverter");
+
+ Assertions.assertEquals(DefaultJobParametersConverter.class, jobParametersConverter.getClass());
+ }
+
+ @Test
+ @DisplayName("When a custom JobParametersConverter implementation is found then it should be used")
+ public void testCustomJobParametersConverterConfiguration() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
+ CustomJobParametersConverterConfiguration.class);
+
+ JobOperator jobOperator = context.getBean(JobOperator.class);
+ JobParametersConverter jobParametersConverter = (JobParametersConverter) ReflectionTestUtils
+ .getField(jobOperator, "jobParametersConverter");
+
+ Assertions.assertEquals(JsonJobParametersConverter.class, jobParametersConverter.getClass());
+ }
+
@Configuration
@EnableBatchProcessing
public static class JobConfigurationWithoutDataSource {
@@ -249,7 +279,7 @@ public JobOperator jobOperator() {
}
@Bean
- public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() {
+ public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton() {
return Mockito.mock();
}
@@ -326,6 +356,30 @@ public String generateKey(Object source) {
}
+ @Configuration
+ @EnableBatchProcessing
+ public static class CustomJobParametersConverterConfiguration {
+
+ @Bean
+ public DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL)
+ .addScript("/org/springframework/batch/core/schema-hsqldb.sql")
+ .generateUniqueName(true)
+ .build();
+ }
+
+ @Bean
+ public JdbcTransactionManager transactionManager(DataSource dataSource) {
+ return new JdbcTransactionManager(dataSource);
+ }
+
+ @Bean
+ public JobParametersConverter jobParametersConverter() {
+ return new JsonJobParametersConverter();
+ }
+
+ }
+
private PlatformTransactionManager getTransactionManagerSetOnJobRepository(JobRepository jobRepository) {
Advised target = (Advised) jobRepository; // proxy created by
// AbstractJobRepositoryFactoryBean
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java
index d80dd21715..b660c40b83 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the original author or authors.
+ * Copyright 2022-2024 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.
@@ -85,9 +85,9 @@ void testConfigurationWithCustomInfrastructureBean() {
Assertions.assertEquals(1, jobRepositories.size());
JobRepository jobRepository = jobRepositories.entrySet().iterator().next().getValue();
Assertions.assertInstanceOf(DummyJobRepository.class, jobRepository);
- Map jobRegistryBeanPostProcessorMap = context
- .getBeansOfType(JobRegistryBeanPostProcessor.class);
- Assertions.assertEquals(1, jobRegistryBeanPostProcessorMap.size());
+ Map jobRegistrySmartInitializingSingletonMap = context
+ .getBeansOfType(JobRegistrySmartInitializingSingleton.class);
+ Assertions.assertEquals(1, jobRegistrySmartInitializingSingletonMap.size());
}
@Test
@@ -101,7 +101,8 @@ void testDefaultInfrastructureBeansRegistration() {
JobExplorer jobExplorer = context.getBean(JobExplorer.class);
JobRegistry jobRegistry = context.getBean(JobRegistry.class);
JobOperator jobOperator = context.getBean(JobOperator.class);
- JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = context.getBean(JobRegistryBeanPostProcessor.class);
+ JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = context
+ .getBean(JobRegistrySmartInitializingSingleton.class);
// then
Assertions.assertNotNull(jobLauncher);
@@ -109,7 +110,7 @@ void testDefaultInfrastructureBeansRegistration() {
Assertions.assertNotNull(jobExplorer);
Assertions.assertNotNull(jobRegistry);
Assertions.assertNotNull(jobOperator);
- Assertions.assertNotNull(jobRegistryBeanPostProcessor);
+ Assertions.assertNotNull(jobRegistrySmartInitializingSingleton);
}
@Configuration
@@ -161,10 +162,10 @@ public JobRepository jobRepository() {
}
@Bean
- public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
- JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
- postProcessor.setJobRegistry(jobRegistry);
- return postProcessor;
+ public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
+ JobRegistrySmartInitializingSingleton smartInitializingSingleton = new JobRegistrySmartInitializingSingleton();
+ smartInitializingSingleton.setJobRegistry(jobRegistry);
+ return smartInitializingSingleton;
}
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingletonTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingletonTests.java
new file mode 100644
index 0000000000..f6db1e0187
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingletonTests.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.configuration.support;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.DuplicateJobException;
+import org.springframework.batch.core.configuration.JobRegistry;
+import org.springframework.batch.core.job.JobSupport;
+import org.springframework.beans.FatalBeanException;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+
+/**
+ * @author Henning Pƶttker
+ * @author Mahmoud Ben Hassine
+ */
+class JobRegistrySmartInitializingSingletonTests {
+
+ private final JobRegistry jobRegistry = new MapJobRegistry();
+
+ private final JobRegistrySmartInitializingSingleton singleton = new JobRegistrySmartInitializingSingleton(
+ jobRegistry);
+
+ private final ListableBeanFactory beanFactory = mock(ListableBeanFactory.class);
+
+ @BeforeEach
+ void setUp() {
+ var job = new JobSupport();
+ job.setName("foo");
+ lenient().when(beanFactory.getBeansOfType(Job.class, false, false)).thenReturn(Map.of("bar", job));
+ singleton.setBeanFactory(beanFactory);
+ }
+
+ @Test
+ void testInitializationFails() {
+ singleton.setJobRegistry(null);
+ var exception = assertThrows(IllegalStateException.class, singleton::afterPropertiesSet);
+ assertEquals("JobRegistry must not be null", exception.getMessage());
+ }
+
+ @Test
+ void testAfterSingletonsInstantiated() {
+ singleton.afterSingletonsInstantiated();
+ Collection jobNames = jobRegistry.getJobNames();
+ assertEquals(1, jobNames.size());
+ assertEquals("foo", jobNames.iterator().next());
+ }
+
+ @Test
+ void testAfterSingletonsInstantiatedWithGroupName() {
+ singleton.setGroupName("jobs");
+ singleton.afterSingletonsInstantiated();
+ Collection jobNames = jobRegistry.getJobNames();
+ assertEquals(1, jobNames.size());
+ assertEquals("jobs.foo", jobNames.iterator().next());
+ }
+
+ @Test
+ void testAfterSingletonsInstantiatedWithDuplicate() {
+ singleton.afterSingletonsInstantiated();
+ var exception = assertThrows(FatalBeanException.class, singleton::afterSingletonsInstantiated);
+ assertInstanceOf(DuplicateJobException.class, exception.getCause());
+ }
+
+ @Test
+ void testUnregisterOnDestroy() throws Exception {
+ singleton.afterSingletonsInstantiated();
+ singleton.destroy();
+ assertTrue(jobRegistry.getJobNames().isEmpty());
+ }
+
+ @Test
+ void testExecutionWithApplicationContext() throws Exception {
+ var context = new ClassPathXmlApplicationContext("test-context-with-smart-initializing-singleton.xml",
+ getClass());
+ var registry = context.getBean("registry", JobRegistry.class);
+ Collection jobNames = registry.getJobNames();
+ String[] names = context.getBeanNamesForType(JobSupport.class);
+ int count = names.length;
+ // Each concrete bean of type JobConfiguration is registered...
+ assertEquals(count, jobNames.size());
+ // N.B. there is a failure / wonky mode where a parent bean is given an
+ // explicit name or beanName (using property setter): in this case then
+ // child beans will have the same name and will be re-registered (and
+ // override, if the registry supports that).
+ assertNotNull(registry.getJob("test-job"));
+ assertEquals(context.getBean("test-job-with-name"), registry.getJob("foo"));
+ assertEquals(context.getBean("test-job-with-bean-name"), registry.getJob("bar"));
+ assertEquals(context.getBean("test-job-with-parent-and-name"), registry.getJob("spam"));
+ assertEquals(context.getBean("test-job-with-parent-and-bean-name"), registry.getJob("bucket"));
+ assertEquals(context.getBean("test-job-with-concrete-parent"), registry.getJob("maps"));
+ assertEquals(context.getBean("test-job-with-concrete-parent-and-name"), registry.getJob("oof"));
+ assertEquals(context.getBean("test-job-with-concrete-parent-and-bean-name"), registry.getJob("rab"));
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests.java
index b8fe6dedfb..525e2ba2a1 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -33,8 +33,6 @@
*
*/
@SpringJUnitConfig
-// FIXME this test fails when upgrading the batch xsd from 2.2 to 3.0:
-// https://github.com/spring-projects/spring-batch/issues/1287
class StopAndRestartFailedJobParserTests extends AbstractJobParserTests {
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests.java
index 1702b6f1a3..6f30120f30 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -29,8 +29,6 @@
*
*/
@SpringJUnitConfig
-// FIXME this test fails when upgrading the batch xsd from 2.2 to 3.0:
-// https://github.com/spring-projects/spring-batch/issues/1287
class StopAndRestartJobParserTests extends AbstractJobParserTests {
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests.java
new file mode 100644
index 0000000000..375e21bb27
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.configuration.xml;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.BatchStatus;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @author Henning Pƶttker
+ */
+@SpringJUnitConfig
+class StopAndRestartWithCustomExitCodeJobParserTests extends AbstractJobParserTests {
+
+ @Test
+ void testStopIncomplete() throws Exception {
+
+ //
+ // First Launch
+ //
+ JobExecution jobExecution = createJobExecution();
+ job.execute(jobExecution);
+ assertEquals(1, stepNamesList.size());
+ assertEquals("[s1]", stepNamesList.toString());
+
+ assertEquals(BatchStatus.STOPPED, jobExecution.getStatus());
+ assertEquals("CUSTOM", jobExecution.getExitStatus().getExitCode());
+
+ StepExecution stepExecution1 = getStepExecution(jobExecution, "s1");
+ assertEquals(BatchStatus.COMPLETED, stepExecution1.getStatus());
+ assertEquals(ExitStatus.COMPLETED.getExitCode(), stepExecution1.getExitStatus().getExitCode());
+
+ //
+ // Second Launch
+ //
+ stepNamesList.clear();
+ jobExecution = createJobExecution();
+ job.execute(jobExecution);
+ assertEquals(1, stepNamesList.size()); // step1 is not executed
+ assertEquals("[s2]", stepNamesList.toString());
+
+ assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
+ assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
+
+ StepExecution stepExecution2 = getStepExecution(jobExecution, "s2");
+ assertEquals(BatchStatus.COMPLETED, stepExecution2.getStatus());
+ assertEquals(ExitStatus.COMPLETED, stepExecution2.getExitStatus());
+
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests.java
index f0fe245e14..e824cd75d9 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -29,8 +29,6 @@
*
*/
@SpringJUnitConfig
-// FIXME this test fails when upgrading the batch xsd from 2.2 to 3.0:
-// https://github.com/spring-projects/spring-batch/issues/1287
class StopCustomStatusJobParserTests extends AbstractJobParserTests {
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests.java
index e6ddfee766..b5e4b8183a 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -29,8 +29,6 @@
*
*/
@SpringJUnitConfig
-// FIXME this test fails when upgrading the batch xsd from 2.2 to 3.0:
-// https://github.com/spring-projects/spring-batch/issues/1287
class StopIncompleteJobParserTests extends AbstractJobParserTests {
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopJobParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopJobParserTests.java
index b2f0d75c71..f05c0b6cba 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopJobParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StopJobParserTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -34,8 +34,6 @@
*
*/
@SpringJUnitConfig
-// FIXME this test fails when upgrading the batch xsd from 2.2 to 3.0:
-// https://github.com/spring-projects/spring-batch/issues/1287
class StopJobParserTests extends AbstractJobParserTests {
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/converter/DefaultJobParametersConverterTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/converter/DefaultJobParametersConverterTests.java
index e413162a41..d39b9ad5eb 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/converter/DefaultJobParametersConverterTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/converter/DefaultJobParametersConverterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -20,6 +20,7 @@
import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.JobParameter;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.util.StringUtils;
@@ -129,6 +130,22 @@ void testGetParametersWithBogusLong() {
}
}
+ @Test
+ void testGetParametersWithEmptyValue() {
+ // given
+ String[] args = new String[] { "parameter=" };
+
+ // when
+ JobParameters jobParameters = factory.getJobParameters(StringUtils.splitArrayElementsIntoProperties(args, "="));
+
+ // then
+ assertEquals(1, jobParameters.getParameters().size());
+ JobParameter> parameter = jobParameters.getParameters().get("parameter");
+ assertEquals("", parameter.getValue());
+ assertEquals(String.class, parameter.getType());
+ assertTrue(parameter.isIdentifying());
+ }
+
@Test
void testGetParametersWithDoubleValueDeclaredAsLong() {
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java
index 79a5684b1f..33b886f584 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/ExtendedAbstractJobTests.java
@@ -17,31 +17,20 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.springframework.batch.core.BatchStatus;
-import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.JobExecutionException;
-import org.springframework.batch.core.JobInterruptedException;
-import org.springframework.batch.core.JobParameters;
-import org.springframework.batch.core.JobParametersInvalidException;
-import org.springframework.batch.core.Step;
-import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.*;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.batch.core.step.StepSupport;
-import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.lang.Nullable;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
/**
* @author Dave Syer
@@ -215,6 +204,10 @@ public StubJob() {
protected void doExecute(JobExecution execution) throws JobExecutionException {
}
+ @Override
+ protected void checkStepNamesUnicity() {
+ }
+
@Override
public Step getStep(String stepName) {
return null;
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java
index 37a1390600..c0d54fc017 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java
@@ -513,6 +513,25 @@ void testGetMultipleJobParameters() throws Exception {
}
+ @Test
+ public void testMultipleStepsWithSameName() {
+ job.setName("MultipleStepsWithSameName");
+ String sharedName = "stepName";
+ final List executionsCallbacks = new ArrayList<>();
+ StubStep sharedNameStep1 = new StubStep(sharedName, jobRepository);
+ sharedNameStep1.setCallback(() -> executionsCallbacks.add("step1"));
+ job.addStep(sharedNameStep1);
+ StubStep sharedNameStep2 = new StubStep(sharedName, jobRepository);
+ sharedNameStep2.setCallback(() -> executionsCallbacks.add("step2"));
+ job.addStep(sharedNameStep2);
+ StubStep sharedNameStep3 = new StubStep(sharedName, jobRepository);
+ sharedNameStep3.setCallback(() -> executionsCallbacks.add("step3"));
+ job.addStep(sharedNameStep3);
+ job.execute(jobExecution);
+ assertEquals(List.of("step1", "step2", "step3"), executionsCallbacks);
+ assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
+ }
+
/*
* Check JobRepository to ensure status is being saved.
*/
@@ -588,12 +607,7 @@ public void execute(StepExecution stepExecution)
stepExecution.addFailureException(exception);
return;
}
- if (exception instanceof JobInterruptedException) {
- stepExecution.setExitStatus(ExitStatus.FAILED);
- stepExecution.setStatus(BatchStatus.FAILED);
- stepExecution.addFailureException(exception);
- return;
- }
+
if (runnable != null) {
runnable.run();
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java
index dcff2e0eb3..e3d950df0c 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java
@@ -15,24 +15,11 @@
*/
package org.springframework.batch.core.job.builder;
-import java.util.Arrays;
-
-import javax.sql.DataSource;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.springframework.batch.core.BatchStatus;
-import org.springframework.batch.core.ExitStatus;
-import org.springframework.batch.core.Job;
-import org.springframework.batch.core.JobExecution;
-import org.springframework.batch.core.JobInterruptedException;
-import org.springframework.batch.core.JobParameters;
-import org.springframework.batch.core.JobParametersBuilder;
-import org.springframework.batch.core.Step;
-import org.springframework.batch.core.StepExecution;
-import org.springframework.batch.core.UnexpectedJobExecutionException;
+import org.springframework.batch.core.*;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobScope;
import org.springframework.batch.core.job.flow.Flow;
@@ -40,27 +27,38 @@
import org.springframework.batch.core.job.flow.JobExecutionDecider;
import org.springframework.batch.core.job.flow.support.SimpleFlow;
import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
+import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
+import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.StepSupport;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.item.support.ListItemReader;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
-import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.support.JdbcTransactionManager;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
+import javax.sql.DataSource;
+import java.util.Arrays;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
/**
* @author Dave Syer
* @author Mahmoud Ben Hassine
- *
*/
class FlowJobBuilderTests {
@@ -262,26 +260,6 @@ public FlowExecutionStatus decide(JobExecution jobExecution, @Nullable StepExecu
assertEquals(1, execution.getStepExecutions().size());
}
- @Test
- void testBuildWithDeciderPriorityOnWildcardCount() {
- JobExecutionDecider decider = (jobExecution, stepExecution) -> new FlowExecutionStatus("COMPLETED_PARTIALLY");
- JobFlowBuilder builder = new JobBuilder("flow_priority", jobRepository).start(decider);
- builder.on("**").end();
- builder.on("*").fail();
- builder.build().preventRestart().build().execute(execution);
- assertEquals(BatchStatus.COMPLETED, execution.getStatus());
- }
-
- @Test
- void testBuildWithDeciderPriorityWithEqualWildcard() {
- JobExecutionDecider decider = (jobExecution, stepExecution) -> new FlowExecutionStatus("COMPLETED_PARTIALLY");
- JobFlowBuilder builder = new JobBuilder("flow_priority", jobRepository).start(decider);
- builder.on("COMPLETED*").end();
- builder.on("*").fail();
- builder.build().preventRestart().build().execute(execution);
- assertEquals(BatchStatus.COMPLETED, execution.getStatus());
- }
-
@Test
void testBuildWithDeciderPriority() {
JobExecutionDecider decider = (jobExecution, stepExecution) -> new FlowExecutionStatus("COMPLETED_PARTIALLY");
@@ -383,6 +361,121 @@ void testBuildWithJobScopedStep() throws Exception {
assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
}
+ // https://github.com/spring-projects/spring-batch/issues/3757#issuecomment-1821593539
+ @Test
+ void testStepNamesMustBeUniqueWithinFlowDefinition() throws JobInstanceAlreadyCompleteException,
+ JobExecutionAlreadyRunningException, JobParametersInvalidException, JobRestartException {
+ ApplicationContext context = new AnnotationConfigApplicationContext(JobConfigurationForStepNameUnique.class);
+ JobLauncher jobLauncher = context.getBean(JobLauncher.class);
+ Job job = context.getBean(Job.class);
+ JobExecution jobExecution = jobLauncher.run(job,
+ new JobParametersBuilder().addLong("random", 2L)
+ .addString("stepTwo.name", JobConfigurationForStepNameUnique.SHARED_NAME)
+ .toJobParameters());
+ assertTrue(jobExecution.getAllFailureExceptions()
+ .stream()
+ .map(Object::getClass)
+ .anyMatch(AlreadyUsedStepNameException.class::equals));
+ assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode());
+ jobExecution = jobLauncher.run(job,
+ new JobParametersBuilder().addLong("random", 1L)
+ .addString("stepTwo.name", JobConfigurationForStepNameUnique.SHARED_NAME)
+ .toJobParameters());
+ assertTrue(jobExecution.getAllFailureExceptions()
+ .stream()
+ .map(Object::getClass)
+ .anyMatch(AlreadyUsedStepNameException.class::equals));
+ assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode());
+ }
+
+ @EnableBatchProcessing
+ @Configuration
+ static class JobConfigurationForStepNameUnique {
+
+ private static final String SHARED_NAME = "sharedName";
+
+ private static final Log logger = LogFactory.getLog(FlowJobBuilderTests.class);
+
+ @Bean
+ @JobScope
+ public Step conditionalStep(JobRepository jobRepository, PlatformTransactionManager transactionManager,
+ @Value("#{jobParameters['random']}") Integer random) {
+ return new StepBuilder("conditionalStep", jobRepository)
+ .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
+ String exitStatus = (random % 2 == 0) ? "EVEN" : "ODD";
+ logger.info("'conditionalStep' with exitStatus " + exitStatus);
+ contribution.setExitStatus(new ExitStatus(exitStatus));
+ return RepeatStatus.FINISHED;
+ }, transactionManager)
+ .build();
+ }
+
+ @Bean
+ @JobScope
+ public Step stepTwo(JobRepository jobRepository, PlatformTransactionManager transactionManager,
+ @Value("#{jobParameters['stepTwo.name']}") String name) {
+ return new StepBuilder(name, jobRepository)
+ .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
+ logger.info("Hello from stepTwo");
+ return RepeatStatus.FINISHED;
+ }, transactionManager)
+ .build();
+ }
+
+ @Bean
+ public Step stepThree(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
+ return new StepBuilder(SHARED_NAME, jobRepository)
+ .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
+ logger.info("Hello from stepThree");
+ return RepeatStatus.FINISHED;
+ }, transactionManager)
+ .build();
+ }
+
+ @Bean
+ public Step stepFour(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
+ return new StepBuilder(SHARED_NAME, jobRepository)
+ .tasklet((StepContribution contribution, ChunkContext chunkContext) -> {
+ logger.info("Hello from stepFour");
+ return RepeatStatus.FINISHED;
+ }, transactionManager)
+ .build();
+ }
+
+ @Bean
+ public Job job(JobRepository jobRepository, @Qualifier("conditionalStep") Step conditionalStep,
+ @Qualifier("stepFour") Step step4, @Qualifier("stepTwo") Step step2,
+ @Qualifier("stepThree") Step step3) {
+ JobBuilder jobBuilder = new JobBuilder("flow", jobRepository);
+ return jobBuilder.start(conditionalStep)
+ .on("ODD")
+ .to(step2)
+ .from(conditionalStep)
+ .on("EVEN")
+ .to(step3)
+ .from(step3)
+ .next(step4)
+ .from(step2)
+ .next(step4)
+ .end()
+ .build();
+ }
+
+ @Bean
+ public DataSource dataSource() {
+ return new EmbeddedDatabaseBuilder().addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql")
+ .addScript("/org/springframework/batch/core/schema-hsqldb.sql")
+ .generateUniqueName(true)
+ .build();
+ }
+
+ @Bean
+ public JdbcTransactionManager transactionManager(DataSource dataSource) {
+ return new JdbcTransactionManager(dataSource);
+ }
+
+ }
+
@EnableBatchProcessing
@Configuration
static class JobConfiguration {
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparatorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparatorTests.java
index d71e4813b2..45e323f6c4 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparatorTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/DefaultStateTransitionComparatorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2022 the original author or authors.
+ * Copyright 2013-2024 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.
@@ -39,96 +39,96 @@ void testSimpleOrderingEqual() {
void testSimpleOrderingMoreGeneral() {
StateTransition generic = StateTransition.createStateTransition(state, "CONTIN???LE", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTINUABLE", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testSimpleOrderingMostGeneral() {
StateTransition generic = StateTransition.createStateTransition(state, "*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTINUABLE", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testSubstringAndWildcard() {
StateTransition generic = StateTransition.createStateTransition(state, "CONTIN*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTINUABLE", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testSimpleOrderingMostToNextGeneral() {
StateTransition generic = StateTransition.createStateTransition(state, "*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "C?", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testSimpleOrderingAdjacent() {
StateTransition generic = StateTransition.createStateTransition(state, "CON*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CON?", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByNumberOfGenericWildcards() {
StateTransition generic = StateTransition.createStateTransition(state, "*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "**", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByNumberOfSpecificWildcards() {
StateTransition generic = StateTransition.createStateTransition(state, "CONTI??ABLE", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTI?UABLE", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByLengthWithAsteriskEquality() {
StateTransition generic = StateTransition.createStateTransition(state, "CON*", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTINUABLE*", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByLengthWithWildcardEquality() {
StateTransition generic = StateTransition.createStateTransition(state, "CON??", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CONTINUABLE??", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByAlphaWithAsteriskEquality() {
StateTransition generic = StateTransition.createStateTransition(state, "DOG**", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CAT**", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testOrderByAlphaWithWildcardEquality() {
StateTransition generic = StateTransition.createStateTransition(state, "DOG??", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CAT??", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
@Test
void testPriorityOrderingWithAlphabeticComparison() {
StateTransition generic = StateTransition.createStateTransition(state, "DOG", "start");
StateTransition specific = StateTransition.createStateTransition(state, "CAT", "start");
- assertEquals(1, comparator.compare(generic, specific));
- assertEquals(-1, comparator.compare(specific, generic));
+ assertEquals(1, comparator.compare(specific, generic));
+ assertEquals(-1, comparator.compare(generic, specific));
}
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/StateTransitionTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/StateTransitionTests.java
index b194b2a5ca..f5ab5fb5fc 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/StateTransitionTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/flow/support/StateTransitionTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -15,6 +15,8 @@
*/
package org.springframework.batch.core.job.flow.support;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -25,7 +27,7 @@
/**
* @author Dave Syer
* @author Michael Minella
- *
+ * @author Kim Youngwoong
*/
class StateTransitionTests {
@@ -74,6 +76,18 @@ void testMatchesPlaceholder() {
assertTrue(transition.matches("CONTINUABLE"));
}
+ @Test
+ void testEquals() {
+ StateTransition transition1 = StateTransition.createStateTransition(state, "pattern1", "next1");
+ StateTransition transition2 = StateTransition.createStateTransition(state, "pattern1", "next1");
+ StateTransition transition3 = StateTransition.createStateTransition(state, "pattern2", "next2");
+
+ assertEquals(transition1, transition2);
+ assertNotEquals(transition1, transition3);
+ assertEquals(transition1, transition1);
+ assertNotEquals(null, transition1);
+ }
+
@Test
void testToString() {
StateTransition transition = StateTransition.createStateTransition(state, "CONTIN???LE", "start");
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunnerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunnerTests.java
deleted file mode 100644
index 6643bd60e6..0000000000
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunnerTests.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2006-2022 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
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.batch.core.launch.support;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.util.ClassUtils;
-
-/**
- * @author Dave Syer
- *
- */
-class JobRegistryBackgroundJobRunnerTests {
-
- /**
- * Test method for
- * {@link org.springframework.batch.core.launch.support.JobRegistryBackgroundJobRunner#main(java.lang.String[])}.
- */
- @Test
- void testMain() throws Exception {
- JobRegistryBackgroundJobRunner.main(
- ClassUtils.addResourcePathToPackagePath(getClass(), "test-environment-with-registry.xml"),
- ClassUtils.addResourcePathToPackagePath(getClass(), "job.xml"));
- assertEquals(0, JobRegistryBackgroundJobRunner.getErrors().size());
- }
-
- @Test
- void testMainWithAutoRegister() throws Exception {
- JobRegistryBackgroundJobRunner.main(
- ClassUtils.addResourcePathToPackagePath(getClass(),
- "test-environment-with-registry-and-auto-register.xml"),
- ClassUtils.addResourcePathToPackagePath(getClass(), "job.xml"));
- assertEquals(0, JobRegistryBackgroundJobRunner.getErrors().size());
- }
-
- @Test
- void testMainWithJobLoader() throws Exception {
- JobRegistryBackgroundJobRunner.main(
- ClassUtils.addResourcePathToPackagePath(getClass(), "test-environment-with-loader.xml"),
- ClassUtils.addResourcePathToPackagePath(getClass(), "job.xml"));
- assertEquals(0, JobRegistryBackgroundJobRunner.getErrors().size());
- }
-
- @BeforeEach
- void setUp() {
- JobRegistryBackgroundJobRunner.getErrors().clear();
- System.setProperty(JobRegistryBackgroundJobRunner.EMBEDDED, "");
- }
-
- @AfterEach
- void tearDown() {
- System.clearProperty(JobRegistryBackgroundJobRunner.EMBEDDED);
- JobRegistryBackgroundJobRunner.getErrors().clear();
- JobRegistryBackgroundJobRunner.stop();
- }
-
-}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/SimpleJobOperatorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/SimpleJobOperatorTests.java
index d5d3951c2c..e61a8fb5a1 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/SimpleJobOperatorTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/SimpleJobOperatorTests.java
@@ -455,6 +455,10 @@ protected void doExecute(JobExecution execution) throws JobExecutionException {
}
+ @Override
+ protected void checkStepNamesUnicity() {
+ }
+
}
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java
new file mode 100644
index 0000000000..015a90e034
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.explore.support.MongoJobExplorerFactoryBean;
+import org.springframework.batch.core.job.builder.JobBuilder;
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.step.builder.StepBuilder;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.MongoDatabaseFactory;
+import org.springframework.data.mongodb.MongoTransactionManager;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+
+/**
+ * @author Mahmoud Ben Hassine
+ */
+@Configuration
+@EnableBatchProcessing
+class MongoDBIntegrationTestConfiguration {
+
+ @Bean
+ public JobRepository jobRepository(MongoTemplate mongoTemplate, MongoTransactionManager transactionManager)
+ throws Exception {
+ MongoJobRepositoryFactoryBean jobRepositoryFactoryBean = new MongoJobRepositoryFactoryBean();
+ jobRepositoryFactoryBean.setMongoOperations(mongoTemplate);
+ jobRepositoryFactoryBean.setTransactionManager(transactionManager);
+ jobRepositoryFactoryBean.afterPropertiesSet();
+ return jobRepositoryFactoryBean.getObject();
+ }
+
+ @Bean
+ public JobExplorer jobExplorer(MongoTemplate mongoTemplate, MongoTransactionManager transactionManager)
+ throws Exception {
+ MongoJobExplorerFactoryBean jobExplorerFactoryBean = new MongoJobExplorerFactoryBean();
+ jobExplorerFactoryBean.setMongoOperations(mongoTemplate);
+ jobExplorerFactoryBean.setTransactionManager(transactionManager);
+ jobExplorerFactoryBean.afterPropertiesSet();
+ return jobExplorerFactoryBean.getObject();
+ }
+
+ @Bean
+ public MongoDatabaseFactory mongoDatabaseFactory(@Value("${mongo.connectionString}") String connectionString) {
+ MongoClient mongoClient = MongoClients.create(connectionString);
+ return new SimpleMongoClientDatabaseFactory(mongoClient, "test");
+ }
+
+ @Bean
+ public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDatabaseFactory) {
+ MongoTemplate template = new MongoTemplate(mongoDatabaseFactory);
+ MappingMongoConverter converter = (MappingMongoConverter) template.getConverter();
+ converter.setMapKeyDotReplacement(".");
+ return template;
+ }
+
+ @Bean
+ public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoDatabaseFactory) {
+ MongoTransactionManager mongoTransactionManager = new MongoTransactionManager();
+ mongoTransactionManager.setDatabaseFactory(mongoDatabaseFactory);
+ mongoTransactionManager.afterPropertiesSet();
+ return mongoTransactionManager;
+ }
+
+ @Bean
+ public Job job(JobRepository jobRepository, MongoTransactionManager transactionManager) {
+ return new JobBuilder("job", jobRepository)
+ .start(new StepBuilder("step1", jobRepository)
+ .tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED, transactionManager)
+ .build())
+ .next(new StepBuilder("step2", jobRepository)
+ .tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED, transactionManager)
+ .build())
+ .build();
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java
new file mode 100644
index 0000000000..a6ed1c9bb9
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.explore.JobExplorer;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * @author Henning Pƶttker
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@SpringJUnitConfig(MongoDBIntegrationTestConfiguration.class)
+public class MongoDBJobExplorerIntegrationTests {
+
+ private static final DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:8.0.1");
+
+ @Container
+ public static MongoDBContainer mongodb = new MongoDBContainer(MONGODB_IMAGE);
+
+ @DynamicPropertySource
+ static void setMongoDbConnectionString(DynamicPropertyRegistry registry) {
+ registry.add("mongo.connectionString", mongodb::getConnectionString);
+ }
+
+ @BeforeAll
+ static void setUp(@Autowired MongoTemplate mongoTemplate) {
+ mongoTemplate.createCollection("BATCH_JOB_INSTANCE");
+ mongoTemplate.createCollection("BATCH_JOB_EXECUTION");
+ mongoTemplate.createCollection("BATCH_STEP_EXECUTION");
+ mongoTemplate.createCollection("BATCH_SEQUENCES");
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_INSTANCE_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_EXECUTION_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_STEP_EXECUTION_SEQ", "count", 0L)));
+ }
+
+ @Test
+ void testGetJobExecutionById(@Autowired JobLauncher jobLauncher, @Autowired Job job,
+ @Autowired JobExplorer jobExplorer) throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().addString("name", "testGetJobExecutionById")
+ .addLocalDateTime("runtime", LocalDateTime.now())
+ .toJobParameters();
+ JobExecution jobExecution = jobLauncher.run(job, jobParameters);
+
+ // when
+ JobExecution actual = jobExplorer.getJobExecution(jobExecution.getId());
+
+ // then
+ assertNotNull(actual);
+ assertNotNull(actual.getJobInstance());
+ assertEquals(jobExecution.getJobId(), actual.getJobId());
+ assertFalse(actual.getExecutionContext().isEmpty());
+ }
+
+ @Test
+ void testGetStepExecutionByIds(@Autowired JobLauncher jobLauncher, @Autowired Job job,
+ @Autowired JobExplorer jobExplorer) throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().addString("name", "testGetStepExecutionByIds")
+ .addLocalDateTime("runtime", LocalDateTime.now())
+ .toJobParameters();
+ JobExecution jobExecution = jobLauncher.run(job, jobParameters);
+ StepExecution stepExecution = jobExecution.getStepExecutions().stream().findFirst().orElseThrow();
+
+ // when
+ StepExecution actual = jobExplorer.getStepExecution(jobExecution.getId(), stepExecution.getId());
+
+ // then
+ assertNotNull(actual);
+ assertEquals(stepExecution.getId(), actual.getId());
+ assertFalse(actual.getExecutionContext().isEmpty());
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java
new file mode 100644
index 0000000000..b45aa7bd19
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import com.mongodb.client.MongoCollection;
+import org.bson.Document;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.mongodb.core.MongoTemplate;
+
+/**
+ * @author Mahmoud Ben Hassine
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@SpringJUnitConfig(MongoDBIntegrationTestConfiguration.class)
+public class MongoDBJobRepositoryIntegrationTests {
+
+ private static final DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:8.0.1");
+
+ @Container
+ public static MongoDBContainer mongodb = new MongoDBContainer(MONGODB_IMAGE);
+
+ @DynamicPropertySource
+ static void setMongoDbConnectionString(DynamicPropertyRegistry registry) {
+ registry.add("mongo.connectionString", mongodb::getConnectionString);
+ }
+
+ @Autowired
+ private MongoTemplate mongoTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ mongoTemplate.createCollection("BATCH_JOB_INSTANCE");
+ mongoTemplate.createCollection("BATCH_JOB_EXECUTION");
+ mongoTemplate.createCollection("BATCH_STEP_EXECUTION");
+ mongoTemplate.createCollection("BATCH_SEQUENCES");
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_INSTANCE_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_EXECUTION_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_STEP_EXECUTION_SEQ", "count", 0L)));
+ }
+
+ @Test
+ void testJobExecution(@Autowired JobLauncher jobLauncher, @Autowired Job job) throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().addString("name", "foo")
+ .addLocalDateTime("runtime", LocalDateTime.now())
+ .toJobParameters();
+
+ // when
+ JobExecution jobExecution = jobLauncher.run(job, jobParameters);
+
+ // then
+ Assertions.assertNotNull(jobExecution);
+ Assertions.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus());
+
+ MongoCollection jobInstancesCollection = mongoTemplate.getCollection("BATCH_JOB_INSTANCE");
+ MongoCollection jobExecutionsCollection = mongoTemplate.getCollection("BATCH_JOB_EXECUTION");
+ MongoCollection stepExecutionsCollection = mongoTemplate.getCollection("BATCH_STEP_EXECUTION");
+
+ Assertions.assertEquals(1, jobInstancesCollection.countDocuments());
+ Assertions.assertEquals(1, jobExecutionsCollection.countDocuments());
+ Assertions.assertEquals(2, stepExecutionsCollection.countDocuments());
+
+ // dump results for inspection
+ dump(jobInstancesCollection, "job instance = ");
+ dump(jobExecutionsCollection, "job execution = ");
+ dump(stepExecutionsCollection, "step execution = ");
+ }
+
+ private static void dump(MongoCollection collection, String prefix) {
+ for (Document document : collection.find()) {
+ System.out.println(prefix + document.toJson());
+ }
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java
new file mode 100644
index 0000000000..7b71ca8505
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+import org.bson.Document;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.core.repository.dao.ExecutionContextDao;
+import org.springframework.batch.core.repository.dao.MongoExecutionContextDao;
+import org.springframework.batch.core.repository.support.MongoExecutionContextDaoIntegrationTests.ExecutionContextDaoConfiguration;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.core.MongoOperations;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * @author Henning Pƶttker
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@SpringJUnitConfig({ MongoDBIntegrationTestConfiguration.class, ExecutionContextDaoConfiguration.class })
+public class MongoExecutionContextDaoIntegrationTests {
+
+ private static final DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:8.0.1");
+
+ @Container
+ public static MongoDBContainer mongodb = new MongoDBContainer(MONGODB_IMAGE);
+
+ @DynamicPropertySource
+ static void setMongoDbConnectionString(DynamicPropertyRegistry registry) {
+ registry.add("mongo.connectionString", mongodb::getConnectionString);
+ }
+
+ @BeforeAll
+ static void setUp(@Autowired MongoTemplate mongoTemplate) {
+ mongoTemplate.createCollection("BATCH_JOB_INSTANCE");
+ mongoTemplate.createCollection("BATCH_JOB_EXECUTION");
+ mongoTemplate.createCollection("BATCH_STEP_EXECUTION");
+ mongoTemplate.createCollection("BATCH_SEQUENCES");
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_INSTANCE_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_JOB_EXECUTION_SEQ", "count", 0L)));
+ mongoTemplate.getCollection("BATCH_SEQUENCES")
+ .insertOne(new Document(Map.of("_id", "BATCH_STEP_EXECUTION_SEQ", "count", 0L)));
+ }
+
+ @Test
+ void testGetJobExecutionWithEmptyResult(@Autowired ExecutionContextDao executionContextDao) {
+ // given
+ JobExecution jobExecution = new JobExecution(12345678L);
+
+ // when
+ ExecutionContext actual = executionContextDao.getExecutionContext(jobExecution);
+
+ // then
+ assertNotNull(actual);
+ assertTrue(actual.isEmpty());
+ }
+
+ @Test
+ void testSaveJobExecution(@Autowired JobLauncher jobLauncher, @Autowired Job job,
+ @Autowired ExecutionContextDao executionContextDao) throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().addString("name", "testSaveJobExecution")
+ .addLocalDateTime("runtime", LocalDateTime.now())
+ .toJobParameters();
+ JobExecution jobExecution = jobLauncher.run(job, jobParameters);
+
+ // when
+ jobExecution.getExecutionContext().putString("foo", "bar");
+ executionContextDao.saveExecutionContext(jobExecution);
+ ExecutionContext actual = executionContextDao.getExecutionContext(jobExecution);
+
+ // then
+ assertTrue(actual.containsKey("foo"));
+ assertEquals("bar", actual.get("foo"));
+ }
+
+ @Test
+ void testGetStepExecutionWithEmptyResult(@Autowired ExecutionContextDao executionContextDao) {
+ // given
+ JobExecution jobExecution = new JobExecution(12345678L);
+ StepExecution stepExecution = new StepExecution("step", jobExecution, 23456789L);
+
+ // when
+ ExecutionContext actual = executionContextDao.getExecutionContext(stepExecution);
+
+ // then
+ assertNotNull(actual);
+ assertTrue(actual.isEmpty());
+ }
+
+ @Test
+ void testSaveStepExecution(@Autowired JobLauncher jobLauncher, @Autowired Job job,
+ @Autowired ExecutionContextDao executionContextDao) throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().addString("name", "testSaveJobExecution")
+ .addLocalDateTime("runtime", LocalDateTime.now())
+ .toJobParameters();
+ JobExecution jobExecution = jobLauncher.run(job, jobParameters);
+ StepExecution stepExecution = jobExecution.getStepExecutions().stream().findFirst().orElseThrow();
+
+ // when
+ stepExecution.getExecutionContext().putString("foo", "bar");
+ executionContextDao.saveExecutionContext(stepExecution);
+ ExecutionContext actual = executionContextDao.getExecutionContext(stepExecution);
+
+ // then
+ assertTrue(actual.containsKey("foo"));
+ assertEquals("bar", actual.get("foo"));
+ }
+
+ @Configuration
+ static class ExecutionContextDaoConfiguration {
+
+ @Bean
+ ExecutionContextDao executionContextDao(MongoOperations mongoOperations) {
+ return new MongoExecutionContextDao(mongoOperations);
+ }
+
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/ResourcelessJobRepositoryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/ResourcelessJobRepositoryTests.java
new file mode 100644
index 0000000000..9e5f6d6386
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/ResourcelessJobRepositoryTests.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.repository.support;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Test class for {@link ResourcelessJobRepository}.
+ *
+ * @author Mahmoud Ben Hassine
+ */
+class ResourcelessJobRepositoryTests {
+
+ private final ResourcelessJobRepository jobRepository = new ResourcelessJobRepository();
+
+ @Test
+ void isJobInstanceExists() {
+ assertFalse(this.jobRepository.isJobInstanceExists("job", new JobParameters()));
+ }
+
+ @Test
+ void createJobInstance() {
+ // given
+ String jobName = "job";
+ JobParameters jobParameters = new JobParameters();
+
+ // when
+ JobInstance jobInstance = this.jobRepository.createJobInstance(jobName, jobParameters);
+
+ // then
+ assertNotNull(jobInstance);
+ assertEquals(jobName, jobInstance.getJobName());
+ assertEquals(1L, jobInstance.getInstanceId());
+ }
+
+ @Test
+ void createJobExecution() {
+ // given
+ String jobName = "job";
+ JobParameters jobParameters = new JobParameters();
+
+ // when
+ JobExecution jobExecution = this.jobRepository.createJobExecution(jobName, jobParameters);
+
+ // then
+ assertNotNull(jobExecution);
+ assertEquals(1L, jobExecution.getId());
+ assertEquals(jobName, jobExecution.getJobInstance().getJobName());
+ assertEquals(1L, jobExecution.getJobInstance().getInstanceId());
+ }
+
+ @Test
+ void getLastJobExecution() {
+ // given
+ String jobName = "job";
+ JobParameters jobParameters = new JobParameters();
+ this.jobRepository.createJobExecution(jobName, jobParameters);
+
+ // when
+ JobExecution jobExecution = this.jobRepository.getLastJobExecution(jobName, jobParameters);
+
+ // then
+ assertNotNull(jobExecution);
+ assertEquals(1L, jobExecution.getId());
+ assertEquals(jobName, jobExecution.getJobInstance().getJobName());
+ assertEquals(1L, jobExecution.getJobInstance().getInstanceId());
+ }
+
+}
\ No newline at end of file
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/SimpleJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/SimpleJobRepositoryIntegrationTests.java
index 53c33b178e..eb81dbd246 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/SimpleJobRepositoryIntegrationTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/SimpleJobRepositoryIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2022 the original author or authors.
+ * Copyright 2008-2024 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.
@@ -32,12 +32,13 @@
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
-import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.fail;
/**
* Repository tests using JDBC DAOs (rather than mocks).
@@ -152,11 +153,7 @@ void testGetStepExecutionCountAndLastStepExecution() throws Exception {
@Transactional
@Test
void testSaveExecutionContext() throws Exception {
- ExecutionContext ctx = new ExecutionContext() {
- {
- putLong("crashedPosition", 7);
- }
- };
+ ExecutionContext ctx = new ExecutionContext(Map.of("crashedPosition", 7));
JobExecution jobExec = jobRepository.createJobExecution(job.getName(), jobParameters);
jobExec.setStartTime(LocalDateTime.now());
jobExec.setExecutionContext(ctx);
@@ -169,11 +166,6 @@ void testSaveExecutionContext() throws Exception {
StepExecution retrievedStepExec = jobRepository.getLastStepExecution(jobExec.getJobInstance(), step.getName());
assertEquals(stepExec, retrievedStepExec);
assertEquals(ctx, retrievedStepExec.getExecutionContext());
-
- // JobExecution retrievedJobExec =
- // jobRepository.getLastJobExecution(jobExec.getJobInstance());
- // assertEquals(jobExec, retrievedJobExec);
- // assertEquals(ctx, retrievedJobExec.getExecutionContext());
}
/*
@@ -205,7 +197,7 @@ void testGetLastJobExecution() throws Exception {
jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
StepExecution stepExecution = new StepExecution("step1", jobExecution);
jobRepository.add(stepExecution);
- jobExecution.addStepExecutions(Arrays.asList(stepExecution));
+ jobExecution.addStepExecutions(List.of(stepExecution));
assertEquals(jobExecution, jobRepository.getLastJobExecution(job.getName(), jobParameters));
assertEquals(stepExecution, jobExecution.getStepExecutions().iterator().next());
}
@@ -233,42 +225,41 @@ void testReExecuteWithSameJobParameters() throws Exception {
*/
@Transactional
@Test
- public void testReExecuteWithSameJobParametersWhenRunning() throws Exception {
+ void testReExecuteWithSameJobParametersWhenRunning() throws Exception {
JobParameters jobParameters = new JobParametersBuilder().addString("stringKey", "stringValue")
.toJobParameters();
// jobExecution with status STARTING
JobExecution jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
- try {
- jobRepository.createJobExecution(job.getName(), jobParameters);
- fail();
- }
- catch (JobExecutionAlreadyRunningException e) {
- // expected
- }
+ assertThrows(JobExecutionAlreadyRunningException.class,
+ () -> jobRepository.createJobExecution(job.getName(), jobParameters));
// jobExecution with status STARTED
jobExecution.setStatus(BatchStatus.STARTED);
jobExecution.setStartTime(LocalDateTime.now());
jobRepository.update(jobExecution);
- try {
- jobRepository.createJobExecution(job.getName(), jobParameters);
- fail();
- }
- catch (JobExecutionAlreadyRunningException e) {
- // expected
- }
+ assertThrows(JobExecutionAlreadyRunningException.class,
+ () -> jobRepository.createJobExecution(job.getName(), jobParameters));
// jobExecution with status STOPPING
jobExecution.setStatus(BatchStatus.STOPPING);
jobRepository.update(jobExecution);
- try {
- jobRepository.createJobExecution(job.getName(), jobParameters);
- fail();
- }
- catch (JobExecutionAlreadyRunningException e) {
- // expected
- }
+ assertThrows(JobExecutionAlreadyRunningException.class,
+ () -> jobRepository.createJobExecution(job.getName(), jobParameters));
+ }
+
+ @Transactional
+ @Test
+ void testDeleteJobInstance() throws Exception {
+ var jobParameters = new JobParametersBuilder().addString("foo", "bar").toJobParameters();
+ var jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
+ var stepExecution = new StepExecution("step", jobExecution);
+ jobRepository.add(stepExecution);
+
+ jobRepository.deleteJobInstance(jobExecution.getJobInstance());
+
+ assertEquals(0, jobRepository.findJobInstancesByName(job.getName(), 0, 1).size());
+ assertNull(jobRepository.getLastJobExecution(job.getName(), jobParameters));
}
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java
new file mode 100644
index 0000000000..8d761fa197
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.step;
+
+import java.time.LocalDateTime;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.batch.core.StepExecutionListener;
+import org.springframework.batch.core.repository.JobRepository;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link AbstractStep}.
+ */
+class AbstractStepTests {
+
+ @Test
+ void testEndTimeInListener() throws Exception {
+ // given
+ StepExecution execution = new StepExecution("step",
+ new JobExecution(new JobInstance(1L, "job"), new JobParameters()));
+ AbstractStep tested = new AbstractStep() {
+ @Override
+ protected void doExecute(StepExecution stepExecution) {
+ }
+ };
+ JobRepository jobRepository = mock();
+ Listener stepListener = new Listener();
+ tested.setStepExecutionListeners(new StepExecutionListener[] { stepListener });
+ tested.setJobRepository(jobRepository);
+
+ // when
+ tested.execute(execution);
+
+ // then
+ assertNotNull(stepListener.getStepEndTime());
+ }
+
+ static class Listener implements StepExecutionListener {
+
+ private LocalDateTime stepEndTime;
+
+ @Override
+ public ExitStatus afterStep(StepExecution stepExecution) {
+ this.stepEndTime = stepExecution.getEndTime();
+ return ExitStatus.COMPLETED;
+ }
+
+ public LocalDateTime getStepEndTime() {
+ return this.stepEndTime;
+ }
+
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilderTests.java
new file mode 100644
index 0000000000..6cd6f2374e
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/AbstractTaskletStepBuilderTests.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2024 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.step.builder;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.batch.core.repository.JobRepository;
+import org.springframework.batch.core.step.tasklet.TaskletStep;
+import org.springframework.batch.item.ItemProcessor;
+import org.springframework.batch.item.ItemReader;
+import org.springframework.batch.item.ItemWriter;
+import org.springframework.batch.repeat.support.TaskExecutorRepeatTemplate;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Test cases for verifying the {@link AbstractTaskletStepBuilder} and faultTolerant()
+ * functionality.
+ *
+ * Issue: https://github.com/spring-projects/spring-batch/issues/4438
+ *
+ * @author Ilpyo Yang
+ * @author Mahmoud Ben Hassine
+ */
+public class AbstractTaskletStepBuilderTests {
+
+ private final JobRepository jobRepository = mock(JobRepository.class);
+
+ private final PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
+
+ private final int chunkSize = 10;
+
+ private final ItemReader itemReader = mock(ItemReader.class);
+
+ private final ItemProcessor itemProcessor = mock(ItemProcessor.class);
+
+ private final ItemWriter itemWriter = mock(ItemWriter.class);
+
+ private final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
+
+ @Test
+ void testSetTaskExecutorBeforeFaultTolerant() {
+ TaskletStep step = new StepBuilder("step-name", jobRepository)
+ .chunk(chunkSize, transactionManager)
+ .taskExecutor(taskExecutor)
+ .reader(itemReader)
+ .processor(itemProcessor)
+ .writer(itemWriter)
+ .faultTolerant()
+ .build();
+
+ Object stepOperations = ReflectionTestUtils.getField(step, "stepOperations");
+ assertInstanceOf(TaskExecutorRepeatTemplate.class, stepOperations);
+ }
+
+ @Test
+ void testSetTaskExecutorAfterFaultTolerant() {
+ TaskletStep step = new StepBuilder("step-name", jobRepository)
+ .chunk(chunkSize, transactionManager)
+ .reader(itemReader)
+ .processor(itemProcessor)
+ .writer(itemWriter)
+ .faultTolerant()
+ .taskExecutor(taskExecutor)
+ .build();
+
+ Object stepOperations = ReflectionTestUtils.getField(step, "stepOperations");
+ assertInstanceOf(TaskExecutorRepeatTemplate.class, stepOperations);
+ }
+
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilderTests.java
index 1fadefc974..ecb3bcd1b8 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilderTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 the original author or authors.
+ * Copyright 2021-2024 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.
@@ -22,6 +22,7 @@
import org.springframework.batch.core.configuration.xml.DummyItemWriter;
import org.springframework.batch.core.configuration.xml.DummyJobRepository;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
+import java.lang.reflect.Field;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -53,4 +54,16 @@ void testAnnotationBasedStepExecutionListenerRegistration() {
assertNotNull(step);
}
+ @Test
+ void testSkipLimitDefaultValue() throws NoSuchFieldException, IllegalAccessException {
+ FaultTolerantStepBuilder, ?> stepBuilder = new FaultTolerantStepBuilder<>(
+ new StepBuilder("step", new DummyJobRepository()));
+
+ Field field = stepBuilder.getClass().getDeclaredField("skipLimit");
+ field.setAccessible(true);
+ int skipLimit = (int) field.get(stepBuilder);
+
+ assertEquals(10, skipLimit);
+ }
+
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessorTests.java
index 5070b277a4..d30e06917e 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessorTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2023 the original author or authors.
+ * Copyright 2008-2024 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.
@@ -18,6 +18,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.ArrayList;
@@ -97,6 +98,16 @@ public String process(String item) throws Exception {
assertEquals(1, contribution.getFilterCount());
}
+ @Test
+ void testTransformChunkEnd() throws Exception {
+ Chunk inputs = new Chunk<>(Arrays.asList("1", "2"));
+ inputs.setEnd();
+ processor.initializeUserData(inputs);
+ Chunk outputs = processor.transform(contribution, inputs);
+ assertEquals(Arrays.asList("1", "2"), outputs.getItems());
+ assertTrue(outputs.isEnd());
+ }
+
@Test
void testFilterCountOnSkip() throws Exception {
processor.setProcessSkipPolicy(new AlwaysSkipItemSkipPolicy());
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRetryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRetryTests.java
index 84cb8a0449..9f5ec22ea7 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRetryTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanRetryTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -61,10 +61,12 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
/**
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author jojoldu
*
*/
class FaultTolerantStepFactoryBeanRetryTests {
@@ -134,7 +136,7 @@ void testType() {
@SuppressWarnings("cast")
@Test
void testDefaultValue() throws Exception {
- assertTrue(factory.getObject() instanceof Step);
+ assertInstanceOf(Step.class, factory.getObject());
}
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java
index 88c2982aa3..f476e4e72c 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -35,10 +35,12 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
/**
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author jojoldu
*
*/
class RepeatOperationsStepFactoryBeanTests {
@@ -66,7 +68,7 @@ void testType() {
@Test
@SuppressWarnings("cast")
void testDefaultValue() throws Exception {
- assertTrue(factory.getObject() instanceof Step);
+ assertInstanceOf(Step.class, factory.getObject());
}
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleChunkProcessorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleChunkProcessorTests.java
index e9a7e0e678..5ebcb49ced 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleChunkProcessorTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleChunkProcessorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2023 the original author or authors.
+ * Copyright 2008-2024 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.
@@ -16,6 +16,7 @@
package org.springframework.batch.core.step.item;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.Arrays;
@@ -76,4 +77,15 @@ void testProcess() throws Exception {
assertEquals(2, contribution.getWriteCount());
}
+ @Test
+ void testTransform() throws Exception {
+ Chunk inputs = new Chunk<>();
+ inputs.add("foo");
+ inputs.add("bar");
+ inputs.setEnd();
+ Chunk outputs = processor.transform(contribution, inputs);
+ assertEquals(Arrays.asList("foo", "bar"), outputs.getItems());
+ assertTrue(outputs.isEnd());
+ }
+
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java
index 09743e3c69..d4f6137dbb 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -328,6 +328,87 @@ public void afterChunkError(ChunkContext context) {
assertTrue(writeListener.trail.startsWith("1234"), "Listener order not as expected: " + writeListener.trail);
}
+ @Test
+ void testChunkListenersThrowException() throws Exception {
+ String[] items = new String[] { "1", "2", "3", "4", "5", "6", "7" };
+ int commitInterval = 3;
+
+ SimpleStepFactoryBean factory = getStepFactory(items);
+ class AssertingWriteListener extends StepListenerSupport
@@ -183,6 +195,10 @@
org.slf4jslf4j-api
+
+ org.springframework
+ spring-expression
+
@@ -197,10 +213,16 @@
+
+ org.mongodb
+ mongodb-driver-core
+ ${mongodb-driver.version}
+ true
+ org.mongodbmongodb-driver-sync
- ${mongodb-driver-sync.version}
+ ${mongodb-driver.version}true
@@ -325,6 +347,84 @@
${derby.version}test
+
+ org.testcontainers
+ junit-jupiter
+ ${testcontainers.version}
+ test
+
+
+ com.mysql
+ mysql-connector-j
+ ${mysql-connector-j.version}
+ test
+
+
+ org.testcontainers
+ mysql
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ oracle-xe
+ ${testcontainers.version}
+ test
+
+
+ com.oracle.database.jdbc
+ ojdbc10
+ ${oracle.version}
+ test
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ ${mariadb-java-client.version}
+ test
+
+
+ org.testcontainers
+ mariadb
+ ${testcontainers.version}
+ test
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+ test
+
+
+ org.testcontainers
+ postgresql
+ ${testcontainers.version}
+ test
+
+
+ com.ibm.db2
+ jcc
+ ${db2.version}
+ test
+
+
+ org.testcontainers
+ db2
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ mssqlserver
+ ${testcontainers.version}
+ test
+
+
+ com.microsoft.sqlserver
+ mssql-jdbc
+ ${sqlserver.version}
+ test
+ com.thoughtworks.xstreamxstream
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/Chunk.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/Chunk.java
index dd39b70e76..52895ca79d 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/Chunk.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/Chunk.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -38,9 +38,9 @@
*/
public class Chunk implements Iterable, Serializable {
- private List items = new ArrayList<>();
+ private final List items = new ArrayList<>();
- private List> skips = new ArrayList<>();
+ private final List> skips = new ArrayList<>();
private final List errors = new ArrayList<>();
@@ -67,10 +67,10 @@ public Chunk(List extends W> items) {
public Chunk(List extends W> items, List> skips) {
super();
if (items != null) {
- this.items = new ArrayList<>(items);
+ this.items.addAll(items);
}
if (skips != null) {
- this.skips = new ArrayList<>(skips);
+ this.skips.addAll(skips);
}
}
@@ -103,7 +103,7 @@ public void clear() {
* @return a copy of the items to be processed as an unmodifiable list
*/
public List getItems() {
- return List.copyOf(items);
+ return Collections.unmodifiableList(items);
}
/**
@@ -154,6 +154,13 @@ public int size() {
/**
* Flag to indicate if the source data is exhausted.
+ *
+ *
+ * Note: This may return false if the last chunk has the same number of items as the
+ * configured commit interval. Consequently, in such cases,there will be a last empty
+ * chunk that won't be processed. It is recommended to consider this behavior when
+ * utilizing this method.
+ *
* @return true if there is no more data to process
*/
public boolean isEnd() {
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ItemStreamSupport.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ItemStreamSupport.java
index 6d72ffa4ac..86446bc0ca 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ItemStreamSupport.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ItemStreamSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -29,39 +29,6 @@ public abstract class ItemStreamSupport implements ItemStream {
private final ExecutionContextUserSupport executionContextUserSupport = new ExecutionContextUserSupport();
- /**
- * No-op.
- * @see org.springframework.batch.item.ItemStream#close()
- * @deprecated since 5.0 in favor of {@link ItemStream#close()}. Scheduled for removal
- * in 5.2.
- */
- @Deprecated(since = "5.0", forRemoval = true)
- @Override
- public void close() {
- }
-
- /**
- * No-op.
- * @see org.springframework.batch.item.ItemStream#open(ExecutionContext)
- * @deprecated since 5.0 in favor of {@link ItemStream#open(ExecutionContext)} ()}.
- * Scheduled for removal in 5.2.
- */
- @Override
- @Deprecated(since = "5.0", forRemoval = true)
- public void open(ExecutionContext executionContext) {
- }
-
- /**
- * Return empty {@link ExecutionContext}.
- * @see org.springframework.batch.item.ItemStream#update(ExecutionContext)
- * @deprecated since 5.0 in favor of {@link ItemStream#update(ExecutionContext)} ()}.
- * Scheduled for removal in 5.2.
- */
- @Override
- @Deprecated(since = "5.0", forRemoval = true)
- public void update(ExecutionContext executionContext) {
- }
-
/**
* The name of the component which will be used as a stem for keys in the
* {@link ExecutionContext}. Subclasses should provide a default value, e.g. the short
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java
index c7982e506d..043e54b7ba 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/AbstractPaginatedDataItemReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2023 the original author or authors.
+ * Copyright 2013-2024 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.
@@ -103,6 +103,14 @@ protected void doOpen() throws Exception {
@Override
protected void doClose() throws Exception {
+ this.lock.lock();
+ try {
+ this.page = 0;
+ this.results = null;
+ }
+ finally {
+ this.lock.unlock();
+ }
}
@Override
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
index 9ea598c5d4..b7aa27f375 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -34,7 +34,6 @@
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
-import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -188,7 +187,7 @@ public void write(Chunk extends T> chunk) throws Exception {
* @param chunk the chunk of items to be persisted.
*/
protected void doWrite(Chunk extends T> chunk) {
- if (!CollectionUtils.isEmpty(chunk.getItems())) {
+ if (!chunk.isEmpty()) {
switch (this.mode) {
case INSERT -> insert(chunk);
case REMOVE -> remove(chunk);
@@ -263,7 +262,7 @@ private Chunk getCurrentBuffer() {
public void beforeCommit(boolean readOnly) {
Chunk chunk = (Chunk) TransactionSynchronizationManager.getResource(bufferKey);
- if (!CollectionUtils.isEmpty(chunk.getItems())) {
+ if (!chunk.isEmpty()) {
if (!readOnly) {
doWrite(chunk);
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoPagingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoPagingItemReader.java
index 442d6956e2..5c2278cacc 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoPagingItemReader.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoPagingItemReader.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2012-2023 the original author or authors.
+ * Copyright 2012-2024 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.
@@ -15,8 +15,14 @@
*/
package org.springframework.batch.item.data;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.util.ClassUtils;
@@ -70,4 +76,69 @@ public MongoPagingItemReader() {
setName(ClassUtils.getShortName(MongoPagingItemReader.class));
}
+ @Override
+ public void setTemplate(MongoOperations template) {
+ super.setTemplate(template);
+ }
+
+ @Override
+ public void setQuery(Query query) {
+ super.setQuery(query);
+ }
+
+ @Override
+ public void setQuery(String queryString) {
+ super.setQuery(queryString);
+ }
+
+ @Override
+ public void setTargetType(Class extends T> type) {
+ super.setTargetType(type);
+ }
+
+ @Override
+ public void setParameterValues(List