diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/continuous-inspection.yml b/.github/workflows/continuous-inspection.yml deleted file mode 100644 index f496eba5c9..0000000000 --- a/.github/workflows/continuous-inspection.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Continuous inspection build - -on: - schedule: - - cron: '0 10 * * *' # Once per day at 10am UTC - workflow_dispatch: - -jobs: - code-quality-analysis: - name: code quality analysis report - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: 'maven' - - - name: Analyse test coverage with Jacoco - run: mvn -P test-coverage verify - - - name: Analyse code quality with Sonar - if: github.repository == 'spring-projects/spring-batch' - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_URL }} - run: mvn sonar:sonar -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.login=$SONAR_TOKEN - diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ef81b3d739..37dc5a6925 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,6 +1,7 @@ name: CI/CD build on: + workflow_dispatch: push: branches: [ "main" ] @@ -53,5 +54,5 @@ jobs: working-directory: spring-batch-docs/target run: | unzip spring-batch-$PROJECT_VERSION-javadocs.zip - ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST cd $DOCS_PATH && mkdir -p $PROJECT_VERSION/api - scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION/api + ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST "cd $DOCS_PATH && mkdir -p $PROJECT_VERSION" + scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$PROJECT_VERSION diff --git a/.github/workflows/documentation-upload.yml b/.github/workflows/documentation-upload.yml index 7f72b7bf1a..bf3f725cd7 100644 --- a/.github/workflows/documentation-upload.yml +++ b/.github/workflows/documentation-upload.yml @@ -55,8 +55,8 @@ jobs: working-directory: spring-batch-docs/target run: | unzip spring-batch-$RELEASE_VERSION-javadocs.zip - ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST cd $DOCS_PATH && mkdir -p $RELEASE_VERSION/api - scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$RELEASE_VERSION/api + ssh -i $HOME/.ssh/key $DOCS_USERNAME@$DOCS_HOST "cd $DOCS_PATH && mkdir -p $RELEASE_VERSION" + scp -i $HOME/.ssh/key -r api $DOCS_USERNAME@$DOCS_HOST:$DOCS_PATH/$RELEASE_VERSION unzip spring-batch-$RELEASE_VERSION-schemas.zip scp -i $HOME/.ssh/key batch/*.xsd $DOCS_USERNAME@$DOCS_HOST:$BATCH_SCHEMA_PATH diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 779b711d58..c6ad7d3a70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,9 +26,11 @@ about how to report issues. Not sure what a *pull request* is, or how to submit one? Take a look at the excellent [GitHub help documentation][] first. Please create a new issue *before* submitting a pull request unless the change is truly trivial, e.g. typo fixes, removing compiler warnings, etc. -### Sign the contributor license agreement +### Sign-off commits according to the Developer Certificate of Origin -If you have not previously done so, please fill out and submit the [Contributor License Agreement](https://cla.pivotal.io/sign/spring). +All commits must include a Signed-off-by trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin](https://developercertificate.org). + +For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). ### Fork the Repository diff --git a/README.md b/README.md index 3dd6c56f63..5cc775db30 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +# Latest news + +* December 18, 2024: [Spring Batch 5.1.3 and 5.2.1 available now](https://spring.io/blog/2024/12/18/spring-batch-5-1-3-and-5-2-1-available-now) +* November 24, 2024: [Bootiful Spring Boot 3.4: Spring Batch](https://spring.io/blog/2024/11/24/bootiful-34-batch) +* November 20, 2024: [Spring Batch 5.2.0 goes GA!](https://spring.io/blog/2024/11/20/spring-batch-5-2-0-goes-ga) + # Spring Batch [![build status](https://github.com/spring-projects/spring-batch/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/spring-projects/spring-batch/actions/workflows/continuous-integration.yml) @@ -222,4 +228,4 @@ Please see our [code of conduct](https://github.com/spring-projects/.github/blob # License -Spring Batch is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). \ No newline at end of file +Spring Batch is Open Source software released under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). diff --git a/pom.xml b/pom.xml index 47fa59050f..348c8d6771 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ designed to enable the development of robust batch applications vital for the daily operations of enterprise systems. Spring Batch is part of the Spring Portfolio. - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT pom https://projects.spring.io/spring-batch @@ -61,98 +61,117 @@ 17 - 6.1.4-SNAPSHOT - 2.0.5-SNAPSHOT - 6.2.2-SNAPSHOT - 1.12.3-SNAPSHOT + 6.2.1 + 2.0.11 + 6.4.1 + 1.14.2 - 3.2.3-SNAPSHOT - 3.2.3-SNAPSHOT - 3.2.3-SNAPSHOT - 4.2.3-SNAPSHOT - 3.1.2-SNAPSHOT - 3.1.2-SNAPSHOT - 3.2.2-SNAPSHOT + 3.4.1 + 3.4.1 + 3.4.1 + 4.4.1 + 3.3.1 + 3.2.1 + 3.2.9 - 2.15.3 - 1.11.3 - 2.10.1 - 6.3.1.Final - 2.1.1 - 2.1.2 + 2.18.2 + 1.12.0 + 2.11.0 + 6.6.3.Final + 3.0.0 + 2.1.3 3.1.0 - 3.0.2 + 3.1.0 3.1.0 - 4.0.8 - 4.11.1 - 5.10.1 + 4.0.13 + 5.2.1 + 5.11.4 3.0.2 - 1.2.3-SNAPSHOT + 1.4.1 1.4.20 4.13.2 ${junit-jupiter.version} - 2.2 - 3.24.2 - 5.7.0 - 2.9.1 - 2.15.0 - 2.11.0 - 2.0.9 - 2.7.2 - 2.2.224 - 3.44.0.0 - 10.16.1.1 - 2.18.13 - 2.31.2 - 4.0.4 - 2.22.0 - 8.0.1.Final - 5.0.1 + 3.0 + 3.26.3 + 5.14.2 + 2.10.0 + 2.18.0 + 2.13.0 + 2.0.16 + 2.7.4 + 2.3.232 + 3.47.1.0 + 10.16.1.1 + 2.21.11 + 2.38.0 + 4.0.5 + 2.24.3 + 8.0.2.Final + 6.0.1 4.0.2 2.0.1 - 4.0.1 - 2.0.2 - 6.5.1 - 1.9.20.1 - 8.2.0 - 3.3.0 - 42.7.0 - 11.5.8.0 - 19.21.0.0 + 4.0.2 + 2.0.3 + 7.1.0 + 1.9.22.1 + 9.1.0 + 3.5.1 + 42.7.4 + 11.5.9.0 + 19.24.0.0 11.2.3.jre17 1.3.1 - 1.19.3 - 1.5.1 + 1.20.4 + 1.5.3 ${spring-amqp.version} 2.3.2 0.16.0 - 3.0.19 + 3.0.22 0.0.4 - 3.11.0 - 3.1.2 - 3.1.2 - 3.6.0 - 3.3.0 - 0.8.10 - 1.5.0 - 3.1.1 - 3.6.0 - 3.3.0 + 3.13.0 + 3.5.0 + 3.5.0 + 3.10.0 + 3.3.1 + 1.6.0 + 3.1.3 + 3.7.1 + 3.4.2 0.0.39 + + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + @@ -305,32 +324,6 @@ - - test-coverage - - - - org.jacoco - jacoco-maven-plugin - ${jacoco-maven-plugin.version} - - - prepare-agent - - prepare-agent - - - - report - - report - - - - - - - artifactory-staging diff --git a/spring-batch-bom/pom.xml b/spring-batch-bom/pom.xml index 7ad96c8a70..a9680681b6 100644 --- a/spring-batch-bom/pom.xml +++ b/spring-batch-bom/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-bom pom diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index 3de563e6e4..5a4187bb5d 100644 --- a/spring-batch-core/pom.xml +++ b/spring-batch-core/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-core jar @@ -96,6 +96,51 @@ ${aspectj.version} true + + org.springframework.data + spring-data-mongodb + ${spring-data-mongodb.version} + true + + + org.slf4j + slf4j-api + + + org.mongodb + mongodb-driver-core + + + org.mongodb + mongodb-driver-sync + + + org.springframework + spring-expression + + + org.springframework.data + spring-data-commons + + + + + org.springframework.data + spring-data-commons + ${spring-data-commons.version} + + + org.mongodb + mongodb-driver-core + ${mongodb-driver.version} + true + + + org.mongodb + mongodb-driver-sync + ${mongodb-driver.version} + true + @@ -128,6 +173,12 @@ ${testcontainers.version} test + + org.testcontainers + mongodb + ${testcontainers.version} + test + org.mariadb.jdbc mariadb-java-client @@ -199,6 +250,12 @@ sqlite-jdbc ${sqlite.version} test + + + org.slf4j + slf4j-api + + com.h2database diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java index d7339459cd..951410235b 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/ChunkListener.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. @@ -20,11 +20,15 @@ /** * Listener interface for the lifecycle of a chunk. A chunk can be thought of as a * collection of items that are committed together. + *

