package com.netflix.suro.routing.filter.parser; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.netflix.suro.routing.filter.MessageFilter; import com.netflix.suro.routing.filter.MessageFilterCompiler; import com.netflix.suro.routing.filter.lang.InvalidFilterException; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import static com.netflix.suro.routing.filter.parser.FilterPredicate.*; import static org.junit.Assert.assertEquals; @RunWith(Parameterized.class) public class CompositeMessageFilterParsingTest { private static final String TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss:SSS"; private final static DateTime now = DateTime.now(); // An example composite filter string: // (xpath("//a/b/c") = "foo" or xpath("//a/b/d") > 5 or xpath("//a/f") is null) and not xpath("timestamp") > time-millis("yyyy-MM-dd'T'HH:mm:ss:SSS", "2012-08-22T08:45:56:086") and xpath("//a/b/e") between (5, 10) and xpath("//a/b/f") =~ "null" and xpath("//a/g") <= time-string("yyyy-MM-dd'T'HH:mm:ss:SSS", "yyyy-MM-dd'T'HH:mm:ss:SSS", "2012-08-22T16:45:56:086") and xpath("//a/h") in (a,b,c,d) or xpath("//a/b/g/f") in (1,2,3,4) or not xpath("//no/no") exists @Parameters public static Collection<Object[]> data() { return Arrays.asList(new Object[][] { { // (xpath("//a/b/c") = "foo" or xpath("//a/b/d") > 5 or xpath("//a/f") is null) // and not xpath("timestamp") > time-millis(TIME_FORMAT, 3 hours ago) // and xpath("//a/b/e") between (5, 10) // and xpath("//a/b/f") =~ "[0-9a-zA-Z]+" // and xpath("//a/g") <= time-string(TIME_FORMAT, TIME_FORMAT, 5 hours later) // and xpath("//a/h") in ("a", "b", "c", "d") // or xpath("//a/b/g/f") in (1, 2, 3, 4) // or not xpath("//no/no") exists and( bracket( or( stringComp.create("//a/b/c", "=", "foo"), numberComp.create("//a/b/d", ">", 5), isNull.create("//a/f") ) ), not( timeComp.create("timestamp", ">", timeMillisNHoursAway(-3, TIME_FORMAT)) ), and( between.create("//a/b/e", null, new Object[]{5, 10}), regex.create("//a/b/f", "[0-9a-zA-Z]+"), timeComp.create("//a/g", "<=", timeStringNHoursAway(5, TIME_FORMAT)) ), or( inPred.create("//a/h", null, wrap(new Object[]{"a", "b", "c", "d"})), inPred.create("//a/b/g/f", null, new Object[]{1, 2, 3, 4}), not ( existsRight.create("//no/no") ) ) ), new Object[][]{ // This event matches the filter above new Object[]{ createEvent( new Object[]{"//a/b/c", "bar"}, // It's OK. There's a number of "or" clauses. new Object[]{"//a/b/d", 10}, new Object[]{"//a/f", "not null"}, // It's OK. The previous one is true new Object[]{"timestamp", nHoursAway(-4)}, // note this is not > new Object[]{"//a/b/e", 7}, new Object[]{"//a/b/f", "123a43bsdfA"}, new Object[]{"//a/g", nHoursAway(0, TIME_FORMAT)}, new Object[]{"//a/h", "c"}, new Object[]{"//a/b/g/f", 3}, new Object[]{"//no/no", "something"} // this will fail the last clause ), true, }, new Object[]{ createEvent( new Object[]{"//a/b/c", "bar"}, // not match new Object[]{"//a/b/d", 0}, // not match new Object[]{"//a/f", "not null"}, // not match new Object[]{"timestamp", nHoursAway(-4)}, // match new Object[]{"//a/b/e", 7}, new Object[]{"//a/b/f", "123a43bsdfA"}, new Object[]{"//a/g", nHoursAway(0, TIME_FORMAT)}, new Object[]{"//a/h", "c"}, new Object[]{"//a/b/g/f", 10}, // match new Object[]{"//no/no", "something"} // match ), false } } } }); } private static String wrap(String value){ return String.format("\"%s\"", value); } private static String[] wrap(Object[] strings) { String[] result = new String[strings.length]; for(int i = 0; i < strings.length; ++i) { result[i] = wrap(strings[i].toString()); } return result; } private static long nHoursAway(int n) { return now.plusHours(n).getMillis(); } private static String nHoursAway(int n, String format) { return DateTimeFormat.forPattern(format).print(nHoursAway(n)); } // Creates a time-millis that is n hours away from now private static String timeMillisNHoursAway(int n, String format) { return String.format( "time-millis(\"%s\", \"%s\")", format, nHoursAway(n, format)); } private static String timeStringNHoursAway(int n, String format) { return String.format( "time-string(\"%s\", \"%s\", \"%s\")", format, format, nHoursAway(n, format)); } private String filterString; private MessageFilter filter; private Object[][] eventResultPairs; public CompositeMessageFilterParsingTest(String filterString, Object[][]eventResultPairs) throws InvalidFilterException { this.filterString = filterString; this.filter = MessageFilterCompiler.compile(this.filterString); this.eventResultPairs = eventResultPairs; } @Test public void trans() { String input = and( stringComp.create("//a/b/c", "=", "foo"), numberComp.create("//a/b/d", ">", 5), between.create("//a/b/e", null, new Object[]{5, 10}), regex.create("//a/b/f", "[0-9a-zA-Z]+"), timeComp.create("//a/g", "<=", timeStringNHoursAway(5, TIME_FORMAT)), inPred.create("//a/h", null, wrap(new Object[]{"a", "b", "c", "d"})) ); System.out.println("Generated filter string: "+input); MessageFilter ef = null; try { ef = MessageFilterCompiler.compile(input); } catch (InvalidFilterException e) { throw new AssertionError("Invalid filter string generated. Error: " + e.getMessage()); } } @Test public void test() throws Exception { for(Object[] pair : eventResultPairs){ Object event = pair[0]; boolean result = (Boolean) pair[1]; assertEquals(String.format("Event object: %s\nFilter string: %s\n", event, this.filterString), result, filter.apply(event)); } } private static String or(String firstTerm, String secondTerm, Object...rest) { return Joiner.on(" or ").join(firstTerm, secondTerm, rest); } private static String and(String first, String second, Object...rest) { return Joiner.on(" and ").join(first, second, rest); } private static String not(String term) { return "not "+term; } private static String bracket(String term) { return String.format("(%s)", term); } private static Object createEvent(Object[]... pairs) { Map<String, ? super Object> object = Maps.newHashMap(); for(Object[] pair: pairs) { String path = (String)pair[0]; List<String> steps = toSteps(path); Object value = pair[1]; updateObjectWithPathAndValue(object, steps, value); } return object; } private static List<String> toSteps(String xpath) { List<String> steps = ImmutableList .copyOf(Splitter.on('/') .trimResults() .omitEmptyStrings() .split(xpath)); return steps; } // Bypass JXPathContext#createPathAndValue() because it doesn't support context-dependent predicates private static void updateObjectWithPathAndValue(Map<String, ? super Object> object, List<String> path, Object value) { if(path.isEmpty()) { throw new IllegalArgumentException("There should be at least one step in the given path"); } if(path.size() == 1) { object.put(path.get(0), value); return; } String key = path.get(0); @SuppressWarnings("unchecked") Map<String, ? super Object> next = (Map)object.get(key); if(next == null){ next = Maps.newHashMap(); object.put(key,next); } updateObjectWithPathAndValue(next, path.subList(1, path.size()), value); } }