diff --git a/engine-api/src/main/java/io/nosqlbench/engine/api/util/TagFilter.java b/engine-api/src/main/java/io/nosqlbench/engine/api/util/TagFilter.java index 708046719..63a21b71d 100644 --- a/engine-api/src/main/java/io/nosqlbench/engine/api/util/TagFilter.java +++ b/engine-api/src/main/java/io/nosqlbench/engine/api/util/TagFilter.java @@ -18,77 +18,112 @@ package io.nosqlbench.engine.api.util; import java.util.*; +import java.util.function.BiFunction; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** - * This class makes it easy to associate tags and tag values with {@link Tagged} - * items, filtering matching Tagged items from a set of candidates. + *

TagFilter Synopsis

+ *

+ * This class makes it easy to associate tags and tag values with {@link Tagged} items, filtering matching Tagged items + * from a set of candidates.

+ * * - * Tag names and filter names must be simple words. Filter values can have regex expressions, however. + * + *

Tag Names and Values

+ *

+ * Any type which implements the Tagged interface can provide a set of tags in the form of a map. These are free-form, + * although they must + *

+ * + *

Tag Filters

+ *

Tag names and filter names must be simple words. Filter values can have regex expressions, however. * When a filter value starts and ends with a single quote, the quotes are removed as a convencience * for deal with shell escapes, etc. This means that value 'five-oh.*five' * is the same as five-oh.*five, except that the former will not cause undesirable - * shell expansion on command lines. - *

- * When a Tagged item is filtered, the following checks are made for each - * tag specified in the filter: + * shell expansion on command lines.

+ * + *

When a Tagged item is filtered, the following checks are made for each tag specified in the filter:

+ * *
    - *
  1. The Tagged item must have a tag with the same name as a filter.
  2. - *
  3. If the filter has a value in addition to the tag name, then the Tagged item - * must also have a value for that tag name. Furthermore, the value has to match.
  4. - *
  5. If the filter value, converted to a Regex, matches the tag value, - * it is deemed to be a match.
  6. + *
  7. The Tagged item must have a tag with the same name as a filter.
  8. + *
  9. If the filter has a value in addition to the tag name, then the Tagged item must also have a value + * for that tag name. Furthermore, the value has to match.
  10. + *
  11. If the filter value, converted to a Regex, matches the tag value, it is deemed to be a match.
  12. *
- *

- * Because advanced tag usage can sometimes be unintuitive, the tag filtering logic has + * + *

Because advanced tag usage can sometimes be unintuitive, the tag filtering logic has * a built-in log which can explain why a candidate item did or did not match a particular - * set of filters. + * set of filters.

+ * + *

Tag Filters

+ *

+ * All of the following forms are acceptable for a filter spec: + *

+ * + *

+ * That is, you can use spaces or commas between tag (name,value) pairs, and you can also use colons or equals + * between the actual tag names and values. This is not to support mixed formatting, but it does allow for some + * flexibility when integrating with other formats. Extra spaces between (name,value) pairs are ignored.

+ * + *

As well, you can include regex patterns in your tag filter values. You can also use single quotes to + * guard against shell expansion of internal characters or spaces. However, the following forms are not acceptable + * for a tag spec: + * + *

