diff --git a/debezium-connector-mysql/src/main/java/io/debezium/connector/mysql/MySqlTaskContext.java b/debezium-connector-mysql/src/main/java/io/debezium/connector/mysql/MySqlTaskContext.java index 4f4c4b7dc..9ea86f56d 100644 --- a/debezium-connector-mysql/src/main/java/io/debezium/connector/mysql/MySqlTaskContext.java +++ b/debezium-connector-mysql/src/main/java/io/debezium/connector/mysql/MySqlTaskContext.java @@ -47,8 +47,8 @@ public MySqlTaskContext(Configuration config) { // Set up the GTID filter ... String gtidSetIncludes = config.getString(MySqlConnectorConfig.GTID_SOURCE_INCLUDES); String gtidSetExcludes = config.getString(MySqlConnectorConfig.GTID_SOURCE_EXCLUDES); - this.gtidSourceFilter = gtidSetIncludes != null ? Predicates.includes(gtidSetIncludes) - : (gtidSetExcludes != null ? Predicates.excludes(gtidSetExcludes) : null); + this.gtidSourceFilter = gtidSetIncludes != null ? Predicates.includesUuids(gtidSetIncludes) + : (gtidSetExcludes != null ? Predicates.excludesUuids(gtidSetExcludes) : null); // Set up the MySQL schema ... this.dbSchema = new MySqlSchema(config, serverName(), this.gtidSourceFilter); diff --git a/debezium-core/src/main/java/io/debezium/function/Predicates.java b/debezium-core/src/main/java/io/debezium/function/Predicates.java index f58ad76fc..a90300f63 100644 --- a/debezium-core/src/main/java/io/debezium/function/Predicates.java +++ b/debezium-core/src/main/java/io/debezium/function/Predicates.java @@ -5,6 +5,11 @@ */ package io.debezium.function; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -15,11 +20,142 @@ /** * Utilities for constructing various predicates. + * * @author Randall Hauch * */ public class Predicates { + /** + * Generate a predicate function that for any supplied UUID strings returns {@code true} if any of the comma-separated + * UUID literals or regular expressions matches the predicate parameter. This supplied strings can be a mixture + * of regular expressions and UUID literals, and the most efficient method will be used for each. + * + * @param uuidPatterns the comma-separated UUID literals or regular expression patterns; may not be null + * @return the predicate function that performs the matching + * @throws PatternSyntaxException if the string includes an invalid regular expression + */ + public static Predicate includesUuids(String uuidPatterns) { + return includesLiteralsOrPatterns(uuidPatterns, Strings::isUuid, (s) -> s); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if none of the regular + * expressions or literals in the supplied comma-separated list matches the predicate parameter. This supplied strings can be + * a mixture of regular expressions and UUID literals, and the most efficient method will be used for each. + * + * @param uuidPatterns the comma-separated regular expression pattern (or literal) strings; may not be null + * @return the predicate function that performs the matching + * @throws PatternSyntaxException if the string includes an invalid regular expression + */ + public static Predicate excludesUuids(String uuidPatterns) { + return includesUuids(uuidPatterns).negate(); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if any of the regular expressions + * or literals in the supplied comma-separated list matches the predicate parameter. This supplied strings can be a mixture + * of regular expressions and literals, and the most efficient method will be used for each. + * + * @param literalsOrPatterns the comma-separated regular expression pattern (or literal) strings; may not be null + * @param isLiteral function that determines if a given pattern is a literal string; may not be null + * @param conversion the function that converts each predicate-supplied value to a string that can be matched against the + * regular expressions; may not be null + * @return the predicate function that performs the matching + * @throws PatternSyntaxException if the string includes an invalid regular expression + */ + public static Predicate includesLiteralsOrPatterns(String literalsOrPatterns, Predicate isLiteral, + Function conversion) { + // First create the predicates that handle either literals or patterns ... + Set literals = new HashSet<>(); + List patterns = new ArrayList<>(); + for (String literalOrPattern : literalsOrPatterns.split(",")) { + if (isLiteral.test(literalOrPattern)) { + literals.add(literalOrPattern.toLowerCase()); + } else { + patterns.add(Pattern.compile(literalOrPattern, Pattern.CASE_INSENSITIVE)); + } + } + Predicate patternsPredicate = includedInPatterns(patterns, conversion); + Predicate literalsPredicate = includedInLiterals(literals, conversion); + + // Now figure out which predicate(s) we need to use ... + if (patterns.isEmpty()) { + return literalsPredicate; + } + if (literals.isEmpty()) { + return patternsPredicate; + } + return literalsPredicate.or(patternsPredicate); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if none of the regular + * expressions or literals in the supplied comma-separated list matches the predicate parameter. This supplied strings can be + * a mixture of regular expressions and literals, and the most efficient method will be used for each. + * + * @param patterns the comma-separated regular expression pattern (or literal) strings; may not be null + * @param isLiteral function that determines if a given pattern is a literal string; may not be null + * @param conversion the function that converts each predicate-supplied value to a string that can be matched against the + * regular expressions; may not be null + * @return the predicate function that performs the matching + * @throws PatternSyntaxException if the string includes an invalid regular expression + */ + public static Predicate excludesLiteralsOrPatterns(String patterns, Predicate isLiteral, + Function conversion) { + return includesLiteralsOrPatterns(patterns, isLiteral, conversion).negate(); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if any of the literals in + * the supplied comma-separated list case insensitively matches the predicate parameter. + * + * @param literals the comma-separated literal strings; may not be null + * @return the predicate function that performs the matching + */ + public static Predicate includesLiterals(String literals) { + return includesLiterals(literals, (s) -> s); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if none of the literals in + * the supplied comma-separated list case insensitively matches the predicate parameter. + * + * @param literals the comma-separated literal strings; may not be null + * @return the predicate function that performs the matching + */ + public static Predicate excludesLiterals(String literals) { + return includesLiterals(literals).negate(); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if any of the literals in + * the supplied comma-separated list case insensitively matches the predicate parameter. + * + * @param literals the comma-separated literal strings; may not be null + * @param conversion the function that converts each predicate-supplied value to a string that can be matched against the + * regular expressions; may not be null + * @return the predicate function that performs the matching + */ + public static Predicate includesLiterals(String literals, Function conversion) { + String[] literalValues = literals.toLowerCase().split(","); + Set literalSet = new HashSet<>(Arrays.asList(literalValues)); + return includedInLiterals(literalSet, conversion); + } + + /** + * Generate a predicate function that for any supplied string returns {@code true} if none of the literals in + * the supplied comma-separated list case insensitively matches the predicate parameter. + * + * @param literals the comma-separated literal strings; may not be null + * @param conversion the function that converts each predicate-supplied value to a string that can be matched against the + * regular expressions; may not be null + * @return the predicate function that performs the matching + */ + public static Predicate excludesLiterals(String literals, Function conversion) { + return includesLiterals(literals, conversion).negate(); + } + /** * Generate a predicate function that for any supplied string returns {@code true} if any of the regular expressions in * the supplied comma-separated list matches the predicate parameter. @@ -55,17 +191,28 @@ public static Predicate excludes(String regexPatterns) { * @throws PatternSyntaxException if the string includes an invalid regular expression */ public static Predicate includes(String regexPatterns, Function conversion) { - Set patterns = Strings.listOfRegex(regexPatterns,Pattern.CASE_INSENSITIVE); + Set patterns = Strings.listOfRegex(regexPatterns, Pattern.CASE_INSENSITIVE); + return includedInPatterns(patterns, conversion); + } + + protected static Predicate includedInPatterns(Collection patterns, Function conversion) { return (t) -> { String str = conversion.apply(t); - if ( str != null ) { - for ( Pattern p : patterns ) { - if ( p.matcher(str).matches()) return true; + if (str != null) { + for (Pattern p : patterns) { + if (p.matcher(str).matches()) return true; } } return false; }; } + + protected static Predicate includedInLiterals(Collection literals, Function conversion) { + return (s) -> { + String str = conversion.apply(s).toLowerCase(); + return literals.contains(str); + }; + } /** * Generate a predicate function that for any supplied parameter returns {@code true} if none of the regular diff --git a/debezium-core/src/main/java/io/debezium/util/Strings.java b/debezium-core/src/main/java/io/debezium/util/Strings.java index 35289b7e6..b215ceb25 100644 --- a/debezium-core/src/main/java/io/debezium/util/Strings.java +++ b/debezium-core/src/main/java/io/debezium/util/Strings.java @@ -19,6 +19,7 @@ import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; +import java.util.UUID; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -782,6 +783,21 @@ private static List split(String str, return l; } + /** + * Determine if the supplied string is a valid {@link UUID}. + * @param str the string to evaluate + * @return {@code true} if the string is a valid representation of a UUID, or {@code false} otherwise + */ + public static boolean isUuid(String str) { + if (str == null) return false; + try { + UUID.fromString(str); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + private Strings() { } } diff --git a/debezium-core/src/test/java/io/debezium/function/PredicatesTest.java b/debezium-core/src/test/java/io/debezium/function/PredicatesTest.java index 6f7df31e9..673ce9dbd 100644 --- a/debezium-core/src/test/java/io/debezium/function/PredicatesTest.java +++ b/debezium-core/src/test/java/io/debezium/function/PredicatesTest.java @@ -5,6 +5,7 @@ */ package io.debezium.function; +import java.util.UUID; import java.util.function.Predicate; import org.junit.Test; @@ -55,4 +56,37 @@ public void shouldMatchCommaSeparatedLiteralExcludes() { assertThat(p.test(-1)).isTrue(); } + @Test + public void shouldMatchCommaSeparatedUuidLiterals() { + String uuid1 = UUID.randomUUID().toString(); + String uuid2 = UUID.randomUUID().toString(); + String uuid3 = UUID.randomUUID().toString(); + String uuid4 = UUID.randomUUID().toString(); + String uuid4Prefix = uuid4.substring(0,10) + ".*"; + Predicate p = Predicates.includesUuids(uuid1 + "," + uuid2); + assertThat(p.test(uuid1)).isTrue(); + assertThat(p.test(uuid2)).isTrue(); + assertThat(p.test(uuid3)).isFalse(); + assertThat(p.test(uuid4)).isFalse(); + + p = Predicates.excludesUuids(uuid1 + "," + uuid2); + assertThat(p.test(uuid1)).isFalse(); + assertThat(p.test(uuid2)).isFalse(); + assertThat(p.test(uuid3)).isTrue(); + assertThat(p.test(uuid4)).isTrue(); + + p = Predicates.includesUuids(uuid1 + "," + uuid2 + "," + uuid4Prefix); + assertThat(p.test(uuid1)).isTrue(); + assertThat(p.test(uuid2)).isTrue(); + assertThat(p.test(uuid3)).isFalse(); + assertThat(p.test(uuid4)).isTrue(); + + p = Predicates.excludesUuids(uuid1 + "," + uuid2 + "," + uuid4Prefix); + assertThat(p.test(uuid1)).isFalse(); + assertThat(p.test(uuid2)).isFalse(); + assertThat(p.test(uuid3)).isTrue(); + assertThat(p.test(uuid4)).isFalse(); + + } + }