From 88dca4ac27158c0a3e0efb3016f836f9b02c0978 Mon Sep 17 00:00:00 2001 From: Martin Norbury Date: Wed, 9 Aug 2023 11:34:20 +0100 Subject: [PATCH] chore: refactor to use same `glob` as Go Implement the `Glob` class in Java to be the same a Go to make sure we have parity between Java and Go with the globbing behaviour. --- .../main/java/io/cloudquery/glob/Glob.java | 42 ++++++++ lib/src/main/java/io/cloudquery/glob/LICENSE | 21 ++++ .../main/java/io/cloudquery/glob/README.md | 3 + .../io/cloudquery/helper/GlobMatcher.java | 23 ---- .../main/java/io/cloudquery/schema/Table.java | 25 ++--- .../java/io/cloudquery/glob/GlobTest.java | 100 ++++++++++++++++++ .../io/cloudquery/helper/GlobMatcherTest.java | 41 ------- 7 files changed, 177 insertions(+), 78 deletions(-) create mode 100644 lib/src/main/java/io/cloudquery/glob/Glob.java create mode 100644 lib/src/main/java/io/cloudquery/glob/LICENSE create mode 100644 lib/src/main/java/io/cloudquery/glob/README.md delete mode 100644 lib/src/main/java/io/cloudquery/helper/GlobMatcher.java create mode 100644 lib/src/test/java/io/cloudquery/glob/GlobTest.java delete mode 100644 lib/src/test/java/io/cloudquery/helper/GlobMatcherTest.java diff --git a/lib/src/main/java/io/cloudquery/glob/Glob.java b/lib/src/main/java/io/cloudquery/glob/Glob.java new file mode 100644 index 0000000..14eb355 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/glob/Glob.java @@ -0,0 +1,42 @@ +package io.cloudquery.glob; + +public class Glob { + public static final String GLOB = "*"; + + public static boolean match(String pattern, String subject) { + if (pattern.isEmpty()) { + return subject.equals(pattern); + } + + if (pattern.equals(GLOB)) { + return true; + } + + String[] parts = pattern.split("\\" + GLOB, -1); + if (parts.length == 1) { + return subject.equals(pattern); + } + + boolean leadingGlob = pattern.startsWith(GLOB); + boolean trailingGlob = pattern.endsWith(GLOB); + int end = parts.length - 1; + + for (int i = 0; i < end; i++) { + int idx = subject.indexOf(parts[i]); + + if (i == 0) { + if (!leadingGlob && idx != 0) { + return false; + } + } else { + if (idx < 0) { + return false; + } + } + + subject = subject.substring(idx + parts[i].length()); + } + + return trailingGlob || subject.endsWith(parts[end]); + } +} diff --git a/lib/src/main/java/io/cloudquery/glob/LICENSE b/lib/src/main/java/io/cloudquery/glob/LICENSE new file mode 100644 index 0000000..a9f1724 --- /dev/null +++ b/lib/src/main/java/io/cloudquery/glob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ryan Uber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/lib/src/main/java/io/cloudquery/glob/README.md b/lib/src/main/java/io/cloudquery/glob/README.md new file mode 100644 index 0000000..380f61d --- /dev/null +++ b/lib/src/main/java/io/cloudquery/glob/README.md @@ -0,0 +1,3 @@ +# Glob Matching Library + +This glob-matching library was copied from [ryanuber/go-glob](https://github.com/ryanuber/go-glob) and therefore falls under its [license](LICENSE). diff --git a/lib/src/main/java/io/cloudquery/helper/GlobMatcher.java b/lib/src/main/java/io/cloudquery/helper/GlobMatcher.java deleted file mode 100644 index 36ecad4..0000000 --- a/lib/src/main/java/io/cloudquery/helper/GlobMatcher.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.cloudquery.helper; - -import lombok.Getter; - -import java.nio.file.FileSystems; -import java.nio.file.Path; -import java.nio.file.PathMatcher; - -public class GlobMatcher { - private final PathMatcher pathMatcher; - - @Getter - private final String stringMatch; - - public GlobMatcher(String stringMatch) { - this.stringMatch = stringMatch; - this.pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + stringMatch); - } - - public boolean matches(String name) { - return pathMatcher.matches(Path.of(name)); - } -} diff --git a/lib/src/main/java/io/cloudquery/schema/Table.java b/lib/src/main/java/io/cloudquery/schema/Table.java index d2c6fac..df1b83a 100644 --- a/lib/src/main/java/io/cloudquery/schema/Table.java +++ b/lib/src/main/java/io/cloudquery/schema/Table.java @@ -1,6 +1,6 @@ package io.cloudquery.schema; -import io.cloudquery.helper.GlobMatcher; +import io.cloudquery.glob.Glob; import lombok.Builder; import lombok.Getter; @@ -28,38 +28,35 @@ public static List flattenTables(List
tables) { } public static List
filterDFS(List
tables, List includeConfiguration, List skipConfiguration, boolean skipDependentTables) throws SchemaException { - List includes = includeConfiguration.stream().map(GlobMatcher::new).toList(); - List excludes = skipConfiguration.stream().map(GlobMatcher::new).toList(); - List
flattenedTables = flattenTables(tables); - for (GlobMatcher includeMatcher : includes) { + for (String includePattern : includeConfiguration) { boolean includeMatch = false; for (Table table : flattenedTables) { - if (includeMatcher.matches(table.getName())) { + if (Glob.match(includePattern, table.getName())) { includeMatch = true; break; } } if (!includeMatch) { - throw new SchemaException("table configuration includes a pattern \"" + includeMatcher.getStringMatch() + "\" with no matches"); + throw new SchemaException("table configuration includes a pattern \"" + includePattern + "\" with no matches"); } } - for (GlobMatcher excludeMatcher : excludes) { + for (String excludePattern : skipConfiguration) { boolean excludeMatch = false; for (Table table : flattenedTables) { - if (excludeMatcher.matches(table.getName())) { + if (Glob.match(excludePattern, table.getName())) { excludeMatch = true; break; } } if (!excludeMatch) { - throw new SchemaException("skip configuration includes a pattern \"" + excludeMatcher.getStringMatch() + "\" with no matches"); + throw new SchemaException("skip configuration includes a pattern \"" + excludePattern + "\" with no matches"); } } Predicate
include = table -> { - for (GlobMatcher matcher : includes) { - if (matcher.matches(table.getName())) { + for (String includePattern : includeConfiguration) { + if (Glob.match(includePattern, table.getName())) { return true; } } @@ -67,8 +64,8 @@ public static List
filterDFS(List
tables, List includeConf }; Predicate
exclude = table -> { - for (GlobMatcher matcher : excludes) { - if (matcher.matches(table.getName())) { + for (String excludePattern : skipConfiguration) { + if (Glob.match(excludePattern, table.getName())) { return true; } } diff --git a/lib/src/test/java/io/cloudquery/glob/GlobTest.java b/lib/src/test/java/io/cloudquery/glob/GlobTest.java new file mode 100644 index 0000000..51e6847 --- /dev/null +++ b/lib/src/test/java/io/cloudquery/glob/GlobTest.java @@ -0,0 +1,100 @@ +package io.cloudquery.glob; + +import org.junit.Test; + +import java.util.List; + +import static io.cloudquery.glob.Glob.GLOB; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class GlobTest { + @Test + public void testEmptyPattern() { + assertGlobMatch("", ""); + assertNotGlobMatch("", "test"); + } + + @Test + public void testEmptySubject() { + for (String s : List.of("", + "*", + "**", + "***", + "****************", + GLOB.repeat(1000000) + )) { + assertGlobMatch(s, ""); + } + + for (String pattern : List.of( + // No globs/non-glob characters + "test", + "*test*", + + // Trailing characters + "*x", + "*****************x", + GLOB.repeat(1000000) + "x", + + // Leading characters + "x*", + "x*****************", + "x" + GLOB.repeat(1000000), + + // Mixed leading/trailing characters + "x*x", + "x****************x", + "x" + GLOB.repeat(1000000) + "x" + )) { + assertNotGlobMatch(pattern, ""); + } + } + + @Test + public void testPatternWithoutGlobs() { + assertGlobMatch("test", "test"); + } + + @Test + public void testGlobs() { + for (String pattern : List.of( + "*test", // Leading glob + "this*", // Trailing glob + "this*test", // Middle glob + "*is *", // String in between two globs + "*is*a*", // Lots of globs + "**test**", // Double glob characters + "**is**a***test*", // Varying number of globs + "* *", // White space between globs + "*", // Lone glob + "**********", // Nothing but globs + "*Ѿ*", // Unicode with globs + "*is a ϗѾ *" // Mixed ASCII/unicode + )) { + assertGlobMatch(pattern, "this is a ϗѾ test"); + } + + for(String pattern:List.of( + "test*", // Implicit substring match + "*is", // Partial match + "*no*", // Globs without a match between them + " ", // Plain white space + "* ", // Trailing white space + " *", // Leading white space + "*ʤ*", // Non-matching unicode + "this*this is a test" // Repeated prefix + )) { + assertNotGlobMatch(pattern, "this is a test"); + } + } + + public void assertGlobMatch(String pattern, String subject) { + assertTrue(String.format("\"%s\" should match \"%s\"", pattern, subject), Glob.match(pattern, subject)); + } + + public void assertNotGlobMatch(String pattern, String subject) { + assertFalse(String.format("\"%s\" should not match \"%s\"", pattern, subject), Glob.match(pattern, subject)); + } + +} \ No newline at end of file diff --git a/lib/src/test/java/io/cloudquery/helper/GlobMatcherTest.java b/lib/src/test/java/io/cloudquery/helper/GlobMatcherTest.java deleted file mode 100644 index e62c622..0000000 --- a/lib/src/test/java/io/cloudquery/helper/GlobMatcherTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.cloudquery.helper; - -import org.junit.Test; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class GlobMatcherTest { - @Test - public void shouldMatchWildcard() { - GlobMatcher globMatcher = new GlobMatcher("*"); - - assertTrue(globMatcher.matches("aws_ec2_vpc")); - assertTrue(globMatcher.matches("aws_ec2_eip")); - assertTrue(globMatcher.matches("aws_ec2_instance")); - } - - @Test - public void shouldMatchWildcardSuffix() { - GlobMatcher globMatcher = new GlobMatcher("aws_*"); - - assertTrue(globMatcher.matches("aws_ec2_vpc")); - assertTrue(globMatcher.matches("aws_ec2_eip")); - assertTrue(globMatcher.matches("aws_ec2_instance")); - - assertFalse(globMatcher.matches("gcp_project")); - assertFalse(globMatcher.matches("other_aws_resource")); - } - - @Test - public void shouldMatchWildcardPrefixAndSuffix() { - GlobMatcher globMatcher = new GlobMatcher("*ec2*"); - - assertTrue(globMatcher.matches("aws_ec2_vpc")); - assertTrue(globMatcher.matches("aws_ec2_eip")); - assertTrue(globMatcher.matches("aws_ec2_instance")); - - assertFalse(globMatcher.matches("gcp_project")); - assertFalse(globMatcher.matches("other_aws_resource")); - } -} \ No newline at end of file