From 02151edd2bd91ba90ef38b6bef7b304e935ce75f Mon Sep 17 00:00:00 2001 From: dhgarrette Date: Wed, 28 Jan 2009 19:09:00 +0000 Subject: [PATCH] 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. --- .../PrefixMatchingCompositeLineMapper.java | 26 ++-- .../PrefixMatchingCompositeLineTokenizer.java | 51 ++------ .../batch/support/PatternMatcher.java | 72 ++++++++++- ...refixMatchingCompositeLineMapperTests.java | 33 +----- ...ixMatchingCompositeLineTokenizerTests.java | 49 +------- .../batch/support/PatternMatcherTests.java | 112 ++++++++++++++++++ 6 files changed, 207 insertions(+), 136 deletions(-) create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/support/PatternMatcherTests.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 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()); + } +}