BATCH-1033: Moved matchPrefix method to PatternMatcher class. Also modified matchPrefix to ensure that the most specific prefix wins. Changed Prefix*LineMapper to have Prefix*LineTokenizer as a property.

This commit is contained in:
dhgarrette
2009-01-28 19:09:00 +00:00
parent c49b374f2d
commit 02151edd2b
6 changed files with 207 additions and 136 deletions

View File

@@ -21,26 +21,32 @@ import java.util.Map;
import org.springframework.batch.item.file.transform.LineTokenizer;
import org.springframework.batch.item.file.transform.PrefixMatchingCompositeLineTokenizer;
import org.springframework.batch.support.PatternMatcher;
import org.springframework.beans.factory.InitializingBean;
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.
* to delegate {@link FieldSetMapper}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.
* then the corresponding delegate will be used. Prefixes are sorted starting
* with the most specific, and the first match always succeeds.
*
* @see PrefixMatchingCompositeLineTokenizer
* @see PatternMatcher#matchPrefix(String, Map)
*
* @author Dan Garrette
* @since 2.0
*/
public class PrefixMatchingCompositeLineMapper<T> extends PrefixMatchingCompositeLineTokenizer implements LineMapper<T> {
public class PrefixMatchingCompositeLineMapper<T> implements LineMapper<T>, InitializingBean {
private PrefixMatchingCompositeLineTokenizer tokenizer = new PrefixMatchingCompositeLineTokenizer();
private Map<String, FieldSetMapper<T>> fieldSetMappers = null;
/*
@@ -50,7 +56,7 @@ public class PrefixMatchingCompositeLineMapper<T> extends PrefixMatchingComposit
* int)
*/
public T mapLine(String line, int lineNumber) throws Exception {
return this.matchPrefix(line, this.fieldSetMappers).mapFieldSet(this.tokenize(line));
return PatternMatcher.matchPrefix(line, this.fieldSetMappers).mapFieldSet(this.tokenizer.tokenize(line));
}
/*
@@ -59,11 +65,15 @@ public class PrefixMatchingCompositeLineMapper<T> extends PrefixMatchingComposit
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
this.tokenizer.afterPropertiesSet();
Assert.isTrue(this.fieldSetMappers != null && this.fieldSetMappers.size() > 0,
"The 'fieldSetMappers' property must be non-empty");
}
public void setTokenizers(Map<String, LineTokenizer> tokenizers) {
this.tokenizer.setTokenizers(tokenizers);
}
public void setFieldSetMappers(Map<String, FieldSetMapper<T>> fieldSetMappers) {
this.fieldSetMappers = new LinkedHashMap<String, FieldSetMapper<T>>(fieldSetMappers);
}

View File

@@ -19,6 +19,7 @@ package org.springframework.batch.item.file.transform;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.batch.support.PatternMatcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
@@ -26,11 +27,13 @@ import org.springframework.util.Assert;
* A {@link LineTokenizer} implementation that stores a mapping of String
* prefixes to delegate {@link LineTokenizer}s. Each line tokenizied will be
* checked for its prefix. If the prefix matches a key in the map of delegates,
* then the corresponding delegate {@link LineTokenizer} will be used.
* Otherwise, the default {@link LineTokenizer} will be used. The default
* {@link LineTokenizer} can be configured in the delegate map by setting its
* corresponding prefix to the empty string.
* then the corresponding delegate {@link LineTokenizer} will be used. Prefixes
* are sorted starting with the most specific, and the first match always
* succeeds.
*
* @see PatternMatcher#matchPrefix(String, Map)
*
* @author Ben Hale
* @author Dan Garrette
*/
public class PrefixMatchingCompositeLineTokenizer implements LineTokenizer, InitializingBean {
@@ -43,45 +46,7 @@ public class PrefixMatchingCompositeLineTokenizer implements LineTokenizer, Init
* @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 (key != null) {
if ("".equals(key)) {
defaultDelegate = delegates.get(key);
}
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;
return PatternMatcher.matchPrefix(line, this.tokenizers).tokenize(line);
}
/*

View File

@@ -15,23 +15,41 @@
*/
package org.springframework.batch.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
* @author Dave Syer
*
* @author Dan Garrette
*/
public class PatternMatcher {
/**
* Used for reverse-sorting strings
*/
private static Comparator<String> STRING_REVERSE_COMPARATOR = new Comparator<String>() {
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
};
/**
* Lifted from AntPathMatcher in Spring Core. Tests whether or not a string
* matches against a pattern. The pattern may contain two special
* characters:<br>
* '*' means zero or more characters<br>
* '?' means one and only one character
* @param pattern pattern to match against. Must not be <code>null</code>.
* @param str string which must be matched against the pattern. Must not be
* <code>null</code>.
*
* @param pattern
* pattern to match against. Must not be <code>null</code>.
* @param str
* string which must be matched against the pattern. Must not be
* <code>null</code>.
* @return <code>true</code> if the string matches against the pattern, or
* <code>false</code> otherwise.
* <code>false</code> otherwise.
*/
public static boolean match(String pattern, String str) {
char[] patArr = pattern.toCharArray();
@@ -159,4 +177,48 @@ public class PatternMatcher {
return true;
}
/**
* <p>
* This method takes a String line and a map from String prefixes to values
* of any type. During processing, the method will identify the most
* specific prefix in the map that matches the beginning of the line. Once
* the correct is identified, its value is returned. Note that if the map
* contains the empty string as a prefix, then it will serve as the
* "default" case, matching every non-null line given.
*
* <p>
* If no matching prefix is found, a {@link IllegalStateException} will be
* thrown.
*
* <p>
* Null keys are not allowed in the map.
*
* @param line
* An input string
* @param map
* A map with String prefixes as keys.
* @return the value whose prefix matches the given line
*/
public static <S> S matchPrefix(String line, Map<String, S> map) {
S value = null;
if (line == null) {
throw new IllegalStateException("Could not handle a null line");
}
// Sort keys to start with the most specific
List<String> sorted = new ArrayList<String>(map.keySet());
Collections.sort(sorted, STRING_REVERSE_COMPARATOR);
for (String key : sorted) {
if (line.startsWith(key)) {
value = map.get(key);
break;
}
}
if (value == null) {
throw new IllegalStateException("Could not find a matching prefix for line=[" + line + "]");
}
return value;
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.batch.item.file.transform.Name;
/**
* @author Dan Garrette
* @since 2.0
*/
public class PrefixMatchingCompositeLineMapperTests {
@@ -44,38 +45,6 @@ public class PrefixMatchingCompositeLineMapperTests {
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>();

View File

@@ -26,6 +26,7 @@ import java.util.Map;
import org.junit.Test;
/**
* @author Ben Hale
* @author Dan Garrette
*/
public class PrefixMatchingCompositeLineTokenizerTests {
@@ -38,54 +39,6 @@ public class PrefixMatchingCompositeLineTokenizerTests {
tokenizer.tokenize("a line");
}
@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 LineTokenizer() {
public FieldSet tokenize(String line) {
return new DefaultFieldSet(new String[] { "a" });
}
});
map.put("foo", new LineTokenizer() {
public FieldSet tokenize(String line) {
return new DefaultFieldSet(new String[] { "b" });
}
});
tokenizer.setTokenizers(map);
tokenizer.afterPropertiesSet();
FieldSet fields = tokenizer.tokenize(null);
assertEquals(1, fields.getFieldCount());
assertEquals("a", fields.readString(0));
}
@Test
public void testNullKey() throws Exception {
Map<String, LineTokenizer> map = new HashMap<String, LineTokenizer>();
map.put(null, new LineTokenizer() {
public FieldSet tokenize(String line) {
return new DefaultFieldSet(new String[] { "a" });
}
});
map.put("foo", new LineTokenizer() {
public FieldSet tokenize(String line) {
return new DefaultFieldSet(new String[] { "b" });
}
});
tokenizer.setTokenizers(map);
tokenizer.afterPropertiesSet();
FieldSet fields = tokenizer.tokenize("foo");
assertEquals(1, fields.getFieldCount());
assertEquals("b", fields.readString(0));
}
@Test
public void testEmptyKeyMatchesAnyLine() throws Exception {
Map<String, LineTokenizer> map = new HashMap<String, LineTokenizer>();

View File

@@ -0,0 +1,112 @@
/*
* 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.support;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
/**
* @author Dan Garrette
* @since 2.0
*/
public class PatternMatcherTests {
private static Map<String, Integer> map = new HashMap<String, Integer>();
static {
map.put("an", 3);
map.put("a", 2);
map.put("big", 4);
}
private static Map<String, Integer> defaultMap = new HashMap<String, Integer>();
static {
defaultMap.put("an", 3);
defaultMap.put("a", 2);
defaultMap.put("big", 4);
defaultMap.put("", 1);
}
@Test
public void testMatch_noWildcard_yes() {
assertTrue(PatternMatcher.match("abc", "abc"));
}
@Test
public void testMatch_noWildcard_no() {
assertFalse(PatternMatcher.match("abc", "ab"));
}
@Test
public void testMatch_qMark_yes() {
assertTrue(PatternMatcher.match("a?c", "abc"));
}
@Test
public void testMatch_qMark_no() {
assertFalse(PatternMatcher.match("a?c", "ab"));
}
@Test
public void testMatch_star_yes() {
assertTrue(PatternMatcher.match("a*c", "abdegc"));
}
@Test
public void testMatch_star_no() {
assertFalse(PatternMatcher.match("a*c", "abdeg"));
}
@Test
public void testMatchPrefix_subsumed() {
assertEquals(2, PatternMatcher.matchPrefix("apple", map).intValue());
}
@Test
public void testMatchPrefix_subsuming() {
assertEquals(3, PatternMatcher.matchPrefix("animal", map).intValue());
}
@Test
public void testMatchPrefix_unrelated() {
assertEquals(4, PatternMatcher.matchPrefix("biggest", map).intValue());
}
@Test(expected = IllegalStateException.class)
public void testMatchPrefix_noMatch() {
PatternMatcher.matchPrefix("bat", map);
}
@Test
public void testMatchPrefix_defaultValue_unrelated() {
assertEquals(4, PatternMatcher.matchPrefix("biggest", defaultMap).intValue());
}
@Test
public void testMatchPrefix_defaultValue_emptyString() {
assertEquals(1, PatternMatcher.matchPrefix("", defaultMap).intValue());
}
@Test
public void testMatchPrefix_defaultValue_noMatch() {
assertEquals(1, PatternMatcher.matchPrefix("bat", defaultMap).intValue());
}
}