From 337b844fc9202598913596b4f4af23cbff2abc6a Mon Sep 17 00:00:00 2001 From: Michael Minella Date: Mon, 21 Apr 2014 14:52:24 -0500 Subject: [PATCH] Bringing the LDIF based ItemReader from Spring LDAP to Spring Batch to break circular dependency --- spring-batch-core-tests/pom.xml | 9 +- .../batch/core/test/ldif/LdifReaderTests.java | 115 +++++++++ .../test/ldif/MappingLdifReaderTests.java | 116 +++++++++ .../batch/core/test/ldif/MyMapper.java | 35 +++ .../resources/applicationContext-test1.xml | 39 +++ .../resources/applicationContext-test2.xml | 59 +++++ .../src/test/resources/expectedOutput.ldif | 74 ++++++ .../resources/simple-job-launcher-context.xml | 6 +- .../src/test/resources/test.ldif | 235 ++++++++++++++++++ spring-batch-infrastructure/pom.xml | 17 +- .../batch/item/ldif/LdifAggregator.java | 40 +++ .../batch/item/ldif/LdifReader.java | 168 +++++++++++++ .../batch/item/ldif/MappingLdifReader.java | 177 +++++++++++++ .../item/ldif/RecordCallbackHandler.java | 36 +++ .../batch/item/ldif/RecordMapper.java | 38 +++ .../batch/item/ldif/package-info.java | 6 + spring-batch-parent/pom.xml | 18 ++ 17 files changed, 1179 insertions(+), 9 deletions(-) create mode 100644 spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/LdifReaderTests.java create mode 100644 spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MappingLdifReaderTests.java create mode 100644 spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MyMapper.java create mode 100644 spring-batch-core-tests/src/test/resources/applicationContext-test1.xml create mode 100644 spring-batch-core-tests/src/test/resources/applicationContext-test2.xml create mode 100644 spring-batch-core-tests/src/test/resources/expectedOutput.ldif create mode 100644 spring-batch-core-tests/src/test/resources/test.ldif create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifAggregator.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/MappingLdifReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordCallbackHandler.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordMapper.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/package-info.java diff --git a/spring-batch-core-tests/pom.xml b/spring-batch-core-tests/pom.xml index 4dbe3397f..57df73c34 100644 --- a/spring-batch-core-tests/pom.xml +++ b/spring-batch-core-tests/pom.xml @@ -107,10 +107,6 @@ junit junit - - - - log4j log4j @@ -137,6 +133,11 @@ org.springframework spring-aop + + org.springframework.ldap + spring-ldap-ldif-core + true + org.aspectj aspectjrt diff --git a/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/LdifReaderTests.java b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/LdifReaderTests.java new file mode 100644 index 000000000..807d3cc70 --- /dev/null +++ b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/LdifReaderTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2005-2014 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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.Assert; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", "/applicationContext-test1.xml"}) +public class LdifReaderTests { + private static Logger log = LoggerFactory.getLogger(LdifReaderTests.class); + + private Resource expected; + private Resource actual; + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + private Job job; + + public LdifReaderTests() { + try { + expected = new UrlResource("file:src/test/resources/expectedOutput.ldif"); + actual = new UrlResource("file:target/test-outputs/output.ldif"); + } catch (MalformedURLException e) { + log.error("Unexpected error", e); + } + } + + @Before + public void checkFiles() { + Assert.isTrue(expected.exists(), "Expected does not exist."); + } + + @Test + public void testValidRun() throws Exception { + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + //Ensure job completed successfully. + Assert.isTrue(jobExecution.getExitStatus().equals(ExitStatus.COMPLETED), "Step Execution did not complete normally: " + jobExecution.getExitStatus()); + + //Check output. + Assert.isTrue(actual.exists(), "Actual does not exist."); + Assert.isTrue(compareFiles(expected.getFile(), actual.getFile())); + } + + @Test + public void testResourceNotExists() throws Exception { + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + Assert.isTrue(jobExecution.getExitStatus().getExitCode().equals("FAILED"), "The job exit status is not FAILED."); + Assert.isTrue(jobExecution.getExitStatus().getExitDescription().contains("Failed to initialize the reader"), "The job failed for the wrong reason."); + } + + private boolean compareFiles(File expected, File actual) throws Exception { + boolean equal = true; + + FileInputStream expectedStream = new FileInputStream(expected); + FileInputStream actualStream = new FileInputStream(actual); + + //Construct BufferedReader from InputStreamReader + BufferedReader expectedReader = new BufferedReader(new InputStreamReader(expectedStream)); + BufferedReader actualReader = new BufferedReader(new InputStreamReader(actualStream)); + + String line = null; + while ((line = expectedReader.readLine()) != null) { + if(!line.equals(actualReader.readLine())) { + equal = false; + break; + } + } + + if(actualReader.readLine() != null) { + equal = false; + } + + expectedReader.close(); + + return equal; + } +} \ No newline at end of file diff --git a/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MappingLdifReaderTests.java b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MappingLdifReaderTests.java new file mode 100644 index 000000000..06aa0186a --- /dev/null +++ b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MappingLdifReaderTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2005-2014 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; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.Assert; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStreamReader; +import java.net.MalformedURLException; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "/simple-job-launcher-context.xml", "/applicationContext-test2.xml"}) +public class MappingLdifReaderTests { + private static Logger log = LoggerFactory.getLogger(MappingLdifReaderTests.class); + + private Resource expected; + private Resource actual; + + @Autowired + private JobLauncher launcher; + + @Autowired + private Job job; + + public MappingLdifReaderTests() { + try { + expected = new UrlResource("file:src/test/resources/expectedOutput.ldif"); + actual = new UrlResource("file:target/test-outputs/output.ldif"); + } catch (MalformedURLException e) { + log.error("Unexpected error", e); + } + } + + @Before + public void checkFiles() { + Assert.isTrue(expected.exists(), "Expected does not exist."); + } + + @Test + public void testValidRun() throws Exception { + JobExecution jobExecution = launcher.run(job, new JobParameters()); + + //Ensure job completed successfully. + Assert.isTrue(jobExecution.getExitStatus().equals(ExitStatus.COMPLETED), "Step Execution did not complete normally: " + jobExecution.getExitStatus()); + + //Check output. + Assert.isTrue(actual.exists(), "Actual does not exist."); + Assert.isTrue(compareFiles(expected.getFile(), actual.getFile())); + } + + @Test + public void testResourceNotExists() throws Exception { + JobExecution jobExecution = launcher.run(job, new JobParameters()); + + Assert.isTrue(jobExecution.getExitStatus().getExitCode().equals("FAILED"), "The job exit status is not FAILED."); + Assert.isTrue(jobExecution.getExitStatus().getExitDescription().contains("Failed to initialize the reader"), "The job failed for the wrong reason."); + } + + + private boolean compareFiles(File expected, File actual) throws Exception { + boolean equal = true; + + FileInputStream expectedStream = new FileInputStream(expected); + FileInputStream actualStream = new FileInputStream(actual); + + //Construct BufferedReader from InputStreamReader + BufferedReader expectedReader = new BufferedReader(new InputStreamReader(expectedStream)); + BufferedReader actualReader = new BufferedReader(new InputStreamReader(actualStream)); + + String line = null; + while ((line = expectedReader.readLine()) != null) { + if(!line.equals(actualReader.readLine())) { + equal = false; + break; + } + } + + if(actualReader.readLine() != null) { + equal = false; + } + + expectedReader.close(); + + return equal; + } +} \ No newline at end of file diff --git a/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MyMapper.java b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MyMapper.java new file mode 100644 index 000000000..0accc14e1 --- /dev/null +++ b/spring-batch-core-tests/src/test/java/org/springframework/batch/core/test/ldif/MyMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright 2005-2014 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; + +import org.springframework.batch.item.ldif.RecordMapper; +import org.springframework.ldap.core.LdapAttributes; + +/** + * This default implementation simply returns the LdapAttributes object and is only intended for test. As its not required + * to return an object of a specific type to make the MappingLdifReader implementation work, this basic setting is sufficient + * to demonstrate its function. + * + * @author Keith Barlow + * + */ +public class MyMapper implements RecordMapper { + + public LdapAttributes mapRecord(LdapAttributes attributes) { + return attributes; + } + +} \ No newline at end of file diff --git a/spring-batch-core-tests/src/test/resources/applicationContext-test1.xml b/spring-batch-core-tests/src/test/resources/applicationContext-test1.xml new file mode 100644 index 000000000..88f2fdedb --- /dev/null +++ b/spring-batch-core-tests/src/test/resources/applicationContext-test1.xml @@ -0,0 +1,39 @@ + + + + + + + + org.springframework.ldap.ldif.InvalidAttributeFormatException + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core-tests/src/test/resources/applicationContext-test2.xml b/spring-batch-core-tests/src/test/resources/applicationContext-test2.xml new file mode 100644 index 000000000..d41f6995f --- /dev/null +++ b/spring-batch-core-tests/src/test/resources/applicationContext-test2.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-batch-core-tests/src/test/resources/expectedOutput.ldif b/spring-batch-core-tests/src/test/resources/expectedOutput.ldif new file mode 100644 index 000000000..4d345c052 --- /dev/null +++ b/spring-batch-core-tests/src/test/resources/expectedOutput.ldif @@ -0,0 +1,74 @@ +dn: cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Bjorn Jensen + +dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: bjensen +description: Babs is a big sailing fan, and travels extensively in search of perfect sailing conditions. +objectclass: top +objectclass: person +objectclass: organizationalPerson +title: Product Manager, Rod and Reel Division +sn: Jensen +cn: Barbara Jensen +cn: Barbara J Jensen +cn: Babs Jensen + +dn: cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: gernj +description:: V2hhdCBhIGNhcmVmdWwgcmVhZGVyIHlvdSBhcmUhICBUaGlzIHZhbHVlIGlzIGJhc2UtNjQtZW5j +b2RlZCBiZWNhdXNlIGl0IGhhcyBhIGNvbnRyb2wgY2hhcmFjdGVyIGluIGl0IChhIENSKS4NICBC +eSB0aGUgd2F5LCB5b3Ugc2hvdWxkIHJlYWxseSBnZXQgb3V0IG1vcmUu +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Gern Jensen +cn: Gern O Jensen + +dn:: b3U95Za25qWt6YOoLG89QWlyaXVz +ou:: 5Za25qWt6YOo +ou:: 44GI44GE44GO44KH44GG44G2 +ou: Sales +description: Japanese office +objectclass: top +objectclass: organizationalUnit + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +givenname:: 44Ot44OJ44OL44O8 +givenname:: 44KN44Gp44Gr44O8 +givenname: Rodney +sn:: 5bCP56yg5Y6f +sn:: 44GK44GM44GV44KP44KJ +sn: Ogasawara +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +mail: rogasawara@airius.co.jp +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +preferredlanguage: ja +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +cn:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +cn: Rodney Ogasawara +title:: 5Za25qWt6YOoIOmDqOmVtw== +title:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +title: Sales, Director + +dn: cn=Horatio Jensen,ou=Product Testing,dc=airius,dc=com +telephonenumber: +1 408 555 1212 +uid: hjensen +jpegphoto:< file:///usr/local/directory/photos/hjensen.jpg +objectclass: top +objectclass: person +objectclass: organizationalPerson +sn: Jensen +cn: Horatio Jensen +cn: Horatio N Jensen diff --git a/spring-batch-core-tests/src/test/resources/simple-job-launcher-context.xml b/spring-batch-core-tests/src/test/resources/simple-job-launcher-context.xml index 3256fcc04..682f45343 100644 --- a/spring-batch-core-tests/src/test/resources/simple-job-launcher-context.xml +++ b/spring-batch-core-tests/src/test/resources/simple-job-launcher-context.xml @@ -1,11 +1,9 @@ + http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd"> + ,o=Airius +objectclass: top +objectclass: organizationalUnit +ou:: 5Za25qWt6YOo +# ou:: +ou;lang-ja:: 5Za25qWt6YOo +# ou;lang-ja:: +ou;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2 +# ou;lang-ja:: +ou;lang-en: Sales +description: Japanese office + +dn:: dWlkPXJvZ2FzYXdhcmEsb3U95Za25qWt6YOoLG89QWlyaXVz +# dn:: uid=,ou=,o=Airius +userpassword: {SHA}O3HSv1MusyL4kTjP+HKI5uxuNoM= +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +uid: rogasawara +mail: rogasawara@airius.co.jp +givenname;lang-ja:: 44Ot44OJ44OL44O8 +# givenname;lang-ja:: +sn;lang-ja:: 5bCP56yg5Y6f +# sn;lang-ja:: +cn;lang-ja:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn;lang-ja:: +title;lang-ja:: 5Za25qWt6YOoIOmDqOmVtw== +# title;lang-ja:: +preferredlanguage: ja +givenname:: 44Ot44OJ44OL44O8 +# givenname:: +sn:: 5bCP56yg5Y6f +# sn:: +cn:: 5bCP56yg5Y6fIOODreODieODi+ODvA== +# cn:: +title:: 5Za25qWt6YOoIOmDqOmVtw== +# title:: +givenname;lang-ja;phonetic:: 44KN44Gp44Gr44O8 +# givenname;lang-ja;phonetic:: +# +sn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJ +# sn;lang-ja;phonetic:: +cn;lang-ja;phonetic:: 44GK44GM44GV44KP44KJIOOCjeOBqeOBq+ODvA== +# cn;lang-ja;phonetic:: +title;lang-ja;phonetic:: 44GI44GE44GO44KH44GG44G2IOOBtuOBoeOCh+OBhg== +# title;lang-ja;phonetic:: +# +givenname;lang-en: Rodney +sn;lang-en: Ogasawara +cn;lang-en: Rodney Ogasawara +title;lang-en: Sales, Director + + +# +# Example 5: An LDIF file containing an invalid attribute +# + +dn: cn=Harry Jacobs, ou=Product Development, dc=airius, dc=com +objectClass: top +objectClass: person +cn: Harry Jacobs +description: :A big sailing fan. + + +# +# Example 6: A file containing a reference to an external file +# + +dn: cn=Horatio Jensen, ou=Product Testing, dc=airius, dc=com +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Horatio Jensen +cn: Horatio N Jensen +sn: Jensen +uid: hjensen +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/hjensen.jpg + + +# +# Example 7: A file containing a series of change records and comments +# + +# Add a new entry +dn: cn=Fiona Jensen, ou=Marketing, dc=airius, dc=com +changetype: add +objectclass: top +objectclass: person +objectclass: organizationalPerson +cn: Fiona Jensen +sn: Jensen +uid: fiona +telephonenumber: +1 408 555 1212 +jpegphoto:< file:///usr/local/directory/photos/fiona.jpg + +# Delete an existing entry +dn: cn=Robert Jensen, ou=Marketing, dc=airius, dc=com +changetype: delete + +# Modify an entry's relative distinguished name +dn: cn=Paul Jensen, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: cn=Paula Jensen +deleteoldrdn: 1 + +# Rename an entry and move all of its children to a new location in +# the directory tree (only implemented by LDAPv3 servers). +dn: ou=PD Accountants, ou=Product Development, dc=airius, dc=com +changetype: modrdn +newrdn: ou=Product Development Accountants +deleteoldrdn: 0 +newsuperior: ou=Accounting, dc=airius, dc=com + + +# Modify an entry: add an additional value to the postaladdress +# attribute, completely delete the description attribute, replace +# the telephonenumber attribute with two values, and delete a specific +# value from the facsimiletelephonenumber attribute +dn: cn=Paula Jensen, ou=Product Development, dc=airius, dc=com +changetype: modify +add: postaladdress +postaladdress: 123 Anystreet $ Sunnyvale, CA $ 94086 +- + +delete: description +- +replace: telephonenumber +telephonenumber: +1 408 555 1234 +telephonenumber: +1 408 555 5678 +- +delete: facsimiletelephonenumber +facsimiletelephonenumber: +1 408 555 9876 +- + +# Modify an entry: replace the postaladdress attribute with an empty +# set of values (which will cause the attribute to be removed), and +# delete the entire description attribute. Note that the first will +# always succeed, while the second will only succeed if at least +# one value for the description attribute is present. +dn: cn=Ingrid Jensen, ou=Product Support, dc=airius, dc=com +changetype: modify +replace: postaladdress +- +delete: description +- + + +# +# Example 8: An LDIF file containing a change record with a control +# + +# Delete an entry. The operation will attach the LDAPv3 +# Tree Delete Control defined in [9]. The criticality +# field is "true" and the controlValue field is +# absent, as required by [9]. +dn: ou=Product Development, dc=airius, dc=com +control: 1.2.840.113556.1.4.805 true +changetype: delete diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index 56cf24870..89aaf1827 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -245,7 +245,22 @@ spring-rabbit true - + + org.springframework.ldap + spring-ldap-core + true + + + org.springframework.ldap + spring-ldap-core-tiger + true + + + org.springframework.ldap + spring-ldap-ldif-core + true + + org.mockito mockito-all test diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifAggregator.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifAggregator.java new file mode 100644 index 000000000..d6689d15d --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifAggregator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2005-2014 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; + +import org.springframework.batch.item.file.transform.LineAggregator; +import org.springframework.ldap.core.LdapAttributes; + +/** + * The {@link LdifAggregator LdifAggregator} object is an implementation of the {@link org.springframework.batch.item.file.transform.LineAggregator LineAggregator} + * interface for use with a {@link org.springframework.batch.item.file.FlatFileItemWriter FlatFileItemWriter} to write LDIF records to a file. + * + * @author Keith Barlow + * + */ +public class LdifAggregator implements LineAggregator { + + /** + * Returns a {@link java.lang.String String} containing a properly formated LDIF. + * + * @param item LdapAttributes object to convert to string. + * @return string representation of the object LDIF format (in accordance with RFC 2849). + */ + public String aggregate(LdapAttributes item) { + return item.toString(); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifReader.java new file mode 100644 index 000000000..c54a58399 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/LdifReader.java @@ -0,0 +1,168 @@ +/* + * Copyright 2005-2014 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.parser.LdifParser; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * The {@link LdifReader LdifReader} is an adaptation of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * built around an {@link LdifParser LdifParser}. + *

