diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilder.java new file mode 100644 index 000000000..936041fc9 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilder.java @@ -0,0 +1,360 @@ +/* + * 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.database.builder; + +import javax.sql.DataSource; + +import org.springframework.batch.item.database.AbstractCursorItemReader; + +import org.springframework.batch.item.database.StoredProcedureItemReader; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A fluent builder API for the configuration of a {@link StoredProcedureItemReader}. + * + * @author Michael Minella + * @since 4.0.0 + * @see StoredProcedureItemReader + */ +public class StoredProcedureItemReaderBuilder { + + public static final int VALUE_NOT_SET = -1; + + private int currentItemCount = 0; + + private int maxItemCount = Integer.MAX_VALUE; + + private boolean saveState = true; + + private DataSource dataSource; + + private int fetchSize = VALUE_NOT_SET; + + private int maxRows = VALUE_NOT_SET; + + private int queryTimeout = VALUE_NOT_SET; + + private boolean ignoreWarnings = true; + + private boolean verifyCursorPosition = true; + + private boolean driverSupportsAbsolute = false; + + private boolean useSharedExtendedConnection = false; + + private PreparedStatementSetter preparedStatementSetter; + + private RowMapper rowMapper; + + private String procedureName; + + private SqlParameter[] parameters = new SqlParameter[0]; + + private boolean function = false; + + private int refCursorPosition = 0; + + private String name; + + /** + * 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 StoredProcedureItemReaderBuilder 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 StoredProcedureItemReaderBuilder 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 StoredProcedureItemReaderBuilder 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 StoredProcedureItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + + return this; + } + + /** + * The {@link DataSource} to read from + * + * @param dataSource a relational data base + * @return this instance for method chaining + * @see StoredProcedureItemReader#setDataSource(DataSource) + */ + public StoredProcedureItemReaderBuilder dataSource(DataSource dataSource) { + this.dataSource = dataSource; + + return this; + } + + /** + * A hint to the driver as to how many rows to return with each fetch. + * + * @param fetchSize the hint + * @return this instance for method chaining + * @see StoredProcedureItemReader#setFetchSize(int) + */ + public StoredProcedureItemReaderBuilder fetchSize(int fetchSize) { + this.fetchSize = fetchSize; + + return this; + } + + /** + * The max number of rows the {@link java.sql.ResultSet} can contain + * + * @param maxRows the max + * @return this instance for method chaining + * @see StoredProcedureItemReader#setMaxRows(int) + */ + public StoredProcedureItemReaderBuilder maxRows(int maxRows) { + this.maxRows = maxRows; + + return this; + } + + /** + * The time in milliseconds for the query to timeout + * + * @param queryTimeout timeout + * @return this instance for method chaining + * @see StoredProcedureItemReader#setQueryTimeout(int) + */ + public StoredProcedureItemReaderBuilder queryTimeout(int queryTimeout) { + this.queryTimeout = queryTimeout; + + return this; + } + + /** + * Indicates if SQL warnings should be ignored or if an exception should be thrown. + * + * @param ignoreWarnings indicator. Defaults to true + * @return this instance for method chaining + * @see AbstractCursorItemReader#setIgnoreWarnings(boolean) + */ + public StoredProcedureItemReaderBuilder ignoreWarnings(boolean ignoreWarnings) { + this.ignoreWarnings = ignoreWarnings; + + return this; + } + + /** + * Indicates if the reader should verify the current position of the + * {@link java.sql.ResultSet} after being passed to the {@link RowMapper}. Defaults + * to true. + * + * @param verifyCursorPosition indicator + * @return this instance for method chaining + * @see StoredProcedureItemReader#setVerifyCursorPosition(boolean) + */ + public StoredProcedureItemReaderBuilder verifyCursorPosition(boolean verifyCursorPosition) { + this.verifyCursorPosition = verifyCursorPosition; + + return this; + } + + /** + * Indicates if the JDBC driver supports setting the absolute row on the + * {@link java.sql.ResultSet}. + * + * @param driverSupportsAbsolute indicator + * @return this instance for method chaining + * @see StoredProcedureItemReader#setDriverSupportsAbsolute(boolean) + */ + public StoredProcedureItemReaderBuilder driverSupportsAbsolute(boolean driverSupportsAbsolute) { + this.driverSupportsAbsolute = driverSupportsAbsolute; + + return this; + } + + /** + * Indicates that the connection used for the cursor is being used by all other + * processing, therefor part of the same transaction. + * + * @param useSharedExtendedConnection indicator + * @return this instance for method chaining + * @see StoredProcedureItemReader#setUseSharedExtendedConnection(boolean) + */ + public StoredProcedureItemReaderBuilder useSharedExtendedConnection(boolean useSharedExtendedConnection) { + this.useSharedExtendedConnection = useSharedExtendedConnection; + + return this; + } + + /** + * Configures the provided {@link PreparedStatementSetter} to be used to populate any + * arguments in the SQL query to be executed for the reader. + * + * @param preparedStatementSetter setter + * @return this instance for method chaining + * @see StoredProcedureItemReader#setPreparedStatementSetter(PreparedStatementSetter) + */ + public StoredProcedureItemReaderBuilder preparedStatementSetter(PreparedStatementSetter preparedStatementSetter) { + this.preparedStatementSetter = preparedStatementSetter; + + return this; + } + + /** + * The {@link RowMapper} used to map the results of the cursor to each item. + * + * @param rowMapper {@link RowMapper} + * @return this instance for method chaining + * @see StoredProcedureItemReader#setRowMapper(RowMapper) + */ + public StoredProcedureItemReaderBuilder rowMapper(RowMapper rowMapper) { + this.rowMapper = rowMapper; + + return this; + } + + /** + * The name of the stored procedure to execute + * + * @param procedureName name of the procedure + * @return this instance for method chaining + * @see StoredProcedureItemReader#setProcedureName(String) + */ + public StoredProcedureItemReaderBuilder procedureName(String procedureName) { + this.procedureName = procedureName; + + return this; + } + + /** + * SQL parameters to be set when executing the stored procedure + * + * @param parameters parameters to be set + * @return this instance for method chaining + * @see StoredProcedureItemReader#setParameters(SqlParameter[]) + */ + public StoredProcedureItemReaderBuilder parameters(SqlParameter[] parameters) { + this.parameters = parameters; + + return this; + } + + /** + * Indicates the stored procedure is a function + * + * @return this instance for method chaining + * @see StoredProcedureItemReader#setFunction(boolean) + */ + public StoredProcedureItemReaderBuilder function() { + this.function = true; + + return this; + } + + /** + * The parameter position of the REF CURSOR. Only used for Oracle and PostgreSQL that + * use REF CURSORs. For any other database, this should remain as the default (0). + * + * @param refCursorPosition the parameter position + * @return this instance for method chaining + * @see StoredProcedureItemReader#setRefCursorPosition(int) + */ + public StoredProcedureItemReaderBuilder refCursorPosition(int refCursorPosition) { + this.refCursorPosition = refCursorPosition; + + return this; + } + + /** + * Validates configuration and builds a new reader instance + * + * @return a fully constructerd {@link StoredProcedureItemReader} + */ + public StoredProcedureItemReader build() { + if(this.saveState) { + Assert.hasText(this.name, + "A name is required when saveSate is set to true"); + } + + Assert.notNull(this.procedureName, "The name of the stored procedure must be provided"); + Assert.notNull(this.dataSource, "A datasource is required"); + Assert.notNull(this.rowMapper, "A rowmapper is required"); + + StoredProcedureItemReader itemReader = new StoredProcedureItemReader<>(); + + if(StringUtils.hasText(this.name)) { + itemReader.setName(this.name); + } + + itemReader.setProcedureName(this.procedureName); + itemReader.setRowMapper(this.rowMapper); + itemReader.setParameters(this.parameters); + itemReader.setPreparedStatementSetter(this.preparedStatementSetter); + itemReader.setFunction(this.function); + itemReader.setRefCursorPosition(this.refCursorPosition); + itemReader.setCurrentItemCount(this.currentItemCount); + itemReader.setDataSource(this.dataSource); + itemReader.setDriverSupportsAbsolute(this.driverSupportsAbsolute); + itemReader.setFetchSize(this.fetchSize); + itemReader.setIgnoreWarnings(this.ignoreWarnings); + itemReader.setMaxItemCount(this.maxItemCount); + itemReader.setMaxRows(this.maxRows); + itemReader.setQueryTimeout(this.queryTimeout); + itemReader.setSaveState(this.saveState); + itemReader.setUseSharedExtendedConnection(this.useSharedExtendedConnection); + itemReader.setVerifyCursorPosition(this.verifyCursorPosition); + + return itemReader; + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilderTests.java new file mode 100644 index 000000000..a97358812 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/StoredProcedureItemReaderBuilderTests.java @@ -0,0 +1,249 @@ +/* + * 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.database.builder; + +import java.util.Collections; +import javax.sql.DataSource; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import test.jdbc.datasource.DataSourceInitializer; +import test.jdbc.datasource.DerbyDataSourceFactoryBean; +import test.jdbc.datasource.DerbyShutdownBean; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.database.FooRowMapper; +import org.springframework.batch.item.database.StoredProcedureItemReader; +import org.springframework.batch.item.database.support.ListPreparedStatementSetter; +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.jdbc.core.SqlParameter; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Michael Minella + */ +public class StoredProcedureItemReaderBuilderTests { + + private DataSource dataSource; + + private ConfigurableApplicationContext context; + + @Before + public void setUp() { + this.context = new AnnotationConfigApplicationContext(TestDataSourceConfiguration.class); + this.dataSource = (DataSource) this.context.getBean("dataSource"); + } + + @After + public void tearDown() { + this.context.close(); + } + + @Test + public void testSunnyScenario() throws Exception { + StoredProcedureItemReader reader = new StoredProcedureItemReaderBuilder() + .name("foo_reader") + .dataSource(this.dataSource) + .procedureName("read_foos") + .rowMapper(new FooRowMapper()) + .verifyCursorPosition(false) + .build(); + + reader.open(new ExecutionContext()); + + Foo item1 = reader.read(); + assertEquals(1, item1.getId()); + assertEquals("bar1", item1.getName()); + assertEquals(1, item1.getValue()); + + reader.close(); + } + + @Test + public void testConfiguration() { + ListPreparedStatementSetter preparedStatementSetter = new ListPreparedStatementSetter(Collections.EMPTY_LIST); + + SqlParameter[] parameters = new SqlParameter[0]; + + StoredProcedureItemReader reader = new StoredProcedureItemReaderBuilder() + .name("foo_reader") + .dataSource(this.dataSource) + .procedureName("read_foos") + .rowMapper(new FooRowMapper()) + .verifyCursorPosition(false) + .refCursorPosition(3) + .useSharedExtendedConnection(true) + .preparedStatementSetter(preparedStatementSetter) + .parameters(parameters) + .function() + .fetchSize(5) + .driverSupportsAbsolute(true) + .currentItemCount(6) + .ignoreWarnings(false) + .maxItemCount(7) + .queryTimeout(8) + .maxRows(9) + .build(); + + assertEquals(3, ReflectionTestUtils.getField(reader, "refCursorPosition")); + assertEquals(preparedStatementSetter, ReflectionTestUtils.getField(reader, "preparedStatementSetter")); + assertEquals(parameters, ReflectionTestUtils.getField(reader, "parameters")); + assertEquals(5, ReflectionTestUtils.getField(reader, "fetchSize")); + assertEquals(6, ReflectionTestUtils.getField(reader, "currentItemCount")); + assertEquals(7, ReflectionTestUtils.getField(reader, "maxItemCount")); + assertEquals(8, ReflectionTestUtils.getField(reader, "queryTimeout")); + assertEquals(9, ReflectionTestUtils.getField(reader, "maxRows")); + assertTrue((Boolean) ReflectionTestUtils.getField(reader, "useSharedExtendedConnection")); + assertTrue((Boolean) ReflectionTestUtils.getField(reader, "function")); + assertTrue((Boolean) ReflectionTestUtils.getField(reader, "driverSupportsAbsolute")); + assertFalse((Boolean) ReflectionTestUtils.getField(reader, "ignoreWarnings")); + } + + @Test + public void testNoSaveState() throws Exception { + StoredProcedureItemReader reader = new StoredProcedureItemReaderBuilder() + .dataSource(this.dataSource) + .procedureName("read_foos") + .rowMapper(new FooRowMapper()) + .verifyCursorPosition(false) + .saveState(false) + .build(); + + ExecutionContext executionContext = new ExecutionContext(); + reader.open(executionContext); + + reader.read(); + reader.read(); + + reader.update(executionContext); + + assertEquals(0, executionContext.size()); + + reader.close(); + } + + @Test + public void testValidation() { + try { + new StoredProcedureItemReaderBuilder() + .build(); + + fail("Exception was not thrown for missing the name"); + } + catch (IllegalArgumentException iae) { + assertEquals("A name is required when saveSate is set to true", + iae.getMessage()); + } + + try { + new StoredProcedureItemReaderBuilder() + .saveState(false) + .build(); + + fail("Exception was not thrown for missing the stored procedure name"); + } + catch (IllegalArgumentException iae) { + assertEquals("The name of the stored procedure must be provided", + iae.getMessage()); + } + + try { + new StoredProcedureItemReaderBuilder() + .saveState(false) + .procedureName("read_foos") + .build(); + + fail("Exception was not thrown for missing the DataSource"); + } + catch (IllegalArgumentException iae) { + assertEquals("A datasource is required", + iae.getMessage()); + } + + try { + new StoredProcedureItemReaderBuilder() + .saveState(false) + .procedureName("read_foos") + .dataSource(this.dataSource) + .build(); + + fail("Exception was not thrown for missing the RowMapper"); + } + catch (IllegalArgumentException iae) { + assertEquals("A rowmapper is required", + iae.getMessage()); + } + } + + @Configuration + public static class TestDataSourceConfiguration { + + @Bean + public DerbyDataSourceFactoryBean dataSource() { + DerbyDataSourceFactoryBean derbyDataSourceFactoryBean = new DerbyDataSourceFactoryBean(); + + derbyDataSourceFactoryBean.setDataDirectory("build/derby-home"); + + return derbyDataSourceFactoryBean; + } + + @Bean + public DerbyShutdownBean dbShutdown(DataSource dataSource) { + DerbyShutdownBean shutdownBean = new DerbyShutdownBean(); + + shutdownBean.setDataSource(dataSource); + + return shutdownBean; + } + + @Bean + public PlatformTransactionManager transactionManager(DataSource dataSource) { + DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); + + transactionManager.setDataSource(dataSource); + + return transactionManager; + } + + @Bean + public DataSourceInitializer initializer(DataSource dataSource) { + DataSourceInitializer initializer = new DataSourceInitializer(); + + initializer.setDataSource(dataSource); + initializer.setInitScripts(new ClassPathResource[]{ + new ClassPathResource("org/springframework/batch/item/database/init-foo-schema-derby.sql") + }); + initializer.setDestroyScripts(new ClassPathResource[]{ + new ClassPathResource("org/springframework/batch/item/database/drop-foo-schema-derby.sql") + }); + + return initializer; + } + } +} diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/stored-procedure-context.xml b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/stored-procedure-context.xml index 3b9eb7210..bf4ca7f5d 100644 --- a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/stored-procedure-context.xml +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/stored-procedure-context.xml @@ -1,8 +1,7 @@ - +