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.

This commit is contained in:
dhgarrette
2009-01-28 05:00:06 +00:00
parent 88cd809b94
commit 18f7870ba2
7 changed files with 340 additions and 138 deletions

View File

@@ -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;
/**
* <p>
* 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.
*
* <p>
* 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<T> extends PrefixMatchingCompositeLineTokenizer implements LineMapper<T> {
private Map<String, FieldSetMapper<T>> 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<String, FieldSetMapper<T>> fieldSetMappers) {
this.fieldSetMappers = new LinkedHashMap<String, FieldSetMapper<T>>(fieldSetMappers);
}
}

View File

@@ -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<String, LineTokenizer> tokenizers = new HashMap<String, LineTokenizer>();
private Map<String, LineTokenizer> 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> S matchPrefix(String line, Map<String, S> 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<String, LineTokenizer> tokenizers) {
this.tokenizers = new LinkedHashMap<String, LineTokenizer>(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);
}
}

View File

@@ -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<Name> mapper = new PrefixMatchingCompositeLineMapper<Name>();
@Test(expected = IllegalArgumentException.class)
public void test_NoMappers() throws Exception {
mapper.setTokenizers(Collections.singletonMap("", (LineTokenizer) new DelimitedLineTokenizer()));
Map<String, FieldSetMapper<Name>> fieldSetMappers = Collections.emptyMap();
mapper.setFieldSetMappers(fieldSetMappers);
mapper.afterPropertiesSet();
}
@Test
public void test_NullLine() throws Exception {
Map<String, LineTokenizer> tokenizers = new HashMap<String, LineTokenizer>();
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<String, FieldSetMapper<Name>> fieldSetMappers = new HashMap<String, FieldSetMapper<Name>>();
fieldSetMappers.put(null, new FieldSetMapper<Name>() {
public Name mapFieldSet(FieldSet fs) {
return new Name(fs.readString(0), fs.readString(1), 0);
}
});
fieldSetMappers.put("bar", new FieldSetMapper<Name>() {
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<String, LineTokenizer> tokenizers = new HashMap<String, LineTokenizer>();
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<String, FieldSetMapper<Name>> fieldSetMappers = new HashMap<String, FieldSetMapper<Name>>();
fieldSetMappers.put("foo", new FieldSetMapper<Name>() {
public Name mapFieldSet(FieldSet fs) {
return new Name(fs.readString(0), fs.readString(1), 0);
}
});
fieldSetMappers.put("bar", new FieldSetMapper<Name>() {
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<String, LineTokenizer> tokenizers = new HashMap<String, LineTokenizer>();
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<String, FieldSetMapper<Name>> fieldSetMappers = new HashMap<String, FieldSetMapper<Name>>();
fieldSetMappers.put("foo", new FieldSetMapper<Name>() {
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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, LineTokenizer> map = new HashMap<String, LineTokenizer>();
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<String, LineTokenizer> map = new HashMap<String, LineTokenizer>();
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<String, LineTokenizer> map = new LinkedHashMap<String, LineTokenizer>();
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));

View File

@@ -21,58 +21,48 @@
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader">
<property name="resource" value="data/iosample/input/multiRecordType.txt" />
<property name="lineMapper">
<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
<property name="lineTokenizer" ref="compositeLineTokenizer" />
<property name="fieldSetMapper" ref="compositeFieldSetMapper" />
</bean>
</property>
<property name="lineMapper" ref="prefixMatchingLineMapper"/>
</bean>
<bean id="compositeLineTokenizer"
class="org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer">
<bean id="prefixMatchingLineMapper"
class="org.springframework.batch.item.file.mapping.PrefixMatchingCompositeLineMapper">
<property name="tokenizers">
<map>
<entry key="TRAD" value-ref="tradeLineTokenizer" />
<entry key="CUST" value-ref="customerLineTokenizer" />
</map>
</property>
<property name="fieldSetMappers">
<map>
<entry key="TRAD" value-ref="tradeFieldSetMapper" />
<entry key="CUST" value-ref="customerFieldSetMapper" />
</map>
</property>
</bean>
<bean id="tradeLineTokenizer"
class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="isin,quantity,price,customer,prefix" />
<property name="columns" value="5-16,17-19,20-25,26-34,1-4" />
<property name="names" value="isin,quantity,price,customer" />
<property name="columns" value="5-16,17-19,20-25,26-34" />
</bean>
<bean id="customerLineTokenizer"
class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
<property name="names" value="id,name,credit,prefix" />
<property name="columns" value="5-9,10-18,19-26,1-4" />
</bean>
<bean id="compositeFieldSetMapper"
class="org.springframework.batch.sample.iosample.internal.PrefixMatchingCompositeFieldSetMapper">
<property name="mappers">
<map>
<entry key="TRAD" value-ref="tradeFieldSetMapper" />
<entry key="CUST" value-ref="customerFieldSetMapper" />
</map>
</property>
<property name="names" value="id,name,credit" />
<property name="columns" value="5-9,10-18,19-26" />
</bean>
<bean id="tradeFieldSetMapper"
class="org.springframework.batch.sample.domain.trade.internal.TradeFieldSetMapper" />
class="org.springframework.batch.sample.domain.trade.internal.TradeFieldSetMapper" />
<bean id="customerFieldSetMapper"
class="org.springframework.batch.sample.domain.trade.internal.CustomerCreditFieldSetMapper" />
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
<property name="resource"
value="file:target/test-outputs/multiRecordTypeOutput.txt" />
<property name="resource" value="file:target/test-outputs/multiRecordTypeOutput.txt" />
<property name="lineAggregator">
<bean
class="org.springframework.batch.sample.iosample.internal.DelegatingTradeLineAggregator">
<bean class="org.springframework.batch.sample.iosample.internal.DelegatingTradeLineAggregator">
<property name="tradeLineAggregator" ref="tradeLineAggregator" />
<property name="customerLineAggregator" ref="customerLineAggregator" />
</bean>
@@ -82,8 +72,7 @@
<bean id="tradeLineAggregator"
class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
<property name="fieldExtractor">
<bean
class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="isin,quantity,price,customer" />
</bean>
</property>
@@ -93,8 +82,7 @@
<bean id="customerLineAggregator"
class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
<property name="fieldExtractor">
<bean
class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
<property name="names" value="id,name,credit" />
</bean>
</property>

View File

@@ -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<T> implements FieldSetMapper<T> {
private Map<String, FieldSetMapper<? extends T>> mappers = new HashMap<String, FieldSetMapper<? extends T>>();
public T mapFieldSet(FieldSet fieldSet) {
String prefix = fieldSet.readString("prefix");
return this.mappers.get(prefix).mapFieldSet(fieldSet);
}
public void setMappers(Map<String, FieldSetMapper<? extends T>> mappers) {
this.mappers = mappers;
}
}