From 402da13f673b0379611f407380fcbc341d30cb1a Mon Sep 17 00:00:00 2001 From: Glenn Renfro Date: Tue, 16 May 2017 13:58:36 -0400 Subject: [PATCH] Added Builder for RepositoryItemWriter resolves BATCH-2609 --- .../batch/item/data/RepositoryItemReader.java | 4 + .../batch/item/data/RepositoryItemWriter.java | 6 +- .../builder/RepositoryItemWriterBuilder.java | 170 ++++++++++++++++++ .../RepositoryItemWriterBuilderTests.java | 125 +++++++++++++ 4 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilder.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilderTests.java 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 950fe2186..716a6699f 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 @@ -59,6 +59,10 @@ import org.springframework.util.MethodInvoker; * saveState=false if used in a multi-threaded client (no restart available). *

* + *

+ * The RepositoryItemReader only reads Java Objects i.e. non primitives. + *

+ * * @author Michael Minella * @since 2.2 */ 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 754da4cf2..b49b1b0ab 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 @@ -47,6 +47,10 @@ import org.springframework.util.MethodInvoker; * transactions. *

* + *

+ * The RepositoryItemWriter only stores Java Objects i.e. non primitives. + *

+ * * @author Michael Minella * @since 2.2 */ @@ -59,7 +63,7 @@ public class RepositoryItemWriter implements ItemWriter, InitializingBean private String methodName; /** - * Specifies what method on the repository to call. This method must the type of + * Specifies what method on the repository to call. This method must have the type of * object passed to this writer as the sole argument. * * @param methodName diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilder.java new file mode 100644 index 000000000..db9372531 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilder.java @@ -0,0 +1,170 @@ +/* + * Copyright 2017 the original author 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.data.builder; + + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.batch.item.data.RepositoryItemWriter; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.MethodInterceptor; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.data.repository.CrudRepository; +import org.springframework.util.Assert; + +/** + * A builder implementation for the {@link RepositoryItemWriter}. + * + * @author Glenn Renfro + * @since 4.0 + * @see RepositoryItemWriter + */ +public class RepositoryItemWriterBuilder { + + private CrudRepository repository; + + private String methodName; + + private RepositoryMethodReference repositoryMethodReference; + + /** + * Specifies what method on the repository to call. This method must have the type of + * object passed to this writer as the sole argument. + * + * @param methodName the name of the method to be used for saving the item. + * @return The current instance of the builder. + * @see RepositoryItemWriter#setMethodName(String) + */ + public RepositoryItemWriterBuilder methodName(String methodName) { + this.methodName = methodName; + + return this; + } + + /** + * Set the {@link org.springframework.data.repository.CrudRepository} implementation + * for persistence + * + * @param repository the Spring Data repository to be set + * @return The current instance of the builder. + * @see RepositoryItemWriter#setRepository(CrudRepository) + */ + public RepositoryItemWriterBuilder repository(CrudRepository repository) { + this.repository = repository; + + return this; + } + + /** + * Specifies a repository and the type-safe method to call for the writer. The method + * configured via this mechanism must take + * {@link org.springframework.data.domain.Pageable} as the last + * argument. This method can be used in place of {@link #repository(CrudRepository)}, + * {@link #methodName(String)}}. + * + * Note: The repository that is used by the repositoryMethodReference must be + * non-final. + * + * @param repositoryMethodReference of the used to get a repository and type-safe + * method for use by the writer. + * @return The current instance of the builder. + * @see RepositoryItemWriter#setMethodName(String) + * @see RepositoryItemWriter#setRepository(CrudRepository) + * + */ + public RepositoryItemWriterBuilder repository(RepositoryItemWriterBuilder.RepositoryMethodReference repositoryMethodReference) { + this.repositoryMethodReference = repositoryMethodReference; + + return this; + } + + /** + * Builds the {@link RepositoryItemWriter}. + * + * @return a {@link RepositoryItemWriter} + */ + public RepositoryItemWriter build() { + if (this.repositoryMethodReference != null) { + this.methodName = this.repositoryMethodReference.getMethodName(); + this.repository = this.repositoryMethodReference.getRepository(); + } + + Assert.hasText(this.methodName, "methodName is required."); + Assert.notNull(this.repository, "repository is required."); + + RepositoryItemWriter writer = new RepositoryItemWriter<>(); + writer.setMethodName(this.methodName); + writer.setRepository(this.repository); + return writer; + } + + /** + * Establishes a proxy that will capture a the Repository and the associated + * methodName that will be used by the writer. + * @param The type of repository that will be used by the writer. The class must + * not be final. + */ + public static class RepositoryMethodReference { + private RepositoryItemWriterBuilder.RepositoryMethodIterceptor repositoryInvocationHandler; + + private CrudRepository repository; + + public RepositoryMethodReference(CrudRepository repository) { + this.repository = repository; + this.repositoryInvocationHandler = new RepositoryItemWriterBuilder.RepositoryMethodIterceptor(); + } + + /** + * The proxy returned prevents actual method execution and is only used to gather, + * information about the method. + * @return T is a proxy of the object passed in in the constructor + */ + public T methodIs() { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(this.repository.getClass()); + enhancer.setCallback(this.repositoryInvocationHandler); + return (T) enhancer.create(); + } + + CrudRepository getRepository() { + return this.repository; + } + + String getMethodName() { + return this.repositoryInvocationHandler.getMethodName(); + } + } + + private static class RepositoryMethodIterceptor implements MethodInterceptor { + private String methodName; + + @Override + public Object intercept(Object o, Method method, Object[] objects, + MethodProxy methodProxy) throws Throwable { + this.methodName = method.getName(); + return null; + } + + String getMethodName() { + return this.methodName; + } + + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilderTests.java new file mode 100644 index 000000000..72bc9f718 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/data/builder/RepositoryItemWriterBuilderTests.java @@ -0,0 +1,125 @@ +/* + * Copyright 2017 the original author 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.data.builder; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.batch.item.data.RepositoryItemWriter; +import org.springframework.data.repository.CrudRepository; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.verify; + +/** + * @author Glenn Renfro + */ +public class RepositoryItemWriterBuilderTests { + @Mock + private TestRepository repository; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testNullRepository() throws Exception { + try { + new RepositoryItemWriterBuilder().methodName("save").build(); + + fail("IllegalArgumentException should have been thrown"); + } + catch (IllegalArgumentException iae) { + assertEquals("IllegalArgumentException message did not match the expected result.", + "repository is required.", iae.getMessage()); + } + } + + @Test + public void testEmptyMethodName() throws Exception { + try { + new RepositoryItemWriterBuilder().repository(this.repository).build(); + + fail("IllegalArgumentException should have been thrown"); + } + catch (IllegalArgumentException iae) { + assertEquals("IllegalArgumentException message did not match the expected result.", + "methodName is required.", iae.getMessage()); + } + } + + @Test + public void testWriteItems() throws Exception { + RepositoryItemWriter writer = new RepositoryItemWriterBuilder() + .methodName("save") + .repository(this.repository) + .build(); + + List items = Collections.singletonList("foo"); + + writer.write(items); + + verify(this.repository).save("foo"); + } + + @Test + public void testWriteItemsTestRepsository() throws Exception { + RepositoryItemWriter writer = new RepositoryItemWriterBuilder() + .methodName("foo") + .repository(this.repository) + .build(); + + List items = Collections.singletonList("foo"); + + writer.write(items); + + verify(this.repository).foo("foo"); + } + + @Test + public void testWriteItemsTestRepsositoryMethodIs() throws Exception { + RepositoryItemWriterBuilder.RepositoryMethodReference + repositoryMethodReference = new RepositoryItemWriterBuilder.RepositoryMethodReference<>( + this.repository); + repositoryMethodReference.methodIs().foo(null); + + RepositoryItemWriter writer = new RepositoryItemWriterBuilder() + .methodName("foo") + .repository(repositoryMethodReference) + .build(); + + List items = Collections.singletonList("foo"); + + writer.write(items); + + verify(this.repository).foo("foo"); + } + + public interface TestRepository extends CrudRepository { + + Object foo(String arg1); + + } +}