+ * Unlike the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link LdifReader LdifReader} + * does not require a mapper. Instead, this version of the {@link LdifReader LdifReader} simply returns an {@link LdapAttributes LdapAttributes} + * object which can be consumed and manipulated as necessary by {@link org.springframework.batch.item.ItemProcessor ItemProcessor} or any + * output service. Alternatively, the {@link RecordMapper RecordMapper} interface can be implemented and set in a + * {@link MappingLdifReader MappingLdifReader} to map records to objects for return. + *

+ * {@link LdifReader LdifReader} usage is mimics that of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * for all intensive purposes. Adjustments have been made to process records instead of lines, however. As such, the + * {@link #recordsToSkip recordsToSkip} attribute indicates the number of records from the top of the file that should not be processed. + * Implementations of the {@link RecordCallbackHandler RecordCallbackHandler} interface can be used to execute operations on those skipped records. + *

+ * As with the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link #strict strict} option differentiates + * between whether or not to require the resource to exist before processing. In the case of a value set to false, a warning is logged instead of + * an exception being thrown. + * + * @author Keith Barlow + * + */ +public class LdifReader extends AbstractItemCountingItemStreamItemReader + implements ResourceAwareItemReaderItemStream, InitializingBean { + + private static final Logger LOG = LoggerFactory.getLogger(LdifReader.class); + + private Resource resource; + + private LdifParser ldifParser; + + private int recordCount = 0; + + private int recordsToSkip = 0; + + private boolean strict = true; + + private RecordCallbackHandler skippedRecordsCallback; + + public LdifReader() { + setName(ClassUtils.getShortName(LdifReader.class)); + } + + /** + * In strict mode the reader will throw an exception on + * {@link #open(org.springframework.batch.item.ExecutionContext)} if the + * input resource does not exist. + * @param strict false by default + */ + public void setStrict(boolean strict) { + this.strict = strict; + } + + /** + * {@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. + */ + public void setSkippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) { + this.skippedRecordsCallback = skippedRecordsCallback; + } + + /** + * 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 + */ + public void setRecordsToSkip(int recordsToSkip) { + this.recordsToSkip = recordsToSkip; + } + + @Override + protected void doClose() throws Exception { + if (ldifParser != null) { + ldifParser.close(); + } + this.recordCount = 0; + } + + @Override + protected void doOpen() throws Exception { + if (resource == null) + throw new IllegalStateException("A resource has not been set."); + + if (!resource.exists()) { + if (strict) { + throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): "+resource); + } else { + LOG.warn("Input resource does not exist " + resource.getDescription()); + return; + } + } + + ldifParser.open(); + + for (int i = 0; i < recordsToSkip; i++) { + LdapAttributes record = ldifParser.getRecord(); + if (skippedRecordsCallback != null) { + skippedRecordsCallback.handleRecord(record); + } + } + } + + @Override + protected LdapAttributes doRead() throws Exception { + LdapAttributes attributes = null; + + try { + if (ldifParser != null) { + while (attributes == null && ldifParser.hasMoreRecords()) { + attributes = ldifParser.getRecord(); + } + recordCount++; + } + + return attributes; + + } catch(Exception ex){ + LOG.error("Parsing error at record " + recordCount + " in resource=" + + resource.getDescription() + ", input=[" + attributes + "]", ex); + throw ex; + } + } + + public void setResource(Resource resource) { + this.resource = resource; + this.ldifParser = new LdifParser(resource); + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource, "A resource is required to parse."); + Assert.notNull(ldifParser); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/MappingLdifReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/MappingLdifReader.java new file mode 100644 index 000000000..147319f96 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/MappingLdifReader.java @@ -0,0 +1,177 @@ +/* + * Copyright 2005-2014 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.ldap.core.LdapAttributes; +import org.springframework.ldap.ldif.parser.LdifParser; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * The {@link MappingLdifReader MappingLdifReader} is an adaptation of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader} + * built around an {@link LdifParser LdifParser}. It differs from the standard {@link LdifReader LdifReader} in its ability to map + * {@link LdapAttributes LdapAttributes} objects to POJOs. + *

+ * The {@link MappingLdifReader MappingLdifReader} requires an {@link RecordMapper RecordMapper} implementation. If mapping + * is not required, the {@link LdifReader LdifReader} should be used instead. It simply returns an {@link LdapAttributes LdapAttributes} + * object which can be consumed and manipulated as necessary by {@link org.springframework.batch.item.ItemProcessor ItemProcessor} or any + * output service. + *

+ * {@link LdifReader LdifReader} usage is mimics that of the FlatFileItemReader for all intensive purposes. Adjustments have been made to + * process records instead of lines, however. As such, the {@link #recordsToSkip recordsToSkip} attribute indicates the number of records + * from the top of the file that should not be processed. Implementations of the {@link RecordCallbackHandler RecordCallbackHandler} + * interface can be used to execute operations on those skipped records. + *

+ * As with the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link #strict strict} option + * differentiates between whether or not to require the resource to exist before processing. In the case of a value set to false, a warning + * is logged instead of an exception being thrown. + * + * @author Keith Barlow + * + */ +public class MappingLdifReader extends AbstractItemCountingItemStreamItemReader + implements ResourceAwareItemReaderItemStream, InitializingBean { + + private static final Logger LOG = LoggerFactory.getLogger(MappingLdifReader.class); + + private Resource resource; + + private LdifParser ldifParser; + + private int recordCount = 0; + + private int recordsToSkip = 0; + + private boolean strict = true; + + private RecordCallbackHandler skippedRecordsCallback; + + private RecordMapper recordMapper; + + public MappingLdifReader() { + setName(ClassUtils.getShortName(MappingLdifReader.class)); + } + + /** + * In strict mode the reader will throw an exception on + * {@link #open(org.springframework.batch.item.ExecutionContext)} if the + * input resource does not exist. + * @param strict false by default + */ + public void setStrict(boolean strict) { + this.strict = strict; + } + + /** + * {@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. + */ + public void setSkippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) { + this.skippedRecordsCallback = skippedRecordsCallback; + } + + /** + * 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 + */ + public void setRecordsToSkip(int recordsToSkip) { + this.recordsToSkip = recordsToSkip; + } + + /** + * Setter for object mapper. This property is required to be set. + * @param recordMapper maps record to an object + */ + public void setRecordMapper(RecordMapper recordMapper) { + this.recordMapper = recordMapper; + } + + @Override + protected void doClose() throws Exception { + if (ldifParser != null) { + ldifParser.close(); + } + this.recordCount = 0; + } + + @Override + protected void doOpen() throws Exception { + if (resource == null) + throw new IllegalStateException("A resource has not been set."); + + if (!resource.exists()) { + if (strict) { + throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): "+resource); + } else { + LOG.warn("Input resource does not exist " + resource.getDescription()); + return; + } + } + + ldifParser.open(); + + for (int i = 0; i < recordsToSkip; i++) { + LdapAttributes record = ldifParser.getRecord(); + if (skippedRecordsCallback != null) { + skippedRecordsCallback.handleRecord(record); + } + } + } + + @Override + protected T doRead() throws Exception { + LdapAttributes attributes = null; + + try { + if (ldifParser != null) { + while (attributes == null && ldifParser.hasMoreRecords()) { + attributes = ldifParser.getRecord(); + } + recordCount++; + return recordMapper.mapRecord(attributes); + } + + return null; + } catch(Exception ex){ + LOG.error("Parsing error at record " + recordCount + " in resource=" + + resource.getDescription() + ", input=[" + attributes + "]", ex); + throw ex; + } + } + + public void setResource(Resource resource) { + this.resource = resource; + this.ldifParser = new LdifParser(resource); + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource, "A resource is required to parse."); + Assert.notNull(ldifParser); + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordCallbackHandler.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordCallbackHandler.java new file mode 100644 index 000000000..b64aa3259 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordCallbackHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2005-2014 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; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * This interface can be used to operate on skipped records in the {@link LdifReader LdifReader} and the + * {@link MappingLdifReader MappingLdifReader}. + * + * @author Keith Barlow + * + */ +public interface RecordCallbackHandler { + + /** + * Execute operations on the supplied record. + * + * @param attributes + */ + void handleRecord(LdapAttributes attributes); + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordMapper.java new file mode 100644 index 000000000..90a650c6d --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/RecordMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2005-2014 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; + +import org.springframework.ldap.core.LdapAttributes; + +/** + * This interface should be implemented to map {@link LdapAttributes LdapAttributes} objects to POJOs. The resulting + * implementations can be used in the {@link MappingLdifReader MappingLdifReader}. + * + * @author Keith Barlow + * + * @param + */ +public interface RecordMapper { + + /** + * Maps an {@link LdapAttributes LdapAttributes} object to the specified type. + * + * @param attributes + * @return object of type T + */ + T mapRecord(LdapAttributes attributes); + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/package-info.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/package-info.java new file mode 100644 index 000000000..5c313e324 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ldif/package-info.java @@ -0,0 +1,6 @@ +/** + *

This package contains the classes required for using the LdifParser in Spring LDAP.

+ * + * @author Michael Minella + */ +package org.springframework.batch.item.ldif; \ No newline at end of file diff --git a/spring-batch-parent/pom.xml b/spring-batch-parent/pom.xml index 9fcedfff8..5b0f13f55 100644 --- a/spring-batch-parent/pom.xml +++ b/spring-batch-parent/pom.xml @@ -763,6 +763,24 @@ 1.0.3.RELEASE true
+ + org.springframework.ldap + spring-ldap-core + 2.0.1.RELEASE + true + + + org.springframework.ldap + spring-ldap-core-tiger + 2.0.1.RELEASE + true + + + org.springframework.ldap + spring-ldap-ldif-core + 2.0.1.RELEASE + true +