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
index b40acc67c..55fd0f7d7 100644
--- 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
@@ -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;
/**
*
* 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.
*
*
* 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 extends PrefixMatchingCompositeLineTokenizer implements LineMapper {
+public class PrefixMatchingCompositeLineMapper implements LineMapper, InitializingBean {
+ private PrefixMatchingCompositeLineTokenizer tokenizer = new PrefixMatchingCompositeLineTokenizer();
private Map> fieldSetMappers = null;
/*
@@ -50,7 +56,7 @@ public class PrefixMatchingCompositeLineMapper 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 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 tokenizers) {
+ this.tokenizer.setTokenizers(tokenizers);
+ }
+
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 597542250..8642c64cf 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
@@ -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 matchPrefix(String line, Map 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);
}
/*
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/PatternMatcher.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/PatternMatcher.java
index 6aaa3573e..36e5f7602 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/PatternMatcher.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/support/PatternMatcher.java
@@ -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_REVERSE_COMPARATOR = new Comparator() {
+ 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:
* '*' means zero or more characters
* '?' means one and only one character
- * @param pattern pattern to match against. Must not be null.
- * @param str string which must be matched against the pattern. Must not be
- * null.
+ *
+ * @param pattern
+ * pattern to match against. Must not be null.
+ * @param str
+ * string which must be matched against the pattern. Must not be
+ * null.
* @return true if the string matches against the pattern, or
- * false otherwise.
+ * false otherwise.
*/
public static boolean match(String pattern, String str) {
char[] patArr = pattern.toCharArray();
@@ -159,4 +177,48 @@ public class PatternMatcher {
return true;
}
+ /**
+ *
+ * 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.
+ *
+ *
+ * If no matching prefix is found, a {@link IllegalStateException} will be
+ * thrown.
+ *
+ *
+ * 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 matchPrefix(String line, Map 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 sorted = new ArrayList(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;
+ }
}
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
index e6882ab8d..d80bee284 100644
--- 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
@@ -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 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();
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 2536ccc61..a3388b333 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
@@ -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 map = new HashMap();
- 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 map = new HashMap();
- 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 map = new HashMap();
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/PatternMatcherTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/PatternMatcherTests.java
new file mode 100644
index 000000000..deab3a72a
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/PatternMatcherTests.java
@@ -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 map = new HashMap();
+ static {
+ map.put("an", 3);
+ map.put("a", 2);
+ map.put("big", 4);
+ }
+
+ private static Map defaultMap = new HashMap();
+ 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());
+ }
+}