+ * {@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... 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 { + + String trail = ""; + + @Override + public void beforeWrite(Chunk chunk) { + trail = trail + "2"; + } + + @Override + public void afterWrite(Chunk items) { + trail = trail + "3"; + } + + } + class CountingChunkListener implements ChunkListener { + + int beforeCount = 0; + + int afterCount = 0; + + int failedCount = 0; + + private final AssertingWriteListener writeListener; + + public CountingChunkListener(AssertingWriteListener writeListener) { + super(); + this.writeListener = writeListener; + } + + @Override + public void afterChunk(ChunkContext context) { + writeListener.trail = writeListener.trail + "4"; + afterCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + @Override + public void beforeChunk(ChunkContext context) { + writeListener.trail = writeListener.trail + "1"; + beforeCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + @Override + public void afterChunkError(ChunkContext context) { + writeListener.trail = writeListener.trail + "5"; + failedCount++; + throw new RuntimeException("Step will be terminated when ChunkListener throws exceptions."); + } + + } + AssertingWriteListener writeListener = new AssertingWriteListener(); + CountingChunkListener chunkListener = new CountingChunkListener(writeListener); + factory.setListeners(new StepListener[] { chunkListener, writeListener }); + factory.setCommitInterval(commitInterval); + + AbstractStep step = (AbstractStep) factory.getObject(); + + job.setSteps(Collections.singletonList((Step) step)); + + JobExecution jobExecution = repository.createJobExecution(job.getName(), new JobParameters()); + job.execute(jobExecution); + + assertEquals(BatchStatus.FAILED, jobExecution.getStatus()); + assertEquals("1", reader.read()); + assertEquals(0, written.size()); + + assertEquals(0, chunkListener.afterCount); + assertEquals(1, chunkListener.beforeCount); + assertEquals(1, chunkListener.failedCount); + assertEquals("15", writeListener.trail); + assertTrue(writeListener.trail.startsWith("15"), "Listener order not as expected: " + writeListener.trail); + } + /* * Commit interval specified is not allowed to be zero or negative. */ diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapperTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapperTests.java index a6df66f121..bbd253f425 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapperTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SimpleSystemProcessExitCodeMapperTests.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. @@ -28,7 +28,7 @@ class SimpleSystemProcessExitCodeMapperTests { private final SimpleSystemProcessExitCodeMapper mapper = new SimpleSystemProcessExitCodeMapper(); /** - * 0 -> ExitStatus.FINISHED else -> ExitStatus.FAILED + * 0 -> ExitStatus.COMPLETED else -> ExitStatus.FAILED */ @Test void testMapping() { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java index 006d9ed877..b703d91ee3 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/SystemCommandTaskletIntegrationTests.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. @@ -323,9 +323,8 @@ public void testExecuteWithFailedCommandRunnerMockExecution() throws Exception { tasklet.setCommand(command); tasklet.afterPropertiesSet(); - RepeatStatus exitStatus = tasklet.execute(stepContribution, null); - - assertEquals(RepeatStatus.FINISHED, exitStatus); + Exception exception = assertThrows(SystemCommandException.class, () -> tasklet.execute(stepContribution, null)); + assertTrue(exception.getMessage().contains("failed with exit code")); assertEquals(ExitStatus.FAILED, stepContribution.getExitStatus()); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/MySQLJdbcJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/MySQLJdbcJobRepositoryIntegrationTests.java index 8936d89023..67009a0465 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/MySQLJdbcJobRepositoryIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/MySQLJdbcJobRepositoryIntegrationTests.java @@ -34,14 +34,10 @@ import org.springframework.batch.core.JobExecution; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersBuilder; -import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; -import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor; -import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.launch.JobLauncher; import org.springframework.batch.core.launch.JobOperator; -import org.springframework.batch.core.launch.support.SimpleJobOperator; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.beans.factory.annotation.Autowired; @@ -151,24 +147,6 @@ public Job job(JobRepository jobRepository, PlatformTransactionManager transacti .build(); } - @Bean - public JobOperator jobOperator(JobLauncher jobLauncher, JobRegistry jobRegistry, JobExplorer jobExplorer, - JobRepository jobRepository) { - SimpleJobOperator jobOperator = new SimpleJobOperator(); - jobOperator.setJobExplorer(jobExplorer); - jobOperator.setJobLauncher(jobLauncher); - jobOperator.setJobRegistry(jobRegistry); - jobOperator.setJobRepository(jobRepository); - return jobOperator; - } - - @Bean - public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) { - JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor(); - jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry); - return jobRegistryBeanPostProcessor; - } - @Bean public ConfigurableConversionService conversionService() { DefaultConversionService conversionService = new DefaultConversionService(); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/SQLServerJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/SQLServerJobRepositoryIntegrationTests.java index 304c7abc66..b8373688f1 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/SQLServerJobRepositoryIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/SQLServerJobRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -58,7 +58,7 @@ class SQLServerJobRepositoryIntegrationTests { // TODO find the best way to externalize and manage image versions private static final DockerImageName SQLSERVER_IMAGE = DockerImageName - .parse("mcr.microsoft.com/mssql/server:2019-CU11-ubuntu-20.04"); + .parse("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"); @Container public static MSSQLServerContainer sqlserver = new MSSQLServerContainer<>(SQLSERVER_IMAGE).acceptLicense(); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java index 99dbc190a5..67d24fcefa 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 the original author or authors. + * Copyright 2010-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. @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; @@ -55,6 +56,7 @@ * Tests for {@link FaultTolerantStepFactoryBean}. */ @SpringJUnitConfig(locations = "/simple-job-launcher-context.xml") +@Disabled("Randomly failing/hanging") // FIXME This test is randomly failing/hanging class FaultTolerantStepFactoryBeanIntegrationTests { private static final int MAX_COUNT = 1000; diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/test-context-with-smart-initializing-singleton.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/test-context-with-smart-initializing-singleton.xml new file mode 100644 index 0000000000..64ae6eed68 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/test-context-with-smart-initializing-singleton.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests-context.xml index 189d63ce13..97df1bef5e 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartFailedJobParserTests-context.xml @@ -2,7 +2,7 @@ diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests-context.xml index fe7ed075ed..de0face964 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartJobParserTests-context.xml @@ -1,7 +1,7 @@ diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests-context.xml new file mode 100644 index 0000000000..dba05231c4 --- /dev/null +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopAndRestartWithCustomExitCodeJobParserTests-context.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml index fbb7f4c6a0..93b0a1b4ea 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopCustomStatusJobParserTests-context.xml @@ -1,7 +1,7 @@ diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests-context.xml index ca269dec17..080f44a374 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopIncompleteJobParserTests-context.xml @@ -1,7 +1,7 @@ diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopJobParserTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopJobParserTests-context.xml index 0f67bf801d..5be5d43f6b 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopJobParserTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/StopJobParserTests-context.xml @@ -1,7 +1,7 @@ diff --git a/spring-batch-docs/antora-playbook.yml b/spring-batch-docs/antora-playbook.yml index 037f9e80d6..bbf8fac2a9 100644 --- a/spring-batch-docs/antora-playbook.yml +++ b/spring-batch-docs/antora-playbook.yml @@ -4,9 +4,10 @@ antora: extensions: - '@springio/antora-extensions/partial-build-extension' + - '@antora/atlas-extension' + - require: '@springio/antora-extensions/latest-version-extension' - require: '@springio/antora-extensions/inject-collector-cache-config-extension' - '@antora/collector-extension' - - '@antora/atlas-extension' - require: '@springio/antora-extensions/root-component-extension' root_component_name: 'batch' - '@springio/antora-extensions/static-page-extension' @@ -37,5 +38,5 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.7/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip snapshot: true \ No newline at end of file diff --git a/spring-batch-docs/modules/ROOT/pages/appendix.adoc b/spring-batch-docs/modules/ROOT/pages/appendix.adoc index 12de632126..28d4ca344e 100644 --- a/spring-batch-docs/modules/ROOT/pages/appendix.adoc +++ b/spring-batch-docs/modules/ROOT/pages/appendix.adoc @@ -36,13 +36,10 @@ It can be configured to read messages from multiple partitions of the same topic This reader stores message offsets in the execution context to support restart capabilities.|No |`FlatFileItemReader`|Reads from a flat file. Includes `ItemStream` and `Skippable` functionality. See link:readersAndWriters.html#flatFileItemReader["`FlatFileItemReader`"].|No -|`HibernateCursorItemReader`|Reads from a cursor based on an HQL query. See - link:readersAndWriters.html#cursorBasedItemReaders[`Cursor-based ItemReaders`].|No -|`HibernatePagingItemReader`|Reads from a paginated HQL query.|Yes |`ItemReaderAdapter`|Adapts any class to the `ItemReader` interface.|Yes |`JdbcCursorItemReader`|Reads from a database cursor over JDBC. See - link:readersAndWriters.html#cursorBasedItemReaders["`Cursor-based ItemReaders`"].|No + link:readers-and-writers/database.html#cursorBasedItemReaders["`Cursor-based ItemReaders`"].|No |`JdbcPagingItemReader`|Given an SQL statement, pages through the rows, such that large datasets can be read without running out of memory.|Yes @@ -89,10 +86,6 @@ This reader stores message offsets in the execution context to support restart c in an injected `List` of `ItemWriter` objects.|Yes |`FlatFileItemWriter`|Writes to a flat file. Includes `ItemStream` and Skippable functionality. See link:readersAndWriters.html#flatFileItemWriter["`FlatFileItemWriter`"].|No -|`HibernateItemWriter`|This item writer is Hibernate-session aware - and handles some transaction-related work that a non-"`hibernate-aware`" - item writer would not need to know about and then delegates - to another item writer to do the actual writing.|Yes |`ItemWriterAdapter`|Adapts any class to the `ItemWriter` interface.|Yes |`JdbcBatchItemWriter`|Uses batching features from a diff --git a/spring-batch-docs/modules/ROOT/pages/index.adoc b/spring-batch-docs/modules/ROOT/pages/index.adoc index d03485e1d8..fb3d878723 100644 --- a/spring-batch-docs/modules/ROOT/pages/index.adoc +++ b/spring-batch-docs/modules/ROOT/pages/index.adoc @@ -9,7 +9,7 @@ xref:spring-batch-intro.adoc[Spring Batch Introduction] :: Background, usage scenarios, and general guidelines. xref:spring-batch-architecture.adoc[Spring Batch Architecture] :: Spring Batch architecture, general batch principles, batch processing strategies. -xref:whatsnew.adoc[What's new in Spring Batch 5.1] :: New features introduced in version 5.1. +xref:whatsnew.adoc[What's new in Spring Batch 5.2] :: New features introduced in version 5.2. xref:domain.adoc[The Domain Language of Batch] :: Core concepts and abstractions of the Batch domain language. xref:job.adoc[Configuring and Running a Job] :: Job configuration, execution, and diff --git a/spring-batch-docs/modules/ROOT/pages/job/advanced-meta-data.adoc b/spring-batch-docs/modules/ROOT/pages/job/advanced-meta-data.adoc index 94fc236f5c..bfa7ff3d1a 100644 --- a/spring-batch-docs/modules/ROOT/pages/job/advanced-meta-data.adoc +++ b/spring-batch-docs/modules/ROOT/pages/job/advanced-meta-data.adoc @@ -173,9 +173,9 @@ The following example shows how to include a `JobRegistry` for a job defined in ==== -You can populate a `JobRegistry` in either of two ways: by using -a bean post processor or by using a registrar lifecycle component. The coming -sections describe these two mechanisms. +You can populate a `JobRegistry` in one of the following ways: by using +a bean post processor, or by using a smart initializing singleton or by using +a registrar lifecycle component. The coming sections describe these mechanisms. [[jobregistrybeanpostprocessor]] === JobRegistryBeanPostProcessor @@ -222,7 +222,46 @@ example has been given an `id` so that it can be included in child contexts (for example, as a parent bean definition) and cause all jobs created there to also be registered automatically. -As of version 5.1, the `@EnableBatchProcessing` annotation automatically registers a `jobRegistryBeanPostProcessor` bean in the application context. +[WARNING] +.Deprecation +==== +As of version 5.2, the `JobRegistryBeanPostProcessor` class is deprecated in favor of +`JobRegistrySmartInitializingSingleton`, see xref:#jobregistrysmartinitializingsingleton[JobRegistrySmartInitializingSingleton]. +==== + +[[jobregistrysmartinitializingsingleton]] +=== JobRegistrySmartInitializingSingleton + +This is a `SmartInitializingSingleton` that registers all singleton jobs within the job registry. + +[tabs] +==== +Java:: ++ +The following example shows how to define a `JobRegistrySmartInitializingSingleton` in Java: ++ +.Java Configuration +[source, java] +---- +@Bean +public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) { + return new JobRegistrySmartInitializingSingleton(jobRegistry); +} +---- + +XML:: ++ +The following example shows how to define a `JobRegistrySmartInitializingSingleton` in XML: ++ +.XML Configuration +[source, xml] +---- + + + +---- + +==== [[automaticjobregistrar]] === AutomaticJobRegistrar diff --git a/spring-batch-docs/modules/ROOT/pages/job/configuring.adoc b/spring-batch-docs/modules/ROOT/pages/job/configuring.adoc index 57f9bbb48c..c7aaa78828 100644 --- a/spring-batch-docs/modules/ROOT/pages/job/configuring.adoc +++ b/spring-batch-docs/modules/ROOT/pages/job/configuring.adoc @@ -251,7 +251,7 @@ it with its own list of listeners to produce a - + @@ -259,12 +259,12 @@ it with its own list of listeners to produce a - + ---- [role="xmlContent"] -See the section on <> +See the section on xref:step/chunk-oriented-processing/inheriting-from-parent.adoc[Inheriting from a Parent Step] for more detailed information. [[jobparametersvalidator]] diff --git a/spring-batch-docs/modules/ROOT/pages/job/java-config.adoc b/spring-batch-docs/modules/ROOT/pages/job/java-config.adoc index 3cbcb727cc..472650b763 100644 --- a/spring-batch-docs/modules/ROOT/pages/job/java-config.adoc +++ b/spring-batch-docs/modules/ROOT/pages/job/java-config.adoc @@ -42,6 +42,7 @@ public class MyJobConfiguration { return new JdbcTransactionManager(dataSource); } + @Bean public Job job(JobRepository jobRepository) { return new JobBuilder("myJob", jobRepository) //define job flow as needed diff --git a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/database.adoc b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/database.adoc index e7764b55ec..a962357913 100644 --- a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/database.adoc +++ b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/database.adoc @@ -211,89 +211,6 @@ step processing. To use this feature, you need a database that supports this and driver supporting JDBC 3.0 or later. Defaults to `false`. |=============== -[[HibernateCursorItemReader]] -=== `HibernateCursorItemReader` - -Just as normal Spring users make important decisions about whether or not to use ORM -solutions, which affect whether or not they use a `JdbcTemplate` or a -`HibernateTemplate`, Spring Batch users have the same options. -`HibernateCursorItemReader` is the Hibernate implementation of the cursor technique. -Hibernate's usage in batch has been fairly controversial. This has largely been because -Hibernate was originally developed to support online application styles. However, that -does not mean it cannot be used for batch processing. The easiest approach for solving -this problem is to use a `StatelessSession` rather than a standard session. This removes -all of the caching and dirty checking Hibernate employs and that can cause issues in a -batch scenario. For more information on the differences between stateless and normal -hibernate sessions, refer to the documentation of your specific hibernate release. The -`HibernateCursorItemReader` lets you declare an HQL statement and pass in a -`SessionFactory`, which will pass back one item per call to read in the same basic -fashion as the `JdbcCursorItemReader`. The following example configuration uses the same -'customer credit' example as the JDBC reader: - -[source, java] ----- -HibernateCursorItemReader itemReader = new HibernateCursorItemReader(); -itemReader.setQueryString("from CustomerCredit"); -//For simplicity sake, assume sessionFactory already obtained. -itemReader.setSessionFactory(sessionFactory); -itemReader.setUseStatelessSession(true); -int counter = 0; -ExecutionContext executionContext = new ExecutionContext(); -itemReader.open(executionContext); -Object customerCredit = new Object(); -while(customerCredit != null){ - customerCredit = itemReader.read(); - counter++; -} -itemReader.close(); ----- - -This configured `ItemReader` returns `CustomerCredit` objects in the exact same manner -as described by the `JdbcCursorItemReader`, assuming hibernate mapping files have been -created correctly for the `Customer` table. The 'useStatelessSession' property defaults -to true but has been added here to draw attention to the ability to switch it on or off. -It is also worth noting that the fetch size of the underlying cursor can be set with the -`setFetchSize` property. As with `JdbcCursorItemReader`, configuration is -straightforward. - - -[tabs] -==== -Java:: -+ -The following example shows how to inject a Hibernate `ItemReader` in Java: -+ -.Java Configuration -[source, java] ----- -@Bean -public HibernateCursorItemReader itemReader(SessionFactory sessionFactory) { - return new HibernateCursorItemReaderBuilder() - .name("creditReader") - .sessionFactory(sessionFactory) - .queryString("from CustomerCredit") - .build(); -} ----- - -XML:: -+ -The following example shows how to inject a Hibernate `ItemReader` in XML: -+ -.XML Configuration -[source, xml] ----- - - - - ----- - -==== - - - [[StoredProcedureItemReader]] === `StoredProcedureItemReader` diff --git a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/item-reader-writer-implementations.adoc b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/item-reader-writer-implementations.adoc index 4b1a0d31eb..628b2b85b7 100644 --- a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/item-reader-writer-implementations.adoc +++ b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/item-reader-writer-implementations.adoc @@ -163,8 +163,6 @@ Spring Batch offers the following database readers: * xref:readers-and-writers/item-reader-writer-implementations.adoc#Neo4jItemReader[`Neo4jItemReader`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#mongoItemReader[`MongoItemReader`] -* xref:readers-and-writers/item-reader-writer-implementations.adoc#hibernateCursorItemReader[`HibernateCursorItemReader`] -* xref:readers-and-writers/item-reader-writer-implementations.adoc#hibernatePagingItemReader[`HibernatePagingItemReader`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#repositoryItemReader[`RepositoryItemReader`] [[Neo4jItemReader]] @@ -179,22 +177,6 @@ The `MongoItemReader` is an `ItemReader` that reads documents from MongoDB by us paging technique. Spring Batch provides a `MongoItemReaderBuilder` to construct an instance of the `MongoItemReader`. -[[hibernateCursorItemReader]] -=== `HibernateCursorItemReader` -The `HibernateCursorItemReader` is an `ItemStreamReader` for reading database records -built on top of Hibernate. It executes the HQL query and then, when initialized, iterates -over the result set as the `read()` method is called, successively returning an object -corresponding to the current row. Spring Batch provides a -`HibernateCursorItemReaderBuilder` to construct an instance of the -`HibernateCursorItemReader`. - -[[hibernatePagingItemReader]] -=== `HibernatePagingItemReader` -The `HibernatePagingItemReader` is an `ItemReader` for reading database records built on -top of Hibernate and reading only up to a fixed number of items at a time. Spring Batch -provides a `HibernatePagingItemReaderBuilder` to construct an instance of the -`HibernatePagingItemReader`. - [[repositoryItemReader]] === `RepositoryItemReader` The `RepositoryItemReader` is an `ItemReader` that reads records by using a @@ -208,7 +190,6 @@ Spring Batch offers the following database writers: * xref:readers-and-writers/item-reader-writer-implementations.adoc#neo4jItemWriter[`Neo4jItemWriter`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#mongoItemWriter[`MongoItemWriter`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#repositoryItemWriter[`RepositoryItemWriter`] -* xref:readers-and-writers/item-reader-writer-implementations.adoc#hibernateItemWriter[`HibernateItemWriter`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#jdbcBatchItemWriter[`JdbcBatchItemWriter`] * xref:readers-and-writers/item-reader-writer-implementations.adoc#jpaItemWriter[`JpaItemWriter`] @@ -230,12 +211,6 @@ The `RepositoryItemWriter` is an `ItemWriter` wrapper for a `CrudRepository` fro Data. Spring Batch provides a `RepositoryItemWriterBuilder` to construct an instance of the `RepositoryItemWriter`. -[[hibernateItemWriter]] -=== `HibernateItemWriter` -The `HibernateItemWriter` is an `ItemWriter` that uses a Hibernate session to save or -update entities that are not part of the current Hibernate session. Spring Batch provides -a `HibernateItemWriterBuilder` to construct an instance of the `HibernateItemWriter`. - [[jdbcBatchItemWriter]] === `JdbcBatchItemWriter` The `JdbcBatchItemWriter` is an `ItemWriter` that uses the batching features from diff --git a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/multi-file-input.adoc b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/multi-file-input.adoc index 08307e720d..cf81b7a417 100644 --- a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/multi-file-input.adoc +++ b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/multi-file-input.adoc @@ -24,10 +24,10 @@ The following example shows how to read files with wildcards in Java: [source, java] ---- @Bean -public MultiResourceItemReader multiResourceReader() { +public MultiResourceItemReader multiResourceReader(@Value("classpath:data/input/file-*.txt") Resource[] resources) { return new MultiResourceItemReaderBuilder() .delegate(flatFileItemReader()) - .resources(resources()) + .resources(resources) .build(); } ---- diff --git a/spring-batch-docs/modules/ROOT/pages/repeat.adoc b/spring-batch-docs/modules/ROOT/pages/repeat.adoc index 4836d338b2..7836d11043 100644 --- a/spring-batch-docs/modules/ROOT/pages/repeat.adoc +++ b/spring-batch-docs/modules/ROOT/pages/repeat.adoc @@ -207,7 +207,7 @@ Java:: The following example uses Java configuration to repeat a service call to a method called `processMessage` (for more detail on how to configure AOP interceptors, see the -<>): +https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop[Spring User Guide]): + [source, java] ---- @@ -234,7 +234,7 @@ XML:: The following example shows declarative iteration that uses the Spring AOP namespace to repeat a service call to a method called `processMessage` (for more detail on how to configure AOP interceptors, see the -<>): +https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop[Spring User Guide]): + [source, xml] ---- diff --git a/spring-batch-docs/modules/ROOT/pages/scalability.adoc b/spring-batch-docs/modules/ROOT/pages/scalability.adoc index 5836fddf56..b00353c4e6 100644 --- a/spring-batch-docs/modules/ROOT/pages/scalability.adoc +++ b/spring-batch-docs/modules/ROOT/pages/scalability.adoc @@ -346,8 +346,8 @@ configuration: [source, java] ---- @Bean -public Step step1Manager() { - return stepBuilderFactory.get("step1.manager") +public Step step1Manager(JobRepository jobRepository) { + return new StepBuilder("step1.manager", jobRepository) .partitioner("step1", partitioner()) .step(step1()) .gridSize(10) diff --git a/spring-batch-docs/modules/ROOT/pages/spring-batch-architecture.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-architecture.adoc index 75e5ab926f..ea0d35f7c9 100644 --- a/spring-batch-docs/modules/ROOT/pages/spring-batch-architecture.adoc +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-architecture.adoc @@ -252,7 +252,7 @@ advisable). The following image illustrates the partitioning approach: image::partitioned.png[Figure 1.2: Partitioned Process, scaledwidth="60%"] The architecture should be flexible enough to allow dynamic configuration of the number -of partitions. You shoul consider both automatic and user controlled configuration. +of partitions. You should consider both automatic and user controlled configuration. Automatic configuration may be based on such parameters as the input file size and the number of input records. diff --git a/spring-batch-docs/modules/ROOT/pages/spring-batch-integration.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-integration.adoc index 45a9fe3cd8..e47243c999 100644 --- a/spring-batch-docs/modules/ROOT/pages/spring-batch-integration.adoc +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-integration.adoc @@ -32,11 +32,8 @@ provide methods to distribute workloads over a number of workers. This section covers the following key concepts: [role="xmlContent"] -* <> +* xref:spring-batch-integration/namespace-support.adoc[Namespace Support] * xref:spring-batch-integration/launching-jobs-through-messages.adoc[Launching Batch Jobs through Messages] * xref:spring-batch-integration/sub-elements.adoc#providing-feedback-with-informational-messages[Providing Feedback with Informational Messages] * xref:spring-batch-integration/sub-elements.adoc#asynchronous-processors[Asynchronous Processors] -* xref:spring-batch-integration/sub-elements.adoc#externalizing-batch-process-execution[Externalizing Batch Process Execution] - -[[namespace-support]] -[role="xmlContent"] +* xref:spring-batch-integration/sub-elements.adoc#externalizing-batch-process-execution[Externalizing Batch Process Execution] \ No newline at end of file diff --git a/spring-batch-docs/modules/ROOT/pages/spring-batch-integration/sub-elements.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-integration/sub-elements.adoc index eac14f4e7c..205a8669e2 100644 --- a/spring-batch-docs/modules/ROOT/pages/spring-batch-integration/sub-elements.adoc +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-integration/sub-elements.adoc @@ -182,7 +182,7 @@ The following example shows the how to add a step-level listener in XML: Asynchronous Processors help you scale the processing of items. In the asynchronous processor use case, an `AsyncItemProcessor` serves as a dispatcher, executing the logic of the `ItemProcessor` for an item on a new thread. Once the item completes, the `Future` is -passed to the `AsynchItemWriter` to be written. +passed to the `AsyncItemWriter` to be written. Therefore, you can increase performance by using asynchronous item processing, basically letting you implement fork-join scenarios. The `AsyncItemWriter` gathers the results and diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc index 16a08cb719..5c5136c825 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc @@ -31,6 +31,8 @@ public Step step1(JobRepository jobRepository, PlatformTransactionManager transa .build(); } ---- ++ +Note: The `skipLimit` can be explicitly set using the `skipLimit()` method. If not specified, the default skip limit is set to 10. XML:: + @@ -91,6 +93,8 @@ public Step step1(JobRepository jobRepository, PlatformTransactionManager transa .build(); } ---- ++ +Note: The `skipLimit` can be explicitly set using the `skipLimit()` method. If not specified, the default skip limit is set to 10. XML:: + diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/inheriting-from-parent.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/inheriting-from-parent.adoc index 00b59bf54a..fd56acbfb9 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/inheriting-from-parent.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/inheriting-from-parent.adoc @@ -93,7 +93,7 @@ In the following example, the `Step` "concreteStep3", is created with two listen - + @@ -102,7 +102,7 @@ In the following example, the `Step` "concreteStep3", is created with two listen - + ---- diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc index bdb7f57b61..023dcaee4f 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc @@ -130,6 +130,9 @@ You can apply a `ChunkListener` when there is no chunk declaration. The `Tasklet responsible for calling the `ChunkListener`, so it applies to a non-item-oriented tasklet as well (it is called before and after the tasklet). +A `ChunkListener` is not designed to throw checked exceptions. Errors must be handled in the +implementation or the step will terminate. + [[itemReadListener]] == `ItemReadListener` @@ -207,9 +210,10 @@ public interface ItemWriteListener extends StepListener { ---- The `beforeWrite` method is called before `write` on the `ItemWriter` and is handed the -list of items that is written. The `afterWrite` method is called after the item has been -successfully written. If there was an error while writing, the `onWriteError` method is -called. The exception encountered and the item that was attempted to be written are +list of items that is written. The `afterWrite` method is called after the items have been +successfully written, but before committing the transaction associated with the chunk's processing. +If there was an error while writing, the `onWriteError` method is called. +The exception encountered and the item that was attempted to be written are provided, so that they can be logged. The annotations corresponding to this interface are: diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc index 8f4af6f71e..20e80bd72d 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc @@ -9,7 +9,7 @@ require some specific configuration. == Setting a Start Limit There are many scenarios where you may want to control the number of times a `Step` can -be started. For example, you might need to configure a particular `Step` might so that it +be started. For example, you might need to configure a particular `Step` so that it runs only once because it invalidates some resource that must be fixed manually before it can be run again. This is configurable on the step level, since different steps may have different requirements. A `Step` that can be executed only once can exist as part of the diff --git a/spring-batch-docs/modules/ROOT/pages/step/controlling-flow.adoc b/spring-batch-docs/modules/ROOT/pages/step/controlling-flow.adoc index 7d3e70ab23..03670bc31b 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/controlling-flow.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/controlling-flow.adoc @@ -294,14 +294,14 @@ the condition of the execution having skipped records, as the following example [source, java] ---- -public class SkipCheckingListener extends StepExecutionListenerSupport { +public class SkipCheckingListener implements StepExecutionListener { + @Override public ExitStatus afterStep(StepExecution stepExecution) { String exitCode = stepExecution.getExitStatus().getExitCode(); if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && - stepExecution.getSkipCount() > 0) { + stepExecution.getSkipCount() > 0) { return new ExitStatus("COMPLETED WITH SKIPS"); - } - else { + } else { return null; } } diff --git a/spring-batch-docs/modules/ROOT/pages/step/late-binding.adoc b/spring-batch-docs/modules/ROOT/pages/step/late-binding.adoc index ceb0d390aa..879464ef21 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/late-binding.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/late-binding.adoc @@ -201,8 +201,8 @@ The following example shows how to access the `ExecutionContext` in XML: NOTE: Any bean that uses late binding must be declared with `scope="step"`. See xref:step/late-binding.adoc#step-scope[Step Scope] for more information. -A `Step` bean should not be step-scoped. If late binding is needed in a step -definition, the components of that step (tasklet, item reader or writer, and so on) +A `Step` bean should not be step-scoped or job-scoped. If late binding is needed in a step +definition, then the components of that step (tasklet, item reade/writer, completion policy, and so on) are the ones that should be scoped instead. NOTE: If you use Spring 3.0 (or above), the expressions in step-scoped beans are in the diff --git a/spring-batch-docs/modules/ROOT/pages/step/tasklet.adoc b/spring-batch-docs/modules/ROOT/pages/step/tasklet.adoc index 2613e34878..7ad23b8dae 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/tasklet.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/tasklet.adoc @@ -122,7 +122,7 @@ public class FileDeletingTasklet implements Tasklet, InitializingBean { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { File dir = directory.getFile(); - Assert.state(dir.isDirectory()); + Assert.state(dir.isDirectory(), "The resource must be a directory"); File[] files = dir.listFiles(); for (int i = 0; i < files.length; i++) { @@ -140,7 +140,7 @@ public class FileDeletingTasklet implements Tasklet, InitializingBean { } public void afterPropertiesSet() throws Exception { - Assert.state(directory != null, "directory must be set"); + Assert.state(directory != null, "Directory must be set"); } } ---- diff --git a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc index b150c6f777..22635de973 100644 --- a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc +++ b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc @@ -1,179 +1,175 @@ [[whatsNew]] -= What's New in Spring Batch 5.1 += What's new in Spring Batch 5.2 -This section shows the major highlights of Spring Batch 5.1. For the complete list of changes, please refer to the https://github.com/spring-projects/spring-batch/releases[release notes]. +This section highlights the major changes in Spring Batch 5.2. For the complete list of changes, please refer to the https://github.com/spring-projects/spring-batch/releases[release notes]. -Spring Batch 5.1 includes the following features: +Spring Batch 5.2 includes the following features: * xref:whatsnew.adoc#dependencies-upgrade[Dependencies upgrade] -* xref:whatsnew.adoc#virtual-threads-support[Virtual Threads support] -* xref:whatsnew.adoc#memory-management-improvement-jpaitemwriter[Memory management improvement in the JpaItemWriter] -* xref:whatsnew.adoc#new-synchronized-decorators[New synchronized decorators for item readers and writers] -* xref:whatsnew.adoc#new-cursor-based-mongo-item-reader[New Cursor-based MongoItemReader] -* xref:whatsnew.adoc#bulk-inserts-support-mongo-item-writer[Bulk inserts support in MongoItemWriter] -* xref:whatsnew.adoc#new-item-reader-and-writer-for-redis[New item reader and writer for Redis] -* xref:whatsnew.adoc#automatic-configuration-of-jobregistrybeanpostprocessor[Automatic configuration of JobRegistryBeanPostProcessor] -* xref:whatsnew.adoc#ability-to-start-a-job-flow-with-a-decision[Ability to start a job flow with a decision] -* xref:whatsnew.adoc#ability-to-provide-a-custom-jobkeygenerator[Ability to provide a custom JobKeyGenerator] -* xref:whatsnew.adoc#new-documentation-based-on-antora[New documentation based on Antora] -* xref:whatsnew.adoc#improved-getting-started-experience[Improved Getting Started experience] - -Moreover, this release introduces the following experimental features: - -* xref:whatsnew.adoc#mongodb-job-repository[MongoDB Job Repository] -* xref:whatsnew.adoc#composite-item-reader[Composite Item Reader] -* xref:whatsnew.adoc#new-chunk-oriented-step-implementation[New Chunk-Oriented Step Implementation] +* xref:whatsnew.adoc#mongodb-job-repository-support[MongoDB job repository support] +* xref:whatsnew.adoc#new-resourceless-job-repository[New resourceless job repository] +* xref:whatsnew.adoc#composite-item-reader-implementation[Composite Item Reader implementation] +* xref:whatsnew.adoc#new-adapters-for-java-util-function-apis[New adapters for java.util.function APIs] +* xref:whatsnew.adoc#concurrent-steps-with-blocking-queue-item-reader-and-writer[Concurrent steps with blocking queue item reader and writer] +* xref:whatsnew.adoc#query-hints-support[Query hints support in JPA item readers] +* xref:whatsnew.adoc#data-class-support[Data class support in JDBC item readers] +* xref:whatsnew.adoc#configurable-line-separator-in-recursivecollectionlineaggregator[Configurable line separator in RecursiveCollectionLineAggregator] +* xref:whatsnew.adoc#job-registration-improvements[Job registration improvements] [[dependencies-upgrade]] == Dependencies upgrade In this release, the Spring dependencies are upgraded to the following versions: -* Spring Framework 6.1.0 -* Spring Integration 6.2.0 -* Spring Data 3.2.0 -* Spring LDAP 3.2.0 -* Spring AMQP 3.1.0 -* Spring Kafka 3.1.0 -* Micrometer 1.12.0 +* Spring Framework 6.2.0 +* Spring Integration 6.4.0 +* Spring Data 3.4.0 +* Spring Retry 2.0.10 +* Spring LDAP 3.2.8 +* Spring AMQP 3.2.0 +* Spring Kafka 3.3.0 +* Micrometer 1.14.1 + +[[mongodb-job-repository-support]] +== MongoDB job repository support + +This release introduces the first NoSQL job repository implementation which is backed by MongoDB. +Similar to relational job repository implementations, Spring Batch comes with a script to create the +necessary collections in MongoDB in order to save and retrieve batch meta-data. + +This implementation requires MongoDB version 4 or later and is based on Spring Data MongoDB. +In order to use this job repository, all you need to do is define a `MongoTemplate` and a +`MongoTransactionManager` which are required by the newly added `MongoJobRepositoryFactoryBean`: + +``` +@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(); +} +``` + +Once the MongoDB job repository defined, you can inject it in any job or step as a regular job repository. +You can find a complete example in the https://github.com/spring-projects/spring-batch/blob/main/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java[MongoDBJobRepositoryIntegrationTests]. + +[[new-resourceless-job-repository]] +== New resourceless job repository + +In v5, the in-memory Map-based job repository implementation was removed for several reasons. +The only job repository implementation that was left in Spring Batch was the JDBC implementation, which requires a data source. +While this works well with in-memory databases like H2 or HSQLDB, requiring a data source was a strong constraint +for many users of our community who used to use the Map-based repository without any additional dependency. + +In this release, we introduce a `JobRepository` implementation that does not use or store batch meta-data in any form +(not even in-memory). It is a "NoOp" implementation that throws away batch meta-data and does not interact with any resource +(hence the name "resourceless job repository", which is named after the "resourceless transaction manager"). + +This implementation 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 is suitable for one-time jobs executed in their own JVM. It works with transactional steps (configured with +a `DataSourceTransactionManager` for instance) as well as non-transactional steps (configured with a `ResourcelessTransactionManager`). +The implementation is not thread-safe and should not be used in any concurrent environment. + +[[composite-item-reader-implementation]] +== Composite Item Reader implementation + +Similar to the `CompositeItemProcessor` and `CompositeItemWriter`, we introduce a new `CompositeItemReader` implementation +that is designed to read data sequentially from several sources having the same format. This is useful when data is spread +over different resources and writing a custom reader is not an option. + +A `CompositeItemReader` works like other composite artifacts, by delegating the reading operation to regular item readers +in order. Here is a quick example showing a composite reader that reads persons data from a flat file then from a database table: + +``` +@Bean +public FlatFileItemReader itemReader1() { + return new FlatFileItemReaderBuilder() + .name("personFileItemReader") + .resource(new FileSystemResource("persons.csv")) + .delimited() + .names("id", "name") + .targetType(Person.class) + .build(); +} + +@Bean +public JdbcCursorItemReader itemReader2() { + String sql = "select * from persons"; + return new JdbcCursorItemReaderBuilder() + .name("personTableItemReader") + .dataSource(dataSource()) + .sql(sql) + .beanRowMapper(Person.class) + .build(); +} + +@Bean +public CompositeItemReader itemReader() { + return new CompositeItemReader<>(Arrays.asList(itemReader1(), itemReader2())); +} +``` + +[[new-adapters-for-java-util-function-apis]] +== New adapters for java.util.function APIs + +Similar to `FucntionItemProcessor` that adapts a `java.util.function.Function` to an item processor, this release +introduces several new adapters for other `java.util.function` interfaces like `Supplier`, `Consumer` and `Predicate`. + +The newly added adapters are: `SupplierItemReader`, `ConsumerItemWriter` and `PredicateFilteringItemProcessor`. +For more details about these new adapters, please refer to the https://github.com/spring-projects/spring-batch/tree/main/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function[org.springframework.batch.item.function] package. + +[[concurrent-steps-with-blocking-queue-item-reader-and-writer]] +== Concurrent steps with blocking queue item reader and writer + +The https://en.wikipedia.org/wiki/Staged_event-driven_architecture[staged event-driven architecture] (SEDA) is a +powerful architecture style to process data in stages connected by queues. This style is directly applicable to data +pipelines and easily implemented in Spring Batch thanks to the ability to design jobs as a sequence of steps. + +The only missing piece here is how to read data from and write data to intermediate queues. This release introduces an item reader +and item writer to read data from and write it to a `BlockingQueue`. With these two new classes, one can design a first step +that prepares data in a queue and a second step that consumes data from the same queue. This way, both steps can run concurrently +to process data efficiently in a non-blocking, event-driven fashion. + +[[query-hints-support]] +== Query hints support in JPA item readers + +Up until version 5.1, the JPA cursor and paging item readers did not support query hints (like the fetch size, timeout, etc). +Users were required to provide a custom query provider in order to specify custom hints. + +In this release, JPA readers and their respective builders were updated to accept query hints when defining the JPA query to use. + +[[data-class-support]] +== Data class support in JDBC item readers + +This release introduces a new method in the builders of JDBC cursor and paging item readers that allows users to specify a +`DataClassRowMapper` when the type of items is a data class (Java record or Kotlin data class). + +The new method named `dataRowMapper(TargetType.class)` is similar to the `beanRowMapper(TargetType.class)` and is designed +to make the configuration of row mappers consistent between regular classes (Java beans) and data classes (Java records). -[[virtual-threads-support]] -== Virtual Threads support +[[configurable-line-separator-in-recursivecollectionlineaggregator]] +== Configurable line separator in RecursiveCollectionLineAggregator -Embracing JDK 21 LTS is one of the main themes for Spring Batch 5.1, especially the support of -virtual threads from Project Loom. In this release, virtual threads can be used in all areas of the -framework, like running a concurrent step with virtual threads or launching multiple steps in parallel -using virtual threads. +Up until now, the line separator property in `RecursiveCollectionLineAggregator` was set to the System's line separator value. +While it is possible to change the value through a System property, this configuration style is not consistent with other properties +of batch artifacts. + +This release introduces a new setter in `RecursiveCollectionLineAggregator` that allows users to configure a custom value of +the line separator without having to use System properties. + +[[job-registration-improvements]] +== Job registration improvements + +In version 5.1, the default configuration of batch infrastructure beans was updated to automatically populate the job registry +by defining a `JobRegistryBeanPostProcessor` bean in the application context. After a recent change in Spring Framework +that changed the log level in `BeanPostProcessorChecker`, several warnings related to the `JobRegistryBeanPostProcessor` were +logged in a typical Spring Batch application. These warnings are due to the `JobRegistryBeanPostProcessor` having a dependency +to a `JobRegistry` bean, which is not recommended and might cause bean lifecycle issues. -Thanks to the well designed separation of concerns in Spring Batch, threads are not managed directly. Thread -management is rather delegated to `TaskExecutor` implementations from Spring Framework. This programming-to-interface -approach allows you to switch between `TaskExecutor` implementations in a transparent and a flexible way. - -In Spring Framework 6.1, a new `TaskExecutor` implementation based on virtual threads has been introduced, which is the -`VirtualThreadTaskExecutor`. This `TaskExecutor` can be used in Spring Batch wherever a `TaskExecutor` is required. - -[[memory-management-improvement-jpaitemwriter]] -== Memory management improvement in the JpaItemWriter - -When using the `JpaItemWriter`, the JPA persistence context can quickly grow when the chunk size -is large enough. This might lead to `OutOfMemoryError` errors if not cleared appropriately in a timely manner. - -In this release, a new option named `clearPersistenceContext` has been introduced in the `JpaItemWriter` -to clear the persistence context after writing each chunk of items. This option improves the memory management -of chunk-oriented steps dealing with large amounts of data and big chunk sizes. - -[[new-synchronized-decorators]] -== New synchronized decorators for item readers and writers - -Up to version 5.0, Spring Batch provided two decorators `SynchronizedItemStreamReader` and `SynchronizedItemStreamWriter` -to synchronize thread access to `ItemStreamReader#read` and `ItemStreamWriter#write`. Those decorators are useful when -using non thread-safe item streams in multi-threaded steps. - -While those decorators work with `ItemStream` implementations, they are not usable with non-item streams. For example, -those decorators cannot be used to synchronize access to `ListItemReader#read` or `KafkaItemWriter#write`. - -For users convenience, this release introduces new decorators for non-item streams as well. With this new feature, all -item readers and writers in Spring Batch can now be synchronized without having to write custom decorators. - -[[new-cursor-based-mongo-item-reader]] -=== New Cursor-based MongoItemReader - -Up to version 5.0, the `MongoItemReader` provided by Spring Batch used pagination, which is based on MongoDB's `skip` operation. -While this works well for small/medium data sets, it starts to perform poorly with large data sets. - -This release introduces the `MongoCursorItemReader`, a new cursor-based item reader for MongoDB. This implementation -uses cursors instead paging to read data from MongoDB, which improves the performance of reads on large collections. -For consistency with other cursor/paging readers, the current `MongoItemReader` has been renamed to `MongoPagingItemReader`. - -[[bulk-inserts-support-mongo-item-writer]] -=== Bulk inserts support in MongoItemWriter - -Up to version 5.0, the `MongoItemWriter` supported two operations: `upsert` and `delete`. While the `upsert` -operation works well for both inserts and updates, it does not perform well for items that are known to be new -in the target collection. - -Similar to the `persist` and `merge` operations in the `JpaItemWriter`, this release adds a new operation named -`insert` in the `MongoItemWriter`, which is designed for bulk inserts. This new option performs better than -`upsert` for new items as it does not require an additional lookup to check if items already exist in the target collection. - -[[new-item-reader-and-writer-for-redis]] -=== New item reader and writer for Redis - -A new `RedisItemReader` is now available in the library of built-in item readers. This reader is based on Spring Data Redis -and can be configured with a `ScanOptions` to scan the key set to read from Redis. - -Similarly, a new `RedisItemWriter` based on Spring Data Redis is now part of the writers library. This writer can be configured -with a `RedisTemplate` to write items to Redis. - -[[automatic-configuration-of-jobregistrybeanpostprocessor]] -=== Automatic configuration of JobRegistryBeanPostProcessor - -When configuring a `JobOperator` in a Spring Batch application, it is necessary to register the jobs in the operator's `JobRegistry`. -This registration process is either done manually or automatically by adding a `JobRegistryBeanPostProcessor` bean to the application -context. - -In this release, the default configuration of Spring Batch (ie by using `@EnableBatchProcessing` or extending `DefaultBatchConfiguration`) -now automatically registers a `JobRegistryBeanPostProcessor` bean to the application context. This simplifies the configuration process -and improves the user experience when using a `JobOperator`. - -[[ability-to-start-a-job-flow-with-a-decision]] -=== Ability to start a job flow with a decision - -When using the XML configuration style, it is possible to start a job flow with a decider thanks to the `` element. -However, up to version 5.0, it was not possible to achieve the same flow definition with the Java API. - -In this release, a new option to start a job flow with a `JobExecutionDecider` was added to the `JobBuilder` API. -This makes both configuration styles more consistent. - -[[ability-to-provide-a-custom-jobkeygenerator]] -=== Ability to provide a custom JobKeyGenerator - -By default, Spring Batch identifies job instances by calculating an MD5 hash of the identifying job parameters. While it is unlikely to -need to customize this identification process, Spring Batch still provide a strategy interface for users to override the default mechanism -through the `JobKeyGenerator` API. - -Up to version 5.0, it was not possible to provide a custom key generator without having to create a custom `JobRepository` and `JobExplorer`. -In this version, it is now possible to provide a custom `JobKeyGenerator` through the factory beans of `JobRepository` and `JobExplorer`. - -[[new-documentation-based-on-antora]] -=== New documentation based on Antora - -The reference documentation was updated to use https://antora.org[Antora]. This update introduces a number of improvements, including but not limited to: - -* Multi-version documentation: it is now possible to navigate from one version to another thanks to the drop down version list in the left side menu. -* Integrated search experience: powered by https://docsearch.algolia.com/[Algolia], the search experience in now better thanks to the integrated search box at the top left of the page -* Improved configuration style toggle: the toggle to switch between the XML and Java configuration styles for code snippets is now located near each sample, rather than the top of each page - -[[improved-getting-started-experience]] -=== Improved Getting Started experience - -In this release, the getting started experience was improved in many ways: - -* Samples are now packaged by feature and are provided in two configuration styles: XML and Java configuration -* A new https://github.com/spring-projects/spring-batch#two-minutes-tutorial[Two minutes tutorial] was added to the README -* The https://spring.io/guides/gs/batch-processing[Getting Started Guide] was updated to the latest and greatest Spring Batch and Spring Boot versions -* The https://github.com/spring-projects/spring-batch/blob/main/ISSUE_REPORTING.md[Issue Reporting Guide] was updated with detailed instructions and project templates to help you easily report issues - -[[mongodb-job-repository]] -=== MongoDB Job Repository (Experimental) - -This feature introduces new implementations of `JobRepository` and `JobExplorer` backed by MongoDB. This long-awaited feature is now available -as experimental and marks the introduction of the first NoSQL meta-data store for Spring Batch. - -Please refer to the https://github.com/spring-projects-experimental/spring-batch-experimental#mongodb-job-repository[Spring Batch Experimental] repository for more details about this feature. - -[[composite-item-reader]] -=== Composite Item Reader (Experimental) - -This feature introduces a composite `ItemReader` implementation. Similar to the `CompositeItemProcessor` and `CompositeItemWriter`, the idea is to delegate reading to a list of item readers in order. -This is useful when there is a requirement to read data having the same format from different sources (files, databases, etc). - -Please refer to the https://github.com/spring-projects-experimental/spring-batch-experimental#composite-item-reader[Spring Batch Experimental] repository for more details about this feature. - -[[new-chunk-oriented-step-implementation]] -=== New Chunk-Oriented Step implementation (Experimental) - -This is not a new feature, but rather a new implementation of the chunk-oriented processing model. -The goal is to address the reported issues with the current implementation and to provide a new base for the upcoming re-designed concurrency model. - -Please refer to the https://github.com/spring-projects-experimental/spring-batch-experimental#new-chunk-oriented-step-implementation[Spring Batch Experimental] repository for more details about this new implementation. \ No newline at end of file +These issues have been resolved in this release by changing the mechanism of populating the `JobRegistry` from using a `BeanPostProcessor` +to using a `SmartInitializingSingleton`. The `JobRegistryBeanPostProcessor` is now deprecated in favor of the newly added `JobRegistrySmartInitializingSingleton`. diff --git a/spring-batch-docs/pom.xml b/spring-batch-docs/pom.xml index 2dee8bc5d2..d31d9dd8cd 100644 --- a/spring-batch-docs/pom.xml +++ b/spring-batch-docs/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-docs Spring Batch Docs @@ -26,7 +26,7 @@ @antora/atlas-extension@1.0.0-alpha.1 @antora/collector-extension@1.0.0-alpha.3 @asciidoctor/tabs@1.0.0-beta.3 - @springio/antora-extensions@1.7.0 + @springio/antora-extensions@1.10.0 @springio/asciidoctor-extensions@1.0.0-alpha.9 diff --git a/spring-batch-docs/src/assembly/javadocs.xml b/spring-batch-docs/src/assembly/javadocs.xml index 9ec174daf0..2cea243ea6 100644 --- a/spring-batch-docs/src/assembly/javadocs.xml +++ b/spring-batch-docs/src/assembly/javadocs.xml @@ -8,7 +8,7 @@ false - ../target/site/apidocs + ../target/reports/apidocs api diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index ba86d44878..dd208f8379 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-infrastructure jar @@ -171,6 +171,18 @@ org.slf4j slf4j-api + + org.springframework.data + spring-data-commons + + + org.mongodb + mongodb-driver-core + + + org.mongodb + mongodb-driver-sync + @@ -183,6 +195,10 @@ org.slf4j slf4j-api + + org.springframework + spring-expression + @@ -197,10 +213,16 @@ + + org.mongodb + mongodb-driver-core + ${mongodb-driver.version} + true + org.mongodb mongodb-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.xstream xstream 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 items) { public Chunk(List 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 chunk) throws Exception { * @param chunk the chunk of items to be persisted. */ protected void doWrite(Chunk 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 type) { + super.setTargetType(type); + } + + @Override + public void setParameterValues(List parameterValues) { + super.setParameterValues(parameterValues); + } + + @Override + public void setFields(String fields) { + super.setFields(fields); + } + + @Override + public void setSort(Map sorts) { + super.setSort(sorts); + } + + @Override + public void setCollection(String collection) { + super.setCollection(collection); + } + + @Override + public void setHint(String hint) { + super.setHint(hint); + } + + @Override + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + } + + @Override + protected Iterator doPageRead() { + return super.doPageRead(); + } + + @Override + protected String replacePlaceholders(String input, List values) { + return super.replacePlaceholders(input, values); + } + + @Override + protected Sort convertToSort(Map sorts) { + return super.convertToSort(sorts); + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/Neo4jItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/Neo4jItemWriter.java index 40cfbdb4cc..c9bcd35bf6 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/Neo4jItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/Neo4jItemWriter.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. @@ -25,7 +25,6 @@ import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** *

    @@ -89,7 +88,7 @@ public void afterPropertiesSet() throws Exception { */ @Override public void write(Chunk chunk) throws Exception { - if (!CollectionUtils.isEmpty(chunk.getItems())) { + if (!chunk.isEmpty()) { doWrite(chunk); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemReader.java index 4494b2d8cb..98ce9941f3 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemReader.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. @@ -117,7 +117,9 @@ public void setArguments(List arguments) { } /** - * Provides ordering of the results so that order is maintained between paged queries + * Provides ordering of the results so that order is maintained between paged queries. + * Use a {@link java.util.LinkedHashMap} in case of multiple sort entries to keep the + * order. * @param sorts the fields to sort by and the directions */ public void setSort(Map sorts) { @@ -227,7 +229,7 @@ protected List doPageRead() throws Exception { List parameters = new ArrayList<>(); - if (arguments != null && arguments.size() > 0) { + if (arguments != null && !arguments.isEmpty()) { parameters.addAll(arguments); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java index eb23f7800a..996bdf5fd3 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.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,7 +27,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.data.repository.CrudRepository; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.MethodInvoker; import org.springframework.util.StringUtils; @@ -93,7 +92,7 @@ public void setRepository(CrudRepository repository) { */ @Override public void write(Chunk chunk) throws Exception { - if (!CollectionUtils.isEmpty(chunk.getItems())) { + if (!chunk.isEmpty()) { doWrite(chunk); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemReaderBuilder.java index e90a21f0ed..8b51679673 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-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. @@ -130,6 +130,8 @@ public RepositoryItemReaderBuilder arguments(Object... arguments) { /** * Provides ordering of the results so that order is maintained between paged queries. + * Use a {@link java.util.LinkedHashMap} in case of multiple sort entries to keep the + * order. * @param sorts the fields to sort by and the directions. * @return The current instance of the builder. * @see RepositoryItemReader#setSort(Map) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java deleted file mode 100644 index ea1bba0348..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java +++ /dev/null @@ -1,205 +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.item.database; - -import java.util.Map; - -import org.hibernate.ScrollableResults; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamReader; -import org.springframework.batch.item.ItemStreamException; -import org.springframework.batch.item.database.orm.HibernateQueryProvider; -import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * {@link ItemStreamReader} for reading database records built on top of Hibernate. It - * executes the HQL query when initialized iterates over the result set as {@link #read()} - * method is called, returning an object corresponding to current row. The query can be - * set directly using {@link #setQueryString(String)}, a named query can be used by - * {@link #setQueryName(String)}, or a query provider strategy can be supplied via - * {@link #setQueryProvider(HibernateQueryProvider)}. - * - *

    - * The reader can be configured to use either {@link StatelessSession} sufficient for - * simple mappings without the need to cascade to associated objects or standard hibernate - * {@link Session} for more advanced mappings or when caching is desired. When stateful - * session is used it will be cleared in the {@link #update(ExecutionContext)} method - * without being flushed (no data modifications are expected). - *

    - * - * The implementation is not thread-safe. - * - * @author Robert Kasanicky - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaCursorItemReader} instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateCursorItemReader extends AbstractItemCountingItemStreamItemReader - implements InitializingBean { - - private final HibernateItemReaderHelper helper = new HibernateItemReaderHelper<>(); - - public HibernateCursorItemReader() { - setName(ClassUtils.getShortName(HibernateCursorItemReader.class)); - } - - private ScrollableResults cursor; - - private boolean initialized = false; - - private int fetchSize; - - private Map parameterValues; - - @Override - public void afterPropertiesSet() throws Exception { - Assert.state(fetchSize >= 0, "fetchSize must not be negative"); - helper.afterPropertiesSet(); - } - - /** - * The parameter values to apply to a query (map of name:value). - * @param parameterValues the parameter values to set - */ - public void setParameterValues(Map parameterValues) { - this.parameterValues = parameterValues; - } - - /** - * A query name for an externalized query. Either this or the { - * {@link #setQueryString(String) query string} or the { - * {@link #setQueryProvider(HibernateQueryProvider) query provider} should be set. - * @param queryName name of a hibernate named query - */ - public void setQueryName(String queryName) { - helper.setQueryName(queryName); - } - - /** - * Fetch size used internally by Hibernate to limit amount of data fetched from - * database per round trip. - * @param fetchSize the fetch size to pass down to Hibernate - */ - public void setFetchSize(int fetchSize) { - this.fetchSize = fetchSize; - } - - /** - * A query provider. Either this or the {{@link #setQueryString(String) query string} - * or the {{@link #setQueryName(String) query name} should be set. - * @param queryProvider Hibernate query provider - */ - public void setQueryProvider(HibernateQueryProvider queryProvider) { - helper.setQueryProvider(queryProvider); - } - - /** - * A query string in HQL. Either this or the { - * {@link #setQueryProvider(HibernateQueryProvider) query provider} or the { - * {@link #setQueryName(String) query name} should be set. - * @param queryString HQL query string - */ - public void setQueryString(String queryString) { - helper.setQueryString(queryString); - } - - /** - * The Hibernate SessionFactory to use the create a session. - * @param sessionFactory the {@link SessionFactory} to set - */ - public void setSessionFactory(SessionFactory sessionFactory) { - helper.setSessionFactory(sessionFactory); - } - - /** - * Can be set only in uninitialized state. - * @param useStatelessSession true to use {@link StatelessSession} - * false to use standard hibernate {@link Session} - */ - public void setUseStatelessSession(boolean useStatelessSession) { - helper.setUseStatelessSession(useStatelessSession); - } - - @Nullable - @Override - protected T doRead() throws Exception { - if (cursor.next()) { - return cursor.get(); - } - return null; - } - - /** - * Open hibernate session and create a forward-only cursor for the query. - */ - @Override - protected void doOpen() throws Exception { - Assert.state(!initialized, "Cannot open an already opened ItemReader, call close first"); - cursor = helper.getForwardOnlyCursor(fetchSize, parameterValues); - initialized = true; - } - - /** - * Update the context and clear the session if stateful. - * @param executionContext the current {@link ExecutionContext} - * @throws ItemStreamException if there is a problem - */ - @Override - public void update(ExecutionContext executionContext) throws ItemStreamException { - super.update(executionContext); - helper.clear(); - } - - /** - * Wind forward through the result set to the item requested. Also clears the session - * every now and then (if stateful) to avoid memory problems. The frequency of session - * clearing is the larger of the fetch size (if set) and 100. - * @param itemIndex the first item to read - * @throws Exception if there is a problem - * @see AbstractItemCountingItemStreamItemReader#jumpToItem(int) - */ - @Override - protected void jumpToItem(int itemIndex) throws Exception { - int flushSize = Math.max(fetchSize, 100); - helper.jumpToItem(cursor, itemIndex, flushSize); - } - - /** - * Close the cursor and hibernate session. - */ - @Override - protected void doClose() throws Exception { - - if (initialized) { - if (cursor != null) { - cursor.close(); - } - - helper.close(); - } - - initialized = false; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java deleted file mode 100644 index 6911572c44..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java +++ /dev/null @@ -1,228 +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.item.database; - -import java.util.Collection; -import java.util.Map; - -import org.hibernate.query.Query; -import org.hibernate.ScrollMode; -import org.hibernate.ScrollableResults; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; - -import org.springframework.batch.item.database.orm.HibernateQueryProvider; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * Internal shared state helper for hibernate readers managing sessions and queries. - * - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @author June Young. Park - * @deprecated since 5.0 for removal in 5.2. Use the JPA item readers instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateItemReaderHelper implements InitializingBean { - - private SessionFactory sessionFactory; - - private String queryString = ""; - - private String queryName = ""; - - private HibernateQueryProvider queryProvider; - - private boolean useStatelessSession = true; - - private StatelessSession statelessSession; - - private Session statefulSession; - - /** - * @param queryName name of a hibernate named query - */ - public void setQueryName(String queryName) { - this.queryName = queryName; - } - - /** - * @param queryString HQL query string - */ - public void setQueryString(String queryString) { - this.queryString = queryString; - } - - /** - * @param queryProvider Hibernate query provider - */ - public void setQueryProvider(HibernateQueryProvider queryProvider) { - this.queryProvider = queryProvider; - } - - /** - * Can be set only in uninitialized state. - * @param useStatelessSession true to use {@link StatelessSession} - * false to use standard hibernate {@link Session} - */ - public void setUseStatelessSession(boolean useStatelessSession) { - Assert.state(statefulSession == null && statelessSession == null, - "The useStatelessSession flag can only be set before a session is initialized."); - this.useStatelessSession = useStatelessSession; - } - - /** - * @param sessionFactory hibernate session factory - */ - public void setSessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - @Override - public void afterPropertiesSet() throws Exception { - - Assert.state(sessionFactory != null, "A SessionFactory must be provided"); - - if (queryProvider == null) { - Assert.state(sessionFactory != null, "session factory must be set"); - Assert.state(StringUtils.hasText(queryString) ^ StringUtils.hasText(queryName), - "queryString or queryName must be set"); - } - } - - /** - * Get a cursor over all of the results, with the forward-only flag set. - * @param fetchSize the fetch size to use retrieving the results - * @param parameterValues the parameter values to use (or null if none). - * @return a forward-only {@link ScrollableResults} - */ - public ScrollableResults getForwardOnlyCursor(int fetchSize, Map parameterValues) { - Query query = createQuery(); - if (!CollectionUtils.isEmpty(parameterValues)) { - query.setProperties(parameterValues); - } - return query.setFetchSize(fetchSize).scroll(ScrollMode.FORWARD_ONLY); - } - - /** - * Open appropriate type of hibernate session and create the query. - * @return a Hibernate Query - */ - public Query createQuery() { - - if (useStatelessSession) { - if (statelessSession == null) { - statelessSession = sessionFactory.openStatelessSession(); - } - if (queryProvider != null) { - queryProvider.setStatelessSession(statelessSession); - } - else { - if (StringUtils.hasText(queryName)) { - return statelessSession.getNamedQuery(queryName); - } - else { - return statelessSession.createQuery(queryString); - } - } - } - else { - if (statefulSession == null) { - statefulSession = sessionFactory.openSession(); - } - if (queryProvider != null) { - queryProvider.setSession(statefulSession); - } - else { - if (StringUtils.hasText(queryName)) { - return statefulSession.getNamedQuery(queryName); - } - else { - return statefulSession.createQuery(queryString); - } - } - } - - // If queryProvider is set use it to create a query - return queryProvider.createQuery(); - - } - - /** - * Scroll through the results up to the item specified. - * @param cursor the results to scroll over - * @param itemIndex index to scroll to - * @param flushInterval the number of items to scroll past before flushing - */ - public void jumpToItem(ScrollableResults cursor, int itemIndex, int flushInterval) { - for (int i = 0; i < itemIndex; i++) { - cursor.next(); - if (i % flushInterval == 0 && !useStatelessSession) { - statefulSession.clear(); // Clears in-memory cache - } - } - } - - /** - * Close the open session (stateful or otherwise). - */ - public void close() { - if (statelessSession != null) { - statelessSession.close(); - statelessSession = null; - } - if (statefulSession != null) { - statefulSession.close(); - statefulSession = null; - } - } - - /** - * Read a page of data, clearing the existing session (if necessary) first, and - * creating a new session before executing the query. - * @param page the page to read (starting at 0) - * @param pageSize the size of the page or maximum number of items to read - * @param fetchSize the fetch size to use - * @param parameterValues the parameter values to use (if any, otherwise null) - * @return a collection of items - */ - public Collection readPage(int page, int pageSize, int fetchSize, - Map parameterValues) { - - clear(); - - Query query = createQuery(); - if (!CollectionUtils.isEmpty(parameterValues)) { - query.setProperties(parameterValues); - } - return query.setFetchSize(fetchSize).setFirstResult(page * pageSize).setMaxResults(pageSize).list(); - - } - - /** - * Clear the session if stateful. - */ - public void clear() { - if (statefulSession != null) { - statefulSession.clear(); - } - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java deleted file mode 100644 index 144c72fdd6..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java +++ /dev/null @@ -1,124 +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.item.database; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.context.spi.CurrentSessionContext; - -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; - -/** - * {@link ItemWriter} that uses a Hibernate session to save or update entities that are - * not part of the current Hibernate session. It will also flush the session after writing - * (i.e. at chunk boundaries if used in a Spring Batch TaskletStep). It will also clear - * the session on write default (see {@link #setClearSession(boolean) clearSession} - * property).
    - *
    - * - * The writer is thread-safe once properties are set (normal singleton behavior) if a - * {@link CurrentSessionContext} that uses only one session per thread is used. - * - * @author Dave Syer - * @author Thomas Risberg - * @author Michael Minella - * @author Mahmoud Ben Hassine - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaItemWriter} instead. - * - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateItemWriter implements ItemWriter, InitializingBean { - - protected static final Log logger = LogFactory.getLog(HibernateItemWriter.class); - - private SessionFactory sessionFactory; - - private boolean clearSession = true; - - /** - * Flag to indicate that the session should be cleared and flushed at the end of the - * write (default true). - * @param clearSession the flag value to set - */ - public void setClearSession(boolean clearSession) { - this.clearSession = clearSession; - } - - /** - * Set the Hibernate SessionFactory to be used internally. - * @param sessionFactory session factory to be used by the writer - */ - public void setSessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - /** - * Check mandatory properties - there must be a sessionFactory. - */ - @Override - public void afterPropertiesSet() { - Assert.state(sessionFactory != null, "SessionFactory must be provided"); - } - - /** - * Save or update any entities not in the current hibernate session and then flush the - * hibernate session. - * - * @see org.springframework.batch.item.ItemWriter#write(Chunk) - */ - @Override - public void write(Chunk items) { - doWrite(sessionFactory, items); - sessionFactory.getCurrentSession().flush(); - if (clearSession) { - sessionFactory.getCurrentSession().clear(); - } - } - - /** - * Do perform the actual write operation using Hibernate's API. This can be overridden - * in a subclass if necessary. - * @param sessionFactory Hibernate SessionFactory to be used - * @param items the list of items to use for the write - */ - protected void doWrite(SessionFactory sessionFactory, Chunk items) { - if (logger.isDebugEnabled()) { - logger.debug("Writing to Hibernate with " + items.size() + " items."); - } - - Session currentSession = sessionFactory.getCurrentSession(); - - if (!items.isEmpty()) { - long saveOrUpdateCount = 0; - for (T item : items) { - if (!currentSession.contains(item)) { - currentSession.saveOrUpdate(item); - saveOrUpdateCount++; - } - } - if (logger.isDebugEnabled()) { - logger.debug(saveOrUpdateCount + " entities saved/updated."); - logger.debug((items.size() - saveOrUpdateCount) + " entities found in session."); - } - } - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java deleted file mode 100644 index 41c50e2674..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java +++ /dev/null @@ -1,166 +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.item.database; - -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.database.orm.HibernateQueryProvider; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * {@link ItemReader} for reading database records built on top of Hibernate and reading - * only up to a fixed number of items at a time. It executes an HQL query when initialized - * is paged as the {@link #read()} method is called. The query can be set directly using - * {@link #setQueryString(String)}, a named query can be used by - * {@link #setQueryName(String)}, or a query provider strategy can be supplied via - * {@link #setQueryProvider(HibernateQueryProvider)}. - * - *

    - * The reader can be configured to use either {@link StatelessSession} sufficient for - * simple mappings without the need to cascade to associated objects or standard hibernate - * {@link Session} for more advanced mappings or when caching is desired. When stateful - * session is used it will be cleared in the {@link #update(ExecutionContext)} method - * without being flushed (no data modifications are expected). - *

    - * - *

    - * The implementation is thread-safe in between calls to {@link #open(ExecutionContext)}, - * but remember to use saveState=false if used in a multi-threaded client (no - * restart available). - *

    - * - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @since 2.1 - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaPagingItemReader} instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernatePagingItemReader extends AbstractPagingItemReader implements InitializingBean { - - private final HibernateItemReaderHelper helper = new HibernateItemReaderHelper<>(); - - private Map parameterValues; - - private int fetchSize; - - public HibernatePagingItemReader() { - setName(ClassUtils.getShortName(HibernatePagingItemReader.class)); - } - - /** - * The parameter values to apply to a query (map of name:value). - * @param parameterValues the parameter values to set - */ - public void setParameterValues(Map parameterValues) { - this.parameterValues = parameterValues; - } - - /** - * A query name for an externalized query. Either this or the { - * {@link #setQueryString(String) query string} or the { - * {@link #setQueryProvider(HibernateQueryProvider) query provider} should be set. - * @param queryName name of a hibernate named query - */ - public void setQueryName(String queryName) { - helper.setQueryName(queryName); - } - - /** - * Fetch size used internally by Hibernate to limit amount of data fetched from - * database per round trip. - * @param fetchSize the fetch size to pass down to Hibernate - */ - public void setFetchSize(int fetchSize) { - this.fetchSize = fetchSize; - } - - /** - * A query provider. Either this or the {{@link #setQueryString(String) query string} - * or the {{@link #setQueryName(String) query name} should be set. - * @param queryProvider Hibernate query provider - */ - public void setQueryProvider(HibernateQueryProvider queryProvider) { - helper.setQueryProvider(queryProvider); - } - - /** - * A query string in HQL. Either this or the { - * {@link #setQueryProvider(HibernateQueryProvider) query provider} or the { - * {@link #setQueryName(String) query name} should be set. - * @param queryString HQL query string - */ - public void setQueryString(String queryString) { - helper.setQueryString(queryString); - } - - /** - * The Hibernate SessionFactory to use the create a session. - * @param sessionFactory the {@link SessionFactory} to set - */ - public void setSessionFactory(SessionFactory sessionFactory) { - helper.setSessionFactory(sessionFactory); - } - - /** - * Can be set only in uninitialized state. - * @param useStatelessSession true to use {@link StatelessSession} - * false to use standard hibernate {@link Session} - */ - public void setUseStatelessSession(boolean useStatelessSession) { - helper.setUseStatelessSession(useStatelessSession); - } - - @Override - public void afterPropertiesSet() throws Exception { - super.afterPropertiesSet(); - Assert.state(fetchSize >= 0, "fetchSize must not be negative"); - helper.afterPropertiesSet(); - } - - @Override - protected void doOpen() throws Exception { - super.doOpen(); - } - - @Override - protected void doReadPage() { - - if (results == null) { - results = new CopyOnWriteArrayList<>(); - } - else { - results.clear(); - } - - results.addAll(helper.readPage(getPage(), getPageSize(), fetchSize, parameterValues)); - - } - - @Override - protected void doClose() throws Exception { - helper.close(); - super.doClose(); - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcPagingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcPagingItemReader.java index 0d484f2c45..547c8e3ff9 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcPagingItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcPagingItemReader.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. @@ -185,7 +185,7 @@ protected void doReadPage() { if (logger.isDebugEnabled()) { logger.debug("SQL used for reading first page: [" + firstPageSql + "]"); } - if (parameterValues != null && parameterValues.size() > 0) { + if (parameterValues != null && !parameterValues.isEmpty()) { if (this.queryProvider.isUsingNamedParameters()) { query = namedParameterJdbcTemplate.query(firstPageSql, getParameterMap(parameterValues, null), rowCallback); @@ -277,7 +277,7 @@ private List getParameterList(Map values, Map parameterList = new ArrayList<>(); parameterList.addAll(sm.values()); - if (sortKeyValue != null && sortKeyValue.size() > 0) { + if (sortKeyValue != null && !sortKeyValue.isEmpty()) { List> keys = new ArrayList<>(sortKeyValue.entrySet()); for (int i = 0; i < keys.size(); i++) { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcParameterUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcParameterUtils.java index 9dff9b026b..92b49280b6 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcParameterUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcParameterUtils.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. @@ -28,9 +28,13 @@ * @author Thomas Risberg * @author Juergen Hoeller * @author Marten Deinum + * @author Taeik Lim * @since 2.0 */ -public class JdbcParameterUtils { +public abstract class JdbcParameterUtils { + + private JdbcParameterUtils() { + } /** * Count the occurrences of the character placeholder in an SQL string diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaCursorItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaCursorItemReader.java index 89324c5c57..aafdc63eed 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaCursorItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaCursorItemReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -43,6 +43,7 @@ * The implementation is not thread-safe. * * @author Mahmoud Ben Hassine + * @author Jinwoo Bae * @param type of items to read * @since 4.3 */ @@ -58,6 +59,8 @@ public class JpaCursorItemReader extends AbstractItemCountingItemStreamItemRe private Map parameterValues; + private Map hintValues; + private Iterator iterator; /** @@ -100,6 +103,17 @@ public void setParameterValues(Map parameterValues) { this.parameterValues = parameterValues; } + /** + * Set the query hint values for the JPA query. Query hints can be used to give + * instructions to the JPA provider. + * @param hintValues a map where each key is the name of the hint, and the + * corresponding value is the hint's value. + * @since 5.2 + */ + public void setHintValues(Map hintValues) { + this.hintValues = hintValues; + } + @Override public void afterPropertiesSet() throws Exception { Assert.state(this.entityManagerFactory != null, "EntityManagerFactory is required"); @@ -123,6 +137,10 @@ protected void doOpen() throws Exception { if (this.parameterValues != null) { this.parameterValues.forEach(query::setParameter); } + if (this.hintValues != null) { + this.hintValues.forEach(query::setHint); + } + this.iterator = query.getResultStream().iterator(); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaPagingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaPagingItemReader.java index 1d33e9a6f8..d99d3c9245 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaPagingItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaPagingItemReader.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. @@ -80,6 +80,7 @@ * @author Dave Syer * @author Will Schipp * @author Mahmoud Ben Hassine + * @author Jinwoo Bae * @since 2.0 */ public class JpaPagingItemReader extends AbstractPagingItemReader { @@ -96,6 +97,8 @@ public class JpaPagingItemReader extends AbstractPagingItemReader { private Map parameterValues; + private Map hintValues; + private boolean transacted = true;// default value public JpaPagingItemReader() { @@ -128,6 +131,17 @@ public void setParameterValues(Map parameterValues) { this.parameterValues = parameterValues; } + /** + * Set the query hint values for the JPA query. Query hints can be used to give + * instructions to the JPA provider. + * @param hintValues a map where each key is the name of the hint, and the + * corresponding value is the hint's value. + * @since 5.2 + */ + public void setHintValues(Map hintValues) { + this.hintValues = hintValues; + } + /** * By default (true) the EntityTransaction will be started and committed around the * read. Can be overridden (false) in cases where the JPA implementation doesn't @@ -202,6 +216,10 @@ protected void doReadPage() { } } + if (this.hintValues != null) { + this.hintValues.forEach(query::setHint); + } + if (results == null) { results = new CopyOnWriteArrayList<>(); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java deleted file mode 100644 index 864a6ad562..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import java.util.Map; - -import org.hibernate.SessionFactory; - -import org.springframework.batch.item.database.HibernateCursorItemReader; -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; -import org.springframework.batch.item.database.orm.HibernateQueryProvider; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * This is a builder for the {@link HibernateCursorItemReader}. When configuring, one of - * the following should be provided (listed in order of precedence): - *
      - *
    • {@link #queryProvider(HibernateQueryProvider)}
    • - *
    • {@link #queryName(String)}
    • - *
    • {@link #queryString(String)}
    • - *
    • {@link #nativeQuery(String)} and {@link #entityClass(Class)}
    • - *
    - * - * @author Michael Minella - * @author Glenn Renfro - * @author Mahmoud Ben Hassine - * @since 4.0 - * @see HibernateCursorItemReader - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaCursorItemReaderBuilder} - * instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateCursorItemReaderBuilder { - - private Map parameterValues; - - private String queryName; - - private int fetchSize; - - private HibernateQueryProvider queryProvider; - - private String queryString; - - private SessionFactory sessionFactory; - - private boolean useStatelessSession; - - private String nativeQuery; - - private Class nativeClass; - - private boolean saveState = true; - - private String name; - - private int maxItemCount = Integer.MAX_VALUE; - - private int currentItemCount; - - /** - * Configure if the state of the - * {@link org.springframework.batch.item.ItemStreamSupport} should be persisted within - * the {@link org.springframework.batch.item.ExecutionContext} for restart purposes. - * @param saveState defaults to true - * @return The current instance of the builder. - */ - public HibernateCursorItemReaderBuilder saveState(boolean saveState) { - this.saveState = saveState; - - return this; - } - - /** - * The name used to calculate the key within the - * {@link org.springframework.batch.item.ExecutionContext}. Required if - * {@link #saveState(boolean)} is set to true. - * @param name name of the reader instance - * @return The current instance of the builder. - * @see org.springframework.batch.item.ItemStreamSupport#setName(String) - */ - public HibernateCursorItemReaderBuilder name(String name) { - this.name = name; - - return this; - } - - /** - * Configure the max number of items to be read. - * @param maxItemCount the max items to be read - * @return The current instance of the builder. - * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int) - */ - public HibernateCursorItemReaderBuilder maxItemCount(int maxItemCount) { - this.maxItemCount = maxItemCount; - - return this; - } - - /** - * Index for the current item. Used on restarts to indicate where to start from. - * @param currentItemCount current index - * @return this instance for method chaining - * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int) - */ - public HibernateCursorItemReaderBuilder currentItemCount(int currentItemCount) { - this.currentItemCount = currentItemCount; - - return this; - } - - /** - * A map of parameter values to be set on the query. The key of the map is the name of - * the parameter to be set with the value being the value to be set. - * @param parameterValues map of values - * @return this instance for method chaining - * @see HibernateCursorItemReader#setParameterValues(Map) - */ - public HibernateCursorItemReaderBuilder parameterValues(Map parameterValues) { - this.parameterValues = parameterValues; - - return this; - } - - /** - * The name of the Hibernate named query to be executed for this reader. - * @param queryName name of the query to execute - * @return this instance for method chaining - * @see HibernateCursorItemReader#setQueryName(String) - */ - public HibernateCursorItemReaderBuilder queryName(String queryName) { - this.queryName = queryName; - - return this; - } - - /** - * The number of items to be returned with each round trip to the database. Used - * internally by Hibernate. - * @param fetchSize number of records to return per fetch - * @return this instance for method chaining - * @see HibernateCursorItemReader#setFetchSize(int) - */ - public HibernateCursorItemReaderBuilder fetchSize(int fetchSize) { - this.fetchSize = fetchSize; - - return this; - } - - /** - * A query provider. This should be set only if {@link #queryString(String)} and - * {@link #queryName(String)} have not been set. - * @param queryProvider the query provider - * @return this instance for method chaining - * @see HibernateCursorItemReader#setQueryProvider(HibernateQueryProvider) - */ - public HibernateCursorItemReaderBuilder queryProvider(HibernateQueryProvider queryProvider) { - this.queryProvider = queryProvider; - - return this; - } - - /** - * The HQL query string to execute. This should only be set if - * {@link #queryProvider(HibernateQueryProvider)} and {@link #queryName(String)} have - * not been set. - * @param queryString the HQL query - * @return this instance for method chaining - * @see HibernateCursorItemReader#setQueryString(String) - */ - public HibernateCursorItemReaderBuilder queryString(String queryString) { - this.queryString = queryString; - - return this; - } - - /** - * The Hibernate {@link SessionFactory} to execute the query against. - * @param sessionFactory the session factory - * @return this instance for method chaining - * @see HibernateCursorItemReader#setSessionFactory(SessionFactory) - */ - public HibernateCursorItemReaderBuilder sessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - - return this; - } - - /** - * Indicator for whether to use a {@link org.hibernate.StatelessSession} - * (true) or a {@link org.hibernate.Session} (false). - * @param useStatelessSession Defaults to false - * @return this instance for method chaining - * @see HibernateCursorItemReader#setUseStatelessSession(boolean) - */ - public HibernateCursorItemReaderBuilder useStatelessSession(boolean useStatelessSession) { - this.useStatelessSession = useStatelessSession; - - return this; - } - - /** - * Used to configure a {@link HibernateNativeQueryProvider}. This is ignored if - * @param nativeQuery {@link String} containing the native query. - * @return this instance for method chaining - */ - public HibernateCursorItemReaderBuilder nativeQuery(String nativeQuery) { - this.nativeQuery = nativeQuery; - - return this; - } - - public HibernateCursorItemReaderBuilder entityClass(Class nativeClass) { - this.nativeClass = nativeClass; - - return this; - } - - /** - * Returns a fully constructed {@link HibernateCursorItemReader}. - * @return a new {@link HibernateCursorItemReader} - */ - public HibernateCursorItemReader build() { - Assert.state(this.fetchSize >= 0, "fetchSize must not be negative"); - Assert.state(this.sessionFactory != null, "A SessionFactory must be provided"); - - if (this.saveState) { - Assert.state(StringUtils.hasText(this.name), "A name is required when saveState is set to true."); - } - - HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); - - reader.setFetchSize(this.fetchSize); - reader.setParameterValues(this.parameterValues); - - if (this.queryProvider != null) { - reader.setQueryProvider(this.queryProvider); - } - else if (StringUtils.hasText(this.queryName)) { - reader.setQueryName(this.queryName); - } - else if (StringUtils.hasText(this.queryString)) { - reader.setQueryString(this.queryString); - } - else if (StringUtils.hasText(this.nativeQuery) && this.nativeClass != null) { - HibernateNativeQueryProvider provider = new HibernateNativeQueryProvider<>(); - provider.setSqlQuery(this.nativeQuery); - provider.setEntityClass(this.nativeClass); - - try { - provider.afterPropertiesSet(); - } - catch (Exception e) { - throw new IllegalStateException("Unable to initialize the HibernateNativeQueryProvider", e); - } - - reader.setQueryProvider(provider); - } - else { - throw new IllegalStateException("A HibernateQueryProvider, queryName, queryString, " - + "or both the nativeQuery and entityClass must be configured"); - } - - reader.setSessionFactory(this.sessionFactory); - reader.setUseStatelessSession(this.useStatelessSession); - reader.setCurrentItemCount(this.currentItemCount); - reader.setMaxItemCount(this.maxItemCount); - reader.setName(this.name); - reader.setSaveState(this.saveState); - - return reader; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilder.java deleted file mode 100644 index ab313ef3d1..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilder.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import org.hibernate.SessionFactory; - -import org.springframework.batch.item.database.HibernateItemWriter; -import org.springframework.util.Assert; - -/** - * A builder for the {@link HibernateItemWriter} - * - * @author Michael Minella - * @author Mahmoud Ben Hassine - * @since 4.0 - * @see HibernateItemWriter - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaItemWriterBuilder} instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateItemWriterBuilder { - - private boolean clearSession = true; - - private SessionFactory sessionFactory; - - /** - * If set to false, the {@link org.hibernate.Session} will not be cleared at the end - * of the chunk. - * @param clearSession defaults to true - * @return this instance for method chaining - * @see HibernateItemWriter#setClearSession(boolean) - */ - public HibernateItemWriterBuilder clearSession(boolean clearSession) { - this.clearSession = clearSession; - - return this; - } - - /** - * The Hibernate {@link SessionFactory} to obtain a session from. Required. - * @param sessionFactory the {@link SessionFactory} - * @return this instance for method chaining - * @see HibernateItemWriter#setSessionFactory(SessionFactory) - */ - public HibernateItemWriterBuilder sessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - - return this; - } - - /** - * Returns a fully built {@link HibernateItemWriter} - * @return the writer - */ - public HibernateItemWriter build() { - Assert.state(this.sessionFactory != null, "SessionFactory must be provided"); - - HibernateItemWriter writer = new HibernateItemWriter<>(); - writer.setSessionFactory(this.sessionFactory); - writer.setClearSession(this.clearSession); - - return writer; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java deleted file mode 100644 index 8a9c20b84c..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import java.util.Map; - -import org.hibernate.SessionFactory; - -import org.springframework.batch.item.database.HibernatePagingItemReader; -import org.springframework.batch.item.database.orm.HibernateQueryProvider; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * A builder for the {@link HibernatePagingItemReader}. When configuring, only one of the - * following should be provided: - *
      - *
    • {@link #queryString(String)}
    • - *
    • {@link #queryName(String)}
    • - *
    • {@link #queryProvider(HibernateQueryProvider)}
    • - *
    - * - * @author Michael Minella - * @author Glenn Renfro - * @author Mahmoud Ben Hassine - * @since 4.0 - * @see HibernatePagingItemReader - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaPagingItemReaderBuilder} - * instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernatePagingItemReaderBuilder { - - private int pageSize = 10; - - private Map parameterValues; - - private String queryName; - - private int fetchSize; - - private HibernateQueryProvider queryProvider; - - private String queryString; - - private SessionFactory sessionFactory; - - private boolean statelessSession = true; - - private boolean saveState = true; - - private String name; - - private int maxItemCount = Integer.MAX_VALUE; - - private int currentItemCount; - - /** - * Configure if the state of the - * {@link org.springframework.batch.item.ItemStreamSupport} should be persisted within - * the {@link org.springframework.batch.item.ExecutionContext} for restart purposes. - * @param saveState defaults to true - * @return The current instance of the builder. - */ - public HibernatePagingItemReaderBuilder saveState(boolean saveState) { - this.saveState = saveState; - - return this; - } - - /** - * The name used to calculate the key within the - * {@link org.springframework.batch.item.ExecutionContext}. Required if - * {@link #saveState(boolean)} is set to true. - * @param name name of the reader instance - * @return The current instance of the builder. - * @see org.springframework.batch.item.ItemStreamSupport#setName(String) - */ - public HibernatePagingItemReaderBuilder name(String name) { - this.name = name; - - return this; - } - - /** - * Configure the max number of items to be read. - * @param maxItemCount the max items to be read - * @return The current instance of the builder. - * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int) - */ - public HibernatePagingItemReaderBuilder maxItemCount(int maxItemCount) { - this.maxItemCount = maxItemCount; - - return this; - } - - /** - * Index for the current item. Used on restarts to indicate where to start from. - * @param currentItemCount current index - * @return this instance for method chaining - * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int) - */ - public HibernatePagingItemReaderBuilder currentItemCount(int currentItemCount) { - this.currentItemCount = currentItemCount; - - return this; - } - - /** - * The number of records to request per page/query. Defaults to 10. Must be greater - * than zero. - * @param pageSize number of items - * @return this instance for method chaining - * @see HibernatePagingItemReader#setPageSize(int) - */ - public HibernatePagingItemReaderBuilder pageSize(int pageSize) { - this.pageSize = pageSize; - - return this; - } - - /** - * A map of parameter values to be set on the query. The key of the map is the name of - * the parameter to be set with the value being the value to be set. - * @param parameterValues map of values - * @return this instance for method chaining - * @see HibernatePagingItemReader#setParameterValues(Map) - */ - public HibernatePagingItemReaderBuilder parameterValues(Map parameterValues) { - this.parameterValues = parameterValues; - - return this; - } - - /** - * The name of the Hibernate named query to be executed for this reader. - * @param queryName name of the query to execute - * @return this instance for method chaining - * @see HibernatePagingItemReader#setQueryName(String) - */ - public HibernatePagingItemReaderBuilder queryName(String queryName) { - this.queryName = queryName; - - return this; - } - - /** - * Fetch size used internally by Hibernate to limit amount of data fetched from - * database per round trip. - * @param fetchSize number of records - * @return this instance for method chaining - * @see HibernatePagingItemReader#setFetchSize(int) - */ - public HibernatePagingItemReaderBuilder fetchSize(int fetchSize) { - this.fetchSize = fetchSize; - - return this; - } - - /** - * A query provider. This should be set only if {@link #queryString(String)} and - * {@link #queryName(String)} have not been set. - * @param queryProvider the query provider - * @return this instance for method chaining - * @see HibernatePagingItemReader#setQueryProvider(HibernateQueryProvider) - */ - public HibernatePagingItemReaderBuilder queryProvider(HibernateQueryProvider queryProvider) { - this.queryProvider = queryProvider; - - return this; - } - - /** - * The HQL query string to execute. This should only be set if - * {@link #queryProvider(HibernateQueryProvider)} and {@link #queryName(String)} have - * not been set. - * @param queryString the HQL query - * @return this instance for method chaining - * @see HibernatePagingItemReader#setQueryString(String) - */ - public HibernatePagingItemReaderBuilder queryString(String queryString) { - this.queryString = queryString; - - return this; - } - - /** - * The Hibernate {@link SessionFactory} to execute the query against. - * @param sessionFactory the session factory - * @return this instance for method chaining - * @see HibernatePagingItemReader#setSessionFactory(SessionFactory) - */ - public HibernatePagingItemReaderBuilder sessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - - return this; - } - - /** - * Indicator for whether to use a {@link org.hibernate.StatelessSession} - * (true) or a {@link org.hibernate.Session} (false). - * @param useStatelessSession Defaults to false - * @return this instance for method chaining - * @see HibernatePagingItemReader#setUseStatelessSession(boolean) - */ - public HibernatePagingItemReaderBuilder useStatelessSession(boolean useStatelessSession) { - this.statelessSession = useStatelessSession; - - return this; - } - - /** - * Returns a fully constructed {@link HibernatePagingItemReader}. - * @return a new {@link HibernatePagingItemReader} - */ - public HibernatePagingItemReader build() { - Assert.notNull(this.sessionFactory, "A SessionFactory must be provided"); - Assert.state(this.fetchSize >= 0, "fetchSize must not be negative"); - - if (this.saveState) { - Assert.hasText(this.name, "A name is required when saveState is set to true"); - } - - if (this.queryProvider == null) { - Assert.state(StringUtils.hasText(queryString) ^ StringUtils.hasText(queryName), - "queryString or queryName must be set"); - } - - HibernatePagingItemReader reader = new HibernatePagingItemReader<>(); - - reader.setSessionFactory(this.sessionFactory); - reader.setSaveState(this.saveState); - reader.setMaxItemCount(this.maxItemCount); - reader.setCurrentItemCount(this.currentItemCount); - reader.setName(this.name); - reader.setFetchSize(this.fetchSize); - reader.setParameterValues(this.parameterValues); - reader.setQueryName(this.queryName); - reader.setQueryProvider(this.queryProvider); - reader.setQueryString(this.queryString); - reader.setPageSize(this.pageSize); - reader.setUseStatelessSession(this.statelessSession); - - return reader; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java index a747228fa0..a4014536d5 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -25,6 +25,7 @@ import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.DataClassRowMapper; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -37,6 +38,8 @@ * @author Mahmoud Ben Hassine * @author Ankur Trapasiya * @author Parikshit Dutta + * @author Fabio Molignoni + * @author Juyoung Kim * @since 4.0 */ public class JdbcCursorItemReaderBuilder { @@ -49,7 +52,7 @@ public class JdbcCursorItemReaderBuilder { private int queryTimeout = AbstractCursorItemReader.VALUE_NOT_SET; - private boolean ignoreWarnings; + private boolean ignoreWarnings = true; private boolean verifyCursorPosition = true; @@ -172,6 +175,11 @@ public JdbcCursorItemReaderBuilder queryTimeout(int queryTimeout) { return this; } + /** + * Set whether SQLWarnings should be ignored (only logged) or exception should be + * thrown. Defaults to {@code true}. + * @param ignoreWarnings if {@code true}, warnings are ignored + */ public JdbcCursorItemReaderBuilder ignoreWarnings(boolean ignoreWarnings) { this.ignoreWarnings = ignoreWarnings; @@ -306,6 +314,19 @@ public JdbcCursorItemReaderBuilder beanRowMapper(Class mappedClass) { return this; } + /** + * Creates a {@link DataClassRowMapper} to be used as your {@link RowMapper}. + * @param mappedClass the class for the row mapper + * @return this instance for method chaining + * @see DataClassRowMapper + * @since 5.2 + */ + public JdbcCursorItemReaderBuilder dataRowMapper(Class mappedClass) { + this.rowMapper = new DataClassRowMapper<>(mappedClass); + + return this; + } + /** * Set whether "autoCommit" should be overridden for the connection used by the * cursor. If not set, defaults to Connection / Datasource default configuration. diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java index 2f9cee1d10..408263ea42 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-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. @@ -36,6 +36,7 @@ import org.springframework.batch.item.database.support.SybasePagingQueryProvider; import org.springframework.batch.support.DatabaseType; import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.DataClassRowMapper; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.util.Assert; @@ -52,6 +53,7 @@ * @author Drummond Dawson * @author Mahmoud Ben Hassine * @author Minsoo Kim + * @author Juyoung Kim * @since 4.0 * @see JdbcPagingItemReader */ @@ -186,6 +188,19 @@ public JdbcPagingItemReaderBuilder beanRowMapper(Class mappedClass) { return this; } + /** + * Creates a {@link DataClassRowMapper} to be used as your {@link RowMapper}. + * @param mappedClass the class for the row mapper + * @return this instance for method chaining + * @see DataClassRowMapper + * @since 5.2 + */ + public JdbcPagingItemReaderBuilder dataRowMapper(Class mappedClass) { + this.rowMapper = new DataClassRowMapper<>(mappedClass); + + return this; + } + /** * A {@link Map} of values to set on the SQL's prepared statement. * @param parameterValues Map of values diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilder.java index 5a1c874fbf..571a5b0a4f 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2021 the original author or authors. + * Copyright 2020-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. @@ -30,6 +30,7 @@ * Builder for {@link JpaCursorItemReader}. * * @author Mahmoud Ben Hassine + * @author Jinwoo Bae * @since 4.3 */ public class JpaCursorItemReaderBuilder { @@ -42,6 +43,8 @@ public class JpaCursorItemReaderBuilder { private Map parameterValues; + private Map hintValues; + private boolean saveState = true; private String name; @@ -112,6 +115,19 @@ public JpaCursorItemReaderBuilder parameterValues(Map paramet return this; } + /** + * A map of hint values to be set on the query. The key of the map is the name of the + * hint to be applied, with the value being the specific setting for that hint. + * @param hintValues map of query hints + * @return this instance for method chaining + * @see JpaCursorItemReader#setHintValues(Map) + * @since 5.2 + */ + public JpaCursorItemReaderBuilder hintValues(Map hintValues) { + this.hintValues = hintValues; + return this; + } + /** * A query provider. This should be set only if {@link #queryString(String)} have not * been set. @@ -169,10 +185,12 @@ public JpaCursorItemReader build() { reader.setQueryProvider(this.queryProvider); reader.setQueryString(this.queryString); reader.setParameterValues(this.parameterValues); + reader.setHintValues(this.hintValues); reader.setCurrentItemCount(this.currentItemCount); reader.setMaxItemCount(this.maxItemCount); reader.setSaveState(this.saveState); reader.setName(this.name); + return reader; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java index adf62a5d87..0bb2a85c46 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-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,7 @@ * * @author Michael Minella * @author Glenn Renfro + * @author Jinwoo Bae * @since 4.0 */ @@ -38,6 +39,8 @@ public class JpaPagingItemReaderBuilder { private Map parameterValues; + private Map hintValues; + private boolean transacted = true; private String queryString; @@ -129,6 +132,19 @@ public JpaPagingItemReaderBuilder parameterValues(Map paramet return this; } + /** + * A map of hint values to be set on the query. The key of the map is the name of the + * hint to be applied, with the value being the specific setting for that hint. + * @param hintValues map of query hints + * @return this instance for method chaining + * @see JpaPagingItemReader#setHintValues(Map) + * @since 5.2 + */ + public JpaPagingItemReaderBuilder hintValues(Map hintValues) { + this.hintValues = hintValues; + return this; + } + /** * A query provider. This should be set only if {@link #queryString(String)} have not * been set. @@ -204,6 +220,7 @@ public JpaPagingItemReader build() { reader.setQueryString(this.queryString); reader.setPageSize(this.pageSize); reader.setParameterValues(this.parameterValues); + reader.setHintValues(this.hintValues); reader.setEntityManagerFactory(this.entityManagerFactory); reader.setQueryProvider(this.queryProvider); reader.setTransacted(this.transacted); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractHibernateQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractHibernateQueryProvider.java deleted file mode 100644 index 23332c065f..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractHibernateQueryProvider.java +++ /dev/null @@ -1,71 +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.item.database.orm; - -import org.hibernate.query.Query; -import org.hibernate.Session; -import org.hibernate.StatelessSession; - -/** - *

    - * Abstract Hibernate Query Provider to serve as a base class for all Hibernate - * {@link Query} providers. - *

    - * - *

    - * The implementing provider can be configured to use either {@link StatelessSession} - * sufficient for simple mappings without the need to cascade to associated objects or - * standard Hibernate {@link Session} for more advanced mappings or when caching is - * desired. - *

    - * - * @author Anatoly Polinsky - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @since 2.1 - * - */ -@Deprecated(since = "5.0", forRemoval = true) -public abstract class AbstractHibernateQueryProvider implements HibernateQueryProvider { - - private StatelessSession statelessSession; - - private Session statefulSession; - - @Override - public void setStatelessSession(StatelessSession statelessSession) { - this.statelessSession = statelessSession; - } - - @Override - public void setSession(Session statefulSession) { - this.statefulSession = statefulSession; - } - - public boolean isStatelessSession() { - return this.statefulSession == null && this.statelessSession != null; - } - - protected StatelessSession getStatelessSession() { - return statelessSession; - } - - protected Session getStatefulSession() { - return statefulSession; - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractJpaQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractJpaQueryProvider.java index 0da409e537..f44cb4da39 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractJpaQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/AbstractJpaQueryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2021 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. @@ -39,8 +39,8 @@ public abstract class AbstractJpaQueryProvider implements JpaQueryProvider, Init /** *

    * Public setter to override the entityManager that was created by this - * {@link HibernateQueryProvider}. This is currently needed to allow - * {@link HibernateQueryProvider} to participate in a user's managed transaction. + * {@link JpaQueryProvider}. This is currently needed to allow + * {@link JpaQueryProvider} to participate in a user's managed transaction. *

    * @param entityManager EntityManager to use */ diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateNativeQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateNativeQueryProvider.java deleted file mode 100644 index c2bd2a9a0e..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateNativeQueryProvider.java +++ /dev/null @@ -1,76 +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.item.database.orm; - -import org.hibernate.query.NativeQuery; -import org.hibernate.query.Query; - -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - *

    - * This query provider creates Hibernate {@link Query}s from injected native SQL queries. - * This is useful if there is a need to utilize database-specific features such as query - * hints, the CONNECT keyword in Oracle, etc. - *

    - * - * @author Anatoly Polinsky - * @author Mahmoud Ben Hassine - * @param entity returned by executing the query - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaNativeQueryProvider} - * instead. - */ -@Deprecated(since = "5.0", forRemoval = true) -public class HibernateNativeQueryProvider extends AbstractHibernateQueryProvider { - - private String sqlQuery; - - private Class entityClass; - - /** - *

    - * Create an {@link NativeQuery} from the session provided (preferring stateless if - * both are available). - *

    - */ - @Override - @SuppressWarnings("unchecked") - public NativeQuery createQuery() { - - if (isStatelessSession()) { - return getStatelessSession().createNativeQuery(sqlQuery).addEntity(entityClass); - } - else { - return getStatefulSession().createNativeQuery(sqlQuery).addEntity(entityClass); - } - } - - public void setSqlQuery(String sqlQuery) { - this.sqlQuery = sqlQuery; - } - - public void setEntityClass(Class entityClazz) { - this.entityClass = entityClazz; - } - - public void afterPropertiesSet() throws Exception { - Assert.state(StringUtils.hasText(sqlQuery), "Native SQL query cannot be empty"); - Assert.state(entityClass != null, "Entity class cannot be NULL"); - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateQueryProvider.java deleted file mode 100644 index c51930baec..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/orm/HibernateQueryProvider.java +++ /dev/null @@ -1,76 +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.item.database.orm; - -import org.hibernate.query.Query; -import org.hibernate.Session; -import org.hibernate.StatelessSession; -import org.springframework.batch.item.ItemReader; - -/** - *

    - * Interface defining the functionality to be provided for generating queries for use with - * Hibernate {@link ItemReader}s or other custom built artifacts. - *

    - * - * @author Anatoly Polinsky - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @since 2.1 - * @deprecated since 5.0 for removal in 5.2. Use the {@link JpaQueryProvider} instead. - * - */ -@Deprecated(since = "5.0", forRemoval = true) -public interface HibernateQueryProvider { - - /** - *

    - * Create the query object which type will be determined by the underline - * implementation (e.g. Hibernate, JPA, etc.) - *

    - * @return created query - */ - Query createQuery(); - - /** - *

    - * Inject a {@link Session} that can be used as a factory for queries. The state of - * the session is controlled by the caller (i.e. it should be closed if necessary). - *

    - * - *

    - * Use either this method or {@link #setStatelessSession(StatelessSession)} - *

    - * @param session the {@link Session} to set - */ - void setSession(Session session); - - /** - *

    - * Inject a {@link StatelessSession} that can be used as a factory for queries. The - * state of the session is controlled by the caller (i.e. it should be closed if - * necessary). - *

    - * - *

    - * Use either this method or {@link #setSession(Session)} - *

    - * @param session the {@link StatelessSession} to set - */ - void setStatelessSession(StatelessSession session); - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/AbstractSqlPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/AbstractSqlPagingQueryProvider.java index af9a17b393..dc78066fc5 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/AbstractSqlPagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/AbstractSqlPagingQueryProvider.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. @@ -194,7 +194,7 @@ public void init(DataSource dataSource) throws Exception { } List namedParameters = new ArrayList<>(); parameterCount = JdbcParameterUtils.countParameterPlaceholders(sql.toString(), namedParameters); - if (namedParameters.size() > 0) { + if (!namedParameters.isEmpty()) { if (parameterCount != namedParameters.size()) { throw new InvalidDataAccessApiUsageException( "You can't use both named parameters and classic \"?\" placeholders: " + sql); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/Db2PagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/Db2PagingQueryProvider.java index f29f868190..660eb430b9 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/Db2PagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/Db2PagingQueryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2021 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. @@ -27,7 +27,7 @@ * @author Mahmoud Ben Hassine * @since 2.0 */ -public class Db2PagingQueryProvider extends SqlWindowingPagingQueryProvider { +public class Db2PagingQueryProvider extends AbstractSqlPagingQueryProvider { @Override public String generateFirstPageQuery(int pageSize) { @@ -44,11 +44,6 @@ public String generateRemainingPagesQuery(int pageSize) { } } - @Override - protected Object getSubQueryAlias() { - return "AS TMP_SUB "; - } - private String buildLimitClause(int pageSize) { return new StringBuilder().append("FETCH FIRST ").append(pageSize).append(" ROWS ONLY").toString(); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DerbyPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DerbyPagingQueryProvider.java index b2f4ab422c..015454f90e 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DerbyPagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DerbyPagingQueryProvider.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. @@ -16,76 +16,37 @@ package org.springframework.batch.item.database.support; -import java.sql.DatabaseMetaData; -import javax.sql.DataSource; - import org.springframework.batch.item.database.PagingQueryProvider; -import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.util.StringUtils; /** - * Derby implementation of a {@link PagingQueryProvider} using standard SQL:2003 windowing - * functions. These features are supported starting with Apache Derby version 10.4.1.3. - *

    - * As the OVER() function does not support the ORDER BY clause a sub query is instead used - * to order the results before the ROW_NUM restriction is applied + * Derby implementation of a {@link PagingQueryProvider} using database specific features. * * @author Thomas Risberg * @author David Thexton * @author Michael Minella + * @author Henning Pƶttker * @since 2.0 */ -public class DerbyPagingQueryProvider extends SqlWindowingPagingQueryProvider { - - private static final String MINIMAL_DERBY_VERSION = "10.4.1.3"; +public class DerbyPagingQueryProvider extends AbstractSqlPagingQueryProvider { @Override - public void init(DataSource dataSource) throws Exception { - super.init(dataSource); - String version = JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getDatabaseProductVersion); - if (!isDerbyVersionSupported(version)) { - throw new InvalidDataAccessResourceUsageException( - "Apache Derby version " + version + " is not supported by this class, Only version " - + MINIMAL_DERBY_VERSION + " or later is supported"); - } - } - - // derby version numbering is M.m.f.p [ {alpha|beta} ] see - // https://db.apache.org/derby/papers/versionupgrade.html#Basic+Numbering+Scheme - private boolean isDerbyVersionSupported(String version) { - String[] minimalVersionParts = MINIMAL_DERBY_VERSION.split("\\."); - String[] versionParts = version.split("[\\. ]"); - for (int i = 0; i < minimalVersionParts.length; i++) { - int minimalVersionPart = Integer.parseInt(minimalVersionParts[i]); - int versionPart = Integer.parseInt(versionParts[i]); - if (versionPart < minimalVersionPart) { - return false; - } - else if (versionPart > minimalVersionPart) { - return true; - } - } - return true; + public String generateFirstPageQuery(int pageSize) { + return SqlPagingQueryUtils.generateLimitSqlQuery(this, false, buildLimitClause(pageSize)); } @Override - protected String getOrderedQueryAlias() { - return "TMP_ORDERED"; - } - - @Override - protected String getOverClause() { - return ""; - } - - @Override - protected String getOverSubstituteClauseStart() { - return " FROM (SELECT " + getSelectClause(); + public String generateRemainingPagesQuery(int pageSize) { + if (StringUtils.hasText(getGroupClause())) { + return SqlPagingQueryUtils.generateLimitGroupedSqlQuery(this, buildLimitClause(pageSize)); + } + else { + return SqlPagingQueryUtils.generateLimitSqlQuery(this, true, buildLimitClause(pageSize)); + } } - @Override - protected String getOverSubstituteClauseEnd() { - return " ) AS " + getOrderedQueryAlias(); + private String buildLimitClause(int pageSize) { + return new StringBuilder("FETCH FIRST ").append(pageSize).append(" ROWS ONLY").toString(); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HsqlPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HsqlPagingQueryProvider.java index fed56bab8b..49e3741a4f 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HsqlPagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HsqlPagingQueryProvider.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. @@ -25,6 +25,7 @@ * * @author Thomas Risberg * @author Michael Minella + * @author Mahmoud Ben Hassine * @since 2.0 */ public class HsqlPagingQueryProvider extends AbstractSqlPagingQueryProvider { @@ -37,7 +38,7 @@ public String generateFirstPageQuery(int pageSize) { @Override public String generateRemainingPagesQuery(int pageSize) { if (StringUtils.hasText(getGroupClause())) { - return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, true, buildTopClause(pageSize)); + return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, buildTopClause(pageSize)); } else { return SqlPagingQueryUtils.generateTopSqlQuery(this, true, buildTopClause(pageSize)); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryUtils.java index f332f92fc3..2ae66e4388 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryUtils.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. @@ -31,9 +31,13 @@ * @author Dave Syer * @author Michael Minella * @author Mahmoud Ben Hassine + * @author Taeik Lim * @since 2.0 */ -public class SqlPagingQueryUtils { +public abstract class SqlPagingQueryUtils { + + private SqlPagingQueryUtils() { + } /** * Generate SQL query string using a LIMIT clause @@ -140,7 +144,10 @@ public static String generateTopSqlQuery(AbstractSqlPagingQueryProvider provider * to the first page (false) * @param topClause the implementation specific top clause to be used * @return the generated query + * @deprecated since v5.2 in favor of + * {@link #generateGroupedTopSqlQuery(AbstractSqlPagingQueryProvider, String)} */ + @Deprecated public static String generateGroupedTopSqlQuery(AbstractSqlPagingQueryProvider provider, boolean remainingPageQuery, String topClause) { StringBuilder sql = new StringBuilder(); @@ -157,6 +164,29 @@ public static String generateGroupedTopSqlQuery(AbstractSqlPagingQueryProvider p return sql.toString(); } + /** + * Generate SQL query string using a TOP clause + * @param provider {@link AbstractSqlPagingQueryProvider} providing the implementation + * specifics + * @param topClause the implementation specific top clause to be used + * @return the generated query + * @since 5.2 + */ + public static String generateGroupedTopSqlQuery(AbstractSqlPagingQueryProvider provider, String topClause) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT ").append(topClause).append(" * FROM ("); + sql.append("SELECT ").append(provider.getSelectClause()); + sql.append(" FROM ").append(provider.getFromClause()); + sql.append(provider.getWhereClause() == null ? "" : " WHERE " + provider.getWhereClause()); + buildGroupByClause(provider, sql); + sql.append(") AS MAIN_QRY "); + sql.append("WHERE "); + buildSortConditions(provider, sql); + sql.append(" ORDER BY ").append(buildSortClause(provider)); + + return sql.toString(); + } + /** * Generate SQL query string using a ROW_NUM condition * @param provider {@link AbstractSqlPagingQueryProvider} providing the implementation diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProvider.java index 59332cf271..b1c79763b1 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2012 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. @@ -25,9 +25,10 @@ * * @author Thomas Risberg * @author Michael Minella + * @author Mahmoud Ben Hassine * @since 2.0 */ -public class SqlServerPagingQueryProvider extends SqlWindowingPagingQueryProvider { +public class SqlServerPagingQueryProvider extends AbstractSqlPagingQueryProvider { @Override public String generateFirstPageQuery(int pageSize) { @@ -37,18 +38,13 @@ public String generateFirstPageQuery(int pageSize) { @Override public String generateRemainingPagesQuery(int pageSize) { if (StringUtils.hasText(getGroupClause())) { - return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, true, buildTopClause(pageSize)); + return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, buildTopClause(pageSize)); } else { return SqlPagingQueryUtils.generateTopSqlQuery(this, true, buildTopClause(pageSize)); } } - @Override - protected Object getSubQueryAlias() { - return "AS TMP_SUB "; - } - private String buildTopClause(int pageSize) { return new StringBuilder().append("TOP ").append(pageSize).toString(); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProvider.java index 1f75726aaf..00e0d04711 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProvider.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,7 +26,9 @@ * @author Thomas Risberg * @author Michael Minella * @since 2.0 + * @deprecated since 5.2.1 with no replacement. Scheduled for removal in 6.0. */ +@Deprecated(forRemoval = true) public class SqlWindowingPagingQueryProvider extends AbstractSqlPagingQueryProvider { @Override diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SybasePagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SybasePagingQueryProvider.java index d91e1f44c4..ade0af5266 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SybasePagingQueryProvider.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SybasePagingQueryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2012 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. @@ -25,9 +25,10 @@ * * @author Thomas Risberg * @author Michael Minella + * @author Mahmoud Ben Hassine * @since 2.0 */ -public class SybasePagingQueryProvider extends SqlWindowingPagingQueryProvider { +public class SybasePagingQueryProvider extends AbstractSqlPagingQueryProvider { @Override public String generateFirstPageQuery(int pageSize) { @@ -37,18 +38,13 @@ public String generateFirstPageQuery(int pageSize) { @Override public String generateRemainingPagesQuery(int pageSize) { if (StringUtils.hasText(getGroupClause())) { - return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, true, buildTopClause(pageSize)); + return SqlPagingQueryUtils.generateGroupedTopSqlQuery(this, buildTopClause(pageSize)); } else { return SqlPagingQueryUtils.generateTopSqlQuery(this, true, buildTopClause(pageSize)); } } - @Override - protected Object getSubQueryAlias() { - return ""; - } - private String buildTopClause(int pageSize) { return new StringBuilder().append("TOP ").append(pageSize).toString(); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/DefaultBufferedReaderFactory.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/DefaultBufferedReaderFactory.java index 7684d8791b..01c5995509 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/DefaultBufferedReaderFactory.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/DefaultBufferedReaderFactory.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. @@ -18,7 +18,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; import org.springframework.core.io.Resource; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java index 1480cba407..835abb3527 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.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. @@ -36,7 +36,11 @@ *

    * Note that new resources are created only at chunk boundaries i.e. the number of items * written into one resource is between the limit set by - * {@link #setItemCountLimitPerResource(int)} and (limit + chunk size). + *

    + * This writer will create an output file only when there are items to write, which means + * there would be no empty file created if no items are passed (for example when all items + * are filtered or skipped during the processing phase). + *

    * * @param item type * @author Robert Kasanicky diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactory.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactory.java index dba352efff..6b8fede984 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactory.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactory.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. @@ -123,25 +123,24 @@ private boolean isEndOfLine(StringBuilder buffer, StringBuilder candidate, int n } char c = (char) next; - if (ending.charAt(0) == c || candidate.length() > 0) { + if (ending.charAt(0) == c || !candidate.isEmpty()) { candidate.append(c); } - - if (candidate.length() == 0) { + else { buffer.append(c); return false; } - boolean end = ending.equals(candidate.toString()); - if (end) { + if (ending.contentEquals(candidate)) { candidate.delete(0, candidate.length()); + return true; } - else if (candidate.length() >= ending.length()) { - buffer.append(candidate); - candidate.delete(0, candidate.length()); + while (!ending.startsWith(candidate.toString())) { + buffer.append(candidate.charAt(0)); + candidate.delete(0, 1); } - return end; + return false; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java index 860a4a660d..a86079cc0f 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 the original author or authors. + * Copyright 2020-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. @@ -69,7 +69,7 @@ public RecordFieldSetMapper(Class targetType, ConversionService conversionSer public T mapFieldSet(FieldSet fieldSet) { Assert.isTrue(fieldSet.getFieldCount() == this.constructorParameterNames.length, "Fields count must be equal to record components count"); - Assert.isTrue(fieldSet.hasNames(), "Field names must specified"); + Assert.isTrue(fieldSet.hasNames(), "Field names must be specified"); Object[] args = new Object[0]; if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { args = new Object[this.constructorParameterNames.length]; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSet.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSet.java index 9214291e09..540a8236aa 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSet.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSet.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. @@ -45,17 +45,13 @@ public class DefaultFieldSet implements FieldSet { private final static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd"; - private DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN); + private DateFormat dateFormat; - { - dateFormat.setLenient(false); - } + private NumberFormat numberFormat; - private NumberFormat numberFormat = NumberFormat.getInstance(Locale.US); + private String grouping; - private String grouping = ","; - - private String decimal = "."; + private String decimal; /** * The fields wrapped by this 'FieldSet' instance. @@ -77,6 +73,10 @@ public final void setNumberFormat(NumberFormat numberFormat) { } } + private static NumberFormat getDefaultNumberFormat() { + return NumberFormat.getInstance(Locale.US); + } + /** * The {@link DateFormat} to use for parsing dates. If unset the default pattern is * ISO standard yyyy-MM-dd. @@ -86,15 +86,35 @@ public void setDateFormat(DateFormat dateFormat) { this.dateFormat = dateFormat; } + private static DateFormat getDefaultDateFormat() { + DateFormat dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN); + dateFormat.setLenient(false); + return dateFormat; + } + /** * Create a FieldSet with anonymous tokens. They can only be retrieved by column * number. * @param tokens the token values + * @param dateFormat the {@link DateFormat} to use + * @param numberFormat the {@link NumberFormat} to use * @see FieldSet#readString(int) + * @since 5.2 */ - public DefaultFieldSet(String[] tokens) { + public DefaultFieldSet(String[] tokens, @Nullable DateFormat dateFormat, @Nullable NumberFormat numberFormat) { this.tokens = tokens == null ? null : tokens.clone(); - setNumberFormat(NumberFormat.getInstance(Locale.US)); + setDateFormat(dateFormat == null ? getDefaultDateFormat() : dateFormat); + setNumberFormat(numberFormat == null ? getDefaultNumberFormat() : numberFormat); + } + + /** + * Create a FieldSet with anonymous tokens. They can only be retrieved by column + * number. + * @param tokens the token values + * @see FieldSet#readString(int) + */ + public DefaultFieldSet(String[] tokens) { + this(tokens, null, null); } /** @@ -102,9 +122,13 @@ public DefaultFieldSet(String[] tokens) { * by name or by column number. * @param tokens the token values * @param names the names of the tokens + * @param dateFormat the {@link DateFormat} to use + * @param numberFormat the {@link NumberFormat} to use * @see FieldSet#readString(String) + * @since 5.2 */ - public DefaultFieldSet(String[] tokens, String[] names) { + public DefaultFieldSet(String[] tokens, String[] names, @Nullable DateFormat dateFormat, + @Nullable NumberFormat numberFormat) { Assert.notNull(tokens, "Tokens must not be null"); Assert.notNull(names, "Names must not be null"); if (tokens.length != names.length) { @@ -113,7 +137,19 @@ public DefaultFieldSet(String[] tokens, String[] names) { } this.tokens = tokens.clone(); this.names = Arrays.asList(names); - setNumberFormat(NumberFormat.getInstance(Locale.US)); + setDateFormat(dateFormat == null ? getDefaultDateFormat() : dateFormat); + setNumberFormat(numberFormat == null ? getDefaultNumberFormat() : numberFormat); + } + + /** + * Create a FieldSet with named tokens. The token values can then be retrieved either + * by name or by column number. + * @param tokens the token values + * @param names the names of the tokens + * @see FieldSet#readString(String) + */ + public DefaultFieldSet(String[] tokens, String[] names) { + this(tokens, names, null, null); } @Override diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSetFactory.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSetFactory.java index a958f50a1f..fe3dd0989c 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSetFactory.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/DefaultFieldSetFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-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,8 @@ import java.text.DateFormat; import java.text.NumberFormat; +import org.springframework.lang.Nullable; + /** * Default implementation of {@link FieldSetFactory} with no special knowledge of the * {@link FieldSet} required. Returns a {@link DefaultFieldSet} from both factory methods. @@ -32,6 +34,23 @@ public class DefaultFieldSetFactory implements FieldSetFactory { private NumberFormat numberFormat; + /** + * Default constructor. + */ + public DefaultFieldSetFactory() { + } + + /** + * Convenience constructor + * @param dateFormat the {@link DateFormat} to use for parsing dates + * @param numberFormat the {@link NumberFormat} to use for parsing numbers + * @since 5.2 + */ + public DefaultFieldSetFactory(@Nullable DateFormat dateFormat, @Nullable NumberFormat numberFormat) { + this.dateFormat = dateFormat; + this.numberFormat = numberFormat; + } + /** * The {@link NumberFormat} to use for parsing numbers. If unset then * {@link java.util.Locale#US} will be used. @@ -55,8 +74,7 @@ public void setDateFormat(DateFormat dateFormat) { */ @Override public FieldSet create(String[] values, String[] names) { - DefaultFieldSet fieldSet = new DefaultFieldSet(values, names); - return enhance(fieldSet); + return new DefaultFieldSet(values, names, dateFormat, numberFormat); } /** @@ -64,18 +82,7 @@ public FieldSet create(String[] values, String[] names) { */ @Override public FieldSet create(String[] values) { - DefaultFieldSet fieldSet = new DefaultFieldSet(values); - return enhance(fieldSet); - } - - private FieldSet enhance(DefaultFieldSet fieldSet) { - if (dateFormat != null) { - fieldSet.setDateFormat(dateFormat); - } - if (numberFormat != null) { - fieldSet.setNumberFormat(numberFormat); - } - return fieldSet; + return new DefaultFieldSet(values, dateFormat, numberFormat); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregator.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregator.java index b5c7fa8ef9..ddb447047d 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregator.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregator.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. @@ -18,16 +18,19 @@ import java.util.Collection; +import org.springframework.util.Assert; + /** * An implementation of {@link LineAggregator} that concatenates a collection of items of - * a common type with the system line separator. + * a common type with a line separator. * * @author Dave Syer + * @author Mahmoud Ben Hassine * */ public class RecursiveCollectionLineAggregator implements LineAggregator> { - private static final String LINE_SEPARATOR = System.getProperty("line.separator"); + private String lineSeparator = System.lineSeparator(); private LineAggregator delegate = new PassThroughLineAggregator<>(); @@ -41,13 +44,23 @@ public void setDelegate(LineAggregator delegate) { this.delegate = delegate; } + /** + * Set the line separator to use. Defaults to the System's line separator. + * @param lineSeparator the line separator to use. Must not be {@code null}. + * @since 5.2 + */ + public void setLineSeparator(String lineSeparator) { + Assert.notNull(lineSeparator, "The line separator must not be null"); + this.lineSeparator = lineSeparator; + } + @Override public String aggregate(Collection items) { StringBuilder builder = new StringBuilder(); for (T value : items) { - builder.append(delegate.aggregate(value)).append(LINE_SEPARATOR); + builder.append(delegate.aggregate(value)).append(lineSeparator); } - return builder.delete(builder.length() - LINE_SEPARATOR.length(), builder.length()).toString(); + return builder.delete(builder.length() - lineSeparator.length(), builder.length()).toString(); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/ConsumerItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/ConsumerItemWriter.java new file mode 100644 index 0000000000..5095659bbb --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/ConsumerItemWriter.java @@ -0,0 +1,49 @@ +/* + * 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.item.function; + +import java.util.function.Consumer; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; +import org.springframework.util.Assert; + +/** + * Adapter for a {@link Consumer} to an {@link ItemWriter}. + * + * @param type of items to write + * @author Mahmoud Ben Hassine + * @since 5.2 + */ +public class ConsumerItemWriter implements ItemWriter { + + private final Consumer consumer; + + /** + * Create a new {@link ConsumerItemWriter}. + * @param consumer the consumer to use to write items. Must not be {@code null}. + */ + public ConsumerItemWriter(Consumer consumer) { + Assert.notNull(consumer, "A consumer is required"); + this.consumer = consumer; + } + + @Override + public void write(Chunk items) throws Exception { + items.forEach(this.consumer); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/PredicateFilteringItemProcessor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/PredicateFilteringItemProcessor.java new file mode 100644 index 0000000000..553c85a797 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/PredicateFilteringItemProcessor.java @@ -0,0 +1,49 @@ +/* + * 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.item.function; + +import java.util.function.Predicate; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.util.Assert; + +/** + * A filtering {@link ItemProcessor} that is based on a {@link Predicate}. Items for which + * the predicate returns {@code true} will be filtered. + * + * @param type of item to process + * @author Mahmoud Ben Hassine + * @since 5.2 + */ +public class PredicateFilteringItemProcessor implements ItemProcessor { + + private final Predicate predicate; + + /** + * Create a new {@link PredicateFilteringItemProcessor}. + * @param predicate the predicate to use to filter items. Must not be {@code null}. + */ + public PredicateFilteringItemProcessor(Predicate predicate) { + Assert.notNull(predicate, "A predicate is required"); + this.predicate = predicate; + } + + @Override + public T process(T item) throws Exception { + return this.predicate.test(item) ? null : item; + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/SupplierItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/SupplierItemReader.java new file mode 100644 index 0000000000..48dd87e89c --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/function/SupplierItemReader.java @@ -0,0 +1,48 @@ +/* + * 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.item.function; + +import java.util.function.Supplier; + +import org.springframework.batch.item.ItemReader; +import org.springframework.util.Assert; + +/** + * Adapter for a {@link Supplier} to an {@link ItemReader}. + * + * @param type of items to read + * @author Mahmoud Ben Hassine + * @since 5.2 + */ +public class SupplierItemReader implements ItemReader { + + private final Supplier supplier; + + /** + * Create a new {@link SupplierItemReader}. + * @param supplier the supplier to use to read items. Must not be {@code null}. + */ + public SupplierItemReader(Supplier supplier) { + Assert.notNull(supplier, "A supplier is required"); + this.supplier = supplier; + } + + @Override + public T read() throws Exception { + return this.supplier.get(); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java index 48787430f1..c1b49fb4bd 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-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,6 +37,7 @@ * * @param type of the target object * @author Mahmoud Ben Hassine + * @author Jimmy Praet * @since 4.1 */ public class GsonJsonObjectReader implements JsonObjectReader { @@ -102,4 +103,11 @@ public void close() throws Exception { this.jsonReader.close(); } + @Override + public void jumpToItem(int itemIndex) throws Exception { + for (int i = 0; i < itemIndex; i++) { + this.jsonReader.skipValue(); + } + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java index 04d7a7b970..df1879240c 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-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,6 +34,7 @@ * * @param type of the target object * @author Mahmoud Ben Hassine + * @author Jimmy Praet * @since 4.1 */ public class JacksonJsonObjectReader implements JsonObjectReader { @@ -98,4 +99,13 @@ public void close() throws Exception { this.jsonParser.close(); } + @Override + public void jumpToItem(int itemIndex) throws Exception { + for (int i = 0; i < itemIndex; i++) { + if (this.jsonParser.nextToken() == JsonToken.START_OBJECT) { + this.jsonParser.skipChildren(); + } + } + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java index c39f9886ea..a7fdc830f1 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020 the original author or authors. + * Copyright 2018-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 @@ * * @param the type of json objects to read * @author Mahmoud Ben Hassine + * @author Jimmy Praet * @since 4.1 */ public class JsonItemReader extends AbstractItemCountingItemStreamItemReader @@ -136,4 +137,9 @@ protected void doClose() throws Exception { this.jsonObjectReader.close(); } + @Override + protected void jumpToItem(int itemIndex) throws Exception { + this.jsonObjectReader.jumpToItem(itemIndex); + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java index 5793d2e092..d143b71c8d 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. @@ -25,6 +25,7 @@ * * @param type of the target object * @author Mahmoud Ben Hassine + * @author Jimmy Praet * @since 4.1 */ public interface JsonObjectReader { @@ -54,4 +55,19 @@ default void close() throws Exception { } + /** + * Move to the given item index. Implementations should override this method if there + * is a more efficient way of moving to given index than re-reading the input using + * {@link #read()}. + * @param itemIndex index of item (0 based) to jump to. + * @throws Exception Allows implementations to throw checked exceptions for + * interpretation by the framework + * @since 5.2 + */ + default void jumpToItem(int itemIndex) throws Exception { + for (int i = 0; i < itemIndex; i++) { + read(); + } + } + } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemReader.java new file mode 100644 index 0000000000..e5e411045b --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemReader.java @@ -0,0 +1,63 @@ +/* + * 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.item.queue; + +import org.springframework.batch.item.ItemReader; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +/** + * This is an {@link ItemReader} that reads items from a {@link BlockingQueue}. It stops + * reading (ie returns {@code null}) if no items are available in the queue after a + * configurable timeout. + * + * @param type of items to read. + * @author Mahmoud Ben Hassine + * @since 5.2.0 + */ +public class BlockingQueueItemReader implements ItemReader { + + private final BlockingQueue queue; + + private long timeout = 1L; + + private TimeUnit timeUnit = TimeUnit.SECONDS; + + /** + * Create a new {@link BlockingQueueItemReader}. + * @param queue the queue to read items from + */ + public BlockingQueueItemReader(BlockingQueue queue) { + this.queue = queue; + } + + /** + * Set the reading timeout and time unit. Defaults to 1 second. + * @param timeout the timeout after which the reader stops reading + * @param timeUnit the unit of the timeout + */ + public void setTimeout(long timeout, TimeUnit timeUnit) { + this.timeout = timeout; + this.timeUnit = timeUnit; + } + + @Override + public T read() throws Exception { + return this.queue.poll(this.timeout, this.timeUnit); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemWriter.java new file mode 100644 index 0000000000..68a667b001 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/BlockingQueueItemWriter.java @@ -0,0 +1,49 @@ +/* + * 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.item.queue; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ItemWriter; + +import java.util.concurrent.BlockingQueue; + +/** + * This is an {@link ItemWriter} that writes items to a {@link BlockingQueue}. + * + * @param type of items to write + * @since 5.2.0 + * @author Mahmoud Ben Hassine + */ +public class BlockingQueueItemWriter implements ItemWriter { + + private final BlockingQueue queue; + + /** + * Create a new {@link BlockingQueueItemWriter}. + * @param queue the queue to write items to + */ + public BlockingQueueItemWriter(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public void write(Chunk items) throws Exception { + for (T item : items) { + this.queue.put(item); + } + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilder.java new file mode 100644 index 0000000000..9c305ca04f --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilder.java @@ -0,0 +1,71 @@ +/* + * 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.item.queue.builder; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.springframework.batch.item.queue.BlockingQueueItemReader; +import org.springframework.util.Assert; + +/** + * Builder for {@link BlockingQueueItemReader}. + * + * @param type of items to read + * @since 5.2.0 + * @author Mahmoud Ben Hassine + */ +public class BlockingQueueItemReaderBuilder { + + private BlockingQueue queue; + + private long timeout = 1L; + + private TimeUnit timeUnit = TimeUnit.SECONDS; + + /** + * Set the queue to read items from. + * @param queue the queue to read items from. + * @return this instance of the builder + */ + public BlockingQueueItemReaderBuilder queue(BlockingQueue queue) { + this.queue = queue; + return this; + } + + /** + * Set the reading timeout. Defaults to 1 second. + * @param timeout the reading timeout. + * @return this instance of the builder + */ + public BlockingQueueItemReaderBuilder timeout(long timeout, TimeUnit timeUnit) { + this.timeout = timeout; + this.timeUnit = timeUnit; + return this; + } + + /** + * Create a configured {@link BlockingQueueItemReader}. + * @return a configured {@link BlockingQueueItemReader}. + */ + public BlockingQueueItemReader build() { + Assert.state(this.queue != null, "The blocking queue is required."); + BlockingQueueItemReader blockingQueueItemReader = new BlockingQueueItemReader<>(this.queue); + blockingQueueItemReader.setTimeout(this.timeout, this.timeUnit); + return blockingQueueItemReader; + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilder.java new file mode 100644 index 0000000000..6e7fe772bd --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilder.java @@ -0,0 +1,53 @@ +/* + * 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.item.queue.builder; + +import java.util.concurrent.BlockingQueue; + +import org.springframework.batch.item.queue.BlockingQueueItemWriter; +import org.springframework.util.Assert; + +/** + * Builder for a {@link BlockingQueueItemWriter}. + * + * @param type of items to write + * @since 5.2.0 + * @author Mahmoud Ben Hassine + */ +public class BlockingQueueItemWriterBuilder { + + private BlockingQueue queue; + + /** + * Create a new {@link BlockingQueueItemWriterBuilder} + * @param queue the queue to write items to + * @return this instance of the builder + */ + public BlockingQueueItemWriterBuilder queue(BlockingQueue queue) { + this.queue = queue; + return this; + } + + /** + * Create a configured {@link BlockingQueueItemWriter}. + * @return a configured {@link BlockingQueueItemWriter}. + */ + public BlockingQueueItemWriter build() { + Assert.state(this.queue != null, "The blocking queue is required."); + return new BlockingQueueItemWriter<>(this.queue); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java new file mode 100644 index 0000000000..06148a346c --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java @@ -0,0 +1,87 @@ +/* + * 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.item.support; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.ItemStreamReader; + +/** + * Composite reader that delegates reading to a list of {@link ItemStreamReader}s. This + * implementation is not thread-safe. + * + * @author Mahmoud Ben Hassine + * @param type of objects to read + * @since 5.2 + */ +public class CompositeItemReader implements ItemStreamReader { + + private final List> delegates; + + private final Iterator> delegatesIterator; + + private ItemStreamReader currentDelegate; + + /** + * Create a new {@link CompositeItemReader}. + * @param delegates the delegate readers to read data + */ + public CompositeItemReader(List> delegates) { + this.delegates = delegates; + this.delegatesIterator = this.delegates.iterator(); + this.currentDelegate = this.delegatesIterator.hasNext() ? this.delegatesIterator.next() : null; + } + + // TODO: check if we need to open/close delegates on the fly in read() to avoid + // opening resources early for a long time + @Override + public void open(ExecutionContext executionContext) throws ItemStreamException { + for (ItemStreamReader delegate : delegates) { + delegate.open(executionContext); + } + } + + @Override + public T read() throws Exception { + if (this.currentDelegate == null) { + return null; + } + T item = currentDelegate.read(); + if (item == null) { + currentDelegate = this.delegatesIterator.hasNext() ? this.delegatesIterator.next() : null; + return read(); + } + return item; + } + + @Override + public void update(ExecutionContext executionContext) throws ItemStreamException { + if (this.currentDelegate != null) { + this.currentDelegate.update(executionContext); + } + } + + @Override + public void close() throws ItemStreamException { + for (ItemStreamReader delegate : delegates) { + delegate.close(); + } + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ListItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ListItemWriter.java index 58247b9ad7..773cd6c3c0 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ListItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/ListItemWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-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,7 +40,7 @@ public void write(Chunk chunk) throws Exception { writtenItems.addAll(chunk.getItems()); } - public List getWrittenItems() { + public List getWrittenItems() { return this.writtenItems; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SingleItemPeekableItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SingleItemPeekableItemReader.java index b8780e3613..84e751e7f6 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SingleItemPeekableItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SingleItemPeekableItemReader.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,7 @@ import org.springframework.batch.item.ItemStream; import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemStreamReader; -import org.springframework.batch.item.ParseException; import org.springframework.batch.item.PeekableItemReader; -import org.springframework.batch.item.UnexpectedInputException; import org.springframework.lang.Nullable; /** diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SynchronizedItemStreamReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SynchronizedItemStreamReader.java index 3affd83e07..a2909b9228 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SynchronizedItemStreamReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/SynchronizedItemStreamReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2023 the original author or authors. + * Copyright 2015-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,9 +20,6 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamReader; -import org.springframework.batch.item.NonTransientResourceException; -import org.springframework.batch.item.ParseException; -import org.springframework.batch.item.UnexpectedInputException; import org.springframework.beans.factory.InitializingBean; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java index d7a8370727..1b82ae1634 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2021 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. @@ -27,8 +27,9 @@ * * @author Peter Zozom * @author Mahmoud Ben Hassine + * @author Taeik Lim */ -public final class FileUtils { +public abstract class FileUtils { // forbids instantiation private FileUtils() { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatSynchronizationManager.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatSynchronizationManager.java index 8a809cfcae..c057138549 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatSynchronizationManager.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatSynchronizationManager.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. @@ -30,6 +30,7 @@ * {@link RepeatOperations} implementations. * * @author Dave Syer + * @author Seungrae Kim * */ public final class RepeatSynchronizationManager { @@ -70,7 +71,7 @@ public static void setCompleteOnly() { */ public static RepeatContext register(RepeatContext context) { RepeatContext oldSession = getContext(); - RepeatSynchronizationManager.contextHolder.set(context); + contextHolder.set(context); return oldSession; } @@ -81,7 +82,7 @@ public static RepeatContext register(RepeatContext context) { */ public static RepeatContext clear() { RepeatContext context = getContext(); - RepeatSynchronizationManager.contextHolder.set(null); + contextHolder.remove(); return context; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/DatabaseType.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/DatabaseType.java index c4086b4539..d727cdc3cc 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/DatabaseType.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/DatabaseType.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. @@ -103,6 +103,9 @@ else if (databaseProductName.contains("AS") && (databaseProductVersion.startsWit databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName); } } + else if (StringUtils.hasText(databaseProductName) && databaseProductName.startsWith("EnterpriseDB")) { + databaseProductName = "PostgreSQL"; + } else { databaseProductName = JdbcUtils.commonDatabaseName(databaseProductName); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/MethodInvokerUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/MethodInvokerUtils.java index 3d26d717b7..b824b36aea 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/MethodInvokerUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/MethodInvokerUtils.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. @@ -34,9 +34,13 @@ * * @author Lucas Ward * @author Mahmoud Ben Hassine + * @author Taeik Lim * @since 2.0 */ -public class MethodInvokerUtils { +public abstract class MethodInvokerUtils { + + private MethodInvokerUtils() { + } /** * Create a {@link MethodInvoker} using the provided method name to search. diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/ReflectionUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/ReflectionUtils.java index f1fc0ecf73..055274af23 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/ReflectionUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/ReflectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-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,9 +29,10 @@ * * @author Michael Minella * @author Mahmoud Ben Hassine + * @author Taeik Lim * @since 2.2.6 */ -public class ReflectionUtils { +public abstract class ReflectionUtils { private ReflectionUtils() { } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/SystemPropertyInitializer.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/SystemPropertyInitializer.java index 08e96b2eb1..ea2ff9bacf 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/SystemPropertyInitializer.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/SystemPropertyInitializer.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. @@ -24,8 +24,9 @@ * exists it is not changed). * * @author Dave Syer - * + * @deprecated since 5.2 with no replacement. */ +@Deprecated(since = "5.2.0", forRemoval = true) public class SystemPropertyInitializer implements InitializingBean { /** diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/annotation/Classifier.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/annotation/Classifier.java deleted file mode 100644 index 03bebcd404..0000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/annotation/Classifier.java +++ /dev/null @@ -1,41 +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.support.annotation; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Mark a method as capable of classifying its input to an instance of its output. Should - * only be used on non-void methods with one parameter. - * - * @author Dave Syer - * @author Mahmoud Ben Hassine - * @deprecated since 5.0 with no replacement. Scheduled for removal in 5.2. - * - */ -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -@Documented -@Deprecated(since = "5.0", forRemoval = true) -public @interface Classifier { - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java index fe4fd5bb76..96e19dfc43 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.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,18 +15,24 @@ */ package org.springframework.batch.item; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +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.assertNotNull; 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 java.io.Serializable; - -import org.junit.jupiter.api.Test; -import org.springframework.util.SerializationUtils; - /** * @author Lucas Ward * @author Mahmoud Ben Hassine @@ -196,4 +202,62 @@ public boolean equals(Object obj) { } + @DisplayName("testGetByType") + @Test + void givenAList_whenGettingAccordingToListType_thenReturnCorrectObject() { + // given - a list + String key = "aListObject"; + List value = List.of("value1", "value2"); + context.put(key, value); + // when - getting according to list type + @SuppressWarnings("unchecked") + List result = (List) context.get(key, List.class); + // then - return the correct list + assertEquals(result, value); + assertEquals(result.get(0), value.get(0)); + assertEquals(result.get(1), value.get(1)); + } + + @DisplayName("testGetNullByDefaultParam") + @Test + void givenANonExistingKey_whenGettingTheNullList_thenReturnNull() { + // given - a non existing key + String key = "aListObjectButNull"; + // when - getting according to the key + @SuppressWarnings("unchecked") + List result = (List) context.get(key, List.class, null); + List result2 = (List) context.get(key, List.class); + // then - return the defined null list + assertNull(result); + assertNull(result2); + } + + @DisplayName("testGetNullByNotNullDefaultParam") + @Test + void givenAnNullList_whenGettingNullWithNonNullDefault_thenReturnDefinedDefaultValue() { + // given - a non existing key + String key = "aListObjectButNull"; + List defaultValue = new ArrayList<>(); + defaultValue.add("value1"); + @SuppressWarnings("unchecked") + // when - getting according to list type and default value + List result = (List) context.get(key, List.class, defaultValue); + // then - return defined default value + assertNotNull(result); + assertEquals(result, defaultValue); + assertEquals(result.get(0), defaultValue.get(0)); + } + + @DisplayName("testGetWithWrongType") + @Test + void givenAList_whenGettingWithWrongType_thenThrowClassCastException() { + // given - another normal list + String key = "anotherListObject"; + List value = List.of("value1", "value2", "value3"); + context.put(key, value); + // when - getting according to map type + // then - throw exception + assertThrows(ClassCastException.class, () -> context.get(key, Map.class)); + } + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/MongoPagingItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/MongoPagingItemReaderTests.java index 16552fd947..3593ab49cf 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/MongoPagingItemReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/MongoPagingItemReaderTests.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. @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -34,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 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.mockito.ArgumentMatchers.any; @@ -347,4 +349,18 @@ void testSortThrowsExceptionWhenInvokedWithNull() { .withMessage("Sorts must not be null"); } + @Test + void testClose() throws Exception { + // given + when(template.find(any(), any())).thenReturn(List.of("string")); + reader.read(); + + // when + reader.close(); + + // then + assertEquals(0, reader.page); + assertNull(reader.results); + } + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractHibernateCursorItemReaderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractHibernateCursorItemReaderIntegrationTests.java deleted file mode 100644 index cecc0262c3..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractHibernateCursorItemReaderIntegrationTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2010-2012 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.item.database; - -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; - -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.sample.Foo; -import org.springframework.core.io.ClassPathResource; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; - -/** - * Tests for {@link HibernateCursorItemReader} using {@link StatelessSession}. - * - * @author Robert Kasanicky - * @author Dave Syer - */ -public abstract class AbstractHibernateCursorItemReaderIntegrationTests - extends AbstractGenericDataSourceItemReaderIntegrationTests { - - @Override - protected ItemReader createItemReader() throws Exception { - - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource); - factoryBean.setMappingLocations(new ClassPathResource("Foo.hbm.xml", getClass())); - customizeSessionFactory(factoryBean); - factoryBean.afterPropertiesSet(); - - SessionFactory sessionFactory = factoryBean.getObject(); - - HibernateCursorItemReader hibernateReader = new HibernateCursorItemReader<>(); - setQuery(hibernateReader); - hibernateReader.setSessionFactory(sessionFactory); - hibernateReader.setUseStatelessSession(isUseStatelessSession()); - hibernateReader.afterPropertiesSet(); - hibernateReader.setSaveState(true); - - return hibernateReader; - - } - - protected void customizeSessionFactory(LocalSessionFactoryBean factoryBean) { - } - - protected void setQuery(HibernateCursorItemReader reader) throws Exception { - reader.setQueryString("from Foo"); - } - - protected boolean isUseStatelessSession() { - return true; - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java deleted file mode 100644 index d99adf816c..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2008-2013 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.item.database; - -import org.hibernate.SessionFactory; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.sample.Foo; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; - -public class HibernateCursorItemReaderCommonTests extends AbstractDatabaseItemStreamItemReaderTests { - - @Override - protected ItemReader getItemReader() throws Exception { - - SessionFactory sessionFactory = createSessionFactory(); - - String hsqlQuery = "from Foo"; - - HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); - reader.setQueryString(hsqlQuery); - reader.setSessionFactory(sessionFactory); - reader.setUseStatelessSession(true); - reader.setFetchSize(10); - reader.afterPropertiesSet(); - reader.setSaveState(true); - - return reader; - } - - private SessionFactory createSessionFactory() throws Exception { - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(getDataSource()); - factoryBean.setMappingLocations(new Resource[] { new ClassPathResource("Foo.hbm.xml", getClass()) }); - factoryBean.afterPropertiesSet(); - - return factoryBean.getObject(); - - } - - @Override - protected void pointToEmptyInput(ItemReader tested) throws Exception { - HibernateCursorItemReader reader = (HibernateCursorItemReader) tested; - reader.close(); - reader.setQueryString("from Foo foo where foo.id = -1"); - reader.afterPropertiesSet(); - reader.open(new ExecutionContext()); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderIntegrationTests.java deleted file mode 100644 index 3bd5ed7a6d..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderIntegrationTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2008-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.item.database; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -import org.hibernate.StatelessSession; -import org.junit.jupiter.api.Test; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.sample.Foo; - -/** - * Tests for {@link HibernateCursorItemReader} using {@link StatelessSession}. - * - * @author Robert Kasanicky - */ -class HibernateCursorItemReaderIntegrationTests extends AbstractHibernateCursorItemReaderIntegrationTests { - - /** - * Exception scenario. - *

    - * {@link HibernateCursorItemReader#setUseStatelessSession(boolean)} can be called - * only in uninitialized state. - */ - @Test - void testSetUseStatelessSession() { - HibernateCursorItemReader inputSource = (HibernateCursorItemReader) reader; - - // initialize and call setter => error - inputSource.open(new ExecutionContext()); - assertThrows(IllegalStateException.class, () -> inputSource.setUseStatelessSession(false)); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNativeQueryIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNativeQueryIntegrationTests.java deleted file mode 100644 index d46b9f8b47..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderNativeQueryIntegrationTests.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2010 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.item.database; - -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; -import org.springframework.batch.item.sample.Foo; - -/** - * @author Anatoly Polinsky - * @author Dave Syer - */ -public class HibernateCursorItemReaderNativeQueryIntegrationTests - extends AbstractHibernateCursorItemReaderIntegrationTests { - - @Override - protected void setQuery(HibernateCursorItemReader hibernateReader) throws Exception { - - String nativeQuery = "select * from T_FOOS"; - - // creating a native query provider as it would be created in configuration - HibernateNativeQueryProvider queryProvider = new HibernateNativeQueryProvider<>(); - - queryProvider.setSqlQuery(nativeQuery); - queryProvider.setEntityClass(Foo.class); - queryProvider.afterPropertiesSet(); - - hibernateReader.setQueryProvider(queryProvider); - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderParametersIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderParametersIntegrationTests.java deleted file mode 100644 index 90772286c1..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderParametersIntegrationTests.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2010-2012 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.item.database; - -import java.util.Collections; - -import org.hibernate.StatelessSession; - -import org.springframework.batch.item.sample.Foo; - -/** - * Tests for {@link HibernateCursorItemReader} using {@link StatelessSession}. - * - * @author Robert Kasanicky - * @author Dave Syer - */ -public class HibernateCursorItemReaderParametersIntegrationTests - extends AbstractHibernateCursorItemReaderIntegrationTests { - - @Override - protected void setQuery(HibernateCursorItemReader reader) { - reader.setQueryString("from Foo where name like :name"); - reader.setParameterValues(Collections.singletonMap("name", "bar%")); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulIntegrationTests.java deleted file mode 100644 index cc361b11f3..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderStatefulIntegrationTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2008-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.item.database; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.query.Query; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.sample.Foo; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link HibernateCursorItemReader} using standard hibernate {@link Session}. - * - * @author Robert Kasanicky - * @author Will Schipp - */ -class HibernateCursorItemReaderStatefulIntegrationTests extends AbstractHibernateCursorItemReaderIntegrationTests { - - @Override - protected boolean isUseStatelessSession() { - return false; - } - - // Ensure close is called on the stateful session correctly. - @Test - @SuppressWarnings("unchecked") - void testStatefulClose() { - - SessionFactory sessionFactory = mock(); - Session session = mock(); - Query scrollableResults = mock(); - HibernateCursorItemReader itemReader = new HibernateCursorItemReader<>(); - itemReader.setSessionFactory(sessionFactory); - itemReader.setQueryString("testQuery"); - itemReader.setUseStatelessSession(false); - - when(sessionFactory.openSession()).thenReturn(session); - when(session.createQuery("testQuery")).thenReturn(scrollableResults); - when(scrollableResults.setFetchSize(0)).thenReturn(scrollableResults); - - itemReader.open(new ExecutionContext()); - itemReader.close(); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorProjectionItemReaderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorProjectionItemReaderIntegrationTests.java deleted file mode 100644 index e79849849e..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorProjectionItemReaderIntegrationTests.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2008-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.item.database; - -import javax.sql.DataSource; - -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.ExecutionContext; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * Tests for {@link HibernateCursorItemReader} using {@link StatelessSession}. - * - * @author Robert Kasanicky - * @author Mahmoud Ben Hassine - */ -@SpringJUnitConfig(locations = "classpath:data-source-context.xml") -class HibernateCursorProjectionItemReaderIntegrationTests { - - @Autowired - private DataSource dataSource; - - private void initializeItemReader(HibernateCursorItemReader reader, String hsqlQuery) throws Exception { - - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource); - factoryBean.setMappingLocations(new Resource[] { new ClassPathResource("Foo.hbm.xml", getClass()) }); - factoryBean.afterPropertiesSet(); - - SessionFactory sessionFactory = factoryBean.getObject(); - - reader.setQueryString(hsqlQuery); - reader.setSessionFactory(sessionFactory); - reader.afterPropertiesSet(); - reader.setSaveState(true); - reader.open(new ExecutionContext()); - - } - - @Test - void testMultipleItemsInProjection() throws Exception { - HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); - initializeItemReader(reader, "select f.value, f.name from Foo f"); - Object[] foo1 = reader.read(); - assertEquals(1, foo1[0]); - } - - @Test - void testSingleItemInProjection() throws Exception { - HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); - initializeItemReader(reader, "select f.value from Foo f"); - Object foo1 = reader.read(); - assertEquals(1, foo1); - } - - @Test - void testSingleItemInProjectionWithArrayType() throws Exception { - HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); - initializeItemReader(reader, "select f.value from Foo f"); - assertThrows(ClassCastException.class, () -> { - Object[] foo1 = reader.read(); - }); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemReaderHelperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemReaderHelperTests.java deleted file mode 100644 index fd2d54f75e..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemReaderHelperTests.java +++ /dev/null @@ -1,71 +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.item.database; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; - -/** - * @author Dave Syer - * @author Will Schipp - * - */ -class HibernateItemReaderHelperTests { - - private final HibernateItemReaderHelper helper = new HibernateItemReaderHelper<>(); - - private final SessionFactory sessionFactory = mock(); - - @Test - void testOneSessionForAllPages() { - - StatelessSession session = mock(); - when(sessionFactory.openStatelessSession()).thenReturn(session); - - helper.setSessionFactory(sessionFactory); - - helper.createQuery(); - // Multiple calls to createQuery only creates one session - helper.createQuery(); - - } - - @Test - void testSessionReset() { - - StatelessSession session = mock(); - when(sessionFactory.openStatelessSession()).thenReturn(session); - - helper.setSessionFactory(sessionFactory); - - helper.createQuery(); - assertNotNull(ReflectionTestUtils.getField(helper, "statelessSession")); - - helper.close(); - assertNull(ReflectionTestUtils.getField(helper, "statelessSession")); - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java deleted file mode 100644 index 9d4272130f..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java +++ /dev/null @@ -1,127 +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.item.database; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.Chunk; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Dave Syer - * @author Thomas Risberg - * @author Michael Minella - * @author Will Schipp - * @author Mahmoud Ben Hassine - */ -class HibernateItemWriterTests { - - HibernateItemWriter writer; - - SessionFactory factory; - - Session currentSession; - - @BeforeEach - void setUp() { - writer = new HibernateItemWriter<>(); - factory = mock(); - currentSession = mock(); - - when(this.factory.getCurrentSession()).thenReturn(this.currentSession); - } - - /** - * Test method for - * {@link org.springframework.batch.item.database.HibernateItemWriter#afterPropertiesSet()} - */ - @Test - void testAfterPropertiesSet() { - writer = new HibernateItemWriter<>(); - Exception exception = assertThrows(IllegalStateException.class, writer::afterPropertiesSet); - String message = exception.getMessage(); - assertTrue(message.contains("SessionFactory"), "Wrong message for exception: " + message); - } - - /** - * Test method for - * {@link org.springframework.batch.item.database.HibernateItemWriter#afterPropertiesSet()} - */ - @Test - void testAfterPropertiesSetWithDelegate() { - writer.setSessionFactory(this.factory); - writer.afterPropertiesSet(); - } - - @Test - void testWriteAndFlushSunnyDayHibernate3() { - this.writer.setSessionFactory(this.factory); - when(this.currentSession.contains("foo")).thenReturn(true); - when(this.currentSession.contains("bar")).thenReturn(false); - this.currentSession.saveOrUpdate("bar"); - this.currentSession.flush(); - this.currentSession.clear(); - - Chunk items = Chunk.of("foo", "bar"); - writer.write(items); - - } - - @Test - void testWriteAndFlushWithFailureHibernate3() { - this.writer.setSessionFactory(this.factory); - final RuntimeException ex = new RuntimeException("ERROR"); - when(this.currentSession.contains("foo")).thenThrow(ex); - - Exception exception = assertThrows(RuntimeException.class, () -> writer.write(Chunk.of("foo"))); - assertEquals("ERROR", exception.getMessage()); - } - - @Test - void testWriteAndFlushSunnyDayHibernate4() { - writer.setSessionFactory(factory); - when(factory.getCurrentSession()).thenReturn(currentSession); - when(currentSession.contains("foo")).thenReturn(true); - when(currentSession.contains("bar")).thenReturn(false); - currentSession.saveOrUpdate("bar"); - currentSession.flush(); - currentSession.clear(); - - Chunk items = Chunk.of("foo", "bar"); - writer.write(items); - } - - @Test - void testWriteAndFlushWithFailureHibernate4() { - writer.setSessionFactory(factory); - final RuntimeException ex = new RuntimeException("ERROR"); - - when(factory.getCurrentSession()).thenReturn(currentSession); - when(currentSession.contains("foo")).thenThrow(ex); - - Exception exception = assertThrows(RuntimeException.class, () -> writer.write(Chunk.of("foo"))); - assertEquals("ERROR", exception.getMessage()); - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernatePagingItemReaderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernatePagingItemReaderIntegrationTests.java deleted file mode 100644 index b2bf1fcd7a..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernatePagingItemReaderIntegrationTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2010-2012 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.item.database; - -import org.hibernate.SessionFactory; -import org.hibernate.StatelessSession; - -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.sample.Foo; -import org.springframework.core.io.ClassPathResource; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; - -/** - * Tests for {@link HibernateCursorItemReader} using {@link StatelessSession}. - * - * @author Robert Kasanicky - * @author Dave Syer - */ -public class HibernatePagingItemReaderIntegrationTests extends AbstractGenericDataSourceItemReaderIntegrationTests { - - @Override - protected ItemReader createItemReader() throws Exception { - - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource); - factoryBean.setMappingLocations(new ClassPathResource("Foo.hbm.xml", getClass())); - customizeSessionFactory(factoryBean); - factoryBean.afterPropertiesSet(); - - SessionFactory sessionFactory = factoryBean.getObject(); - - HibernatePagingItemReader hibernateReader = new HibernatePagingItemReader<>(); - setQuery(hibernateReader); - hibernateReader.setPageSize(2); - hibernateReader.setSessionFactory(sessionFactory); - hibernateReader.setUseStatelessSession(isUseStatelessSession()); - hibernateReader.afterPropertiesSet(); - hibernateReader.setSaveState(true); - - return hibernateReader; - - } - - protected void customizeSessionFactory(LocalSessionFactoryBean factoryBean) { - } - - protected void setQuery(HibernatePagingItemReader reader) throws Exception { - reader.setQueryString("from Foo"); - } - - protected boolean isUseStatelessSession() { - return true; - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java deleted file mode 100644 index 0611e6b53b..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import java.util.HashMap; -import java.util.Map; -import javax.sql.DataSource; - -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.database.HibernateCursorItemReader; -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; -import org.springframework.batch.item.sample.Foo; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.init.DataSourceInitializer; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * @author Michael Minella - * @author Mahmoud Ben Hassine - */ -class HibernateCursorItemReaderBuilderTests { - - private SessionFactory sessionFactory; - - private ConfigurableApplicationContext context; - - @BeforeEach - void setUp() { - this.context = new AnnotationConfigApplicationContext(TestDataSourceConfiguration.class); - this.sessionFactory = (SessionFactory) context.getBean("sessionFactory"); - } - - @AfterEach - void tearDown() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - void testConfiguration() throws Exception { - HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .fetchSize(2) - .currentItemCount(2) - .maxItemCount(4) - .queryName("allFoos") - .useStatelessSession(true) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - Foo item1 = reader.read(); - Foo item2 = reader.read(); - assertNull(reader.read()); - reader.update(executionContext); - reader.close(); - - assertEquals(3, item1.getId()); - assertEquals("bar3", item1.getName()); - assertEquals(3, item1.getValue()); - assertEquals(4, item2.getId()); - assertEquals("bar4", item2.getName()); - assertEquals(4, item2.getValue()); - - assertEquals(2, executionContext.size()); - } - - @Test - void testConfigurationNoSaveState() throws Exception { - Map parameters = new HashMap<>(); - parameters.put("value", 2); - - HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .queryString("from Foo foo where foo.id > :value") - .parameterValues(parameters) - .saveState(false) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - - int i = 0; - while (reader.read() != null) { - i++; - } - - reader.update(executionContext); - reader.close(); - - assertEquals(3, i); - assertEquals(0, executionContext.size()); - } - - @Test - void testConfigurationQueryProvider() throws Exception { - - HibernateNativeQueryProvider provider = new HibernateNativeQueryProvider<>(); - provider.setEntityClass(Foo.class); - provider.setSqlQuery("select * from T_FOOS"); - provider.afterPropertiesSet(); - - HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .queryProvider(provider) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - - int i = 0; - while (reader.read() != null) { - i++; - } - - reader.update(executionContext); - reader.close(); - - assertEquals(5, i); - } - - @Test - void testConfigurationNativeQuery() throws Exception { - HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .nativeQuery("select * from T_FOOS") - .entityClass(Foo.class) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - - int i = 0; - while (reader.read() != null) { - i++; - } - - reader.update(executionContext); - reader.close(); - - assertEquals(5, i); - } - - @Test - void testValidation() { - Exception exception = assertThrows(IllegalStateException.class, - () -> new HibernateCursorItemReaderBuilder().fetchSize(-2).build()); - assertEquals("fetchSize must not be negative", exception.getMessage()); - - exception = assertThrows(IllegalStateException.class, - () -> new HibernateCursorItemReaderBuilder().build()); - assertEquals("A SessionFactory must be provided", exception.getMessage()); - - exception = assertThrows(IllegalStateException.class, - () -> new HibernateCursorItemReaderBuilder().sessionFactory(this.sessionFactory) - .saveState(true) - .build()); - assertEquals("A name is required when saveState is set to true.", exception.getMessage()); - - exception = assertThrows(IllegalStateException.class, - () -> new HibernateCursorItemReaderBuilder().sessionFactory(this.sessionFactory) - .saveState(false) - .build()); - assertEquals("A HibernateQueryProvider, queryName, queryString, " - + "or both the nativeQuery and entityClass must be configured", exception.getMessage()); - } - - @Configuration - public static class TestDataSourceConfiguration { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder().generateUniqueName(true).build(); - } - - @Bean - public DataSourceInitializer initializer(DataSource dataSource) { - DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); - dataSourceInitializer.setDataSource(dataSource); - - Resource create = new ClassPathResource("org/springframework/batch/item/database/init-foo-schema.sql"); - dataSourceInitializer.setDatabasePopulator(new ResourceDatabasePopulator(create)); - - return dataSourceInitializer; - } - - @Bean - public SessionFactory sessionFactory() throws Exception { - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource()); - factoryBean.setMappingLocations( - new ClassPathResource("/org/springframework/batch/item/database/Foo.hbm.xml", getClass())); - factoryBean.afterPropertiesSet(); - - return factoryBean.getObject(); - - } - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilderTests.java deleted file mode 100644 index 455496e1a7..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateItemWriterBuilderTests.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; - -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.database.HibernateItemWriter; -import org.springframework.batch.item.sample.Foo; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * @author Michael Minella - * @author Mahmoud Ben Hassine - */ -@MockitoSettings(strictness = Strictness.LENIENT) -class HibernateItemWriterBuilderTests { - - @Mock - private SessionFactory sessionFactory; - - @Mock - private Session session; - - @BeforeEach - void setUp() { - when(this.sessionFactory.getCurrentSession()).thenReturn(this.session); - } - - @Test - void testConfiguration() { - HibernateItemWriter itemWriter = new HibernateItemWriterBuilder().sessionFactory(this.sessionFactory) - .build(); - - itemWriter.afterPropertiesSet(); - - Chunk foos = getFoos(); - - itemWriter.write(foos); - - verify(this.session).saveOrUpdate(foos.getItems().get(0)); - verify(this.session).saveOrUpdate(foos.getItems().get(1)); - verify(this.session).saveOrUpdate(foos.getItems().get(2)); - } - - @Test - void testConfigurationClearSession() { - HibernateItemWriter itemWriter = new HibernateItemWriterBuilder().sessionFactory(this.sessionFactory) - .clearSession(false) - .build(); - - itemWriter.afterPropertiesSet(); - - Chunk foos = getFoos(); - - itemWriter.write(foos); - - verify(this.session).saveOrUpdate(foos.getItems().get(0)); - verify(this.session).saveOrUpdate(foos.getItems().get(1)); - verify(this.session).saveOrUpdate(foos.getItems().get(2)); - verify(this.session, never()).clear(); - } - - @Test - void testValidation() { - Exception exception = assertThrows(IllegalStateException.class, - () -> new HibernateItemWriterBuilder().build()); - assertEquals("SessionFactory must be provided", exception.getMessage()); - } - - private Chunk getFoos() { - Chunk foos = new Chunk<>(); - - for (int i = 1; i < 4; i++) { - Foo foo = new Foo(); - foo.setName("foo" + i); - foo.setValue(i); - foos.add(foo); - } - - return foos; - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java deleted file mode 100644 index 843cc867b2..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2017-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.item.database.builder; - -import java.util.HashMap; -import java.util.Map; -import javax.sql.DataSource; - -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.database.HibernateItemReaderHelper; -import org.springframework.batch.item.database.HibernatePagingItemReader; -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; -import org.springframework.batch.item.sample.Foo; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.init.DataSourceInitializer; -import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.test.util.ReflectionTestUtils; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -/** - * @author Michael Minella - * @author Mahmoud Ben Hassine - */ -class HibernatePagingItemReaderBuilderTests { - - private SessionFactory sessionFactory; - - private ConfigurableApplicationContext context; - - @BeforeEach - void setUp() { - this.context = new AnnotationConfigApplicationContext( - HibernatePagingItemReaderBuilderTests.TestDataSourceConfiguration.class); - this.sessionFactory = (SessionFactory) context.getBean("sessionFactory"); - } - - @AfterEach - void tearDown() { - if (this.context != null) { - this.context.close(); - } - } - - @Test - @SuppressWarnings("unchecked") - void testConfiguration() throws Exception { - HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .fetchSize(2) - .currentItemCount(2) - .maxItemCount(4) - .pageSize(5) - .queryName("allFoos") - .useStatelessSession(false) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - Foo item1 = reader.read(); - Foo item2 = reader.read(); - assertNull(reader.read()); - reader.update(executionContext); - reader.close(); - - assertEquals(3, item1.getId()); - assertEquals("bar3", item1.getName()); - assertEquals(3, item1.getValue()); - assertEquals(4, item2.getId()); - assertEquals("bar4", item2.getName()); - assertEquals(4, item2.getValue()); - - assertEquals(2, executionContext.size()); - assertEquals(5, ReflectionTestUtils.getField(reader, "pageSize")); - - HibernateItemReaderHelper helper = (HibernateItemReaderHelper) ReflectionTestUtils.getField(reader, - "helper"); - assertEquals(false, ReflectionTestUtils.getField(helper, "useStatelessSession")); - } - - @Test - void testConfigurationNoSaveState() throws Exception { - Map parameters = new HashMap<>(); - parameters.put("value", 2); - - HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .queryString("from Foo foo where foo.id > :value") - .parameterValues(parameters) - .saveState(false) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - - int i = 0; - while (reader.read() != null) { - i++; - } - - reader.update(executionContext); - reader.close(); - - assertEquals(3, i); - assertEquals(0, executionContext.size()); - } - - @Test - void testConfigurationQueryProvider() throws Exception { - - HibernateNativeQueryProvider provider = new HibernateNativeQueryProvider<>(); - provider.setEntityClass(Foo.class); - provider.setSqlQuery("select * from T_FOOS"); - provider.afterPropertiesSet(); - - HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder().name("fooReader") - .sessionFactory(this.sessionFactory) - .queryProvider(provider) - .build(); - - reader.afterPropertiesSet(); - - ExecutionContext executionContext = new ExecutionContext(); - - reader.open(executionContext); - - int i = 0; - while (reader.read() != null) { - i++; - } - - reader.update(executionContext); - reader.close(); - - assertEquals(5, i); - } - - @Test - void testValidation() { - Exception exception = assertThrows(IllegalStateException.class, - () -> new HibernatePagingItemReaderBuilder().sessionFactory(this.sessionFactory) - .fetchSize(-2) - .build()); - assertEquals("fetchSize must not be negative", exception.getMessage()); - - exception = assertThrows(IllegalArgumentException.class, - () -> new HibernatePagingItemReaderBuilder().build()); - assertEquals("A SessionFactory must be provided", exception.getMessage()); - - exception = assertThrows(IllegalArgumentException.class, - () -> new HibernatePagingItemReaderBuilder().sessionFactory(this.sessionFactory) - .saveState(true) - .build()); - assertEquals("A name is required when saveState is set to true", exception.getMessage()); - - exception = assertThrows(IllegalStateException.class, - () -> new HibernatePagingItemReaderBuilder().sessionFactory(this.sessionFactory) - .saveState(false) - .build()); - assertEquals("queryString or queryName must be set", exception.getMessage()); - } - - @Configuration - public static class TestDataSourceConfiguration { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder().generateUniqueName(true).build(); - } - - @Bean - public DataSourceInitializer initializer(DataSource dataSource) { - DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); - dataSourceInitializer.setDataSource(dataSource); - - Resource create = new ClassPathResource("org/springframework/batch/item/database/init-foo-schema.sql"); - dataSourceInitializer.setDatabasePopulator(new ResourceDatabasePopulator(create)); - - return dataSourceInitializer; - } - - @Bean - public SessionFactory sessionFactory() throws Exception { - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource()); - factoryBean.setMappingLocations( - new ClassPathResource("/org/springframework/batch/item/database/Foo.hbm.xml", getClass())); - factoryBean.afterPropertiesSet(); - - return factoryBean.getObject(); - - } - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java index c8b2528e1a..16e33c6107 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2023 the original author or authors. + * Copyright 2016-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 Ankur Trapasiya * @author Parikshit Dutta * @author Mahmoud Ben Hassine + * @author Juyoung Kim */ class JdbcCursorItemReaderBuilderTests { @@ -328,12 +329,38 @@ void testValidation() { assertEquals("A rowmapper is required", exception.getMessage()); } + @Test + void testDataRowMapper() throws Exception { + JdbcCursorItemReader reader = new JdbcCursorItemReaderBuilder().name("barReader") + .dataSource(this.dataSource) + .sql("SELECT * FROM BAR ORDER BY FIRST") + .dataRowMapper(Bar.class) + .build(); + + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + + validateBar(reader.read(), 0, 1, "2", "3"); + validateBar(reader.read(), 1, 4, "5", "6"); + validateBar(reader.read(), 2, 7, "8", "9"); + + assertNull(reader.read()); + } + private void validateFoo(Foo item, int first, String second, String third) { assertEquals(first, item.getFirst()); assertEquals(second, item.getSecond()); assertEquals(third, item.getThird()); } + private void validateBar(Bar item, int id, int first, String second, String third) { + assertEquals(id, item.id()); + assertEquals(first, item.first()); + assertEquals(second, item.second()); + assertEquals(third, item.third()); + } + public static class Foo { private int first; @@ -368,6 +395,9 @@ public void setThird(String third) { } + public record Bar(int id, int first, String second, String third) { + } + @Configuration public static class TestDataSourceConfiguration { @@ -376,12 +406,22 @@ CREATE TABLE FOO ( ID BIGINT IDENTITY NOT NULL PRIMARY KEY , FIRST BIGINT , SECOND VARCHAR(5) NOT NULL, - THIRD VARCHAR(5) NOT NULL);"""; + THIRD VARCHAR(5) NOT NULL); + + CREATE TABLE BAR ( + ID BIGINT IDENTITY NOT NULL PRIMARY KEY , + FIRST BIGINT , + SECOND VARCHAR(5) NOT NULL, + THIRD VARCHAR(5) NOT NULL) ;"""; private static final String INSERT_SQL = """ INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (1, '2', '3'); INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (4, '5', '6'); - INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (7, '8', '9');"""; + INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (7, '8', '9'); + + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (1, '2', '3'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (4, '5', '6'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (7, '8', '9');"""; @Bean public DataSource dataSource() { diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java index 47999e920d..a6220cbeb1 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2023 the original author or authors. + * Copyright 2017-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 Michael Minella * @author Drummond Dawson * @author Mahmoud Ben Hassine + * @author Juyoung Kim */ class JdbcPagingItemReaderBuilderTests { @@ -262,6 +263,33 @@ void testBeanRowMapper() throws Exception { assertEquals("12", item1.getThird()); } + @Test + void testDataRowMapper() throws Exception { + Map sortKeys = new HashMap<>(1); + sortKeys.put("ID", Order.DESCENDING); + + JdbcPagingItemReader reader = new JdbcPagingItemReaderBuilder().name("barReader") + .dataSource(this.dataSource) + .currentItemCount(1) + .maxItemCount(2) + .selectClause("SELECT ID, FIRST, SECOND, THIRD") + .fromClause("BAR") + .sortKeys(sortKeys) + .dataRowMapper(Bar.class) + .build(); + + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + Bar item1 = reader.read(); + assertNull(reader.read()); + + assertEquals(3, item1.id()); + assertEquals(10, item1.first()); + assertEquals("11", item1.second()); + assertEquals("12", item1.third()); + } + @Test void testValidation() { var builder = new JdbcPagingItemReaderBuilder(); @@ -354,6 +382,9 @@ public void setThird(String third) { } + public record Bar(int id, int first, String second, String third) { + } + @Configuration public static class TestDataSourceConfiguration { @@ -362,6 +393,12 @@ CREATE TABLE FOO ( ID BIGINT IDENTITY NOT NULL PRIMARY KEY , FIRST BIGINT , SECOND VARCHAR(5) NOT NULL, + THIRD VARCHAR(5) NOT NULL) ; + + CREATE TABLE BAR ( + ID BIGINT IDENTITY NOT NULL PRIMARY KEY , + FIRST BIGINT , + SECOND VARCHAR(5) NOT NULL, THIRD VARCHAR(5) NOT NULL) ;"""; private static final String INSERT_SQL = """ @@ -369,7 +406,13 @@ SECOND VARCHAR(5) NOT NULL, INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (4, '5', '6'); INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (7, '8', '9'); INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (10, '11', '12'); - INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (13, '14', '15');"""; + INSERT INTO FOO (FIRST, SECOND, THIRD) VALUES (13, '14', '15'); + + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (1, '2', '3'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (4, '5', '6'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (7, '8', '9'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (10, '11', '12'); + INSERT INTO BAR (FIRST, SECOND, THIRD) VALUES (13, '14', '15');"""; @Bean public DataSource dataSource() { diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/AbstractPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/AbstractPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..15f3ced073 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/AbstractPagingQueryProviderIntegrationTests.java @@ -0,0 +1,81 @@ +/* + * 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.item.database.support; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.springframework.batch.item.database.Order; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Henning Pƶttker + */ +abstract class AbstractPagingQueryProviderIntegrationTests { + + private final JdbcTemplate jdbcTemplate; + + private final AbstractSqlPagingQueryProvider queryProvider; + + AbstractPagingQueryProviderIntegrationTests(DataSource dataSource, AbstractSqlPagingQueryProvider queryProvider) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + this.queryProvider = queryProvider; + } + + @Test + void testWithoutGrouping() { + queryProvider.setSelectClause("ID, STRING"); + queryProvider.setFromClause("TEST_TABLE"); + Map sortKeys = new HashMap<>(); + sortKeys.put("ID", Order.ASCENDING); + queryProvider.setSortKeys(sortKeys); + + List firstPage = jdbcTemplate.query(queryProvider.generateFirstPageQuery(2), MAPPER); + assertEquals(List.of(new Item(1, "Spring"), new Item(2, "Batch")), firstPage); + + List secondPage = jdbcTemplate.query(queryProvider.generateRemainingPagesQuery(2), MAPPER, 2); + assertEquals(List.of(new Item(3, "Infrastructure")), secondPage); + } + + @Test + void testWithGrouping() { + queryProvider.setSelectClause("STRING"); + queryProvider.setFromClause("GROUPING_TEST_TABLE"); + queryProvider.setGroupClause("STRING"); + Map sortKeys = new HashMap<>(); + sortKeys.put("STRING", Order.ASCENDING); + queryProvider.setSortKeys(sortKeys); + + List firstPage = jdbcTemplate.queryForList(queryProvider.generateFirstPageQuery(2), String.class); + assertEquals(List.of("Batch", "Infrastructure"), firstPage); + + List secondPage = jdbcTemplate.queryForList(queryProvider.generateRemainingPagesQuery(2), String.class, + "Infrastructure"); + assertEquals(List.of("Spring"), secondPage); + } + + private record Item(Integer id, String string) { + } + + private static final RowMapper MAPPER = (rs, rowNum) -> new Item(rs.getInt("id"), rs.getString("string")); + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..19d876b9d1 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.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.item.database.support; + +import javax.sql.DataSource; + +import com.ibm.db2.jcc.DB2SimpleDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.Db2Container; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class Db2PagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.5.1"); + + @Container + public static Db2Container db2 = new Db2Container(DB2_IMAGE).acceptLicense(); + + Db2PagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new Db2PagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + DB2SimpleDataSource dataSource = new DB2SimpleDataSource(); + dataSource.setDatabaseName(db2.getDatabaseName()); + dataSource.setUser(db2.getUsername()); + dataSource.setPassword(db2.getPassword()); + dataSource.setDriverType(4); + dataSource.setServerName(db2.getHost()); + dataSource.setPortNumber(db2.getMappedPort(Db2Container.DB2_PORT)); + dataSource.setSslConnection(false); + return dataSource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..9a06de9369 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderIntegrationTests.java @@ -0,0 +1,50 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * @author Henning Pƶttker + */ +@SpringJUnitConfig +class DerbyPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + DerbyPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new DerbyPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.DERBY) + .addScript("/org/springframework/batch/item/database/support/query-provider-fixture.sql") + .generateUniqueName(true) + .build(); + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderTests.java index 5bd891ddfa..c93f979c9b 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DerbyPagingQueryProviderTests.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,20 +15,9 @@ */ package org.springframework.batch.item.database.support; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; - -import javax.sql.DataSource; import org.junit.jupiter.api.Test; -import org.springframework.batch.item.database.Order; -import org.springframework.dao.InvalidDataAccessResourceUsageException; /** * @author Thomas Risberg @@ -41,43 +30,10 @@ class DerbyPagingQueryProviderTests extends AbstractSqlPagingQueryProviderTests pagingQueryProvider = new DerbyPagingQueryProvider(); } - @Test - void testInit() throws Exception { - DataSource ds = mock(); - Connection con = mock(); - DatabaseMetaData dmd = mock(); - when(dmd.getDatabaseProductVersion()).thenReturn("10.4.1.3"); - when(con.getMetaData()).thenReturn(dmd); - when(ds.getConnection()).thenReturn(con); - pagingQueryProvider.init(ds); - } - - @Test - void testInitWithRecentVersion() throws Exception { - DataSource ds = mock(); - Connection con = mock(); - DatabaseMetaData dmd = mock(); - when(dmd.getDatabaseProductVersion()).thenReturn("10.10.1.1"); - when(con.getMetaData()).thenReturn(dmd); - when(ds.getConnection()).thenReturn(con); - pagingQueryProvider.init(ds); - } - - @Test - void testInitWithUnsupportedVersion() throws Exception { - DataSource ds = mock(); - Connection con = mock(); - DatabaseMetaData dmd = mock(); - when(dmd.getDatabaseProductVersion()).thenReturn("10.2.9.9"); - when(con.getMetaData()).thenReturn(dmd); - when(ds.getConnection()).thenReturn(con); - assertThrows(InvalidDataAccessResourceUsageException.class, () -> pagingQueryProvider.init(ds)); - } - @Test @Override void testGenerateFirstPageQuery() { - String sql = "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 ORDER BY id ASC"; + String sql = "SELECT id, name, age FROM foo WHERE bar = 1 ORDER BY id ASC FETCH FIRST 100 ROWS ONLY"; String s = pagingQueryProvider.generateFirstPageQuery(pageSize); assertEquals(sql, s); } @@ -85,60 +41,37 @@ void testGenerateFirstPageQuery() { @Test @Override void testGenerateRemainingPagesQuery() { - String sql = "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 AND ((id > ?)) ORDER BY id ASC"; + String sql = "SELECT id, name, age FROM foo WHERE (bar = 1) AND ((id > ?)) ORDER BY id ASC FETCH FIRST 100 ROWS ONLY"; String s = pagingQueryProvider.generateRemainingPagesQuery(pageSize); assertEquals(sql, s); } - /** - * Older versions of Derby don't allow order by in the sub select. This should work - * with 10.6.1 and above. - */ @Test @Override - void testQueryContainsSortKey() { - String s = pagingQueryProvider.generateFirstPageQuery(pageSize).toLowerCase(); - assertTrue(s.contains("id asc"), "Wrong query: " + s); - } - - /** - * Older versions of Derby don't allow order by in the sub select. This should work - * with 10.6.1 and above. - */ - @Test - @Override - void testQueryContainsSortKeyDesc() { - pagingQueryProvider.getSortKeys().put("id", Order.DESCENDING); - String s = pagingQueryProvider.generateFirstPageQuery(pageSize).toLowerCase(); - assertTrue(s.contains("id desc"), "Wrong query: " + s); - } - - @Override - @Test void testGenerateFirstPageQueryWithGroupBy() { pagingQueryProvider.setGroupClause("dep"); - String sql = "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 ORDER BY id ASC"; + String sql = "SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep ORDER BY id ASC FETCH FIRST 100 ROWS ONLY"; String s = pagingQueryProvider.generateFirstPageQuery(pageSize); assertEquals(sql, s); } - @Override @Test + @Override void testGenerateRemainingPagesQueryWithGroupBy() { pagingQueryProvider.setGroupClause("dep"); - String sql = "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 AND ((id > ?)) ORDER BY id ASC"; + String sql = "SELECT * FROM (SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep) AS MAIN_QRY WHERE ((id > ?)) ORDER BY id ASC FETCH FIRST 100 ROWS ONLY"; String s = pagingQueryProvider.generateRemainingPagesQuery(pageSize); assertEquals(sql, s); } @Override String getFirstPageSqlWithMultipleSortKeys() { - return "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 ORDER BY name ASC, id DESC"; + return "SELECT id, name, age FROM foo WHERE bar = 1 ORDER BY name ASC, id DESC FETCH FIRST 100 ROWS ONLY"; } @Override String getRemainingSqlWithMultipleSortKeys() { - return "SELECT * FROM ( SELECT TMP_ORDERED.*, ROW_NUMBER() OVER () AS ROW_NUMBER FROM (SELECT id, name, age FROM foo WHERE bar = 1 ) AS TMP_ORDERED) AS TMP_SUB WHERE TMP_SUB.ROW_NUMBER <= 100 AND ((name > ?) OR (name = ? AND id < ?)) ORDER BY name ASC, id DESC"; + return "SELECT id, name, age FROM foo WHERE (bar = 1) AND ((name > ?) OR (name = ? AND id < ?)) ORDER BY name ASC, id DESC FETCH FIRST 100 ROWS ONLY"; } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderIntegrationTests.java deleted file mode 100644 index a8a20696f6..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderIntegrationTests.java +++ /dev/null @@ -1,97 +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.item.database.support; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.ArrayList; -import java.util.List; - -import javax.sql.DataSource; - -import org.hibernate.query.Query; -import org.hibernate.SessionFactory; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; -import org.springframework.batch.item.sample.Foo; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.orm.hibernate5.LocalSessionFactoryBean; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.transaction.annotation.Transactional; - -/** - * @author Anatoly Polinsky - * @author Dave Syer - * @author Mahmoud Ben Hassine - */ -@SpringJUnitConfig(locations = "classpath:data-source-context.xml") -class HibernateNativeQueryProviderIntegrationTests { - - @Autowired - private DataSource dataSource; - - private final HibernateNativeQueryProvider hibernateQueryProvider; - - private SessionFactory sessionFactory; - - HibernateNativeQueryProviderIntegrationTests() { - hibernateQueryProvider = new HibernateNativeQueryProvider<>(); - hibernateQueryProvider.setEntityClass(Foo.class); - } - - @BeforeEach - void setUp() throws Exception { - - LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); - factoryBean.setDataSource(dataSource); - factoryBean.setMappingLocations(new Resource[] { new ClassPathResource("../Foo.hbm.xml", getClass()) }); - factoryBean.afterPropertiesSet(); - - sessionFactory = factoryBean.getObject(); - - } - - @Test - @Transactional - void shouldRetrieveAndMapAllFoos() throws Exception { - - String nativeQuery = "select * from T_FOOS"; - - hibernateQueryProvider.setSqlQuery(nativeQuery); - hibernateQueryProvider.afterPropertiesSet(); - hibernateQueryProvider.setSession(sessionFactory.openSession()); - - Query query = hibernateQueryProvider.createQuery(); - - List expectedFoos = new ArrayList<>(); - - expectedFoos.add(new Foo(1, "bar1", 1)); - expectedFoos.add(new Foo(2, "bar2", 2)); - expectedFoos.add(new Foo(3, "bar3", 3)); - expectedFoos.add(new Foo(4, "bar4", 4)); - expectedFoos.add(new Foo(5, "bar5", 5)); - - List actualFoos = query.list(); - - assertEquals(actualFoos, expectedFoos); - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderTests.java deleted file mode 100644 index a03276fd0e..0000000000 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HibernateNativeQueryProviderTests.java +++ /dev/null @@ -1,82 +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.item.database.support; - -import org.hibernate.Session; -import org.hibernate.StatelessSession; -import org.hibernate.query.NativeQuery; -import org.junit.jupiter.api.Test; - -import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author Anatoly Polinsky - * @author Dave Syer - * @author Will Schipp - */ -class HibernateNativeQueryProviderTests { - - private final HibernateNativeQueryProvider hibernateQueryProvider; - - HibernateNativeQueryProviderTests() { - hibernateQueryProvider = new HibernateNativeQueryProvider<>(); - hibernateQueryProvider.setEntityClass(Foo.class); - } - - @Test - @SuppressWarnings("unchecked") - void testCreateQueryWithStatelessSession() { - String sqlQuery = "select * from T_FOOS"; - hibernateQueryProvider.setSqlQuery(sqlQuery); - - StatelessSession session = mock(); - NativeQuery query = mock(); - - when(session.createNativeQuery(sqlQuery)).thenReturn(query); - when(query.addEntity(Foo.class)).thenReturn(query); - - hibernateQueryProvider.setStatelessSession(session); - assertNotNull(hibernateQueryProvider.createQuery()); - - } - - @Test - @SuppressWarnings("unchecked") - void shouldCreateQueryWithStatefulSession() { - String sqlQuery = "select * from T_FOOS"; - hibernateQueryProvider.setSqlQuery(sqlQuery); - - Session session = mock(); - NativeQuery query = mock(); - - when(session.createNativeQuery(sqlQuery)).thenReturn(query); - when(query.addEntity(Foo.class)).thenReturn(query); - - hibernateQueryProvider.setSession(session); - assertNotNull(hibernateQueryProvider.createQuery()); - - } - - private static class Foo { - - } - -} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HsqlPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HsqlPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..f0ce2f3821 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HsqlPagingQueryProviderIntegrationTests.java @@ -0,0 +1,50 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * @author Henning Pƶttker + */ +@SpringJUnitConfig +class HsqlPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + HsqlPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new HsqlPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL) + .addScript("/org/springframework/batch/item/database/support/query-provider-fixture.sql") + .generateUniqueName(true) + .build(); + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MariaDBPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MariaDBPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..e96aeb1242 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MariaDBPagingQueryProviderIntegrationTests.java @@ -0,0 +1,65 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import org.mariadb.jdbc.MariaDbDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class MariaDBPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName MARIADB_IMAGE = DockerImageName.parse("mariadb:10.9.3"); + + @Container + public static MariaDBContainer mariaDBContainer = new MariaDBContainer<>(MARIADB_IMAGE); + + MariaDBPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new MySqlPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + MariaDbDataSource datasource = new MariaDbDataSource(); + datasource.setUrl(mariaDBContainer.getJdbcUrl()); + datasource.setUser(mariaDBContainer.getUsername()); + datasource.setPassword(mariaDBContainer.getPassword()); + return datasource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MySqlPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MySqlPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..4b1da2044b --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/MySqlPagingQueryProviderIntegrationTests.java @@ -0,0 +1,66 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import com.mysql.cj.jdbc.MysqlDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class MySqlPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.0.31"); + + @Container + public static MySQLContainer mysql = new MySQLContainer<>(MYSQL_IMAGE); + + MySqlPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new MySqlPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + MysqlDataSource datasource = new MysqlDataSource(); + datasource.setURL(mysql.getJdbcUrl()); + datasource.setUser(mysql.getUsername()); + datasource.setPassword(mysql.getPassword()); + datasource.setUseSSL(false); + return datasource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/OraclePagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/OraclePagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..23d767c384 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/OraclePagingQueryProviderIntegrationTests.java @@ -0,0 +1,75 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import oracle.jdbc.pool.OracleDataSource; +import org.junit.jupiter.api.Disabled; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * Official Docker images for Oracle are not publicly available. Oracle support is tested + * semi-manually for the moment: 1. Build a docker image for oracle/database:11.2.0.2-xe: + * ... + * 2. Run the test `testJobExecution` + * + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +@Disabled("Official Docker images for Oracle are not publicly available") +class OraclePagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName ORACLE_IMAGE = DockerImageName.parse("oracle/database:11.2.0.2-xe"); + + @Container + public static OracleContainer oracle = new OracleContainer(ORACLE_IMAGE); + + OraclePagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new OraclePagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + OracleDataSource oracleDataSource = new OracleDataSource(); + oracleDataSource.setUser(oracle.getUsername()); + oracleDataSource.setPassword(oracle.getPassword()); + oracleDataSource.setDatabaseName(oracle.getDatabaseName()); + oracleDataSource.setServerName(oracle.getHost()); + oracleDataSource.setPortNumber(oracle.getOraclePort()); + return oracleDataSource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/PostgresPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/PostgresPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..44798f79fa --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/PostgresPagingQueryProviderIntegrationTests.java @@ -0,0 +1,65 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class PostgresPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName POSTGRESQL_IMAGE = DockerImageName.parse("postgres:13.3"); + + @Container + public static PostgreSQLContainer postgres = new PostgreSQLContainer<>(POSTGRESQL_IMAGE); + + PostgresPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new PostgresPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + PGSimpleDataSource datasource = new PGSimpleDataSource(); + datasource.setURL(postgres.getJdbcUrl()); + datasource.setUser(postgres.getUsername()); + datasource.setPassword(postgres.getPassword()); + return datasource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..21bc1eede6 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlServerPagingQueryProviderIntegrationTests.java @@ -0,0 +1,66 @@ +/* + * 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.item.database.support; + +import javax.sql.DataSource; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class SqlServerPagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + // TODO find the best way to externalize and manage image versions + private static final DockerImageName SQLSERVER_IMAGE = DockerImageName + .parse("mcr.microsoft.com/mssql/server:2022-CU14-ubuntu-22.04"); + + @Container + public static MSSQLServerContainer sqlserver = new MSSQLServerContainer<>(SQLSERVER_IMAGE).acceptLicense(); + + SqlServerPagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new SqlServerPagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + SQLServerDataSource dataSource = new SQLServerDataSource(); + dataSource.setUser(sqlserver.getUsername()); + dataSource.setPassword(sqlserver.getPassword()); + dataSource.setURL(sqlserver.getJdbcUrl()); + return dataSource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProviderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProviderTests.java index bac04788f5..cd58aecfb9 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProviderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlWindowingPagingQueryProviderTests.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. @@ -25,6 +25,7 @@ */ class SqlWindowingPagingQueryProviderTests extends AbstractSqlPagingQueryProviderTests { + @SuppressWarnings("removal") SqlWindowingPagingQueryProviderTests() { pagingQueryProvider = new SqlWindowingPagingQueryProvider(); } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlitePagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlitePagingQueryProviderIntegrationTests.java new file mode 100644 index 0000000000..db6826c832 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/SqlitePagingQueryProviderIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * 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.item.database.support; + +import java.nio.file.Path; +import javax.sql.DataSource; + +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.sqlite.SQLiteDataSource; + +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; + +/** + * @author Henning Pƶttker + */ +@SpringJUnitConfig +@Sql(scripts = "query-provider-fixture.sql", executionPhase = BEFORE_TEST_CLASS) +class SqlitePagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { + + @TempDir + private static Path TEMP_DIR; + + SqlitePagingQueryProviderIntegrationTests(@Autowired DataSource dataSource) { + super(dataSource, new SqlitePagingQueryProvider()); + } + + @Configuration + static class TestConfiguration { + + @Bean + public DataSource dataSource() throws Exception { + SQLiteDataSource dataSource = new SQLiteDataSource(); + dataSource.setUrl("jdbc:sqlite:" + TEMP_DIR.resolve("spring-batch.sqlite")); + return dataSource; + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactoryTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactoryTests.java index 0a814af5aa..202fcc6476 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactoryTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/SimpleBinaryBufferedReaderFactoryTests.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. @@ -21,6 +21,8 @@ import java.io.BufferedReader; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.core.io.ByteArrayResource; /** @@ -75,16 +77,27 @@ void testCreateWithLineEndingAtEnd() throws Exception { assertNull(reader.readLine()); } - @Test - void testCreateWithFalseLineEnding() throws Exception { + @ParameterizedTest + @ValueSource(strings = { "||", "|||" }) + void testCreateWithFalseLineEnding(String lineEnding) throws Exception { SimpleBinaryBufferedReaderFactory factory = new SimpleBinaryBufferedReaderFactory(); - factory.setLineEnding("||"); + factory.setLineEnding(lineEnding); @SuppressWarnings("resource") - BufferedReader reader = factory.create(new ByteArrayResource("a|b||".getBytes()), "UTF-8"); + BufferedReader reader = factory.create(new ByteArrayResource(("a|b" + lineEnding).getBytes()), "UTF-8"); assertEquals("a|b", reader.readLine()); assertNull(reader.readLine()); } + @Test + void testCreateWithFalseMixedCharacterLineEnding() throws Exception { + SimpleBinaryBufferedReaderFactory factory = new SimpleBinaryBufferedReaderFactory(); + factory.setLineEnding("#@"); + @SuppressWarnings("resource") + BufferedReader reader = factory.create(new ByteArrayResource(("a##@").getBytes()), "UTF-8"); + assertEquals("a#", reader.readLine()); + assertNull(reader.readLine()); + } + @Test void testCreateWithIncompleteLineEnding() throws Exception { SimpleBinaryBufferedReaderFactory factory = new SimpleBinaryBufferedReaderFactory(); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java index 379220931d..a3fc68ff44 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -65,29 +65,10 @@ void testMapFieldSetWhenFieldNamesAreNotSpecified() { // when Exception exception = assertThrows(IllegalArgumentException.class, () -> recordFieldSetMapper.mapFieldSet(fieldSet)); - assertEquals("Field names must specified", exception.getMessage()); + assertEquals("Field names must be specified", exception.getMessage()); } - public static class Person { - - // TODO change to record in v5 - private final int id; - - private final String name; - - public Person(int id, String name) { - this.id = id; - this.name = name; - } - - public int id() { - return id; - } - - public String name() { - return name; - } - + record Person(int id, String name) { } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionItemTransformerTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregatorTests.java similarity index 72% rename from spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionItemTransformerTests.java rename to spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregatorTests.java index 4e0048ce35..eef13e3db3 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionItemTransformerTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/RecursiveCollectionLineAggregatorTests.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. @@ -19,6 +19,7 @@ import java.util.Collections; import org.junit.jupiter.api.Test; + import org.springframework.util.StringUtils; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -28,9 +29,7 @@ * @author Mahmoud Ben Hassine * */ -class RecursiveCollectionItemTransformerTests { - - private static final String LINE_SEPARATOR = System.getProperty("line.separator"); +class RecursiveCollectionLineAggregatorTests { private final RecursiveCollectionLineAggregator aggregator = new RecursiveCollectionLineAggregator<>(); @@ -41,9 +40,18 @@ void testSetDelegateAndPassInString() { } @Test - void testTransformList() { + void testAggregateListWithDefaultLineSeparator() { + String result = aggregator.aggregate(Arrays.asList(StringUtils.commaDelimitedListToStringArray("foo,bar"))); + String[] array = StringUtils.delimitedListToStringArray(result, System.lineSeparator()); + assertEquals("foo", array[0]); + assertEquals("bar", array[1]); + } + + @Test + void testAggregateListWithCustomLineSeparator() { + aggregator.setLineSeparator("#"); String result = aggregator.aggregate(Arrays.asList(StringUtils.commaDelimitedListToStringArray("foo,bar"))); - String[] array = StringUtils.delimitedListToStringArray(result, LINE_SEPARATOR); + String[] array = StringUtils.delimitedListToStringArray(result, "#"); assertEquals("foo", array[0]); assertEquals("bar", array[1]); } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/ConsumerItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/ConsumerItemWriterTests.java new file mode 100644 index 0000000000..4fef1da57e --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/ConsumerItemWriterTests.java @@ -0,0 +1,57 @@ +/* + * 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.item.function; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.Chunk; + +/** + * Test class for {@link ConsumerItemWriter}. + * + * @author Mahmoud Ben Hassine + */ +class ConsumerItemWriterTests { + + private final List items = new ArrayList<>(); + + private final Consumer consumer = items::add; + + @Test + void testMandatoryConsumer() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ConsumerItemWriter(null), + "A consumer is required"); + } + + @Test + void testWrite() throws Exception { + // given + Chunk chunk = Chunk.of("foo", "bar"); + ConsumerItemWriter consumerItemWriter = new ConsumerItemWriter<>(this.consumer); + + // when + consumerItemWriter.write(chunk); + + // then + Assertions.assertIterableEquals(chunk, this.items); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/PredicateFilteringItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/PredicateFilteringItemProcessorTests.java new file mode 100644 index 0000000000..aa6b79d456 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/PredicateFilteringItemProcessorTests.java @@ -0,0 +1,49 @@ +/* + * 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.item.function; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link PredicateFilteringItemProcessor}. + * + * @author Mahmoud Ben Hassine + */ +class PredicateFilteringItemProcessorTests { + + private final Predicate foos = item -> item.startsWith("foo"); + + @Test + void testMandatoryPredicate() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new PredicateFilteringItemProcessor(null), + "A predicate is required"); + } + + @Test + void testProcess() throws Exception { + // given + PredicateFilteringItemProcessor processor = new PredicateFilteringItemProcessor<>(this.foos); + + // when & then + Assertions.assertNull(processor.process("foo1")); + Assertions.assertNull(processor.process("foo2")); + Assertions.assertEquals("bar", processor.process("bar")); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/SupplierItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/SupplierItemReaderTests.java new file mode 100644 index 0000000000..f7587661d1 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/function/SupplierItemReaderTests.java @@ -0,0 +1,56 @@ +/* + * 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.item.function; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link SupplierItemReader}. + * + * @author Mahmoud Ben Hassine + */ +class SupplierItemReaderTests { + + private final Supplier supplier = new Supplier<>() { + private int count = 1; + + @Override + public String get() { + return count <= 2 ? "foo" + count++ : null; + } + }; + + @Test + void testMandatorySupplier() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new SupplierItemReader(null), + "A supplier is required"); + } + + @Test + void testRead() throws Exception { + // given + SupplierItemReader supplierItemReader = new SupplierItemReader<>(supplier); + + // when & then + Assertions.assertEquals("foo1", supplierItemReader.read()); + Assertions.assertEquals("foo2", supplierItemReader.read()); + Assertions.assertNull(supplierItemReader.read()); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java index 665356f1c1..7b6561f45f 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-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. @@ -130,4 +130,25 @@ void testInvalidResourceContent() { assertTrue(getJsonParsingException().isInstance(expectedException.getCause())); } + @Test + void testJumpToItem() throws Exception { + // given + JsonItemReader itemReader = new JsonItemReaderBuilder().jsonObjectReader(getJsonObjectReader()) + .resource(new ClassPathResource("org/springframework/batch/item/json/trades.json")) + .name("tradeJsonItemReader") + .build(); + itemReader.open(new ExecutionContext()); + + // when + itemReader.jumpToItem(3); + + // then + Trade trade = itemReader.read(); + assertNotNull(trade); + assertEquals("100", trade.getIsin()); + assertEquals("barfoo", trade.getCustomer()); + assertEquals(new BigDecimal("1.8"), trade.getPrice()); + assertEquals(4, trade.getQuantity()); + } + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemReaderTests.java new file mode 100644 index 0000000000..5806e576e3 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemReaderTests.java @@ -0,0 +1,48 @@ +/* + * 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.item.queue; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.queue.builder.BlockingQueueItemReaderBuilder; + +/** + * Test class for {@link BlockingQueueItemReader}. + * + * @author Mahmoud Ben Hassine + */ +class BlockingQueueItemReaderTests { + + @Test + void testRead() throws Exception { + // given + BlockingQueue queue = new ArrayBlockingQueue<>(10); + queue.put("foo"); + BlockingQueueItemReader reader = new BlockingQueueItemReaderBuilder().queue(queue) + .timeout(10, TimeUnit.MILLISECONDS) + .build(); + + // when & then + Assertions.assertEquals("foo", reader.read()); + Assertions.assertNull(reader.read()); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemWriterTests.java new file mode 100644 index 0000000000..cfd47b26f7 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/BlockingQueueItemWriterTests.java @@ -0,0 +1,49 @@ +/* + * 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.item.queue; + +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.queue.builder.BlockingQueueItemWriterBuilder; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test class for {@link BlockingQueueItemWriter}. + * + * @author Mahmoud Ben Hassine + */ +class BlockingQueueItemWriterTests { + + @Test + void testWrite() throws Exception { + // given + BlockingQueue queue = new ArrayBlockingQueue<>(10); + BlockingQueueItemWriter writer = new BlockingQueueItemWriterBuilder().queue(queue).build(); + + // when + writer.write(Chunk.of("foo", "bar")); + + // then + assertTrue(queue.containsAll(List.of("foo", "bar"))); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilderTests.java new file mode 100644 index 0000000000..1676e5051c --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemReaderBuilderTests.java @@ -0,0 +1,55 @@ +/* + * 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.item.queue.builder; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.queue.BlockingQueueItemReader; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test class for {@link BlockingQueueItemReaderBuilder}. + * + * @author Mahmoud Ben Hassine + */ +class BlockingQueueItemReaderBuilderTests { + + @Test + void testMandatoryQueue() { + assertThrows(IllegalStateException.class, () -> new BlockingQueueItemReaderBuilder().build()); + } + + @Test + void testBuildReader() { + // given + BlockingQueue queue = new ArrayBlockingQueue<>(5); + + // when + BlockingQueueItemReader reader = new BlockingQueueItemReaderBuilder().queue(queue).build(); + + // then + assertNotNull(reader); + assertEquals(queue, ReflectionTestUtils.getField(reader, "queue")); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilderTests.java new file mode 100644 index 0000000000..6a8eec4cd8 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/queue/builder/BlockingQueueItemWriterBuilderTests.java @@ -0,0 +1,56 @@ +/* + * 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.item.queue.builder; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.queue.BlockingQueueItemReader; +import org.springframework.batch.item.queue.BlockingQueueItemWriter; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test class for {@link BlockingQueueItemWriterBuilder}. + * + * @author Mahmoud Ben Hassine + */ +class BlockingQueueItemWriterBuilderTests { + + @Test + void testMandatoryQueue() { + assertThrows(IllegalStateException.class, () -> new BlockingQueueItemWriterBuilder().build()); + } + + @Test + void testBuildWriter() { + // given + BlockingQueue queue = new ArrayBlockingQueue<>(5); + + // when + BlockingQueueItemWriter writer = new BlockingQueueItemWriterBuilder().queue(queue).build(); + + // then + assertNotNull(writer); + assertEquals(queue, ReflectionTestUtils.getField(writer, "queue")); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java new file mode 100644 index 0000000000..3775c4299c --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java @@ -0,0 +1,110 @@ +/* + * 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.item.support; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamReader; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Test class for {@link CompositeItemReader}. + * + * @author Mahmoud Ben Hassine + */ +public class CompositeItemReaderTests { + + @Test + void testCompositeItemReaderOpen() { + // given + ItemStreamReader reader1 = mock(); + ItemStreamReader reader2 = mock(); + CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2)); + ExecutionContext executionContext = new ExecutionContext(); + + // when + compositeItemReader.open(executionContext); + + // then + verify(reader1).open(executionContext); + verify(reader2).open(executionContext); + } + + @Test + void testCompositeItemReaderRead() throws Exception { + // given + ItemStreamReader reader1 = mock(); + ItemStreamReader reader2 = mock(); + CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2)); + when(reader1.read()).thenReturn("foo1", "foo2", null); + when(reader2.read()).thenReturn("bar1", "bar2", null); + + // when & then + compositeItemReader.read(); + verify(reader1, times(1)).read(); + compositeItemReader.read(); + verify(reader1, times(2)).read(); + compositeItemReader.read(); + verify(reader1, times(3)).read(); + + compositeItemReader.read(); + verify(reader2, times(2)).read(); + compositeItemReader.read(); + verify(reader2, times(3)).read(); + compositeItemReader.read(); + verify(reader2, times(3)).read(); + } + + @Test + void testCompositeItemReaderUpdate() { + // given + ItemStreamReader reader1 = mock(); + ItemStreamReader reader2 = mock(); + CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2)); + ExecutionContext executionContext = new ExecutionContext(); + + // when + compositeItemReader.update(executionContext); + + // then + verify(reader1).update(executionContext); + verifyNoInteractions(reader2); // reader1 is the current delegate in this setup + } + + @Test + void testCompositeItemReaderClose() { + // given + ItemStreamReader reader1 = mock(); + ItemStreamReader reader2 = mock(); + CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2)); + + // when + compositeItemReader.close(); + + // then + verify(reader1).close(); + verify(reader2).close(); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java index 9c786e8310..2374153b3b 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.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. @@ -138,6 +138,12 @@ void testFromMetaDataForPostgres() throws Exception { assertEquals(POSTGRES, DatabaseType.fromMetaData(ds)); } + @Test + void testFromMetaDataForEnterpriseDB() throws Exception { + DataSource ds = DatabaseTypeTestUtils.getMockDataSource("EnterpriseDB"); + assertEquals(POSTGRES, DatabaseType.fromMetaData(ds)); + } + @Test void testFromMetaDataForSybase() throws Exception { DataSource ds = DatabaseTypeTestUtils.getMockDataSource("Adaptive Server Enterprise"); diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/support/query-provider-fixture.sql b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/support/query-provider-fixture.sql new file mode 100644 index 0000000000..f320010978 --- /dev/null +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/support/query-provider-fixture.sql @@ -0,0 +1,20 @@ +CREATE TABLE TEST_TABLE ( + ID INTEGER NOT NULL, + STRING VARCHAR(16) NOT NULL +); + +INSERT INTO TEST_TABLE (ID, STRING) VALUES (1, 'Spring'); +INSERT INTO TEST_TABLE (ID, STRING) VALUES (2, 'Batch'); +INSERT INTO TEST_TABLE (ID, STRING) VALUES (3, 'Infrastructure'); + +CREATE TABLE GROUPING_TEST_TABLE ( + ID INTEGER NOT NULL, + STRING VARCHAR(16) NOT NULL +); + +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (1, 'Spring'); +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (2, 'Batch'); +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (3, 'Batch'); +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (4, 'Infrastructure'); +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (5, 'Infrastructure'); +INSERT INTO GROUPING_TEST_TABLE (ID, STRING) VALUES (6, 'Infrastructure'); \ No newline at end of file diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml index d02bb86b6e..a57e5f58c5 100644 --- a/spring-batch-integration/pom.xml +++ b/spring-batch-integration/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-integration Spring Batch Integration @@ -32,6 +32,12 @@ org.springframework.integration spring-integration-core ${spring-integration.version} + + + org.springframework.retry + spring-retry + + org.springframework diff --git a/spring-batch-integration/src/main/java/META-INF/MANIFEST.MF b/spring-batch-integration/src/main/java/META-INF/MANIFEST.MF deleted file mode 100644 index 5e9495128c..0000000000 --- a/spring-batch-integration/src/main/java/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/aot/IntegrationRuntimeHints.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/aot/IntegrationRuntimeHints.java index 72674495fb..5f42caf800 100644 --- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/aot/IntegrationRuntimeHints.java +++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/aot/IntegrationRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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,9 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.batch.integration.chunk.ChunkRequest; import org.springframework.batch.integration.chunk.ChunkResponse; +import org.springframework.batch.integration.partition.MessageChannelPartitionHandler; +import org.springframework.batch.integration.partition.StepExecutionRequest; +import org.springframework.batch.integration.partition.StepExecutionRequestHandler; /** * AOT hints for Spring Batch integration module. @@ -32,12 +35,16 @@ public class IntegrationRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // reflection hints - hints.reflection().registerType(ChunkRequest.class, MemberCategory.values()); - hints.reflection().registerType(ChunkResponse.class, MemberCategory.values()); + MemberCategory[] memberCategories = MemberCategory.values(); + hints.reflection().registerType(ChunkRequest.class, memberCategories); + hints.reflection().registerType(ChunkResponse.class, memberCategories); + hints.reflection().registerType(StepExecutionRequestHandler.class, memberCategories); + hints.reflection().registerType(MessageChannelPartitionHandler.class, memberCategories); // serialization hints hints.serialization().registerType(ChunkRequest.class); hints.serialization().registerType(ChunkResponse.class); + hints.serialization().registerType(StepExecutionRequest.class); } } diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/MessageChannelPartitionHandler.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/MessageChannelPartitionHandler.java index a5cc624196..f0c710c544 100644 --- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/MessageChannelPartitionHandler.java +++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/MessageChannelPartitionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2009-2023 the original author or authors. + * Copyright 2009-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. @@ -251,24 +251,22 @@ protected Set doHandle(StepExecution managerStepExecution, private Set pollReplies(final StepExecution managerStepExecution, final Set split) throws Exception { - final Set result = new HashSet<>(split.size()); + Set partitionStepExecutionIds = split.stream().map(StepExecution::getId).collect(Collectors.toSet()); Callable> callback = () -> { - Set currentStepExecutionIds = split.stream().map(StepExecution::getId).collect(Collectors.toSet()); JobExecution jobExecution = jobExplorer.getJobExecution(managerStepExecution.getJobExecutionId()); - jobExecution.getStepExecutions() + Set finishedStepExecutions = jobExecution.getStepExecutions() .stream() - .filter(stepExecution -> currentStepExecutionIds.contains(stepExecution.getId())) - .filter(stepExecution -> !result.contains(stepExecution)) + .filter(stepExecution -> partitionStepExecutionIds.contains(stepExecution.getId())) .filter(stepExecution -> !stepExecution.getStatus().isRunning()) - .forEach(result::add); + .collect(Collectors.toSet()); if (logger.isDebugEnabled()) { logger.debug(String.format("Currently waiting on %s partitions to finish", split.size())); } - if (result.size() == split.size()) { - return result; + if (finishedStepExecutions.size() == split.size()) { + return finishedStepExecutions; } else { return null; diff --git a/spring-batch-integration/src/main/java/META-INF/spring/aot.factories b/spring-batch-integration/src/main/resources/META-INF/spring/aot.factories similarity index 100% rename from spring-batch-integration/src/main/java/META-INF/spring/aot.factories rename to spring-batch-integration/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/step/DelegateStep.java b/spring-batch-integration/src/test/java/org/springframework/batch/integration/step/DelegateStep.java similarity index 92% rename from spring-batch-integration/src/main/java/org/springframework/batch/integration/step/DelegateStep.java rename to spring-batch-integration/src/test/java/org/springframework/batch/integration/step/DelegateStep.java index 3c09af11f3..4338a9ba70 100644 --- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/step/DelegateStep.java +++ b/spring-batch-integration/src/test/java/org/springframework/batch/integration/step/DelegateStep.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.batch.integration.step; import org.springframework.batch.core.Step; @@ -27,10 +26,8 @@ * * @author Dave Syer * @author Mahmoud Ben Hassine - * @deprecated since 5.0 with no replacement. Scheduled for removal in 5.2. * */ -@Deprecated(since = "5.0", forRemoval = true) public class DelegateStep extends AbstractStep { private Step delegate; @@ -56,4 +53,4 @@ protected void doExecute(StepExecution stepExecution) throws Exception { delegate.execute(stepExecution); } -} +} \ No newline at end of file diff --git a/spring-batch-samples/README.md b/spring-batch-samples/README.md index 046ca1adea..770c7fd938 100644 --- a/spring-batch-samples/README.md +++ b/spring-batch-samples/README.md @@ -26,6 +26,7 @@ Here is a list of samples with checks to indicate which features each one demons | [Hello world Job Sample](#hello-world-job-sample) | | | | | | | | | | X | | | [Amqp Job Sample](#amqp-job-sample) | | | | | | | | | | X | | | [BeanWrapperMapper Sample](#beanwrappermapper-sample) | | | | X | | | | | | | | +| [Composite ItemReader Sample](#composite-itemreader-sample) | | | | | | | X | | | | | | [Composite ItemWriter Sample](#composite-itemwriter-sample) | | | | | | | X | | | | | | [Customer Filter Sample](#customer-filter-sample) | | | | | | | | | | | X | | [Reader Writer Adapter Sample](#reader-writer-adapter-sample) | | | | | | | X | | | | | @@ -60,6 +61,7 @@ The IO Sample Job has a number of special instances that show different IO featu | [multiResource Sample](#multiresource-input-output-job) | x | | | | | | | x | | x | | x | | [XML Input Output Sample](#xml-input-output) | | | x | | | | | | | | | | | [MongoDB sample](#mongodb-sample) | | | | | x | | | | x | | | | +| [PetClinic sample](#petclinic-sample) | | | | | x | x | | | | | | | ### Common Sample Source Structures @@ -121,6 +123,16 @@ prototype according to the field names in the file. [BeanWrapperMapper Sample](src/main/java/org/springframework/batch/samples/beanwrapper/README.md) +### Composite ItemReader Sample + +This sample shows how to use a composite item reader to read data with +the same format from different data sources. + +In this sample, data items of type `Person` are read from two flat files +and a relational database table. + +[Composite reader Sample](src/main/java/org/springframework/batch/samples/compositereader/README.md) + ### Composite ItemWriter Sample This shows a common use case using a composite pattern, composing @@ -604,6 +616,16 @@ $>docker run --name mongodb --rm -d -p 27017:27017 mongo Once MongoDB is up and running, run the `org.springframework.batch.samples.mongodb.MongoDBSampleApp` class without any argument to start the sample. +### PetClinic sample + +This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use +Spring Batch to export data from a relational database table to a flat file. + +The job in this sample is a single-step job that exports data from the `owners` table +to a flat file named `owners.csv`. + +[PetClinic Sample](src/main/java/org/springframework/batch/samples/petclinic/README.md) + ### Adhoc Loop and JMX Sample This job is simply an infinite loop. It runs forever so it is diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml index 90e14a1e2d..9bf671940f 100644 --- a/spring-batch-samples/pom.xml +++ b/spring-batch-samples/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-samples jar @@ -52,9 +52,11 @@ spring-context-support ${spring-framework.version} + io.micrometer - micrometer-registry-prometheus + micrometer-registry-prometheus-simpleclient ${micrometer.version} @@ -118,6 +120,33 @@ org.springframework.data spring-data-mongodb ${spring-data-mongodb.version} + + + org.slf4j + slf4j-api + + + org.mongodb + mongodb-driver-core + + + org.mongodb + mongodb-driver-sync + + + org.springframework + spring-expression + + + org.springframework.data + spring-data-commons + + + + + org.springframework.data + spring-data-commons + ${spring-data-commons.version} org.springframework.data @@ -128,6 +157,12 @@ org.springframework.amqp spring-amqp ${spring-amqp.version} + + + org.springframework.retry + spring-retry + + org.springframework.amqp @@ -159,10 +194,15 @@ jakarta.el ${jakarta.el.version} + + org.mongodb + mongodb-driver-core + ${mongodb-driver.version} + org.mongodb mongodb-driver-sync - ${mongodb-driver-sync.version} + ${mongodb-driver.version} io.prometheus diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/compositereader/README.md b/spring-batch-samples/src/main/java/org/springframework/batch/samples/compositereader/README.md new file mode 100644 index 0000000000..4342fbf054 --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/compositereader/README.md @@ -0,0 +1,18 @@ +## Composite ItemReader Sample + +### About + +This sample shows how to use a composite item reader to read data with +the same format from different data sources. + +In this sample, data items of type `Person` are read from two flat files +and a relational database table. + +### Run the sample + +You can run the sample from the command line as following: + +``` +$>cd spring-batch-samples +$>../mvnw -Dtest=CompositeItemReaderSampleFunctionalTests#testJobLaunch test +``` \ No newline at end of file diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateAwareCustomerCreditItemWriter.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateAwareCustomerCreditItemWriter.java deleted file mode 100644 index 55172e1f92..0000000000 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateAwareCustomerCreditItemWriter.java +++ /dev/null @@ -1,72 +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.samples.domain.trade.internal; - -import org.hibernate.SessionFactory; - -import org.springframework.batch.item.Chunk; -import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.samples.domain.trade.CustomerCredit; -import org.springframework.batch.samples.domain.trade.CustomerCreditDao; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; - -/** - * Delegates writing to a custom DAO and flushes + clears hibernate session to fulfill the - * {@link ItemWriter} contract. - * - * @author Robert Kasanicky - * @author Michael Minella - * @author Mahmoud Ben Hassine - */ -public class HibernateAwareCustomerCreditItemWriter implements ItemWriter, InitializingBean { - - private CustomerCreditDao dao; - - private SessionFactory sessionFactory; - - @Override - public void write(Chunk items) throws Exception { - for (CustomerCredit credit : items) { - dao.writeCredit(credit); - } - try { - sessionFactory.getCurrentSession().flush(); - } - finally { - // this should happen automatically on commit, but to be on the safe - // side... - sessionFactory.getCurrentSession().clear(); - } - - } - - public void setDao(CustomerCreditDao dao) { - this.dao = dao; - } - - public void setSessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - @Override - public void afterPropertiesSet() throws Exception { - Assert.state(sessionFactory != null, "Hibernate SessionFactory is required"); - Assert.state(dao != null, "Delegate DAO must be set"); - } - -} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateCreditDao.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateCreditDao.java deleted file mode 100644 index b0f3284fd4..0000000000 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/domain/trade/internal/HibernateCreditDao.java +++ /dev/null @@ -1,102 +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.samples.domain.trade.internal; - -import java.util.ArrayList; -import java.util.List; - -import org.hibernate.SessionFactory; -import org.springframework.batch.repeat.RepeatContext; -import org.springframework.batch.repeat.RepeatListener; -import org.springframework.batch.repeat.RepeatStatus; -import org.springframework.batch.samples.domain.trade.CustomerCredit; -import org.springframework.batch.samples.domain.trade.CustomerCreditDao; - -/** - * @author Lucas Ward - * @author Dave Syer - * @author Mahmoud Ben Hassine - * - */ -public class HibernateCreditDao implements CustomerCreditDao, RepeatListener { - - private int failOnFlush = -1; - - private final List errors = new ArrayList<>(); - - private SessionFactory sessionFactory; - - public void setSessionFactory(SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - /** - * Public accessor for the errors property. - * @return the errors - a list of Throwable instances - */ - public List getErrors() { - return errors; - } - - @Override - public void writeCredit(CustomerCredit customerCredit) { - if (customerCredit.getId() == failOnFlush) { - // try to insert one with a duplicate ID - CustomerCredit newCredit = new CustomerCredit(); - newCredit.setId(customerCredit.getId()); - newCredit.setName(customerCredit.getName()); - newCredit.setCredit(customerCredit.getCredit()); - sessionFactory.getCurrentSession().save(newCredit); - } - else { - sessionFactory.getCurrentSession().update(customerCredit); - } - } - - public void write(Object output) { - writeCredit((CustomerCredit) output); - } - - /** - * Public setter for the failOnFlush property. - * @param failOnFlush the ID of the record you want to fail on flush (for testing) - */ - public void setFailOnFlush(int failOnFlush) { - this.failOnFlush = failOnFlush; - } - - @Override - public void onError(RepeatContext context, Throwable e) { - errors.add(e); - } - - @Override - public void after(RepeatContext context, RepeatStatus result) { - } - - @Override - public void before(RepeatContext context) { - } - - @Override - public void close(RepeatContext context) { - } - - @Override - public void open(RepeatContext context) { - } - -} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunner.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/misc/jmx/JobRegistryBackgroundJobRunner.java similarity index 98% rename from spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunner.java rename to spring-batch-samples/src/main/java/org/springframework/batch/samples/misc/jmx/JobRegistryBackgroundJobRunner.java index da01a1af5b..7a1a36c231 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobRegistryBackgroundJobRunner.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/misc/jmx/JobRegistryBackgroundJobRunner.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.batch.core.launch.support; +package org.springframework.batch.samples.misc.jmx; import java.io.IOException; import java.util.ArrayList; @@ -57,10 +57,8 @@ * * @author Dave Syer * @author Mahmoud Ben Hassine - * @deprecated since 5.0 with no replacement. Scheduled for removal in 5.2. * */ -@Deprecated(since = "5.0", forRemoval = true) public class JobRegistryBackgroundJobRunner { /** diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java index 42dcd42d9f..ca50af5af3 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-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,14 +20,13 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 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.item.data.MongoItemReader; import org.springframework.batch.item.data.MongoItemWriter; -import org.springframework.batch.item.data.builder.MongoItemReaderBuilder; +import org.springframework.batch.item.data.MongoPagingItemReader; import org.springframework.batch.item.data.builder.MongoItemWriterBuilder; +import org.springframework.batch.item.data.builder.MongoPagingItemReaderBuilder; import org.springframework.context.annotation.Bean; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; @@ -38,18 +37,17 @@ /** * This job will remove document "foo3" from collection "person_out" using - * {@link MongoItemWriter#setDelete(boolean)}. + * {@link MongoItemWriter#setMode(MongoItemWriter.Mode)}}. * * @author Mahmoud Ben Hassine */ -@EnableBatchProcessing public class DeletionJobConfiguration { @Bean - public MongoItemReader mongoPersonReader(MongoTemplate mongoTemplate) { + public MongoPagingItemReader mongoPersonReader(MongoTemplate mongoTemplate) { Map sortOptions = new HashMap<>(); sortOptions.put("name", Sort.Direction.DESC); - return new MongoItemReaderBuilder().name("personItemReader") + return new MongoPagingItemReaderBuilder().name("personItemReader") .collection("person_out") .targetType(Person.class) .template(mongoTemplate) @@ -61,14 +59,14 @@ public MongoItemReader mongoPersonReader(MongoTemplate mongoTemplate) { @Bean public MongoItemWriter mongoPersonRemover(MongoTemplate mongoTemplate) { return new MongoItemWriterBuilder().template(mongoTemplate) - .delete(true) + .mode(MongoItemWriter.Mode.REMOVE) .collection("person_out") .build(); } @Bean public Step deletionStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, - MongoItemReader mongoPersonReader, MongoItemWriter mongoPersonRemover) { + MongoPagingItemReader mongoPersonReader, MongoItemWriter mongoPersonRemover) { return new StepBuilder("step", jobRepository).chunk(2, transactionManager) .reader(mongoPersonReader) .writer(mongoPersonRemover) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java index 1e1488d50b..8bbf2b0932 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-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,14 +20,13 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.Step; -import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; 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.item.data.MongoItemReader; +import org.springframework.batch.item.data.MongoPagingItemReader; import org.springframework.batch.item.data.MongoItemWriter; -import org.springframework.batch.item.data.builder.MongoItemReaderBuilder; import org.springframework.batch.item.data.builder.MongoItemWriterBuilder; +import org.springframework.batch.item.data.builder.MongoPagingItemReaderBuilder; import org.springframework.context.annotation.Bean; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; @@ -35,18 +34,17 @@ /** * This job will copy documents from collection "person_in" into collection "person_out" - * using {@link MongoItemReader} and {@link MongoItemWriter}. + * using {@link MongoPagingItemReader} and {@link MongoItemWriter}. * * @author Mahmoud Ben Hassine */ -@EnableBatchProcessing public class InsertionJobConfiguration { @Bean - public MongoItemReader mongoItemReader(MongoTemplate mongoTemplate) { + public MongoPagingItemReader mongoItemReader(MongoTemplate mongoTemplate) { Map sortOptions = new HashMap<>(); sortOptions.put("name", Sort.Direction.DESC); - return new MongoItemReaderBuilder().name("personItemReader") + return new MongoPagingItemReaderBuilder().name("personItemReader") .collection("person_in") .targetType(Person.class) .template(mongoTemplate) @@ -62,7 +60,7 @@ public MongoItemWriter mongoItemWriter(MongoTemplate mongoTemplate) { @Bean public Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager, - MongoItemReader mongoItemReader, MongoItemWriter mongoItemWriter) { + MongoPagingItemReader mongoItemReader, MongoItemWriter mongoItemWriter) { return new StepBuilder("step", jobRepository).chunk(2, transactionManager) .reader(mongoItemReader) .writer(mongoItemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java index 4695a21fd7..45b2994f3a 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2022 the original author or authors. + * Copyright 2020-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,11 @@ import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; +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.repository.JobRepository; +import org.springframework.batch.core.repository.support.MongoJobRepositoryFactoryBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -26,9 +31,11 @@ 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; @Configuration @PropertySource("classpath:/org/springframework/batch/samples/mongodb/mongodb-sample.properties") +@EnableBatchProcessing public class MongoDBConfiguration { @Value("${mongodb.host}") @@ -48,7 +55,10 @@ public MongoClient mongoClient() { @Bean public MongoTemplate mongoTemplate(MongoClient mongoClient) { - return new MongoTemplate(mongoClient, "test"); + MongoTemplate mongoTemplate = new MongoTemplate(mongoClient, "test"); + MappingMongoConverter converter = (MappingMongoConverter) mongoTemplate.getConverter(); + converter.setMapKeyDotReplacement("."); + return mongoTemplate; } @Bean @@ -61,4 +71,24 @@ public MongoTransactionManager transactionManager(MongoDatabaseFactory mongoData return new MongoTransactionManager(mongoDatabaseFactory); } + @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(); + } + } diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBSampleApp.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBSampleApp.java index ae7b53bbb2..7fc8e52f5d 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBSampleApp.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBSampleApp.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2023 the original author or authors. + * Copyright 2020-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. @@ -17,6 +17,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Map; import com.mongodb.client.MongoCollection; import org.bson.Document; @@ -45,6 +46,18 @@ public static void main(String[] args) throws Exception { ApplicationContext context = new AnnotationConfigApplicationContext(configurationClasses); MongoTemplate mongoTemplate = context.getBean(MongoTemplate.class); + // create meta-data collections and sequences + 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))); + // clear collections and insert some documents in "person_in" MongoCollection personsIn = mongoTemplate.getCollection("person_in"); MongoCollection personsOut = mongoTemplate.getCollection("person_out"); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java new file mode 100644 index 0000000000..7a66d7d296 --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java @@ -0,0 +1,19 @@ +/* + * Copyright 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. + * 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.samples.petclinic; + +public record Owner(int id, String firstname, String lastname, String address, String city, String telephone) { +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java new file mode 100644 index 0000000000..4a27ffb23f --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java @@ -0,0 +1,71 @@ +/* + * Copyright 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. + * 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.samples.petclinic; + +import javax.sql.DataSource; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +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.item.database.JdbcCursorItemReader; +import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; +import org.springframework.batch.samples.common.DataSourceConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jdbc.core.DataClassRowMapper; +import org.springframework.jdbc.support.JdbcTransactionManager; + +@Configuration +@EnableBatchProcessing +@Import(DataSourceConfiguration.class) +public class OwnersExportJobConfiguration { + + @Bean + public JdbcCursorItemReader ownersReader(DataSource dataSource) { + return new JdbcCursorItemReaderBuilder().name("ownersReader") + .sql("SELECT * FROM OWNERS") + .dataSource(dataSource) + .rowMapper(new DataClassRowMapper<>(Owner.class)) + .build(); + } + + @Bean + public FlatFileItemWriter ownersWriter() { + return new FlatFileItemWriterBuilder().name("ownersWriter") + .resource(new FileSystemResource("owners.csv")) + .delimited() + .names("id", "firstname", "lastname", "address", "city", "telephone") + .build(); + } + + @Bean + public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, + JdbcCursorItemReader ownersReader, FlatFileItemWriter ownersWriter) { + return new JobBuilder("ownersExportJob", jobRepository) + .start(new StepBuilder("ownersExportStep", jobRepository).chunk(5, transactionManager) + .reader(ownersReader) + .writer(ownersWriter) + .build()) + .build(); + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md new file mode 100644 index 0000000000..12be08e09b --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md @@ -0,0 +1,21 @@ +# PetClinic Job + +## About the sample + +This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use +Spring Batch to export data from a relational database table to a flat file. + +The job in this sample is a single-step job that exports data from the `owners` table +to a flat file named `owners.csv`. + +## Run the sample + +You can run the sample from the command line as following: + +``` +$>cd spring-batch-samples +# Launch the sample using the XML configuration +$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithXmlConfiguration test +# Launch the sample using the Java configuration +$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithJavaConfiguration test +``` \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql index b02b0b89a5..52a8c890f0 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql @@ -9,6 +9,7 @@ DROP TABLE PLAYERS IF EXISTS; DROP TABLE GAMES IF EXISTS; DROP TABLE PLAYER_SUMMARY IF EXISTS; DROP TABLE ERROR_LOG IF EXISTS; +DROP TABLE OWNERS IF EXISTS; -- Autogenerated: do not edit this file @@ -100,3 +101,26 @@ CREATE TABLE ERROR_LOG ( STEP_NAME CHAR(20) , MESSAGE VARCHAR(300) NOT NULL ) ; + +-- PetClinic sample tables + +CREATE TABLE OWNERS ( + ID INTEGER IDENTITY PRIMARY KEY, + FIRSTNAME VARCHAR(30), + LASTNAME VARCHAR(30), + ADDRESS VARCHAR(255), + CITY VARCHAR(80), + TELEPHONE VARCHAR(20) +); + +INSERT INTO OWNERS VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT INTO OWNERS VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT INTO OWNERS VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT INTO OWNERS VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT INTO OWNERS VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT INTO OWNERS VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT INTO OWNERS VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT INTO OWNERS VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT INTO OWNERS VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT INTO OWNERS VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons1.csv b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons1.csv new file mode 100644 index 0000000000..839754d238 --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons1.csv @@ -0,0 +1,2 @@ +1,foo1 +2,foo2 \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons2.csv b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons2.csv new file mode 100644 index 0000000000..e5a88e3407 --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/data/persons2.csv @@ -0,0 +1,2 @@ +3,bar1 +4,bar2 \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/data.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/data.sql new file mode 100644 index 0000000000..6b99ba0b49 --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/data.sql @@ -0,0 +1,2 @@ +insert into person_source values (5, 'baz1'); +insert into person_source values (6, 'baz2'); \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/schema.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/schema.sql new file mode 100644 index 0000000000..1ab4a13663 --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/compositereader/sql/schema.sql @@ -0,0 +1,2 @@ +create table person_source (id int primary key, name varchar(20)); +create table person_target (id int primary key, name varchar(20)); \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/misc/quartz/quartz-job-launcher-context.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/misc/quartz/quartz-job-launcher-context.xml index 076bd0da1d..0be94b1dbe 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/misc/quartz/quartz-job-launcher-context.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/misc/quartz/quartz-job-launcher-context.xml @@ -27,7 +27,7 @@ - + diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml new file mode 100644 index 0000000000..0247f5511f --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java new file mode 100644 index 0000000000..8c90257b6e --- /dev/null +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java @@ -0,0 +1,147 @@ +/* + * 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.samples.compositereader; + +import java.util.Arrays; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +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.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JdbcBatchItemWriter; +import org.springframework.batch.item.database.JdbcCursorItemReader; +import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder; +import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; +import org.springframework.batch.item.file.FlatFileItemReader; +import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; +import org.springframework.batch.item.support.CompositeItemReader; +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.io.ClassPathResource; +import org.springframework.jdbc.core.DataClassRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.jdbc.support.JdbcTransactionManager; +import org.springframework.test.jdbc.JdbcTestUtils; + +public class CompositeItemWriterSampleFunctionalTests { + + record Person(int id, String name) { + } + + @Test + void testJobLaunch() throws Exception { + // given + ApplicationContext context = new AnnotationConfigApplicationContext(JobConfiguration.class); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + Job job = context.getBean(Job.class); + + // when + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + // then + Assertions.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + JdbcTemplate jdbcTemplate = new JdbcTemplate(context.getBean(DataSource.class)); + int personsCount = JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target"); + Assertions.assertEquals(6, personsCount); + } + + @Configuration + @EnableBatchProcessing + static class JobConfiguration { + + @Bean + public FlatFileItemReader itemReader1() { + return new FlatFileItemReaderBuilder().name("personItemReader1") + .resource(new ClassPathResource("org/springframework/batch/samples/compositereader/data/persons1.csv")) + .delimited() + .names("id", "name") + .targetType(Person.class) + .build(); + } + + @Bean + public FlatFileItemReader itemReader2() { + return new FlatFileItemReaderBuilder().name("personItemReader2") + .resource(new ClassPathResource("org/springframework/batch/samples/compositereader/data/persons2.csv")) + .delimited() + .names("id", "name") + .targetType(Person.class) + .build(); + } + + @Bean + public JdbcCursorItemReader itemReader3() { + String sql = "select * from person_source"; + return new JdbcCursorItemReaderBuilder().name("personItemReader3") + .dataSource(dataSource()) + .sql(sql) + .rowMapper(new DataClassRowMapper<>(Person.class)) + .build(); + } + + @Bean + public CompositeItemReader itemReader() { + return new CompositeItemReader<>(Arrays.asList(itemReader1(), itemReader2(), itemReader3())); + } + + @Bean + public JdbcBatchItemWriter itemWriter() { + String sql = "insert into person_target (id, name) values (:id, :name)"; + return new JdbcBatchItemWriterBuilder().dataSource(dataSource()).sql(sql).beanMapped().build(); + } + + @Bean + public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager) { + return new JobBuilder("job", jobRepository) + .start(new StepBuilder("step", jobRepository).chunk(5, transactionManager) + .reader(itemReader()) + .writer(itemWriter()) + .build()) + .build(); + } + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL) + .addScript("/org/springframework/batch/core/schema-drop-hsqldb.sql") + .addScript("/org/springframework/batch/core/schema-hsqldb.sql") + .addScript("/org/springframework/batch/samples/compositereader/sql/schema.sql") + .addScript("/org/springframework/batch/samples/compositereader/sql/data.sql") + .build(); + } + + @Bean + public JdbcTransactionManager transactionManager(DataSource dataSource) { + return new JdbcTransactionManager(dataSource); + } + + } + +} \ No newline at end of file diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java index 32abadfda9..b3b91d0257 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.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. @@ -21,7 +21,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.springframework.batch.core.launch.JobOperator; -import org.springframework.batch.core.launch.support.JobRegistryBackgroundJobRunner; import org.springframework.batch.samples.launch.JobLoader; import org.springframework.jmx.MBeanServerNotFoundException; import org.springframework.jmx.access.InvalidInvocationException; diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java new file mode 100644 index 0000000000..dc8bfce26b --- /dev/null +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 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. + * 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.samples.petclinic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml", + "/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml" }) +class PetClinicJobFunctionalTests { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @BeforeEach + @AfterEach + public void deleteOwnersFile() throws IOException { + Files.deleteIfExists(Paths.get("owners.csv")); + } + + @Test + void testLaunchJobWithXmlConfiguration() throws Exception { + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(); + + // then + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + } + + @Test + void testLaunchJobWithJavaConfiguration() throws Exception { + // given + ApplicationContext context = new AnnotationConfigApplicationContext(OwnersExportJobConfiguration.class); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + Job job = context.getBean(Job.class); + + // when + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + // then + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + } + +} diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml index 15bbf21f7b..ab9e8b20ba 100644 --- a/spring-batch-test/pom.xml +++ b/spring-batch-test/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.0-SNAPSHOT + 5.2.2-SNAPSHOT spring-batch-test Spring Batch Test diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/AssertFile.java b/spring-batch-test/src/main/java/org/springframework/batch/test/AssertFile.java deleted file mode 100644 index 8c1170c6c5..0000000000 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/AssertFile.java +++ /dev/null @@ -1,92 +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.test; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; - -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; - -/** - * This class can be used to assert that two files are the same. - * - * @author Dan Garrette - * @author Glenn Renfro - * @author Mahmoud Ben Hassine - * @since 2.0 - * @deprecated since 5.0 (for removal in 5.2) in favor of test utilities provided by - * modern test libraries like JUnit 5, AssertJ, etc. - */ -@Deprecated(since = "5.0", forRemoval = true) -public abstract class AssertFile { - - public static void assertFileEquals(File expected, File actual) throws Exception { - BufferedReader expectedReader = new BufferedReader(new FileReader(expected)); - BufferedReader actualReader = new BufferedReader(new FileReader(actual)); - try { - int lineNum = 1; - for (String expectedLine = null; (expectedLine = expectedReader.readLine()) != null; lineNum++) { - String actualLine = actualReader.readLine(); - Assert.state(assertStringEqual(expectedLine, actualLine), - "Line number " + lineNum + " does not match."); - } - - String actualLine = actualReader.readLine(); - Assert.state(assertStringEqual(null, actualLine), - "More lines than expected. There should not be a line number " + lineNum + "."); - } - finally { - expectedReader.close(); - actualReader.close(); - } - } - - public static void assertFileEquals(Resource expected, Resource actual) throws Exception { - assertFileEquals(expected.getFile(), actual.getFile()); - } - - public static void assertLineCount(int expectedLineCount, File file) throws Exception { - BufferedReader expectedReader = new BufferedReader(new FileReader(file)); - try { - int lineCount = 0; - while (expectedReader.readLine() != null) { - lineCount++; - } - Assert.state(expectedLineCount == lineCount, String - .format("Line count of %d does not match expected count of %d", lineCount, expectedLineCount)); - } - finally { - expectedReader.close(); - } - } - - public static void assertLineCount(int expectedLineCount, Resource resource) throws Exception { - assertLineCount(expectedLineCount, resource.getFile()); - } - - private static boolean assertStringEqual(String expected, String actual) { - if (expected == null) { - return actual == null; - } - else { - return expected.equals(actual); - } - } - -} diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/DataSourceInitializer.java b/spring-batch-test/src/main/java/org/springframework/batch/test/DataSourceInitializer.java deleted file mode 100755 index c34651ca32..0000000000 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/DataSourceInitializer.java +++ /dev/null @@ -1,206 +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.test; - -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileSystemNotFoundException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; - -import javax.sql.DataSource; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.BeanInitializationException; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.core.io.Resource; -import org.springframework.dao.DataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.JdbcTransactionManager; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; - -/** - * Wrapper for a {@link DataSource} that can run scripts on start up and shut down. Use as - * a bean definition
    - * - * Run this class to initialize a database in a running server process. Make sure the - * server is running first by launching the "hsql-server" from the - * hsql.server project. Then you can right click in Eclipse and Run As -> - * Java Application. Do the same any time you want to wipe the database and start again. - * - * @author Dave Syer - * @author Drummond Dawson - * @author Mahmoud Ben Hassine - * @deprecated since 5.0 in favor of similar utilities provided by Spring Framework. - * Scheduled for removal in 5.2. - * - */ -@Deprecated(since = "5.0", forRemoval = true) -public class DataSourceInitializer implements InitializingBean, DisposableBean { - - private static final Log logger = LogFactory.getLog(DataSourceInitializer.class); - - private Resource[] initScripts; - - private Resource[] destroyScripts; - - private DataSource dataSource; - - private boolean ignoreFailedDrop = true; - - private boolean initialized = false; - - /** - * Main method as convenient entry point. - * @param args arguments to be passed to main. - */ - @SuppressWarnings("resource") - public static void main(String... args) { - new ClassPathXmlApplicationContext(ClassUtils.addResourcePathToPackagePath(DataSourceInitializer.class, - DataSourceInitializer.class.getSimpleName() + "-context.xml")); - } - - @Override - public void destroy() { - if (this.destroyScripts == null) { - return; - } - for (Resource destroyScript : this.destroyScripts) { - try { - doExecuteScript(destroyScript); - } - catch (Exception e) { - if (logger.isDebugEnabled()) { - logger.warn("Could not execute destroy script [" + destroyScript + "]", e); - } - else { - logger.warn("Could not execute destroy script [" + destroyScript + "]"); - } - } - } - } - - @Override - public void afterPropertiesSet() { - Assert.state(this.dataSource != null, "A DataSource is required"); - initialize(); - } - - private void initialize() { - if (!this.initialized) { - destroy(); - if (this.initScripts != null) { - for (Resource initScript : this.initScripts) { - doExecuteScript(initScript); - } - } - this.initialized = true; - } - } - - private void doExecuteScript(final Resource scriptResource) { - if (scriptResource == null || !scriptResource.exists()) { - return; - } - TransactionTemplate transactionTemplate = new TransactionTemplate(new JdbcTransactionManager(this.dataSource)); - transactionTemplate.execute((TransactionCallback) status -> { - JdbcTemplate jdbcTemplate = new JdbcTemplate(this.dataSource); - String[] scripts; - try { - scripts = StringUtils.delimitedListToStringArray(stripComments(getScriptLines(scriptResource)), ";"); - } - catch (IOException e) { - throw new BeanInitializationException("Cannot load script from [" + scriptResource + "]", e); - } - for (String script : scripts) { - String trimmedScript = script.trim(); - if (StringUtils.hasText(trimmedScript)) { - try { - jdbcTemplate.execute(trimmedScript); - } - catch (DataAccessException e) { - if (this.ignoreFailedDrop && trimmedScript.toLowerCase().startsWith("drop") - && logger.isDebugEnabled()) { - logger.debug("DROP script failed (ignoring): " + trimmedScript); - } - else { - throw e; - } - } - } - } - return null; - }); - - } - - private List getScriptLines(Resource scriptResource) throws IOException { - URI uri = scriptResource.getURI(); - initFileSystem(uri); - return Files.readAllLines(Paths.get(uri), StandardCharsets.UTF_8); - } - - private void initFileSystem(URI uri) throws IOException { - try { - FileSystems.getFileSystem(uri); - } - catch (FileSystemNotFoundException e) { - FileSystems.newFileSystem(uri, Collections.emptyMap()); - } - catch (IllegalArgumentException e) { - FileSystems.getDefault(); - } - } - - private String stripComments(List list) { - StringBuilder buffer = new StringBuilder(); - for (String line : list) { - if (!line.startsWith("//") && !line.startsWith("--")) { - buffer.append(line).append("\n"); - } - } - return buffer.toString(); - } - - public void setInitScripts(Resource[] initScripts) { - this.initScripts = initScripts; - } - - public void setDestroyScripts(Resource[] destroyScripts) { - this.destroyScripts = destroyScripts; - } - - public void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; - } - - public void setIgnoreFailedDrop(boolean ignoreFailedDrop) { - this.ignoreFailedDrop = ignoreFailedDrop; - } - -} diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/ExecutionContextTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/ExecutionContextTestUtils.java index 315750b7eb..9d143d80b6 100644 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/ExecutionContextTestUtils.java +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/ExecutionContextTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2018 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. @@ -30,10 +30,14 @@ * * @author Dave Syer * @author Mahmoud Ben Hassine + * @author Taeik Lim * @since 2.1.4 * */ -public class ExecutionContextTestUtils { +public abstract class ExecutionContextTestUtils { + + private ExecutionContextTestUtils() { + } @SuppressWarnings("unchecked") @Nullable diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java index 989512bb46..8c0d4391a0 100644 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.java +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobScopeTestUtils.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,8 +29,12 @@ * @author Dave Syer * @author Jimmy Praet * @author Mahmoud Ben Hassine + * @author Taeik Lim */ -public class JobScopeTestUtils { +public abstract class JobScopeTestUtils { + + private JobScopeTestUtils() { + } public static T doInJobScope(JobExecution jobExecution, Callable callable) throws Exception { try { diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/MetaDataInstanceFactory.java b/spring-batch-test/src/main/java/org/springframework/batch/test/MetaDataInstanceFactory.java index 1348b39e6e..9ade608be6 100644 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/MetaDataInstanceFactory.java +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/MetaDataInstanceFactory.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. @@ -21,9 +21,7 @@ import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.StepExecution; -import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.support.PropertiesConverter; /** * Convenience methods for creating test instances of {@link JobExecution}, @@ -107,24 +105,6 @@ public static JobExecution createJobExecution(String jobName, Long instanceId, L return createJobExecution(jobName, instanceId, executionId, new JobParameters()); } - /** - * Create a {@link JobExecution} with the parameters provided. - * @param jobName the name of the job - * @param instanceId the Id of the {@link JobInstance} - * @param executionId the id for the {@link JobExecution} - * @param jobParameters new line separated key=value pairs - * @return a {@link JobExecution} - * @deprecated use {{@link #createJobExecution(String, Long, Long, JobParameters)}} - * instead. Will be removed in v5.2 - */ - @Deprecated(since = "5.0.1", forRemoval = true) - public static JobExecution createJobExecution(String jobName, Long instanceId, Long executionId, - String jobParameters) { - JobParameters params = new DefaultJobParametersConverter() - .getJobParameters(PropertiesConverter.stringToProperties(jobParameters)); - return createJobExecution(jobName, instanceId, executionId, params); - } - /** * Create a {@link JobExecution} with the parameters provided. * @param jobName the name of the job diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestUtils.java index 1365d5c0ee..93e26151f2 100644 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestUtils.java +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/StepScopeTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2010 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. @@ -27,9 +27,13 @@ * test case that happen to be step scoped in the application context. * * @author Dave Syer + * @author Taeik Lim * */ -public class StepScopeTestUtils { +public abstract class StepScopeTestUtils { + + private StepScopeTestUtils() { + } public static T doInStepScope(StepExecution stepExecution, Callable callable) throws Exception { try { diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/AssertFileTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/AssertFileTests.java deleted file mode 100644 index 894ec9dec5..0000000000 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/AssertFileTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2008-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.test; - -import org.junit.jupiter.api.Test; - -import org.springframework.core.io.FileSystemResource; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -/** - * This class can be used to assert that two files are the same. - * - * @author Dan Garrette - * @author Glenn Renfro - * @since 2.0 - */ -class AssertFileTests { - - private static final String DIRECTORY = "src/test/resources/data/input/"; - - @Test - void testAssertEquals_equal() { - assertDoesNotThrow(() -> executeAssertEquals("input1.txt", "input1.txt")); - } - - @Test - public void testAssertEquals_notEqual() throws Exception { - try { - executeAssertEquals("input1.txt", "input2.txt"); - fail(); - } - catch (IllegalStateException e) { - assertTrue(e.getMessage().startsWith("Line number 3 does not match.")); - } - } - - @Test - public void testAssertEquals_tooLong() throws Exception { - try { - executeAssertEquals("input3.txt", "input1.txt"); - fail(); - } - catch (IllegalStateException e) { - assertTrue(e.getMessage().startsWith("More lines than expected. There should not be a line number 4.")); - } - } - - @Test - public void testAssertEquals_tooShort() throws Exception { - try { - executeAssertEquals("input1.txt", "input3.txt"); - fail(); - } - catch (IllegalStateException e) { - assertTrue(e.getMessage().startsWith("Line number 4 does not match.")); - } - } - - @Test - void testAssertEquals_blank_equal() { - assertDoesNotThrow(() -> executeAssertEquals("blank.txt", "blank.txt")); - } - - @Test - public void testAssertEquals_blank_tooLong() throws Exception { - try { - executeAssertEquals("blank.txt", "input1.txt"); - fail(); - } - catch (IllegalStateException e) { - assertTrue(e.getMessage().startsWith("More lines than expected. There should not be a line number 1.")); - } - } - - @Test - public void testAssertEquals_blank_tooShort() throws Exception { - try { - executeAssertEquals("input1.txt", "blank.txt"); - fail(); - } - catch (IllegalStateException e) { - assertTrue(e.getMessage().startsWith("Line number 1 does not match.")); - } - } - - private void executeAssertEquals(String expected, String actual) throws Exception { - AssertFile.assertFileEquals(new FileSystemResource(DIRECTORY + expected), - new FileSystemResource(DIRECTORY + actual)); - } - - @Test - void testAssertLineCount() { - assertDoesNotThrow(() -> AssertFile.assertLineCount(5, new FileSystemResource(DIRECTORY + "input1.txt"))); - } - -} diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/MetaDataInstanceFactoryTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/MetaDataInstanceFactoryTests.java index d48a037132..2b6412962e 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/MetaDataInstanceFactoryTests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/MetaDataInstanceFactoryTests.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. @@ -70,12 +70,6 @@ void testCreateJobExecutionStringLongLong() { assertNotNull(MetaDataInstanceFactory.createJobExecution(jobName, instanceId, executionId)); } - @Test - void testCreateJobExecutionStringLongLongString() { - assertNotNull( - MetaDataInstanceFactory.createJobExecution(jobName, instanceId, executionId, jobParametersString)); - } - @Test void testCreateJobExecutionStringLongLongJobParameters() { assertNotNull(MetaDataInstanceFactory.createJobExecution(jobName, instanceId, executionId, jobParameters));