From 334a51aeb0d6c21170b317735ced2eda0fd72173 Mon Sep 17 00:00:00 2001 From: brharrington Date: Tue, 8 Aug 2023 12:35:24 -0500 Subject: [PATCH] determine if pattern is starts-with or contains (#1070) Adds support to PatternMatcher to check if a pattern is a strict starts-with or contains check. This can be useful when mapping to some data stores that have optimizations for those operations that will not work with arbitrary regex. Also adds a method to get a contained string that can be used as an initial filter. --- .../spectator/impl/PatternMatcher.java | 30 +++++++- .../impl/matcher/IndexOfMatcher.java | 12 ++- .../spectator/impl/matcher/SeqMatcher.java | 7 +- .../impl/matcher/StartsWithMatcher.java | 17 +++- .../spectator/impl/matcher/ContainsTest.java | 77 +++++++++++++++++++ 5 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 spectator-api/src/test/java/com/netflix/spectator/impl/matcher/ContainsTest.java diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java index 20491aa7f..71059db4f 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 Netflix, Inc. + * Copyright 2014-2023 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,16 @@ default String prefix() { return null; } + /** + * Returns a fixed string that is contained within matching results for the pattern if one + * is available. This can be used with indexed data to help select a subset of values that + * are possible matches. If the pattern does not have a fixed sub-string, then null will be + * returned. + */ + default String containedString() { + return null; + } + /** * The minimum possible length of a matching string. This can be used as a quick check * to see if there is any way a given string could match. @@ -92,6 +102,24 @@ default boolean neverMatches() { return false; } + /** + * Returns true if this matcher is equivalent to performing a starts with check on the + * prefix. This can be useful when mapping to storage that may have optimized prefix + * matching operators. + */ + default boolean isPrefixMatcher() { + return false; + } + + /** + * Returns true if this matcher is equivalent to checking if a string contains a string. + * This can be useful when mapping to storage that may have optimized contains matching + * operators. + */ + default boolean isContainsMatcher() { + return false; + } + /** * Returns a new matcher that matches the same pattern only ignoring the case of the input * string. Note, character classes will be matched as is and must explicitly include both diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/IndexOfMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/IndexOfMatcher.java index cd206ad45..1266d2a65 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/IndexOfMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/IndexOfMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 Netflix, Inc. + * Copyright 2014-2023 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,16 @@ Matcher next() { return next; } + @Override + public String containedString() { + return pattern; + } + + @Override + public boolean isContainsMatcher() { + return next == TrueMatcher.INSTANCE; + } + private int indexOfIgnoreCase(String str, int offset) { final int length = pattern.length(); final int end = (str.length() - length) + 1; diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java index 1f8aecc16..bb2f14f4b 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 Netflix, Inc. + * Copyright 2014-2023 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,6 +76,11 @@ public String prefix() { return matchers[0].prefix(); } + @Override + public String containedString() { + return matchers[0].containedString(); + } + @Override public int minLength() { return minLength; diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java index 41d641484..5665a5364 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 Netflix, Inc. + * Copyright 2014-2023 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,6 +57,21 @@ public String prefix() { return pattern; } + @Override + public String containedString() { + return pattern; + } + + @Override + public boolean isPrefixMatcher() { + return true; + } + + @Override + public boolean isContainsMatcher() { + return true; + } + @Override public int minLength() { return pattern.length(); diff --git a/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/ContainsTest.java b/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/ContainsTest.java new file mode 100644 index 000000000..d0af495f1 --- /dev/null +++ b/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/ContainsTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2014-2023 Netflix, Inc. + * + * 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 com.netflix.spectator.impl.matcher; + +import com.netflix.spectator.impl.PatternMatcher; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ContainsTest { + + private PatternMatcher re(String pattern) { + return PatternMatcher.compile(pattern); + } + + private void assertStartsWith(String pattern) { + PatternMatcher m = re(pattern); + Assertions.assertTrue(m.isPrefixMatcher(), pattern); + Assertions.assertTrue(m.isContainsMatcher(), pattern); + } + + private void assertContainsOnly(String pattern) { + PatternMatcher m = re(pattern); + Assertions.assertFalse(m.isPrefixMatcher(), pattern); + Assertions.assertTrue(m.isContainsMatcher(), pattern); + } + + @Test + public void startsWith() { + assertStartsWith("^abc"); + assertStartsWith("^abc[.]def"); + assertStartsWith("^abc\\.def"); + } + + @Test + public void notStartsWith() { + Assertions.assertFalse(re("abc").isPrefixMatcher()); + Assertions.assertFalse(re("ab[cd]").isPrefixMatcher()); + Assertions.assertFalse(re("^abc.def").isPrefixMatcher()); + } + + @Test + public void contains() { + assertContainsOnly("abc"); + assertContainsOnly(".*abc"); + assertContainsOnly("abc\\.def"); + } + + @Test + public void notContains() { + Assertions.assertFalse(re("ab[cd]").isContainsMatcher()); + Assertions.assertFalse(re("abc.def").isContainsMatcher()); + } + + @Test + public void containedString() { + Assertions.assertEquals("abc", re("abc").containedString()); + Assertions.assertEquals("abc", re(".*abc").containedString()); + Assertions.assertEquals("ab", re(".*ab[cd]").containedString()); + Assertions.assertEquals("abc", re("abc.def").containedString()); + Assertions.assertEquals("abc.def", re("abc\\.def").containedString()); + Assertions.assertEquals("abc", re("^abc.def").containedString()); + Assertions.assertEquals("abc.def", re("^abc\\.def").containedString()); + } +}