diff --git a/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/builder/MappingLdifReaderBuilderTests.java b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/builder/MappingLdifReaderBuilderTests.java new file mode 100644 index 000000000..ae5d1d44c --- /dev/null +++ b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/builder/MappingLdifReaderBuilderTests.java @@ -0,0 +1,218 @@ +/* + * 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.core.test.ldif.builder; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.ldif.MappingLdifReader; +import org.springframework.batch.item.ldif.RecordCallbackHandler; +import org.springframework.batch.item.ldif.RecordMapper; +import org.springframework.batch.item.ldif.builder.MappingLdifReaderBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Glenn Renfro + */ +@RunWith(SpringRunner.class) +public class MappingLdifReaderBuilderTests { + @Autowired + private ApplicationContext context; + + private MappingLdifReader mappingLdifReader; + + private String callbackAttributeName; + + @After + public void tearDown() { + this.callbackAttributeName = null; + if (this.mappingLdifReader != null) { + this.mappingLdifReader.close(); + } + } + + @Test + public void testSkipRecord() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .recordsToSkip(1) + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + LdapAttributes ldapAttributes = firstRead(); + assertEquals("The attribute name for the second record did not match expected result", + "cn=Bjorn Jensen, ou=Accounting, dc=airius, dc=com", ldapAttributes.getName().toString()); + } + + @Test + public void testBasicRead() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + LdapAttributes ldapAttributes = firstRead(); + assertEquals("The attribute name for the first record did not match expected result", + "cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com", ldapAttributes.getName().toString()); + } + + @Test + public void testCurrentItemCount() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .currentItemCount(3) + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + LdapAttributes ldapAttributes = firstRead(); + assertEquals("The attribute name for the third record did not match expected result", + "cn=Gern Jensen, ou=Product Testing, dc=airius, dc=com", ldapAttributes.getName().toString()); + } + + @Test + public void testMaxItemCount() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .maxItemCount(1) + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + LdapAttributes ldapAttributes = firstRead(); + assertEquals("The attribute name for the first record did not match expected result", + "cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com", ldapAttributes.getName().toString()); + ldapAttributes = this.mappingLdifReader.read(); + assertNull("The second read should have returned null", ldapAttributes); + } + + @Test + public void testSkipRecordCallback() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .recordsToSkip(1) + .recordMapper(new TestMapper()) + .skippedRecordsCallback(new TestCallBackHandler()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + firstRead(); + assertEquals("The attribute name from the callback handler did not match the expected result", + "cn=Barbara Jensen, ou=Product Development, dc=airius, dc=com", this.callbackAttributeName); + } + + @Test + public void testSaveState() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .name("foo") + .build(); + ExecutionContext executionContext = new ExecutionContext(); + firstRead(executionContext); + this.mappingLdifReader.update(executionContext); + assertEquals("foo.read.count did not have the expected result", 1, + executionContext.getInt("foo.read.count")); + } + + @Test + public void testSaveStateDisabled() throws Exception { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .saveState(false) + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/test.ldif")) + .build(); + ExecutionContext executionContext = new ExecutionContext(); + firstRead(executionContext); + this.mappingLdifReader.update(executionContext); + assertEquals("ExecutionContext should have been empty", 0, executionContext.size()); + } + + @Test + public void testStrict() { + // Test that strict when enabled will throw an exception. + try { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .recordMapper(new TestMapper()) + .resource(context.getResource("classpath:/teadsfst.ldif")) + .name("foo") + .build(); + this.mappingLdifReader.open(new ExecutionContext()); + fail("IllegalStateException should have been thrown, because strict was set to true"); + } + catch (ItemStreamException ise) { + assertEquals("IllegalStateException message did not match the expected result.", + "Failed to initialize the reader", ise.getMessage()); + } + // Test that strict when disabled will still allow the ldap resource to be opened. + this.mappingLdifReader = new MappingLdifReaderBuilder().strict(false).name("foo") + .recordMapper(new TestMapper()).resource(context.getResource("classpath:/teadsfst.ldif")).build(); + this.mappingLdifReader.open(new ExecutionContext()); + } + + @Test + public void testNullRecordMapper() { + try { + this.mappingLdifReader = new MappingLdifReaderBuilder() + .resource(context.getResource("classpath:/teadsfst.ldif")) + .build(); + fail("IllegalArgumentException should have been thrown"); + } + catch (IllegalArgumentException ise) { + assertEquals("IllegalArgumentException message did not match the expected result.", + "RecordMapper is required.", ise.getMessage()); + } + + } + + private LdapAttributes firstRead() throws Exception { + return firstRead(new ExecutionContext()); + } + + private LdapAttributes firstRead(ExecutionContext executionContext) throws Exception { + this.mappingLdifReader.open(executionContext); + return this.mappingLdifReader.read(); + } + + @Configuration + public static class LdifConfiguration { + + } + + public class TestCallBackHandler implements RecordCallbackHandler { + + @Override + public void handleRecord(LdapAttributes attributes) { + callbackAttributeName = attributes.getName().toString(); + } + } + + public class TestMapper implements RecordMapper { + @Override + public LdapAttributes mapRecord(LdapAttributes attributes) { + return attributes; + } + } +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/builder/MappingLdifReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/builder/MappingLdifReaderBuilder.java new file mode 100644 index 000000000..cd352c98d --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/builder/MappingLdifReaderBuilder.java @@ -0,0 +1,200 @@ +/* + * 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.ldif.builder; + +import org.springframework.batch.item.ldif.MappingLdifReader; +import org.springframework.batch.item.ldif.RecordCallbackHandler; +import org.springframework.batch.item.ldif.RecordMapper; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Creates a fully qualified MappingLdifReader. + * + * @author Glenn Renfro + * + * @since 4.0 + */ +public class MappingLdifReaderBuilder { + private Resource resource; + + private int recordsToSkip = 0; + + private boolean strict = true; + + private RecordCallbackHandler skippedRecordsCallback; + + private RecordMapper recordMapper; + + 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 MappingLdifReaderBuilder 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 MappingLdifReaderBuilder 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 MappingLdifReaderBuilder 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 MappingLdifReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + + return this; + } + + /** + * In strict mode the reader will throw an exception on + * {@link MappingLdifReader#open(org.springframework.batch.item.ExecutionContext)} if + * the input resource does not exist. + * + * @param strict true by default + * @return this instance for method chaining. + * @see MappingLdifReader#setStrict(boolean) + */ + public MappingLdifReaderBuilder strict(boolean strict) { + this.strict = strict; + + return this; + } + + /** + * {@link RecordCallbackHandler RecordCallbackHandler} implementations can be used to + * take action on skipped records. + * + * @param skippedRecordsCallback will be called for each one of the initial skipped + * lines before any items are read. + * @return this instance for method chaining. + * @see MappingLdifReader#setSkippedRecordsCallback(RecordCallbackHandler) + */ + public MappingLdifReaderBuilder skippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) { + this.skippedRecordsCallback = skippedRecordsCallback; + + return this; + } + + /** + * Public setter for the number of lines to skip at the start of a file. Can be used + * if the file contains a header without useful (column name) information, and without + * a comment delimiter at the beginning of the lines. + * + * @param recordsToSkip the number of lines to skip + * @return this instance for method chaining. + * @see MappingLdifReader#setRecordsToSkip(int) + */ + public MappingLdifReaderBuilder recordsToSkip(int recordsToSkip) { + this.recordsToSkip = recordsToSkip; + + return this; + } + + /** + * Establishes the resource that will be used as the input for the MappingLdifReader. + * + * @param resource the resource that will be read. + * @return this instance for method chaining. + * @see MappingLdifReader#setResource(Resource) + */ + public MappingLdifReaderBuilder resource(Resource resource) { + this.resource = resource; + + return this; + } + + /** + * Setter for object mapper. This property is required to be set. + * @param recordMapper maps record to an object + */ + public MappingLdifReaderBuilder recordMapper(RecordMapper recordMapper) { + this.recordMapper = recordMapper; + + return this; + } + + /** + * Returns a fully constructed {@link MappingLdifReader}. + * + * @return a new {@link org.springframework.batch.item.ldif.MappingLdifReader} + */ + public MappingLdifReader build() { + Assert.notNull(this.resource, "Resource is required."); + Assert.notNull(this.recordMapper, "RecordMapper is required."); + if (this.saveState) { + Assert.hasText(this.name, "A name is required when saveState is set to true"); + } + MappingLdifReader reader = new MappingLdifReader<>(); + reader.setResource(this.resource); + reader.setRecordsToSkip(this.recordsToSkip); + reader.setSaveState(saveState); + reader.setCurrentItemCount(this.currentItemCount); + reader.setMaxItemCount(this.maxItemCount); + reader.setRecordMapper(this.recordMapper); + reader.setName(this.name); + if (this.skippedRecordsCallback != null) { + reader.setSkippedRecordsCallback(this.skippedRecordsCallback); + } + reader.setStrict(this.strict); + + return reader; + } +}