+ *
name1: value1
+ *
no extra spaces between the key and value
+ *
name-foo__bar:value1
+ *
No non-word characters in tag names
+ *
name1: value two
+ *
no spaces in tag values
+ *
name1: 'value two'
+ *
no spaces in tag values, even with single-quotes
+ *
*/ public class TagFilter { public static TagFilter MATCH_ALL = new TagFilter(""); private Map filter = new LinkedHashMap<>(); + private Conjugate conjugate = Conjugate.all; + + private final static Pattern conjugateForm = Pattern.compile("^(?\\w+)\\((?.+)\\)$",Pattern.DOTALL|Pattern.MULTILINE); + + private enum Conjugate { + any((i,j) -> (j>0)), + all((i,j) -> (i.intValue()==j.intValue())), + none((i,j) -> (j ==0)); + + private final BiFunction matchfunc; + + Conjugate(BiFunction matchfunc) { + this.matchfunc = matchfunc; + } + } /** - * Create a new tag filter. A tag filter is comprised of zero or more tag names, each with an - * optional value. The tag spec is a simple string format that contains zero or - * more tag names with optional values. - *

- * All of the following forms are acceptable for a filter spec: - *

    - *
  • name1=value1 name2=value2
  • - *
  • name1:value1, name2=value2
  • - *
  • name1=value1 name2=value2,name3:value3
  • - *
  • name1='.*fast.*', name2=1+
  • - *
- *

- * That is, you can use spaces or commas between tag (name,value) pairs, and you can also use - * colons or equals between the actual tag names and values. This is not to support mixed formatting, but it - * does allow for some flexibility when integrating with other formats. Extra spaces between (name,value) - * pairs are ignored.

- *

As well, you can include regex patterns in your tag filter values. You can also use single quotes to - * guard against

- *

- * However, the following forms are not acceptable for a tag spec: - *

- *
name1: value1
- *
no extra spaces between the key and value
- *
name-foo__bar:value1
- *
No non-word characters in tag names
- *
name1: value two
- *
no spaces in tag values
- *
name1: 'value two'
- *
no spaces in tag values, even with single-quotes
- *
+ *

Create a new tag filter. A tag filter is comprised of zero or more tag names, each with an optional value. + * The tag spec is a simple string format that contains zero or more tag names with optional values.

* - * @param filterSpec a filter spec as explained in the javadoc + * @param filterSpec + * a filter spec as explained in the javadoc */ public TagFilter(String filterSpec) { if ((filterSpec != null) && (!filterSpec.isEmpty())) { - filterSpec=unquote(filterSpec); + filterSpec = unquote(filterSpec); + Matcher cmatcher = conjugateForm.matcher(filterSpec); + if (cmatcher.matches()) { + filterSpec=cmatcher.group("filter"); + conjugate = Conjugate.valueOf(cmatcher.group("conjugate").toLowerCase()); + } String[] keyvalues = filterSpec.split("[,] *"); for (String assignment : keyvalues) { @@ -105,28 +140,30 @@ public class TagFilter { } private static String unquote(String filterSpec) { - for (String s : new String[]{"'","\""}) { - if (filterSpec.indexOf(s)==0 && filterSpec.indexOf(s,1)==filterSpec.length()-1) { - filterSpec=filterSpec.substring(1,filterSpec.length()-1); + for (String s : new String[]{"'", "\""}) { + if (filterSpec.indexOf(s) == 0 && filterSpec.indexOf(s, 1) == filterSpec.length() - 1) { + filterSpec = filterSpec.substring(1, filterSpec.length() - 1); } } return filterSpec; } /** - * Although this method could early-exit for certain conditions, the full tag matching logic - * is allowed to complete in order to present more complete diagnostic information back - * to the user. + * Although this method could early-exit for certain conditions, the full tag matching logic is allowed to complete + * in order to present more complete diagnostic information back to the user. + * + * @param tags + * The tags associated with a Tagged item. * - * @param tags The tags associated with a Tagged item. * @return a Result telling whether the tags matched and why or why not */ protected Result matches(Map tags) { List log = new ArrayList<>(); - boolean matched = true; + int totalKeyMatches=0; for (String filterkey : filter.keySet()) { + boolean matchedKey = true; String filterval = filter.get(filterkey); String itemval = tags.get(filterkey); @@ -142,22 +179,24 @@ public class TagFilter { log.add("(☑, ) " + detail + ": matched names"); } else { log.add("(☐, ) " + detail + ": did not match)"); - matched = false; + matchedKey = false; } } else { Pattern filterpattern = Pattern.compile("^" + filterval + "$"); if (itemval == null) { log.add("(☑,☐) " + detail + ": null tag value did not match '" + filterpattern + "'"); - matched = false; + matchedKey = false; } else if (filterpattern.matcher(itemval).matches()) { log.add("(☑,☑) " + detail + ": matched pattern '" + filterpattern + "'"); } else { log.add("(☑,☐) " + detail + ": did not match '" + filterpattern + "'"); - matched = false; + matchedKey = false; } } + totalKeyMatches += matchedKey ? 1 : 0; } + boolean matched = conjugate.matchfunc.apply(filter.size(),totalKeyMatches); return new Result(matched, log); } diff --git a/engine-api/src/test/java/io/nosqlbench/engine/api/util/TagFilterTest.java b/engine-api/src/test/java/io/nosqlbench/engine/api/util/TagFilterTest.java index 367aa8002..3234db195 100644 --- a/engine-api/src/test/java/io/nosqlbench/engine/api/util/TagFilterTest.java +++ b/engine-api/src/test/java/io/nosqlbench/engine/api/util/TagFilterTest.java @@ -36,8 +36,8 @@ public class TagFilterTest { @Test public void testEmptyTagFilterDoesMatch() { - Map itemtags = new HashMap<>() {{ - put("a","tag"); + Map itemtags = new HashMap<>() {{ + put("a", "tag"); }}; TagFilter tf = new TagFilter(""); assertThat(tf.matches(itemtags).matched()).isTrue(); @@ -45,7 +45,7 @@ public class TagFilterTest { @Test public void testSomeFilterTagsNoItemTagsDoesNotMatch() { - Map itemtags = new HashMap<>() {{ + Map itemtags = new HashMap<>() {{ }}; TagFilter tf = new TagFilter("tag=foo"); assertThat(tf.matches(itemtags).matched()).isFalse(); @@ -54,8 +54,8 @@ public class TagFilterTest { @Test public void testEmptyTagFilterValueDoesMatch() { - Map itemtags = new HashMap<>() {{ - put("one","two"); + Map itemtags = new HashMap<>() {{ + put("one", "two"); }}; TagFilter tf = new TagFilter(""); assertThat(tf.matches(itemtags).matched()).isTrue(); @@ -64,15 +64,15 @@ public class TagFilterTest { @Test public void testMatchingTagKeyValueDoesMatch() { - Map itemtags = new HashMap<>() {{ - put("one","two"); + Map itemtags = new HashMap<>() {{ + put("one", "two"); }}; TagFilter tf = new TagFilter("one"); TagFilter.Result result = tf.matches(itemtags); assertThat(result.matched()).isTrue(); - Map itemtags2 = new HashMap<>() {{ - put("one",null); + Map itemtags2 = new HashMap<>() {{ + put("one", null); }}; assertThat(tf.matches(itemtags2).matched()).isTrue(); } @@ -80,8 +80,8 @@ public class TagFilterTest { @Test public void testMatchingKeyMismatchingValueDoesNotMatch() { - Map itemtags = new HashMap<>() {{ - put("one","four"); + Map itemtags = new HashMap<>() {{ + put("one", "four"); }}; TagFilter tf = new TagFilter("one:two"); TagFilter.Result result = tf.matches(itemtags); @@ -90,8 +90,8 @@ public class TagFilterTest { @Test public void testMatchingKeyAndValueDoesMatch() { - Map itemtags = new HashMap<>() {{ - put("one","four"); + Map itemtags = new HashMap<>() {{ + put("one", "four"); }}; TagFilter tf = new TagFilter("one:four"); assertThat(tf.matches(itemtags).matched()).isTrue(); @@ -99,8 +99,8 @@ public class TagFilterTest { @Test public void testMatchingKeyAndValueRegexDoesMatch() { - Map itemtags = new HashMap<>() {{ - put("one","four-five-six"); + Map itemtags = new HashMap<>() {{ + put("one", "four-five-six"); }}; TagFilter tfLeft = new TagFilter("one:'four-.*'"); assertThat(tfLeft.matches(itemtags).matched()).isTrue(); @@ -115,9 +115,9 @@ public class TagFilterTest { @Override public Map getTags() { return new HashMap<>() {{ - put("one","four-five-six"); - put("two","three-seven-nine"); - put("five",null); + put("one", "four-five-six"); + put("two", "three-seven-nine"); + put("five", null); put("six", null); }}; } @@ -136,8 +136,8 @@ public class TagFilterTest { @Test public void testRawSubstringDoesNotMatchRegex() { - Map itemtags = new HashMap<>() {{ - put("one","four-five-six"); + Map itemtags = new HashMap<>() {{ + put("one", "four-five-six"); }}; TagFilter tf = new TagFilter("one:'five'"); assertThat(tf.matches(itemtags).matched()).isFalse(); @@ -145,8 +145,8 @@ public class TagFilterTest { @Test public void testAlternation() { - Map itemtags = new HashMap<>() {{ - put("one","four-five-six"); + Map itemtags = new HashMap<>() {{ + put("one", "four-five-six"); }}; TagFilter tf = new TagFilter("one:'four.*|seven'"); assertThat(tf.matches(itemtags).matched()).isTrue(); @@ -155,12 +155,21 @@ public class TagFilterTest { @Test public void testLeadingSpaceTrimmedInQuotedTag() { - Map itemtags = new HashMap<>() {{ - put("phase","main"); + Map itemtags = new HashMap<>() {{ + put("phase", "main"); }}; TagFilter tf = new TagFilter("\"phase: main\""); assertThat(tf.matches(itemtags).matched()).isTrue(); } + @Test + public void testAnyCondition() { + Map itemtags = Map.of("phase", "main", "truck", "car"); + TagFilter tf = new TagFilter("any(truck:car,phase:moon)"); + assertThat(tf.matches(itemtags).matched()).isTrue(); + TagFilter tf2 = new TagFilter("any(car:truck,phase:moon)"); + assertThat(tf2.matches(itemtags).matched()).isFalse(); + } + }