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.
+ *
*
- * - tags are the actual tags attached to a {@link Tagged} item.
- * - filters are the names and values used to filter the tag sets.
+ * - tags are the actual tags attached to a {@link Tagged} item.
+ * - filters are the names and values used to filter the tag sets.
*
- * 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:
+ *
*
- * - The Tagged item must have a tag with the same name as a filter.
- * - 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.
- * - If the filter value, converted to a Regex, matches the tag value,
- * it is deemed to be a match.
+ * - The Tagged item must have a tag with the same name as a filter.
+ * - 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.
+ * - If the filter value, converted to a Regex, matches the tag value, it is deemed to be a match.
*
- *
- * 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:
+ *
+ * - 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 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();
+ }
+
}