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 f5f5434401..ff02568912 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 @@ -1,5 +1,5 @@ /* - * Copyright 2006-2021 the original author or authors. + * Copyright 2006-2023 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. @@ -19,6 +19,7 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.springframework.util.Assert; @@ -26,6 +27,7 @@ * @author Dave Syer * @author Dan Garrette * @author Marten Deinum + * @author Injae Kim */ public class PatternMatcher { @@ -180,6 +182,19 @@ public static boolean match(String pattern, String str) { return true; } + /** + * Tests whether or not a string matches against a regular expression. + * @param regex regular expression to match against. Must not be {@code null}. + * @param str string which must be matched against the regular expression. Must not be {@code null}. + * @return {@code true} if the string matches against the regular expression, or {@code false} otherwise. + */ + public static boolean matchRegex(String regex, String str) { + Assert.notNull(regex, "Regex must not be null"); + Assert.notNull(str, "Str must not be null"); + + return Pattern.matches(regex, str); + } + /** *

* This method takes a String key and a map from Strings to values of any type. During @@ -202,12 +217,23 @@ public S match(String line) { Assert.notNull(line, "A non-null key must be provided to match against."); for (String key : sorted) { - if (PatternMatcher.match(key, line)) { + if (match(key, line)) { value = map.get(key); break; } } + if (value == null) { + for (String key : sorted) { + try { + if (matchRegex(key, line)) { + value = map.get(key); + break; + } + } catch (Throwable ignored) {} + } + } + if (value == null) { throw new IllegalStateException("Could not find a matching pattern for key=[" + line + "]"); } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PatternMatchingCompositeLineMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PatternMatchingCompositeLineMapperTests.java index 734171b4c8..8fa1750dca 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PatternMatchingCompositeLineMapperTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/PatternMatchingCompositeLineMapperTests.java @@ -33,6 +33,7 @@ * @author Dan Garrette * @author Dave Syer * @author Mahmoud Ben Hassine + * @author Injae Kim * @since 2.0 */ class PatternMatchingCompositeLineMapperTests { @@ -51,6 +52,7 @@ void testKeyFound() throws Exception { Map tokenizers = new HashMap<>(); tokenizers.put("foo*", line -> new DefaultFieldSet(new String[] { "a", "b" })); tokenizers.put("bar*", line -> new DefaultFieldSet(new String[] { "c", "d" })); + tokenizers.put("regex.*", line -> new DefaultFieldSet(new String[] { "e", "f" })); mapper.setTokenizers(tokenizers); Map> fieldSetMappers = new HashMap<>(); @@ -62,15 +64,35 @@ void testKeyFound() throws Exception { assertEquals(new Name("d", "c", 0), name); } + @Test + void testKeyFoundByRegex() throws Exception { + Map tokenizers = new HashMap<>(); + tokenizers.put("foo*", line -> new DefaultFieldSet(new String[] { "a", "b" })); + tokenizers.put("bar*", line -> new DefaultFieldSet(new String[] { "c", "d" })); + tokenizers.put("regex.*", line -> new DefaultFieldSet(new String[] { "e", "f" })); + mapper.setTokenizers(tokenizers); + + Map> fieldSetMappers = new HashMap<>(); + fieldSetMappers.put("foo*", fs -> new Name(fs.readString(0), fs.readString(1), 0)); + fieldSetMappers.put("bar*", fs -> new Name(fs.readString(1), fs.readString(0), 0)); + fieldSetMappers.put("regex.*", fs -> new Name(fs.readString(0), fs.readString(1), 0)); + mapper.setFieldSetMappers(fieldSetMappers); + + Name name = mapper.mapLine("regex-ABC_12345", 1); + assertEquals(new Name("e", "f", 0), name); + } + @Test void testMapperKeyNotFound() { Map tokenizers = new HashMap<>(); tokenizers.put("foo*", line -> new DefaultFieldSet(new String[] { "a", "b" })); tokenizers.put("bar*", line -> new DefaultFieldSet(new String[] { "c", "d" })); + tokenizers.put("regex.*", line -> new DefaultFieldSet(new String[] { "e", "f" })); mapper.setTokenizers(tokenizers); Map> fieldSetMappers = new HashMap<>(); fieldSetMappers.put("foo*", fs -> new Name(fs.readString(0), fs.readString(1), 0)); + fieldSetMappers.put("regex.*", fs -> new Name(fs.readString(0), fs.readString(1), 0)); mapper.setFieldSetMappers(fieldSetMappers); assertThrows(IllegalStateException.class, () -> mapper.mapLine("bar", 1)); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PatternMatchingCompositeLineTokenizerTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PatternMatchingCompositeLineTokenizerTests.java index 6002782b00..06cc59f519 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PatternMatchingCompositeLineTokenizerTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/PatternMatchingCompositeLineTokenizerTests.java @@ -30,6 +30,7 @@ * @author Ben Hale * @author Dan Garrette * @author Dave Syer + * @author Injae Kim */ class PatternMatchingCompositeLineTokenizerTests { @@ -45,6 +46,7 @@ void testEmptyKeyMatchesAnyLine() throws Exception { Map map = new HashMap<>(); map.put("*", new DelimitedLineTokenizer()); map.put("foo", line -> null); + map.put("regex.*", line -> null); tokenizer.setTokenizers(map); tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize("abc"); @@ -53,16 +55,29 @@ void testEmptyKeyMatchesAnyLine() throws Exception { @Test void testEmptyKeyDoesNotMatchWhenAlternativeAvailable() throws Exception { - Map map = new LinkedHashMap<>(); map.put("*", line -> null); map.put("foo*", new DelimitedLineTokenizer()); + map.put("regex.*", line -> null); tokenizer.setTokenizers(map); tokenizer.afterPropertiesSet(); FieldSet fields = tokenizer.tokenize("foo,bar"); assertEquals("bar", fields.readString(1)); } + @Test + void testMatchRegex() throws Exception { + Map map = new HashMap<>(); + map.put("foo", line -> null); + map.put("regex.*", new DelimitedLineTokenizer()); + tokenizer.setTokenizers(map); + tokenizer.afterPropertiesSet(); + FieldSet fields = tokenizer.tokenize("regex-ABC_12345,REGEX"); + assertEquals(2, fields.getFieldCount()); + assertEquals("regex-ABC_12345", fields.readString(0)); + assertEquals("REGEX", fields.readString(1)); + } + @Test void testNoMatch() throws Exception { tokenizer.setTokenizers(Collections.singletonMap("foo", (LineTokenizer) new DelimitedLineTokenizer())); @@ -70,6 +85,13 @@ void testNoMatch() throws Exception { assertThrows(IllegalStateException.class, () -> tokenizer.tokenize("nomatch")); } + @Test + void testNoMatchRegex() throws Exception { + tokenizer.setTokenizers(Collections.singletonMap("foo.*", (LineTokenizer) new DelimitedLineTokenizer())); + tokenizer.afterPropertiesSet(); + assertThrows(IllegalStateException.class, () -> tokenizer.tokenize("nomatch")); + } + @Test void testMatchWithPrefix() throws Exception { tokenizer.setTokenizers( 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 index 2a55ad3906..3dab717e27 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2006-2022 the original author or authors. + * Copyright 2006-2023 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. @@ -22,11 +22,13 @@ import java.util.HashMap; import java.util.Map; +import java.util.regex.PatternSyntaxException; import org.junit.jupiter.api.Test; /** * @author Dan Garrette + * @author Injae Kim * @since 2.0 */ class PatternMatcherTests { @@ -37,6 +39,7 @@ class PatternMatcherTests { map.put("an*", 3); map.put("a*", 2); map.put("big*", 4); + map.put("bcd.*", 5); } private static final Map defaultMap = new HashMap<>(); @@ -49,6 +52,15 @@ class PatternMatcherTests { defaultMap.put("*", 1); } + private static final Map regexMap = new HashMap<>(); + + static { + regexMap.put("abc.*", 1); + regexMap.put("a...e", 2); + regexMap.put("123.[0-9][0-9]\\d", 3); + regexMap.put("*............", 100); // invalid regex format + } + @Test void testMatchNoWildcardYes() { assertTrue(PatternMatcher.match("abc", "abc")); @@ -104,6 +116,29 @@ void testMatchStarNo() { assertFalse(PatternMatcher.match("a*c", "abdeg")); } + @Test + void testMatchRegex() { + assertTrue(PatternMatcher.matchRegex("abc.*", "abcde")); + } + + @Test + void testMatchRegex_notMatched() { + assertFalse(PatternMatcher.matchRegex("abc.*", "cdefg")); + assertFalse(PatternMatcher.matchRegex("abc.", "abcde")); + } + + @Test + void testMatchRegex_thrown_invalidRegexFormat() { + assertThrows(PatternSyntaxException.class, () -> PatternMatcher.matchRegex("*..", "abc")); + } + + @Test + void testMatchRegex_thrown_notNullParam() { + assertThrows(IllegalArgumentException.class, () -> PatternMatcher.matchRegex("regex", null)); + assertThrows(IllegalArgumentException.class, () -> PatternMatcher.matchRegex(null, "str")); + } + + @Test void testMatchPrefixSubsumed() { assertEquals(2, new PatternMatcher<>(map).match("apple").intValue()); @@ -119,6 +154,11 @@ void testMatchPrefixUnrelated() { assertEquals(4, new PatternMatcher<>(map).match("biggest").intValue()); } + @Test + void testMatchByRegex() { + assertEquals(5, new PatternMatcher<>(map).match("bcdef12345").intValue()); + } + @Test void testMatchPrefixNoMatch() { PatternMatcher matcher = new PatternMatcher<>(map); @@ -140,4 +180,24 @@ void testMatchPrefixDefaultValueNoMatch() { assertEquals(1, new PatternMatcher<>(defaultMap).match("bat").intValue()); } + @Test + void testMatchRegexPrefix() { + assertEquals(1, new PatternMatcher<>(regexMap).match("abcdefg").intValue()); + } + + @Test + void testMatchRegexWildCards() { + assertEquals(2, new PatternMatcher<>(regexMap).match("a12De").intValue()); + } + + @Test + void testMatchRegexDigits() { + assertEquals(3, new PatternMatcher<>(regexMap).match("123-789").intValue()); + } + + @Test + void testMatchRegexNotMatched() { + assertThrows(IllegalStateException.class, () -> new PatternMatcher<>(regexMap).match("Hello world!")); + } + }