diff --git a/build.gradle b/build.gradle index da1e27cd1..0531b7cf8 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,7 @@ allprojects { h2databaseVersion = '1.4.196' hibernateVersion = '5.2.12.Final' hibernateValidatorVersion = '6.0.4.Final' + javaxElVersion = '3.0.0' hsqldbVersion = '2.4.0' jackson2Version = '2.9.2' gsonVersion = '2.8.5' @@ -322,7 +323,8 @@ project('spring-batch-infrastructure') { testRuntime "com.sun.mail:javax.mail:$javaMailVersion" testRuntime "org.codehaus.groovy:groovy-jsr223:$groovyVersion" testRuntime "org.jruby:jruby:$jrubyVersion" - + testRuntime "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion" + testRuntime "org.glassfish:javax.el:$javaxElVersion" testRuntime "org.beanshell:bsh:$beanshellVersion" optional "javax.jms:javax.jms-api:$jmsVersion" @@ -604,6 +606,8 @@ project('spring-batch-samples') { testCompile "org.apache.activemq:activemq-kahadb-store:$activemqVersion" testRuntime "com.sun.mail:javax.mail:$javaMailVersion" + testRuntime "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion" + testRuntime "org.glassfish:javax.el:$javaxElVersion" provided "mysql:mysql-connector-java:$mysqlVersion" provided "com.h2database:h2:$h2databaseVersion" diff --git a/spring-batch-docs/asciidoc/readersAndWriters.adoc b/spring-batch-docs/asciidoc/readersAndWriters.adoc index 1de61cf51..1dd3ecd31 100644 --- a/spring-batch-docs/asciidoc/readersAndWriters.adoc +++ b/spring-batch-docs/asciidoc/readersAndWriters.adoc @@ -2659,6 +2659,45 @@ public SpringValidator validator() { } ---- +You can also use the `BeanValidatingItemProcessor` to validate items annotated with +the Bean Validation API (JSR-303) annotations. For example, given the following type `Person`: + +[source, java] +---- +class Person { + + @NotEmpty + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +---- + +you can validate items by declaring a `BeanValidatingItemProcessor` bean in your +application context and register it as a processor in your chunk-oriented step: + +[source, java] +---- +@Bean +public BeanValidatingItemProcessor beanValidatingItemProcessor() throws Exception { + BeanValidatingItemProcessor beanValidatingItemProcessor = new BeanValidatingItemProcessor<>(); + beanValidatingItemProcessor.setFilter(true); + + return beanValidatingItemProcessor; +} +---- + [[process-indicator]] === Preventing State Persistence diff --git a/spring-batch-docs/asciidoc/whatsnew.adoc b/spring-batch-docs/asciidoc/whatsnew.adoc index 2ca1acd60..655138f17 100644 --- a/spring-batch-docs/asciidoc/whatsnew.adoc +++ b/spring-batch-docs/asciidoc/whatsnew.adoc @@ -11,6 +11,7 @@ The Spring Batch 4.1 release adds the following features: * A new `@SpringBatchTest` annotation to simplify testing batch components * A new `@EnableBatchIntegration` annotation to simplify remote chunking configuration * A new `JsonItemReader` and `JsonFileItemWriter` to support the JSON format +* Add support for validating items with the Bean Validation API [[whatsNewTesting]] === `@SpringBatchTest` Annotation @@ -160,3 +161,47 @@ Writing JSON data is also supported through the `JsonFileItemWriter`. For more details about JSON support, see the <> chapter. +[[whatsNewBeanValidationApi]] +=== Bean Validation API support + +This release brings a new `ValidatingItemProcessor` implementation called +`BeanValidatingItemProcessor` which allows you to validate items annotated with +the Bean Validation API (JSR-303) annotations. For example, given the following +type `Person`: + +[source, java] +---- +class Person { + + @NotEmpty + private String name; + + public Person(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} +---- + +you can validate items by declaring a `BeanValidatingItemProcessor` bean in your +application context and register it as a processor in your chunk-oriented step: + +[source, java] +---- +@Bean +public BeanValidatingItemProcessor beanValidatingItemProcessor() throws Exception { + BeanValidatingItemProcessor beanValidatingItemProcessor = new BeanValidatingItemProcessor<>(); + beanValidatingItemProcessor.setFilter(true); + + return beanValidatingItemProcessor; +} +---- + diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/validator/BeanValidatingItemProcessor.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/validator/BeanValidatingItemProcessor.java new file mode 100644 index 000000000..0181c8778 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/validator/BeanValidatingItemProcessor.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.item.validator; + +import javax.validation.Validator; + +import org.apache.shiro.util.Assert; + +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.validation.beanvalidation.SpringValidatorAdapter; + +/** + * A {@link ValidatingItemProcessor} that uses the Bean Validation API (JSR-303) + * to validate items. + * + * @param type of items to validate + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public class BeanValidatingItemProcessor extends ValidatingItemProcessor { + + private Validator validator; + + /** + * Create a new instance of {@link BeanValidatingItemProcessor} with the + * default configuration. + */ + public BeanValidatingItemProcessor() { + LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); + localValidatorFactoryBean.afterPropertiesSet(); + this.validator = localValidatorFactoryBean.getValidator(); + } + + /** + * Create a new instance of {@link BeanValidatingItemProcessor}. + * @param localValidatorFactoryBean used to configure the Bean Validation validator + */ + public BeanValidatingItemProcessor(LocalValidatorFactoryBean localValidatorFactoryBean) { + Assert.notNull(localValidatorFactoryBean, "localValidatorFactoryBean must not be null"); + this.validator = localValidatorFactoryBean.getValidator(); + } + + @Override + public void afterPropertiesSet() throws Exception { + SpringValidatorAdapter springValidatorAdapter = new SpringValidatorAdapter(this.validator); + SpringValidator springValidator = new SpringValidator<>(); + springValidator.setValidator(springValidatorAdapter); + springValidator.afterPropertiesSet(); + setValidator(springValidator); + super.afterPropertiesSet(); + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/validator/BeanValidatingItemProcessorTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/validator/BeanValidatingItemProcessorTest.java new file mode 100644 index 000000000..ec2c1a510 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/validator/BeanValidatingItemProcessorTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.item.validator; + +import javax.validation.constraints.NotEmpty; + +import org.junit.Assert; +import org.junit.Test; + +/** + * @author Mahmoud Ben Hassine + */ +public class BeanValidatingItemProcessorTest { + + @Test + public void testValidObjectValidation() throws Exception { + // given + BeanValidatingItemProcessor validatingItemProcessor = new BeanValidatingItemProcessor<>(); + validatingItemProcessor.afterPropertiesSet(); + Foo foo = new Foo("foo"); + + // when + Foo processed = validatingItemProcessor.process(foo); + + // then + Assert.assertNotNull(processed); + } + + @Test(expected = ValidationException.class) + public void testInvalidObjectValidation() throws Exception { + // given + BeanValidatingItemProcessor validatingItemProcessor = new BeanValidatingItemProcessor<>(); + validatingItemProcessor.afterPropertiesSet(); + Foo foo = new Foo(""); + + // when + validatingItemProcessor.process(foo); + + // then + // expected exception + } + + private static class Foo { + + @NotEmpty + private String name; + + public Foo(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/ValidationSampleConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/ValidationSampleConfiguration.java new file mode 100644 index 000000000..64ab51122 --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/ValidationSampleConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.sample.validation; + +import java.util.Arrays; + +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.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.item.support.ListItemReader; +import org.springframework.batch.item.support.ListItemWriter; +import org.springframework.batch.item.validator.BeanValidatingItemProcessor; +import org.springframework.batch.sample.validation.domain.Person; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Mahmoud Ben Hassine + */ +@Configuration +@EnableBatchProcessing +public class ValidationSampleConfiguration { + + @Autowired + private JobBuilderFactory jobs; + + @Autowired + private StepBuilderFactory steps; + + @Bean + public ListItemReader itemReader() { + Person person1 = new Person(1, "foo"); + Person person2 = new Person(2, ""); + return new ListItemReader<>(Arrays.asList(person1, person2)); + } + + @Bean + public ListItemWriter itemWriter() { + return new ListItemWriter<>(); + } + + @Bean + public BeanValidatingItemProcessor itemValidator() throws Exception { + BeanValidatingItemProcessor validator = new BeanValidatingItemProcessor<>(); + validator.setFilter(true); + validator.afterPropertiesSet(); + + return validator; + } + + @Bean + public Step step() throws Exception { + return this.steps.get("step") + .chunk(1) + .reader(itemReader()) + .processor(itemValidator()) + .writer(itemWriter()) + .build(); + } + + @Bean + public Job job() throws Exception { + return this.jobs.get("job") + .start(step()) + .build(); + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/domain/Person.java b/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/domain/Person.java new file mode 100644 index 000000000..52fc57989 --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/sample/validation/domain/Person.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.sample.validation.domain; + +import javax.validation.constraints.NotEmpty; + +/** + * @author Mahmoud Ben Hassine + */ +public class Person { + + private int id; + + @NotEmpty + private String name; + + public Person() { + } + + public Person(int id, String name) { + this.id = id; + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "Person{" + + "id=" + id + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/validation/ValidationSampleFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/validation/ValidationSampleFunctionalTests.java new file mode 100644 index 000000000..9f53cef1f --- /dev/null +++ b/spring-batch-samples/src/test/java/org/springframework/batch/sample/validation/ValidationSampleFunctionalTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.sample.validation; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +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.launch.JobLauncher; +import org.springframework.batch.item.support.ListItemWriter; +import org.springframework.batch.sample.validation.domain.Person; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Mahmoud Ben Hassine + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = {ValidationSampleConfiguration.class}) +public class ValidationSampleFunctionalTests { + + @Autowired + private Job job; + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + private ListItemWriter listItemWriter; + + @Test + public void testItemValidation() throws Exception { + // given + JobParameters jobParameters = new JobParameters(); + + // when + JobExecution jobExecution = this.jobLauncher.run(this.job, jobParameters); + + // then + Assert.assertEquals(ExitStatus.COMPLETED.getExitCode(), jobExecution.getExitStatus().getExitCode()); + List writtenItems = this.listItemWriter.getWrittenItems(); + Assert.assertEquals(1, writtenItems.size()); + Assert.assertEquals("foo", writtenItems.get(0).getName()); + } +}