From 18f7870ba29c657df514bc82fba02cf7b6e3dfad Mon Sep 17 00:00:00 2001 From: dhgarrette Date: Wed, 28 Jan 2009 05:00:06 +0000 Subject: [PATCH] BATCH-1033: Created PrefixMatchingCompositeLineMapper and reorganized PrefixMatchingCompositeLineTokenizer so that its lookup functionality could be reused. Also updated the multiRecordType sample by removing PrefixMatchingCompositeFieldSetMapper in favor of the new class. --- .../PrefixMatchingCompositeLineMapper.java | 70 +++++++++ .../PrefixMatchingCompositeLineTokenizer.java | 98 ++++++++----- ...refixMatchingCompositeLineMapperTests.java | 137 ++++++++++++++++++ .../batch/item/file/transform/Name.java | 6 + ...ixMatchingCompositeLineTokenizerTests.java | 73 ++++++---- .../jobs/iosample/multiRecordType.xml | 50 +++---- ...PrefixMatchingCompositeFieldSetMapper.java | 44 ------ 7 files changed, 340 insertions(+), 138 deletions(-) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapper.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapperTests.java delete mode 100644 spring-batch-samples/src/test/java/org/springframework/batch/sample/iosample/internal/PrefixMatchingCompositeFieldSetMapper.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapper.java new file mode 100644 index 000000000..b40acc67c --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapper.java @@ -0,0 +1,70 @@ +/* + * Copyright 2006-2007 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.file.mapping; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.batch.item.file.transform.LineTokenizer; +import org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer; +import org.springframework.util.Assert; + +/** + *

+ * A {@link LineMapper} implementation that stores a mapping of String prefixes + * to delegate {@link LineTokenizer}s as well as a mapping of String prefixes + * to delegate {@link LineTokenizer}s. Each line received will be tokenized and + * then mapped to a field set. + * + *

+ * Both the tokenizing and the mapping work in a similar way. The line will be + * checked for its prefix. If the prefix matches a key in the map of delegates, + * then the corresponding delegate will be used. Otherwise, the default + * tokenizer or mapper will be used. The default can be configured in the + * delegate map by setting its corresponding prefix to the empty string. + * + * @author Dan Garrette + */ +public class PrefixMatchingCompositeLineMapper extends PrefixMatchingCompositeLineTokenizer implements LineMapper { + + private Map> fieldSetMappers = null; + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.item.file.mapping.LineMapper#mapLine(java.lang.String, + * int) + */ + public T mapLine(String line, int lineNumber) throws Exception { + return this.matchPrefix(line, this.fieldSetMappers).mapFieldSet(this.tokenize(line)); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + super.afterPropertiesSet(); + Assert.isTrue(this.fieldSetMappers != null && this.fieldSetMappers.size() > 0, + "The 'fieldSetMappers' property must be non-empty"); + } + + public void setFieldSetMappers(Map> fieldSetMappers) { + this.fieldSetMappers = new LinkedHashMap>(fieldSetMappers); + } +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizer.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizer.java index 93179cb1c..21fed00b8 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizer.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizer.java @@ -16,10 +16,11 @@ package org.springframework.batch.item.file.transform; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; /** * A {@link LineTokenizer} implementation that stores a mapping of String @@ -30,46 +31,69 @@ import java.util.Map; * {@link LineTokenizer} can be configured in the delegate map by setting its * corresponding prefix to the empty string. * + * @author Dan Garrette */ -public class PrefixMatchingCompositeLineTokenizer implements LineTokenizer { +public class PrefixMatchingCompositeLineTokenizer implements LineTokenizer, InitializingBean { - private Map tokenizers = new HashMap(); + private Map tokenizers = null; + + /* + * (non-Javadoc) + * + * @see org.springframework.batch.item.file.transform.LineTokenizer#tokenize(java.lang.String) + */ + public FieldSet tokenize(String line) { + return this.matchPrefix(line, this.tokenizers).tokenize(line); + } + + /** + * @param line + * @return the delegate whose prefix matches the given line + */ + protected S matchPrefix(String line, Map delegates) { + S delegate = null; + S defaultDelegate = null; + + if (line != null) { + for (String key : delegates.keySet()) { + if ("".equals(key)) { + defaultDelegate = delegates.get(key); + // don't break here or the delegate may not be found + } + else if (line.startsWith(key)) { + delegate = delegates.get(key); + break; + } + } + + if (delegate == null) { + delegate = defaultDelegate; + } + } + else if (delegates.containsKey(null)) { + delegate = delegates.get(null); + } + else { + throw new IllegalStateException("Could not handle a null line"); + } + + if (delegate == null) { + throw new IllegalStateException("Could not find a matching prefix for line=[" + line + "]"); + } + return delegate; + } + + /* + * (non-Javadoc) + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.isTrue(this.tokenizers != null && this.tokenizers.size() > 0, + "The 'tokenizers' property must be non-empty"); + } public void setTokenizers(Map tokenizers) { this.tokenizers = new LinkedHashMap(tokenizers); } - - public FieldSet tokenize(String line) { - - if (line == null) { - return new DefaultFieldSet(new String[0]); - } - - LineTokenizer tokenizer = null; - LineTokenizer defaultTokenizer = null; - - for (String key : tokenizers.keySet()) { - - if ("".equals(key)) { - defaultTokenizer = (LineTokenizer) tokenizers.get(key); - // don't break here or the tokenizer may not be found - continue; - } - if (line.startsWith(key)) { - tokenizer = (LineTokenizer) tokenizers.get(key); - break; - } - } - - if (tokenizer == null) { - tokenizer = defaultTokenizer; - } - - if (tokenizer == null) { - throw new IllegalStateException("Could not match record to tokenizer for line=[" + line + "]"); - } - - return tokenizer.tokenize(line); - } - } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapperTests.java new file mode 100644 index 000000000..e6882ab8d --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PrefixMatchingCompositeLineMapperTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2006-2007 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.file.mapping; + +import static junit.framework.Assert.assertEquals; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.batch.item.file.transform.DefaultFieldSet; +import org.springframework.batch.item.file.transform.DelimitedLineTokenizer; +import org.springframework.batch.item.file.transform.FieldSet; +import org.springframework.batch.item.file.transform.LineTokenizer; +import org.springframework.batch.item.file.transform.Name; + +/** + * @author Dan Garrette + */ +public class PrefixMatchingCompositeLineMapperTests { + + private PrefixMatchingCompositeLineMapper mapper = new PrefixMatchingCompositeLineMapper(); + + @Test(expected = IllegalArgumentException.class) + public void test_NoMappers() throws Exception { + mapper.setTokenizers(Collections.singletonMap("", (LineTokenizer) new DelimitedLineTokenizer())); + Map> fieldSetMappers = Collections.emptyMap(); + mapper.setFieldSetMappers(fieldSetMappers); + mapper.afterPropertiesSet(); + } + + @Test + public void test_NullLine() throws Exception { + Map tokenizers = new HashMap(); + tokenizers.put(null, new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "a", "b" }); + } + }); + tokenizers.put("bar", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "c", "d" }); + } + }); + mapper.setTokenizers(tokenizers); + + Map> fieldSetMappers = new HashMap>(); + fieldSetMappers.put(null, new FieldSetMapper() { + public Name mapFieldSet(FieldSet fs) { + return new Name(fs.readString(0), fs.readString(1), 0); + } + }); + fieldSetMappers.put("bar", new FieldSetMapper() { + public Name mapFieldSet(FieldSet fs) { + return new Name(fs.readString(1), fs.readString(0), 0); + } + }); + mapper.setFieldSetMappers(fieldSetMappers); + + Name name = mapper.mapLine(null, 1); + assertEquals(new Name("a", "b", 0), name); + } + + @Test + public void test_KeyFound() throws Exception { + Map tokenizers = new HashMap(); + tokenizers.put("foo", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "a", "b" }); + } + }); + tokenizers.put("bar", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "c", "d" }); + } + }); + mapper.setTokenizers(tokenizers); + + Map> fieldSetMappers = new HashMap>(); + fieldSetMappers.put("foo", new FieldSetMapper() { + public Name mapFieldSet(FieldSet fs) { + return new Name(fs.readString(0), fs.readString(1), 0); + } + }); + fieldSetMappers.put("bar", new FieldSetMapper() { + public Name mapFieldSet(FieldSet fs) { + return new Name(fs.readString(1), fs.readString(0), 0); + } + }); + mapper.setFieldSetMappers(fieldSetMappers); + + Name name = mapper.mapLine("bar", 1); + assertEquals(new Name("d", "c", 0), name); + } + + @Test(expected = IllegalStateException.class) + public void test_MapperKeyNotFound() throws Exception { + Map tokenizers = new HashMap(); + tokenizers.put("foo", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "a", "b" }); + } + }); + tokenizers.put("bar", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new DefaultFieldSet(new String[] { "c", "d" }); + } + }); + mapper.setTokenizers(tokenizers); + + Map> fieldSetMappers = new HashMap>(); + fieldSetMappers.put("foo", new FieldSetMapper() { + public Name mapFieldSet(FieldSet fs) { + return new Name(fs.readString(0), fs.readString(1), 0); + } + }); + mapper.setFieldSetMappers(fieldSetMappers); + + Name name = mapper.mapLine("bar", 1); + assertEquals(new Name("d", "c", 0), name); + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/Name.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/Name.java index 9ee3793a3..b440f893d 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/Name.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/Name.java @@ -1,5 +1,7 @@ package org.springframework.batch.item.file.transform; +import org.apache.commons.lang.builder.EqualsBuilder; + public class Name { private String first; private String last; @@ -38,4 +40,8 @@ public class Name { public void setBorn(int born) { this.born = born; } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizerTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizerTests.java index 20ca3b068..bca65b7df 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizerTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PrefixMatchingCompositeLineTokenizerTests.java @@ -16,33 +16,52 @@ package org.springframework.batch.item.file.transform; +import static junit.framework.Assert.assertEquals; + import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; -import junit.framework.TestCase; +import org.junit.Test; +/** + * @author Dan Garrette + */ +public class PrefixMatchingCompositeLineTokenizerTests { -public class PrefixMatchingCompositeLineTokenizerTests extends TestCase { + private PrefixMatchingCompositeLineTokenizer tokenizer = new PrefixMatchingCompositeLineTokenizer(); - PrefixMatchingCompositeLineTokenizer tokenizer = new PrefixMatchingCompositeLineTokenizer(); - + @Test(expected = IllegalArgumentException.class) public void testNoTokenizers() throws Exception { - try { - tokenizer.tokenize("a line"); - fail("Expected IllegalStateException"); - } catch (IllegalStateException e) { - // expected - } + tokenizer.afterPropertiesSet(); + tokenizer.tokenize("a line"); } - - public void testNullLine() throws Exception { - tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new DelimitedLineTokenizer())); + + @Test(expected = IllegalStateException.class) + public void testNullLineNoKey() throws Exception { + tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new DelimitedLineTokenizer())); + tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize(null); assertEquals(0, fields.getFieldCount()); } - + + @Test + public void testNullLineWithKey() throws Exception { + Map map = new HashMap(); + map.put(null, new DelimitedLineTokenizer()); + map.put("foo", new LineTokenizer() { + public FieldSet tokenize(String line) { + return null; + } + }); + tokenizer.setTokenizers(map); + tokenizer.afterPropertiesSet(); + FieldSet fields = tokenizer.tokenize(null); + assertEquals(0, fields.getFieldCount()); + } + + @Test public void testEmptyKeyMatchesAnyLine() throws Exception { Map map = new HashMap(); map.put("", new DelimitedLineTokenizer()); @@ -51,13 +70,15 @@ public class PrefixMatchingCompositeLineTokenizerTests extends TestCase { return null; } }); - tokenizer.setTokenizers(map); + tokenizer.setTokenizers(map); + tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize("abc"); assertEquals(1, fields.getFieldCount()); } + @Test public void testEmptyKeyDoesNotMatchWhenAlternativeAvailable() throws Exception { - + Map map = new LinkedHashMap(); map.put("", new LineTokenizer() { public FieldSet tokenize(String line) { @@ -65,27 +86,27 @@ public class PrefixMatchingCompositeLineTokenizerTests extends TestCase { } }); map.put("foo", new DelimitedLineTokenizer()); - tokenizer.setTokenizers(map); + tokenizer.setTokenizers(map); + tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize("foo,bar"); assertEquals("bar", fields.readString(1)); } + @Test(expected = IllegalStateException.class) public void testNoMatch() throws Exception { - tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new DelimitedLineTokenizer())); - try { - tokenizer.tokenize("nomatch"); - fail("Expected IllegalStateException"); - } catch (IllegalStateException e) { - // expected - } + tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new DelimitedLineTokenizer())); + tokenizer.afterPropertiesSet(); + tokenizer.tokenize("nomatch"); } - + + @Test public void testMatchWithPrefix() throws Exception { tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new LineTokenizer() { public FieldSet tokenize(String line) { - return new DefaultFieldSet(new String[] {line}); + return new DefaultFieldSet(new String[] { line }); } })); + tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize("foo bar"); assertEquals(1, fields.getFieldCount()); assertEquals("foo bar", fields.readString(0)); diff --git a/spring-batch-samples/src/main/resources/jobs/iosample/multiRecordType.xml b/spring-batch-samples/src/main/resources/jobs/iosample/multiRecordType.xml index 2d1dd29f8..c06a2b019 100644 --- a/spring-batch-samples/src/main/resources/jobs/iosample/multiRecordType.xml +++ b/spring-batch-samples/src/main/resources/jobs/iosample/multiRecordType.xml @@ -21,58 +21,48 @@ - - - - - - + - + + + + + + + - - + + - - - - - - - - - - - + + + class="org.springframework.batch.sample.domain.trade.internal.TradeFieldSetMapper" /> + - + - + @@ -82,8 +72,7 @@ - + @@ -93,8 +82,7 @@ - + diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/sample/iosample/internal/PrefixMatchingCompositeFieldSetMapper.java b/spring-batch-samples/src/test/java/org/springframework/batch/sample/iosample/internal/PrefixMatchingCompositeFieldSetMapper.java deleted file mode 100644 index 0557a112f..000000000 --- a/spring-batch-samples/src/test/java/org/springframework/batch/sample/iosample/internal/PrefixMatchingCompositeFieldSetMapper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2006-2007 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.iosample.internal; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.batch.item.file.mapping.FieldSetMapper; -import org.springframework.batch.item.file.transform.FieldSet; - -/** - * This class is used to delegate to a {@link FieldSetMapper} based on one field - * in the {@link FieldSet}. - * - * @author Dan Garrette - * @since 2.0 - */ -public class PrefixMatchingCompositeFieldSetMapper implements FieldSetMapper { - - private Map> mappers = new HashMap>(); - - public T mapFieldSet(FieldSet fieldSet) { - String prefix = fieldSet.readString("prefix"); - return this.mappers.get(prefix).mapFieldSet(fieldSet); - } - - public void setMappers(Map> mappers) { - this.mappers = mappers; - } -}