diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b76637e..fef1196 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,9 @@ #### Benchmark / Performance (for source code changes): ``` - + Java build -> build (ubuntu-X.Y, 8) -> Run benchmarks".> ``` --- diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index cede79d..01391e5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -29,3 +29,5 @@ jobs: cache: 'maven' - name: Verify with Maven run: mvn --batch-mode --errors --update-snapshots verify + - name: Run benchmarks + run: mvn test '-Dtest=Benchmarks#CL2*' diff --git a/README.md b/README.md index fc5b7d4..2e8ff14 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,15 @@ intersection between the event array and rule-array is non-empty. ``` Prefix matches only work on string-valued fields. +###Prefix equals-ignore-case matching + +```javascript +{ + "source": [ { "prefix": { "equals-ignore-case": "EC2" } } ] +} +``` +Prefix equals-ignore-case matches only work on string-valued fields. + ### Suffix matching ```javascript @@ -105,6 +114,15 @@ Prefix matches only work on string-valued fields. ``` Suffix matches only work on string-valued fields. +###Suffix equals-ignore-case matching + + ```javascript + { + "source": [ { "suffix": { "equals-ignore-case": "EC2" } } ] + } + ``` +Suffix equals-ignore-case matches only work on string-valued fields. + ### Equals-ignore-case matching ```javascript @@ -579,7 +597,9 @@ static methods are useful. ```java public static ValuePatterns exactMatch(final String value); public static ValuePatterns prefixMatch(final String prefix); +public static ValuePatterns prefixEqualsIgnoreCaseMatch(final String prefix); public static ValuePatterns suffixMatch(final String suffix); +public static ValuePatterns suffixEqualsIgnoreCaseMatch(final String suffix); public static ValuePatterns equalsIgnoreCaseMatch(final String value); public static ValuePatterns wildcardMatch(final String value); public static AnythingBut anythingButMatch(final String anythingBut); @@ -725,6 +745,8 @@ counts the matches, yields the following on a 2019 MacBook: Events are processed at over 220K/second except for: - equals-ignore-case matches, which are processed at over 200K/second. + - prefix/equals-ignore-case matches, which are processed at over 200K/second. + - suffix/equals-ignore-case matches, which are processed at over 200K/second. - wildcard matches, which are processed at over 170K/second. - anything-but matches, which are processed at over 150K/second. - numeric matches, which are processed at over 120K/second. diff --git a/pom.xml b/pom.xml index fbf987e..ccc7ea3 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ software.amazon.event.ruler event-ruler Event Ruler - 1.4.1 + 1.5.0 Event Ruler is a Java library that allows matching Rules to Events. An event is a list of fields, which may be given as name/value pairs or as a JSON object. A rule associates event field names with lists of possible values. There are two reasons to use Ruler: 1/ It's fast; the time it takes to match Events doesn't diff --git a/src/main/software/amazon/event/ruler/ByteMachine.java b/src/main/software/amazon/event/ruler/ByteMachine.java index 352c226..f8e8a3c 100644 --- a/src/main/software/amazon/event/ruler/ByteMachine.java +++ b/src/main/software/amazon/event/ruler/ByteMachine.java @@ -23,14 +23,18 @@ import javax.annotation.concurrent.ThreadSafe; import static software.amazon.event.ruler.CompoundByteTransition.coalesce; +import static software.amazon.event.ruler.MatchType.EQUALS_IGNORE_CASE; import static software.amazon.event.ruler.MatchType.EXACT; import static software.amazon.event.ruler.MatchType.EXISTS; import static software.amazon.event.ruler.MatchType.SUFFIX; import static software.amazon.event.ruler.MatchType.ANYTHING_BUT_SUFFIX; +import static software.amazon.event.ruler.MatchType.SUFFIX_EQUALS_IGNORE_CASE; +import static software.amazon.event.ruler.input.MultiByte.MAX_CONTINUATION_BYTE; import static software.amazon.event.ruler.input.MultiByte.MAX_FIRST_BYTE_FOR_ONE_BYTE_CHAR; import static software.amazon.event.ruler.input.MultiByte.MAX_FIRST_BYTE_FOR_TWO_BYTE_CHAR; import static software.amazon.event.ruler.input.MultiByte.MAX_NON_FIRST_BYTE; +import static software.amazon.event.ruler.input.MultiByte.MIN_CONTINUATION_BYTE; import static software.amazon.event.ruler.input.MultiByte.MIN_FIRST_BYTE_FOR_ONE_BYTE_CHAR; import static software.amazon.event.ruler.input.MultiByte.MIN_FIRST_BYTE_FOR_TWO_BYTE_CHAR; import static software.amazon.event.ruler.input.DefaultParser.getParser; @@ -142,7 +146,9 @@ void deletePattern(final Patterns pattern) { case EXACT: case NUMERIC_EQ: case PREFIX: + case PREFIX_EQUALS_IGNORE_CASE: case SUFFIX: + case SUFFIX_EQUALS_IGNORE_CASE: case ANYTHING_BUT_PREFIX: case ANYTHING_BUT_SUFFIX: case EQUALS_IGNORE_CASE: @@ -490,10 +496,12 @@ private void doTransitionOn(final String valString, final Set tr // given we are traversing in reverse order (from right to left), only suffix matches are eligible // to be collected. MatchType patternType = match.getPattern().type(); - if (patternType == SUFFIX) { + if (patternType == SUFFIX || patternType == SUFFIX_EQUALS_IGNORE_CASE) { transitionTo.add(new NameStateWithPattern(match.getNextNameState(), match.getPattern())); } else if (patternType == ANYTHING_BUT_SUFFIX) { addToAnythingButsMap(failedAnythingButs, match.getNextNameState(), match.getPattern()); @@ -677,7 +685,9 @@ NameState addPattern(final Patterns pattern, final NameState nameState) { case EXACT: case NUMERIC_EQ: case PREFIX: + case PREFIX_EQUALS_IGNORE_CASE: case SUFFIX: + case SUFFIX_EQUALS_IGNORE_CASE: case EQUALS_IGNORE_CASE: case WILDCARD: assert pattern instanceof ValuePatterns; @@ -818,7 +828,8 @@ private boolean doMultipleTransitionsConvergeForInputByte(ByteState byteState, I return false; } - if (!isNextCharacterFirstByteOfMultiByte(characters, i)) { + boolean isNextCharacterForSuffixMatch = isNextCharacterFirstContinuationByteForSuffixMatch(characters, i); + if (!isNextCharacterFirstByteOfMultiByte(characters, i) && !isNextCharacterForSuffixMatch) { // If we are in the midst of a multi-byte sequence, we know that we are dealing with single transitions. return false; } @@ -834,7 +845,8 @@ private boolean doMultipleTransitionsConvergeForInputByte(ByteState byteState, I // Parse the next Java character into lower and upper case representations. Check if there are multiple // multibytes (paths) and that there exists a transition that both lead to. String value = extractNextJavaCharacterFromInputCharacters(characters, i); - InputCharacter[] inputCharacters = getParser().parse(MatchType.EQUALS_IGNORE_CASE, value); + MatchType matchType = isNextCharacterForSuffixMatch ? SUFFIX_EQUALS_IGNORE_CASE : EQUALS_IGNORE_CASE; + InputCharacter[] inputCharacters = getParser().parse(matchType, value); ByteTransition transition = getTransition(byteState, inputCharacters[0]); return inputCharacters[0] instanceof InputMultiByteSet && transition != null; } @@ -846,7 +858,54 @@ private boolean isNextCharacterFirstByteOfMultiByte(InputCharacter[] characters, return firstByte > MAX_NON_FIRST_BYTE; } + private boolean isNextCharacterFirstContinuationByteForSuffixMatch(InputCharacter[] characters, int i) { + if (hasSuffix.get() <= 0) { + return false; + } + // If the previous byte is a continuation byte, this means that we're in the middle of a multi-byte sequence. + return isContinuationByte(characters, i) && !isContinuationByte(characters, i - 1); + } + + private boolean isContinuationByte(InputCharacter[] characters, int i) { + if (i < 0) { + return false; + } + byte continuationByte = InputByte.cast(characters[i]).getByte(); + // Continuation bytes have bit 7 set, and bit 6 should be unset + return continuationByte >= MIN_CONTINUATION_BYTE && continuationByte <= MAX_CONTINUATION_BYTE; + } + private String extractNextJavaCharacterFromInputCharacters(InputCharacter[] characters, int i) { + if (isNextCharacterFirstByteOfMultiByte(characters, i)) { + return extractNextJavaCharacterFromInputCharactersForForwardArrays(characters, i); + } else { + return extractNextJavaCharacterFromInputCharactersForBackwardsArrays(characters, i); + } + } + + private String extractNextJavaCharacterFromInputCharactersForBackwardsArrays(InputCharacter[] characters, int i) { + List bytesList = new ArrayList<>(); + for (int multiByteIndex = i; multiByteIndex < characters.length; multiByteIndex++) { + if (!isContinuationByte(characters, multiByteIndex)) { + // This is the last byte of the suffix char + bytesList.add(InputByte.cast(characters[multiByteIndex]).getByte()); + break; + } + bytesList.add(InputByte.cast(characters[multiByteIndex]).getByte()); + } + // Undoing the reverse on the byte array to get the valid char + return new String(reverseBytesList(bytesList), StandardCharsets.UTF_8); + } + + private byte[] reverseBytesList(List bytesList) { + byte[] byteArray = new byte[bytesList.size()]; + for (int i = 0; i < bytesList.size(); i++) { + byteArray[bytesList.size() - i - 1] = bytesList.get(i); + } + return byteArray; + } + + private static String extractNextJavaCharacterFromInputCharactersForForwardArrays(InputCharacter[] characters, int i) { byte firstByte = InputByte.cast(characters[i]).getByte(); if (firstByte >= MIN_FIRST_BYTE_FOR_ONE_BYTE_CHAR && firstByte <= MAX_FIRST_BYTE_FOR_ONE_BYTE_CHAR) { return new String(new byte[] { firstByte } , StandardCharsets.UTF_8); @@ -873,7 +932,9 @@ NameState findPattern(final Patterns pattern) { case EXACT: case NUMERIC_EQ: case PREFIX: + case PREFIX_EQUALS_IGNORE_CASE: case SUFFIX: + case SUFFIX_EQUALS_IGNORE_CASE: case ANYTHING_BUT_SUFFIX: case ANYTHING_BUT_PREFIX: case EQUALS_IGNORE_CASE: @@ -1583,11 +1644,13 @@ private void addMatchReferences(ByteMatch match) { switch (pattern.type()) { case EXACT: case PREFIX: + case PREFIX_EQUALS_IGNORE_CASE: case EXISTS: case EQUALS_IGNORE_CASE: case WILDCARD: break; case SUFFIX: + case SUFFIX_EQUALS_IGNORE_CASE: hasSuffix.incrementAndGet(); break; case NUMERIC_EQ: @@ -1685,11 +1748,13 @@ private void updateMatchReferences(ByteMatch match) { switch (pattern.type()) { case EXACT: case PREFIX: + case PREFIX_EQUALS_IGNORE_CASE: case EXISTS: case EQUALS_IGNORE_CASE: case WILDCARD: break; case SUFFIX: + case SUFFIX_EQUALS_IGNORE_CASE: hasSuffix.decrementAndGet(); break; case NUMERIC_EQ: diff --git a/src/main/software/amazon/event/ruler/JsonRuleCompiler.java b/src/main/software/amazon/event/ruler/JsonRuleCompiler.java index d76e22a..12a33cd 100644 --- a/src/main/software/amazon/event/ruler/JsonRuleCompiler.java +++ b/src/main/software/amazon/event/ruler/JsonRuleCompiler.java @@ -366,6 +366,10 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I return pattern; } else if (Constants.PREFIX_MATCH.equals(matchTypeName)) { final JsonToken prefixToken = parser.nextToken(); + if (prefixToken == JsonToken.START_OBJECT) { + return processPrefixEqualsIgnoreCaseExpression(parser); + } + if (prefixToken != JsonToken.VALUE_STRING) { barf(parser, "prefix match pattern must be a string"); } @@ -376,6 +380,10 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I return pattern; } else if (Constants.SUFFIX_MATCH.equals(matchTypeName)) { final JsonToken suffixToken = parser.nextToken(); + if (suffixToken == JsonToken.START_OBJECT) { + return processSuffixEqualsIgnoreCaseExpression(parser); + } + if (suffixToken != JsonToken.VALUE_STRING) { barf(parser, "suffix match pattern must be a string"); } @@ -515,6 +523,56 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I } } + private static Patterns processPrefixEqualsIgnoreCaseExpression(final JsonParser parser) throws IOException { + final JsonToken prefixObject = parser.nextToken(); + if (prefixObject != JsonToken.FIELD_NAME) { + barf(parser, "Prefix expression name not found"); + } + + final String prefixObjectOp = parser.getCurrentName(); + if (!Constants.EQUALS_IGNORE_CASE.equals(prefixObjectOp)) { + barf(parser, "Unsupported prefix pattern: " + prefixObjectOp); + } + + final JsonToken prefixEqualsIgnoreCase = parser.nextToken(); + if (prefixEqualsIgnoreCase != JsonToken.VALUE_STRING) { + barf(parser, "equals-ignore-case match pattern must be a string"); + } + final Patterns pattern = Patterns.prefixEqualsIgnoreCaseMatch('"' + parser.getText()); + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + return pattern; + } + + private static Patterns processSuffixEqualsIgnoreCaseExpression(final JsonParser parser) throws IOException { + final JsonToken suffixObject = parser.nextToken(); + if (suffixObject != JsonToken.FIELD_NAME) { + barf(parser, "Suffix expression name not found"); + } + + final String suffixObjectOp = parser.getCurrentName(); + if (!Constants.EQUALS_IGNORE_CASE.equals(suffixObjectOp)) { + barf(parser, "Unsupported suffix pattern: " + suffixObjectOp); + } + + final JsonToken suffixEqualsIgnoreCase = parser.nextToken(); + if (suffixEqualsIgnoreCase != JsonToken.VALUE_STRING) { + barf(parser, "equals-ignore-case match pattern must be a string"); + } + final Patterns pattern = Patterns.suffixEqualsIgnoreCaseMatch(parser.getText() + '"'); + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + return pattern; + } + private static Patterns processAnythingButListMatchExpression(JsonParser parser) throws JsonParseException { JsonToken token; Set values = new HashSet<>(); diff --git a/src/main/software/amazon/event/ruler/MatchType.java b/src/main/software/amazon/event/ruler/MatchType.java index 883a8e1..ec2631c 100644 --- a/src/main/software/amazon/event/ruler/MatchType.java +++ b/src/main/software/amazon/event/ruler/MatchType.java @@ -8,7 +8,9 @@ public enum MatchType { ABSENT, // absent key pattern EXISTS, // existence pattern PREFIX, // string prefix + PREFIX_EQUALS_IGNORE_CASE, // case-insensitive string prefix SUFFIX, // string suffix + SUFFIX_EQUALS_IGNORE_CASE, // case-insensitive string suffix NUMERIC_EQ, // exact numeric match NUMERIC_RANGE, // numeric range with high & low bound & />= options ANYTHING_BUT, // deny list effect diff --git a/src/main/software/amazon/event/ruler/Patterns.java b/src/main/software/amazon/event/ruler/Patterns.java index 5d5d0a4..42d46b3 100644 --- a/src/main/software/amazon/event/ruler/Patterns.java +++ b/src/main/software/amazon/event/ruler/Patterns.java @@ -45,10 +45,18 @@ public static ValuePatterns prefixMatch(final String prefix) { return new ValuePatterns(MatchType.PREFIX, prefix); } + public static ValuePatterns prefixEqualsIgnoreCaseMatch(final String prefix) { + return new ValuePatterns(MatchType.PREFIX_EQUALS_IGNORE_CASE, prefix); + } + public static ValuePatterns suffixMatch(final String suffix) { return new ValuePatterns(MatchType.SUFFIX, new StringBuilder(suffix).reverse().toString()); } + public static ValuePatterns suffixEqualsIgnoreCaseMatch(final String suffix) { + return new ValuePatterns(MatchType.SUFFIX_EQUALS_IGNORE_CASE, new StringBuilder(suffix).reverse().toString()); + } + public static AnythingBut anythingButMatch(final String anythingBut) { return new AnythingBut(Collections.singleton(anythingBut), false); } diff --git a/src/main/software/amazon/event/ruler/RuleCompiler.java b/src/main/software/amazon/event/ruler/RuleCompiler.java index 01cce52..1929674 100644 --- a/src/main/software/amazon/event/ruler/RuleCompiler.java +++ b/src/main/software/amazon/event/ruler/RuleCompiler.java @@ -263,6 +263,10 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I return pattern; } else if (Constants.PREFIX_MATCH.equals(matchTypeName)) { final JsonToken prefixToken = parser.nextToken(); + if (prefixToken == JsonToken.START_OBJECT) { + return processPrefixEqualsIgnoreCaseExpression(parser); + } + if (prefixToken != JsonToken.VALUE_STRING) { barf(parser, "prefix match pattern must be a string"); } @@ -273,6 +277,10 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I return pattern; } else if (Constants.SUFFIX_MATCH.equals(matchTypeName)) { final JsonToken suffixToken = parser.nextToken(); + if (suffixToken == JsonToken.START_OBJECT) { + return processSuffixEqualsIgnoreCaseExpression(parser); + } + if (suffixToken != JsonToken.VALUE_STRING) { barf(parser, "suffix match pattern must be a string"); } @@ -413,6 +421,56 @@ private static Patterns processMatchExpression(final JsonParser parser) throws I } } + private static Patterns processPrefixEqualsIgnoreCaseExpression(final JsonParser parser) throws IOException { + final JsonToken prefixObject = parser.nextToken(); + if (prefixObject != JsonToken.FIELD_NAME) { + barf(parser, "Prefix expression name not found"); + } + + final String prefixObjectOp = parser.getCurrentName(); + if (!Constants.EQUALS_IGNORE_CASE.equals(prefixObjectOp)) { + barf(parser, "Unsupported prefix pattern: " + prefixObjectOp); + } + + final JsonToken prefixEqualsIgnoreCase = parser.nextToken(); + if (prefixEqualsIgnoreCase != JsonToken.VALUE_STRING) { + barf(parser, "equals-ignore-case match pattern must be a string"); + } + final Patterns pattern = Patterns.prefixEqualsIgnoreCaseMatch('"' + parser.getText()); + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + return pattern; + } + + private static Patterns processSuffixEqualsIgnoreCaseExpression(final JsonParser parser) throws IOException { + final JsonToken suffixObject = parser.nextToken(); + if (suffixObject != JsonToken.FIELD_NAME) { + barf(parser, "Suffix expression name not found"); + } + + final String suffixObjectOp = parser.getCurrentName(); + if (!Constants.EQUALS_IGNORE_CASE.equals(suffixObjectOp)) { + barf(parser, "Unsupported suffix pattern: " + suffixObjectOp); + } + + final JsonToken suffixEqualsIgnoreCase = parser.nextToken(); + if (suffixEqualsIgnoreCase != JsonToken.VALUE_STRING) { + barf(parser, "equals-ignore-case match pattern must be a string"); + } + final Patterns pattern = Patterns.suffixEqualsIgnoreCaseMatch(parser.getText() + '"'); + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + if (parser.nextToken() != JsonToken.END_OBJECT) { + barf(parser, "Only one key allowed in match expression"); + } + return pattern; + } + private static Patterns processAnythingButListMatchExpression(JsonParser parser) throws JsonParseException { JsonToken token; Set values = new HashSet<>(); diff --git a/src/main/software/amazon/event/ruler/Ruler.java b/src/main/software/amazon/event/ruler/Ruler.java index 9cbd28d..eea8192 100644 --- a/src/main/software/amazon/event/ruler/Ruler.java +++ b/src/main/software/amazon/event/ruler/Ruler.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.annotation.concurrent.Immutable; @@ -110,6 +111,22 @@ private static boolean matches(final JsonNode val, final Patterns pattern) { valuePattern = (ValuePatterns) pattern; return val.isTextual() && ('"' + val.asText()).startsWith(valuePattern.pattern()); + case PREFIX_EQUALS_IGNORE_CASE: + valuePattern = (ValuePatterns) pattern; + return val.isTextual() && ('"' + val.asText().toLowerCase(Locale.ROOT)) + .startsWith(valuePattern.pattern().toLowerCase(Locale.ROOT)); + case SUFFIX: + valuePattern = (ValuePatterns) pattern; + // Undoes the reverse on the pattern value to match against the provided value + return val.isTextual() && (val.asText() + '"') + .endsWith(new StringBuilder(valuePattern.pattern()).reverse().toString()); + + case SUFFIX_EQUALS_IGNORE_CASE: + valuePattern = (ValuePatterns) pattern; + // Undoes the reverse on the pattern value to match against the provided value + return val.isTextual() && (val.asText().toLowerCase(Locale.ROOT) + '"') + .endsWith(new StringBuilder(valuePattern.pattern().toLowerCase(Locale.ROOT)).reverse().toString()); + case ANYTHING_BUT: assert (pattern instanceof AnythingBut); AnythingBut anythingButPattern = (AnythingBut) pattern; diff --git a/src/main/software/amazon/event/ruler/input/DefaultParser.java b/src/main/software/amazon/event/ruler/input/DefaultParser.java index ae5469b..22f05dd 100644 --- a/src/main/software/amazon/event/ruler/input/DefaultParser.java +++ b/src/main/software/amazon/event/ruler/input/DefaultParser.java @@ -7,7 +7,9 @@ import static software.amazon.event.ruler.MatchType.ANYTHING_BUT_SUFFIX; import static software.amazon.event.ruler.MatchType.EQUALS_IGNORE_CASE; import static software.amazon.event.ruler.MatchType.ANYTHING_BUT_IGNORE_CASE; +import static software.amazon.event.ruler.MatchType.PREFIX_EQUALS_IGNORE_CASE; import static software.amazon.event.ruler.MatchType.SUFFIX; +import static software.amazon.event.ruler.MatchType.SUFFIX_EQUALS_IGNORE_CASE; import static software.amazon.event.ruler.MatchType.WILDCARD; /** @@ -39,15 +41,18 @@ public class DefaultParser implements MatchTypeParser, ByteParser { private final WildcardParser wildcardParser; private final EqualsIgnoreCaseParser equalsIgnoreCaseParser; private final SuffixParser suffixParser; + private final SuffixEqualsIgnoreCaseParser suffixEqualsIgnoreCaseParser; DefaultParser() { - this(new WildcardParser(), new EqualsIgnoreCaseParser(), new SuffixParser()); + this(new WildcardParser(), new EqualsIgnoreCaseParser(), new SuffixParser(), new SuffixEqualsIgnoreCaseParser()); } - DefaultParser(WildcardParser wildcardParser, EqualsIgnoreCaseParser equalsIgnoreCaseParser, SuffixParser suffixParser) { + DefaultParser(WildcardParser wildcardParser, EqualsIgnoreCaseParser equalsIgnoreCaseParser, SuffixParser suffixParser, + SuffixEqualsIgnoreCaseParser suffixEqualsIgnoreCaseParser) { this.wildcardParser = wildcardParser; this.equalsIgnoreCaseParser = equalsIgnoreCaseParser; this.suffixParser = suffixParser; + this.suffixEqualsIgnoreCaseParser = suffixEqualsIgnoreCaseParser; } public static DefaultParser getParser() { @@ -58,10 +63,12 @@ public static DefaultParser getParser() { public InputCharacter[] parse(final MatchType type, final String value) { if (type == WILDCARD) { return wildcardParser.parse(value); - } else if (type == EQUALS_IGNORE_CASE || type == ANYTHING_BUT_IGNORE_CASE) { + } else if (type == EQUALS_IGNORE_CASE || type == ANYTHING_BUT_IGNORE_CASE || type == PREFIX_EQUALS_IGNORE_CASE) { return equalsIgnoreCaseParser.parse(value); } else if (type == SUFFIX || type == ANYTHING_BUT_SUFFIX) { return suffixParser.parse(value); + } else if (type == SUFFIX_EQUALS_IGNORE_CASE) { + return suffixEqualsIgnoreCaseParser.parse(value); } final byte[] utf8bytes = value.getBytes(StandardCharsets.UTF_8); diff --git a/src/main/software/amazon/event/ruler/input/EqualsIgnoreCaseParser.java b/src/main/software/amazon/event/ruler/input/EqualsIgnoreCaseParser.java index 54a76c3..3b8f7bf 100644 --- a/src/main/software/amazon/event/ruler/input/EqualsIgnoreCaseParser.java +++ b/src/main/software/amazon/event/ruler/input/EqualsIgnoreCaseParser.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.function.Function; /** * A parser to be used specifically for equals-ignore-case rules. For Java characters where lower and upper case UTF-8 @@ -22,10 +23,14 @@ public class EqualsIgnoreCaseParser { EqualsIgnoreCaseParser() { } public InputCharacter[] parse(String value) { + return parse(value, false); + } + + protected InputCharacter[] parse(String value, boolean reverseCharBytes) { List result = new ArrayList<>(value.length()); for (char c : value.toCharArray()) { - byte[] lowerCaseUtf8bytes = String.valueOf(c).toLowerCase(Locale.ROOT).getBytes(StandardCharsets.UTF_8); - byte[] upperCaseUtf8bytes = String.valueOf(c).toUpperCase(Locale.ROOT).getBytes(StandardCharsets.UTF_8); + byte[] lowerCaseUtf8bytes = getCharUtfBytes(c, (ch) -> ch.toLowerCase(Locale.ROOT), reverseCharBytes); + byte[] upperCaseUtf8bytes = getCharUtfBytes(c, (ch) -> ch.toUpperCase(Locale.ROOT), reverseCharBytes); if (Arrays.equals(lowerCaseUtf8bytes, upperCaseUtf8bytes)) { for (int i = 0; i < lowerCaseUtf8bytes.length; i++) { result.add(new InputByte(lowerCaseUtf8bytes[i])); @@ -39,4 +44,20 @@ public InputCharacter[] parse(String value) { } return result.toArray(new InputCharacter[0]); } + + private static byte[] getCharUtfBytes(char c, Function stringTransformer, boolean reverseCharBytes) { + byte[] byteArray = stringTransformer.apply(String.valueOf(c)).getBytes(StandardCharsets.UTF_8); + if (reverseCharBytes) { + return reverseByteArray(byteArray); + } + return byteArray; + } + + private static byte[] reverseByteArray(byte[] byteArray) { + byte[] reversedByteArray = new byte[byteArray.length]; + for (int i = 0; i < byteArray.length; i++) { + reversedByteArray[i] = byteArray[byteArray.length - i - 1]; + } + return reversedByteArray; + } } \ No newline at end of file diff --git a/src/main/software/amazon/event/ruler/input/MultiByte.java b/src/main/software/amazon/event/ruler/input/MultiByte.java index 4ea8cf9..7b3c32e 100644 --- a/src/main/software/amazon/event/ruler/input/MultiByte.java +++ b/src/main/software/amazon/event/ruler/input/MultiByte.java @@ -15,6 +15,12 @@ public class MultiByte { public static final byte MAX_FIRST_BYTE_FOR_ONE_BYTE_CHAR = (byte) 0x7F; public static final byte MIN_FIRST_BYTE_FOR_TWO_BYTE_CHAR = (byte) 0xC2; public static final byte MAX_FIRST_BYTE_FOR_TWO_BYTE_CHAR = (byte) 0xDF; + + /** + * A continuation byte is a byte that is not the first UTF-8 byte in a multibyte character. + */ + public static final byte MIN_CONTINUATION_BYTE = (byte) 0x80; + public static final byte MAX_CONTINUATION_BYTE = (byte) 0xBF; public static final byte MAX_NON_FIRST_BYTE = (byte) 0xBF; private final byte[] bytes; diff --git a/src/main/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParser.java b/src/main/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParser.java new file mode 100644 index 0000000..cee99c4 --- /dev/null +++ b/src/main/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParser.java @@ -0,0 +1,19 @@ +package software.amazon.event.ruler.input; + +/** + * A parser to be used specifically for suffix equals-ignore-case rules. + * + * This extends EqualsIgnoreCaseParser to parse and reverse char bytes into InputMultiByteSet + * to account for lower-case and upper-case variants. + * + */ +public class SuffixEqualsIgnoreCaseParser extends EqualsIgnoreCaseParser { + + SuffixEqualsIgnoreCaseParser() { } + + public InputCharacter[] parse(String value) { + // By using EqualsIgnoreCaseParser, we reverse chars in one pass when getting the char bytes for + // lower-case and upper-case values. + return parse(value, true); + } +} \ No newline at end of file diff --git a/src/test/software/amazon/event/ruler/ACMachineTest.java b/src/test/software/amazon/event/ruler/ACMachineTest.java index 9c1bd2a..d40edcd 100644 --- a/src/test/software/amazon/event/ruler/ACMachineTest.java +++ b/src/test/software/amazon/event/ruler/ACMachineTest.java @@ -350,6 +350,94 @@ public void testPrefixMatching() throws Exception { assertEquals(2, rules.size()); } + @Test + public void testPrefixEqualsIgnoreCase() throws Exception { + String rule1 = "{ \"a\" : [ { \"prefix\": { \"equals-ignore-case\" : \"zoo\" } } ] }"; + String rule2 = "{ \"b\" : [ { \"prefix\": { \"equals-ignore-case\" : \"child\" } } ] }"; + Machine machine = new Machine(); + machine.addRule("r1", rule1); + machine.addRule("r2", rule2); + String[] events = { + "{\"a\": \"zOokeeper\"}", + "{\"a\": \"Zoo\"}", + "{\"b\": \"cHildlike\"}", + "{\"b\": \"chIldish\"}", + "{\"b\": \"childhood\"}" + }; + for (String event : events) { + List rules = machine.rulesForJSONEvent(event); + assertEquals(1, rules.size()); + if (event.contains("\"a\"")) { + assertEquals("r1", rules.get(0)); + } else { + assertEquals("r2", rules.get(0)); + } + } + + machine = new Machine(); + String rule3 = "{ \"a\" : [ { \"prefix\": { \"equals-ignore-case\" : \"al\" } } ] }"; + String rule4 = "{ \"a\" : [ \"ALbert\" ] }"; + machine.addRule("r3", rule3); + machine.addRule("r4", rule4); + String e2 = "{ \"a\": \"ALbert\"}"; + List rules = machine.rulesForJSONEvent(e2); + assertEquals(2, rules.size()); + } + + @Test + public void testSuffixEqualsIgnoreCase() throws Exception { + String rule1 = "{ \"a\" : [ { \"suffix\": { \"equals-ignore-case\" : \"eper\" } } ] }"; + String rule2 = "{ \"b\" : [ { \"suffix\": { \"equals-ignore-case\" : \"hood\" } } ] }"; + Machine machine = new Machine(); + machine.addRule("r1", rule1); + machine.addRule("r2", rule2); + String[] events = { + "{\"a\": \"zookeePer\"}", + "{\"a\": \"Gatekeeper\"}", + "{\"b\": \"hOod\"}", + "{\"b\": \"parenthOod\"}", + "{\"b\": \"brotherhood\"}", + "{\"b\": \"childhOoD\"}" + }; + for (String event : events) { + List rules = machine.rulesForJSONEvent(event); + assertEquals(1, rules.size()); + if (event.contains("\"a\"")) { + assertEquals("r1", rules.get(0)); + } else { + assertEquals("r2", rules.get(0)); + } + } + + machine = new Machine(); + String rule3 = "{ \"a\" : [ { \"suffix\": { \"equals-ignore-case\" : \"ert\" } } ] }"; + String rule4 = "{ \"a\" : [ \"AlbeRT\" ] }"; + machine.addRule("r3", rule3); + machine.addRule("r4", rule4); + String e2 = "{ \"a\": \"AlbeRT\"}"; + List rules = machine.rulesForJSONEvent(e2); + assertEquals(2, rules.size()); + } + + @Test + public void testSuffixEqualsIgnoreCaseChineseMatch() throws Exception { + Machine m = new Machine(); + String rule = "{\n" + + " \"status\": {\n" + + " \"weatherText\": [{\"suffix\": \"统治者\"}]\n" + + " }\n" + + "}"; + String eventStr ="{\n" + + " \"status\": {\n" + + " \"weatherText\": \"事件统治者\",\n" + + " \"pm25\": 23\n" + + " }\n" + + "}"; + m.addRule("r1", rule); + List matchRules = m.rulesForJSONEvent(eventStr); + assertEquals(1, matchRules.size()); + } + @Test public void testSuffixChineseMatch() throws Exception { Machine m = new Machine(); @@ -1697,6 +1785,70 @@ public void testAddAndDeleteTwoRulesSameCaseInsensitivePatternEqualsIgnoreCase() assertTrue(machine.isEmpty()); } + @Test + public void testAddAndDeleteTwoRulesSameCaseInsensitivePatternPrefixEqualsIgnoreCase() throws Exception { + final Machine machine = new Machine(); + String event = "{\n" + + " \"x\": \"yay\"\n" + + "}"; + + String rule1 = "{\n" + + " \"x\": [ { \"prefix\": { \"equals-ignore-case\": \"y\" } } ]\n" + + "}"; + + String rule2 = "{\n" + + " \"x\": [ { \"prefix\": { \"equals-ignore-case\": \"Y\" } } ]\n" + + "}"; + + machine.addRule("rule1", rule1); + machine.addRule("rule2", rule2); + + List found = machine.rulesForJSONEvent(event); + assertEquals(2, found.size()); + assertTrue(found.contains("rule1")); + assertTrue(found.contains("rule2")); + + machine.deleteRule("rule1", rule1); + found = machine.rulesForJSONEvent(event); + assertEquals(1, found.size()); + machine.deleteRule("rule2", rule2); + found = machine.rulesForJSONEvent(event); + assertEquals(0, found.size()); + assertTrue(machine.isEmpty()); + } + + @Test + public void testAddAndDeleteTwoRulesSameCaseInsensitivePatternSuffixEqualsIgnoreCase() throws Exception { + final Machine machine = new Machine(); + String event = "{\n" + + " \"x\": \"yay\"\n" + + "}"; + + String rule1 = "{\n" + + " \"x\": [ { \"suffix\": { \"equals-ignore-case\": \"y\" } } ]\n" + + "}"; + + String rule2 = "{\n" + + " \"x\": [ { \"suffix\": { \"equals-ignore-case\": \"Y\" } } ]\n" + + "}"; + + machine.addRule("rule1", rule1); + machine.addRule("rule2", rule2); + + List found = machine.rulesForJSONEvent(event); + assertEquals(2, found.size()); + assertTrue(found.contains("rule1")); + assertTrue(found.contains("rule2")); + + machine.deleteRule("rule1", rule1); + found = machine.rulesForJSONEvent(event); + assertEquals(1, found.size()); + machine.deleteRule("rule2", rule2); + found = machine.rulesForJSONEvent(event); + assertEquals(0, found.size()); + assertTrue(machine.isEmpty()); + } + @Test public void testDuplicateKeyLastOneWins() throws Exception { final Machine machine = new Machine(); diff --git a/src/test/software/amazon/event/ruler/Benchmarks.java b/src/test/software/amazon/event/ruler/Benchmarks.java index b7453d1..eb9be40 100644 --- a/src/test/software/amazon/event/ruler/Benchmarks.java +++ b/src/test/software/amazon/event/ruler/Benchmarks.java @@ -128,6 +128,35 @@ public class Benchmarks { }; private final int[] PREFIX_MATCHES = { 24, 442, 38, 2387, 328 }; + private final String[] PREFIX_EQUALS_IGNORE_CASE_RULES = { + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"prefix\": { \"equals-ignore-case\": \"Ac\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"prefix\": { \"equals-ignore-case\": \"bL\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"prefix\": { \"equals-ignore-case\": \"dR\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"prefix\": { \"equals-ignore-case\": \"Fu\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"prefix\": { \"equals-ignore-case\": \"rH\" } } ]\n" + + " }\n" + + "}" + }; + private final int[] PREFIX_EQUALS_IGNORE_CASE_MATCHES = { 24, 442, 38, 2387, 328 }; + private final String[] SUFFIX_RULES = { "{\n" + " \"properties\": {\n" + @@ -157,6 +186,35 @@ public class Benchmarks { }; private final int[] SUFFIX_MATCHES = { 17921, 871, 13, 1963, 682 }; + private final String[] SUFFIX_EQUALS_IGNORE_CASE_RULES = { + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"suffix\": { \"equals-ignore-case\": \"oN\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"suffix\": { \"equals-ignore-case\": \"Ke\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"suffix\": { \"equals-ignore-case\": \"mM\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"suffix\": { \"equals-ignore-case\": \"InG\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"properties\": {\n" + + " \"STREET\": [ { \"suffix\": { \"equals-ignore-case\": \"gO\" } } ]\n" + + " }\n" + + "}" + }; + private final int[] SUFFIX_EQUALS_IGNORE_CASE_MATCHES = { 17921, 871, 13, 1963, 682 }; + private final String[] EQUALS_IGNORE_CASE_RULES = { "{\n" + " \"properties\": {\n" + @@ -302,10 +360,10 @@ public class Benchmarks { " \"properties\": {\n" + " \"STREET\": [ { \"anything-but\": {\"equals-ignore-case\": [ \"Fulton\" ] } } ]\n" + " }\n" + - "}", + "}", "{\n" + " \"properties\": {\n" + - " \"STREET\": [ { \"anything-but\": {\"equals-ignore-case\": [ \"Mason\" ] } } ]\n" + + " \"STREET\": [ { \"anything-but\": {\"equals-ignore-case\": [ \"Mason\" ] } } ]\n" + " }\n" + "}", "{\n" + @@ -321,9 +379,9 @@ public class Benchmarks { "{\n" + " \"properties\": {\n" + " \"FROM_ST\": [ { \"anything-but\": {\"equals-ignore-case\": [ \"441\" ] } } ]\n" + - " }\n" + - "}" - }; + " }\n" + + "}" + }; private final int[] ANYTHING_BUT_IGNORE_CASE_MATCHES = { 211158, 210411, 96682, 120, 210615 }; @@ -588,12 +646,24 @@ public void CL2Benchmark() throws Exception { bm = new Benchmarker(); + bm.addRules(PREFIX_EQUALS_IGNORE_CASE_RULES, PREFIX_EQUALS_IGNORE_CASE_MATCHES); + bm.run(citylots2); + System.out.println("PREFIX_EQUALS_IGNORE_CASE_RULES events/sec: " + String.format("%.1f", bm.getEPS())); + + bm = new Benchmarker(); + bm.addRules(SUFFIX_RULES, SUFFIX_MATCHES); bm.run(citylots2); System.out.println("SUFFIX events/sec: " + String.format("%.1f", bm.getEPS())); bm = new Benchmarker(); + bm.addRules(SUFFIX_EQUALS_IGNORE_CASE_RULES, SUFFIX_EQUALS_IGNORE_CASE_MATCHES); + bm.run(citylots2); + System.out.println("SUFFIX_EQUALS_IGNORE_CASE_RULES events/sec: " + String.format("%.1f", bm.getEPS())); + + bm = new Benchmarker(); + bm.addRules(EQUALS_IGNORE_CASE_RULES, EQUALS_IGNORE_CASE_MATCHES); bm.run(citylots2); System.out.println("EQUALS_IGNORE_CASE events/sec: " + String.format("%.1f", bm.getEPS())); diff --git a/src/test/software/amazon/event/ruler/ByteMachineTest.java b/src/test/software/amazon/event/ruler/ByteMachineTest.java index 9075456..861171c 100644 --- a/src/test/software/amazon/event/ruler/ByteMachineTest.java +++ b/src/test/software/amazon/event/ruler/ByteMachineTest.java @@ -1071,6 +1071,504 @@ public void testEqualsIgnoreCaseWithExactMatchLeadingCharacterSameLowerAndUpperC ); } + @Test + public void testPrefixEqualsIgnoreCasePattern() { + String[] noMatches = new String[] { "", "JAV", "jav", "ava", "AVA", "xJAVA", "xjava", "jAV", "AVa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("jAVa"), + "java", "JAVA", "Java", "jAvA", "jAVa", "JaVa", "JAVAx", "javax", "JaVaaaaaaa") + ); + } + + @Test + public void testPrefixEqualsIgnoreCasePatternWithExactMatchAsPrefix() { + String[] noMatches = new String[] { "", "jA", "Ja", "JAV", "jav", "ava", "AVA", "xJAVA", "xjava", "jAV", "AVa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("jAVa"), + "java", "JAVA", "Java", "jAvA", "jAVa", "JaVa", "JAVAx", "javax", "JaVaaaaaaa"), + new PatternMatch(Patterns.exactMatch("ja"), + "ja") + ); + } + + @Test + public void testPrefixEqualsIgnoreCasePatternWithExactMatchAsPrefixLengthOneLess() { + String[] noMatches = new String[] { "", "JAV" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("jAVa"), + "java", "jAVa", "JavA", "JAVA", "javax"), + new PatternMatch(Patterns.exactMatch("jav"), + "jav") + ); + } + + @Test + public void testPrefixEqualsIgnoreCasePatternNonLetterCharacters() { + String[] noMatches = new String[] { "", "2#$^sS我ŐaBc", "1#%^sS我ŐaBc", "1#$^sS大ŐaBc", "1#$^sS我ŏaBc" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("1#$^sS我ŐaBc"), + "1#$^sS我ŐaBcd", "1#$^Ss我ŐAbCaaaaa", "1#$^Ss我ŐAbC我") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCase() { + String[] noMatches = new String[] { "", "12a34", "12A34" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("12ⱥ34"), + "12ⱥ34", "12Ⱥ34", "12ⱥ3478", "12Ⱥ34aa", "12Ⱥ34ȺȺ") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCase() { + String[] noMatches = new String[] { "", "12a34", "12A34" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("12Ⱥ34"), + "12ⱥ34", "12Ⱥ34", "12ⱥ3478", "12Ⱥ34aa", "12Ⱥ34ⱥȺ") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCaseAtStartOfString() { + String[] noMatches = new String[] { "", "a12", "A12" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("ⱥ12"), + "ⱥ12", "Ⱥ12", "ⱥ1234", "Ⱥ12ab", "ⱥ12ⱥⱥ", "Ⱥ12ⱥⱥ") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCaseAtStartOfString() { + String[] noMatches = new String[] { "", "a12", "A12" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("Ⱥ12"), + "ⱥ12", "Ⱥ12", "ⱥ1234", "Ⱥ12ab", "ⱥ12ⱥⱥ", "Ⱥ12ⱥⱥ") + ); + } + + + @Test + public void testPrefixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCaseAtEndOfString() { + String[] noMatches = new String[] { "", "12a", "12A" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("12ⱥ"), + "12ⱥ", "12Ⱥ", "12ⱥⱥⱥⱥⱥ", "12ȺȺȺȺ", "12ȺⱥⱥȺⱥⱥȺ") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCaseAtEndOfString() { + String[] noMatches = new String[] { "", "12a", "12A" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("12Ⱥ"), + "12ⱥ", "12Ⱥ", "12ⱥⱥⱥⱥⱥ", "12ȺȺȺȺ", "12ȺⱥⱥȺⱥⱥȺ") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseManyCharactersWithDifferentByteLengthForLowerCaseAndUpperCase() { + String[] noMatches = new String[] { "", "Ϋ́ȿⱯΐΫ́Η͂k", "ΰⱾɐΪ́ΰῆK" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("ΰɀɐΐΰῆK"), + "Ϋ́ɀⱯΐΫ́Η͂k", "ΰⱿɐΪ́ΰῆK", "Ϋ́ⱿⱯΪ́Ϋ́ῆk", "ΰɀɐΐΰΗ͂K", "Ϋ́ɀⱯΐΫ́Η͂kÄ́ɀⱯΐΫ́Η͂", "ΰⱿɐΪ́ΰῆKä́ɀⱯΐΫ́Η͂") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseMiddleCharacterWithDifferentByteLengthForLowerCaseAndUpperCaseWithPrefixMatches() { + String[] noMatches = new String[] { "", "a", "aa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("abȺcd"), + "abⱥcd", "abȺcd", "abⱥcdea", "abȺcdCCC"), + new PatternMatch(Patterns.prefixMatch("ab"), + "ab", "abⱥ", "abȺ", "abⱥcd", "abȺcd", "abⱥcdea", "abȺcdCCC"), + new PatternMatch(Patterns.prefixMatch("abⱥ"), + "abⱥ", "abⱥcd", "abⱥcdea"), + new PatternMatch(Patterns.prefixMatch("abȺ"), + "abȺ", "abȺcd", "abȺcdCCC") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseLastCharacterWithDifferentByteLengthForLowerCaseAndUpperCaseWithPrefixMatches() { + String[] noMatches = new String[] { "", "ab" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "abcⱥaa", "abcȺbb"), + new PatternMatch(Patterns.prefixMatch("abc"), + "abc", "abca", "abcA", "abcⱥ", "abcȺ", "abcⱥaa", "abcȺbb"), + new PatternMatch(Patterns.prefixMatch("abca"), + "abca"), + new PatternMatch(Patterns.prefixMatch("abcA"), + "abcA") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseFirstCharacterWithDifferentByteLengthForCasesWithLowerCasePrefixMatch() { + String[] noMatches = new String[] { "", "ⱥ", "Ⱥ", "c" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("ⱥc"), + "ⱥc", "Ⱥc", "ⱥcd"), + new PatternMatch(Patterns.prefixMatch("ⱥc"), + "ⱥc", "ⱥcd") + ); + } + + + @Test + public void testPrefixEqualsIgnoreCaseFirstCharacterWithDifferentByteLengthForCasesWithUpperCasePrefixMatch() { + String[] noMatches = new String[] { "", "ⱥ", "Ⱥ", "c" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("Ⱥc"), + "ⱥc", "Ⱥc", "ⱥcdddd", "Ⱥcd"), + new PatternMatch(Patterns.prefixMatch("Ⱥc"), + "Ⱥc", "Ⱥcd") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseWhereLowerAndUpperCaseAlreadyExist() { + String[] noMatches = new String[] { "", "a", "b" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixMatch("ab"), + "ab", "abc", "abC"), + new PatternMatch(Patterns.prefixMatch("AB"), + "AB", "ABC", "ABc"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("ab"), + "ab", "AB", "Ab", "aB", "ab", "abc", "abC", "AB", "ABC", "ABc") + ); + } + + + + @Test + public void testPrefixEqualsIgnoreCasePatternMultipleWithMultipleExactMatch() { + String[] noMatches = new String[] { "", "he", "HEL", "hell", "HELL"}; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("hElLo"), + "hello", "hellox", "HeLlOx", "hElLoX", "HELLOX", "HELLO", "HeLlO", "hElLo"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("HeLlOX"), + "hellox", "HELLOX", "HeLlOx", "hElLoX"), + new PatternMatch(Patterns.exactMatch("hello"), + "hello"), + new PatternMatch(Patterns.exactMatch("HELLO"), + "HELLO"), + new PatternMatch(Patterns.exactMatch("hel"), + "hel") + ); + } + + @Test + public void testPrefixEqualsIgnoreCasePatternMultipleWithMultipleEqualsIgnoreCaseMatch() { + String[] noMatches = new String[] { "", "he", "hell", "HELL" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("hElLo"), + "hello", "hellox", "HELLOX", "HeLlOx", "hElLoX", "helloxx"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("HeLlOX"), + "hellox", "HELLOX", "HeLlOx", "hElLoX", "helloxx"), + new PatternMatch(Patterns.equalsIgnoreCaseMatch("hello"), + "hello"), + new PatternMatch(Patterns.equalsIgnoreCaseMatch("hel"), + "hel", "HEL") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseWithExactMatchLeadingCharacterSameLowerAndUpperCase() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("!b"), + "!b", "!B", "!bcd", "!BCdE"), + new PatternMatch(Patterns.exactMatch("!a"), + "!a") + ); + } + + @Test + public void testPrefixEqualsIgnoreCaseWithPrefixMatchLeadingCharacterSameLowerAndUpperCase() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("!b"), + "!b", "!B", "!bcd", "!BCdE"), + new PatternMatch(Patterns.prefixMatch("!a"), + "!a") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePattern() { + String[] noMatches = new String[] { "", "JAV", "jav", "ava", "AVA", "JAVAx", "javax", "jAV", "AVa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("jAVa"), + "java", "JAVA", "Java", "jAvA", "jAVa", "JaVa", "helloJAVA", "hijava", "jjjjjjJaVa") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePatternWithReverseExactMatchAsSuffix() { + String[] noMatches = new String[] { "", "jA", "Ja", "JAV", "jav", "ava", "AVA", "JAVAx", "javax", "jAV", "AVa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("jAVa"), + "java", "JAVA", "Java", "jAvA", "jAVa", "JaVa", "xJAVA", "xjava", "jjJJJJaVa"), + new PatternMatch(Patterns.exactMatch("av"), + "av") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePatternWithReverseExactMatchAsSuffixLengthOneLess() { + String[] noMatches = new String[] { "", "JAV" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("jAVa"), + "java", "jAVa", "JavA", "JAVA", "xjava"), + new PatternMatch(Patterns.exactMatch("ava"), + "ava") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePatternNonLetterCharacters() { + String[] noMatches = new String[] { "", "1#$^sS我ŐaBd", "1#%^sS我ŐaBc", "1#$^sS大ŐaBc", "1#$^sS我ŏaBc" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("1#$^sS我ŐaBc"), + "aa1#$^sS我ŐaBc", "1111#$^Ss我ŐAbC", "我我1#$^Ss我ŐAbC") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCase() { + String[] noMatches = new String[] { "", "12a34", "12A34" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("12ⱥ34"), + "12ⱥ34", "12Ⱥ34", "7812ⱥ34", "aa12Ⱥ34", "ȺȺ12Ⱥ34") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCase() { + String[] noMatches = new String[] { "", "12a34", "12A34" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("12Ⱥ34"), + "12ⱥ34", "12Ⱥ34", "7812ⱥ34", "aa12Ⱥ34", "ⱥȺ12Ⱥ34") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCaseAtStartOfString() { + String[] noMatches = new String[] { "", "a12", "A12" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("ⱥ12"), + "ⱥ12", "Ⱥ12", "34ⱥ12", "abȺ12", "ⱥⱥⱥ12", "ⱥⱥȺ12") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCaseAtStartOfString() { + String[] noMatches = new String[] { "", "a12", "A12" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("Ⱥ12"), + "ⱥ12", "Ⱥ12", "34ⱥ12", "abȺ12", "ⱥⱥⱥ12", "ⱥⱥȺ12") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseLowerCaseCharacterWithDifferentByteLengthForUpperCaseAtEndOfString() { + String[] noMatches = new String[] { "", "12a", "12A" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("12ⱥ"), + "12ⱥ", "12Ⱥ", "ⱥⱥⱥⱥ12ⱥ", "ȺȺȺ12Ⱥ", "ⱥⱥȺⱥⱥȺ12Ⱥ") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseUpperCaseCharacterWithDifferentByteLengthForLowerCaseAtEndOfString() { + String[] noMatches = new String[] { "", "12a", "12A" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("12Ⱥ"), + "12ⱥ", "12Ⱥ", "ⱥⱥⱥⱥ12ⱥ", "ȺȺȺ12Ⱥ", "ⱥⱥȺⱥⱥȺ12Ⱥ") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseManyCharactersWithDifferentByteLengthForLowerCaseAndUpperCase() { + String[] noMatches = new String[] { "", "Ϋ́ȿⱯΐΫ́Η͂k", "ΰⱾɐΪ́ΰῆK" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("ΰɀɐΐΰῆK"), + "Ϋ́ɀⱯΐΫ́Η͂k", "ΰⱿɐΪ́ΰῆK", "Ϋ́ⱿⱯΪ́Ϋ́ῆk", "ΰɀɐΐΰΗ͂K", "Ä́ɀⱯΐΫ́Η͂Ϋ́ɀⱯΐΫ́Η͂k", "ä́ɀⱯΐΫ́Η͂ΰⱿɐΪ́ΰῆK") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseMiddleCharacterWithDifferentByteLengthForLowerCaseAndUpperCaseWithSuffixMatches() { + String[] noMatches = new String[] { "", "a", "aa" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("abȺcd"), + "abⱥcd", "abȺcd", "eaabⱥcd", "CCCabȺcd"), + new PatternMatch(Patterns.suffixMatch("cd"), + "cd", "ⱥcd", "Ⱥcd", "abⱥcd", "abȺcd", "abⱥcd", "eaabⱥcd", "CCCabȺcd"), + new PatternMatch(Patterns.suffixMatch("ⱥcd"), + "ⱥcd", "abⱥcd", "eaabⱥcd"), + new PatternMatch(Patterns.suffixMatch("Ⱥcd"), + "Ⱥcd", "abȺcd", "CCCabȺcd") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseLastCharacterWithDifferentByteLengthForLowerCaseAndUpperCaseWithSuffixMatches() { + String[] noMatches = new String[] { "", "ab" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "AbcȺ", "aaabcⱥ", "bbabcȺ"), + new PatternMatch(Patterns.suffixMatch("bcȺ"), + "bcȺ", "abcȺ", "AbcȺ", "ⱥbcȺ", "bbabcȺ"), + new PatternMatch(Patterns.suffixMatch("abca"), + "abca"), + new PatternMatch(Patterns.suffixMatch("abcA"), + "abcA") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseFirstCharacterWithDifferentByteLengthForCasesWithLowerCaseSuffixMatch() { + String[] noMatches = new String[] { "", "ⱥ", "Ⱥ", "c" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("ⱥc"), + "ⱥc", "Ⱥc", "dⱥc"), + new PatternMatch(Patterns.suffixMatch("ⱥc"), + "ⱥc", "dⱥc") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseFirstCharacterWithDifferentByteLengthForCasesWithUpperCaseSuffixMatch() { + String[] noMatches = new String[] { "", "ⱥ", "Ⱥ", "c" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("Ⱥc"), + "ⱥc", "Ⱥc", "ddddⱥc", "dȺc"), + new PatternMatch(Patterns.suffixMatch("Ⱥc"), + "Ⱥc", "dȺc") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWhereLowerAndUpperCaseAlreadyExist() { + String[] noMatches = new String[] { "", "a", "b" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixMatch("ab"), + "ab", "cab", "Cab"), + new PatternMatch(Patterns.suffixMatch("AB"), + "AB", "CAB", "cAB"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("ab"), + "ab", "AB", "Ab", "aB", "ab", "cab", "Cab", "AB", "CAB", "cAB") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePatternMultipleWithMultipleExactMatch() { + String[] noMatches = new String[] { "", "he", "HEL", "hell", "HELL"}; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("hElLo"), + "hello", "xhello", "xHeLlO", "XhElLo", "XHELLO", "HELLO", "HeLlO", "hElLo"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("XHeLlO"), + "xhello", "XHELLO", "xHeLlO", "XhElLo"), + new PatternMatch(Patterns.exactMatch("hello"), + "hello"), + new PatternMatch(Patterns.exactMatch("HELLO"), + "HELLO"), + new PatternMatch(Patterns.exactMatch("hel"), + "hel") + ); + } + + @Test + public void testSuffixEqualsIgnoreCasePatternMultipleWithMultipleEqualsIgnoreCaseMatch() { + String[] noMatches = new String[] { "", "he", "hell", "HELL" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("hElLo"), + "hello", "xhello", "XHELLO", "xHeLlO", "XhElLo", "xxhello"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("XHeLlO"), + "xhello", "XHELLO", "xHeLlO", "XhElLo", "xxhello"), + new PatternMatch(Patterns.equalsIgnoreCaseMatch("hello"), + "hello"), + new PatternMatch(Patterns.equalsIgnoreCaseMatch("hel"), + "hel", "HEL") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithExactMatchLeadingCharacterSameLowerAndUpperCase() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("b!"), + "b!", "B!", "cdb!", "CdEB!"), + new PatternMatch(Patterns.exactMatch("!a"), + "!a") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithPrefixMatchLeadingCharacterSameLowerAndUpperCase() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixMatch("!b"), + "!b"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("b!"), + "b!", "B!", "cdb!", "CdEB!"), + new PatternMatch(Patterns.prefixMatch("!a"), + "!a") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithWildcardMatchBeingAddedLater() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("b!"), + "b!", "B!", "cdb!", "CdEB!"), + new PatternMatch(Patterns.wildcardMatch("*b"), + "!b", "b") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithExistingWildcardMatch() { + String[] noMatches = new String[] { "", "!", "!A", "a", "A", "b", "B" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.wildcardMatch("*b"), + "!b", "b"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("b!"), + "b!", "B!", "cdb!", "CdEB!") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithPrefixEqualsIgnoreCaseMatchBeingAddedLater() { + String[] noMatches = new String[] { "", "ab", "bcȺ", "bcⱥ" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "AbcȺ", "aaabcⱥ", "bbabcȺ", "ⱥcbaabcȺ", "ⱥcbaabcⱥ"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("Ⱥcba"), + "ⱥcba", "Ⱥcba", "Ⱥcba", "ⱥcbaaa", "Ⱥcbabb", "ⱥcbaabcȺ", "ⱥcbaabcⱥ"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "AbcȺ", "abcⱥaa", "abcȺbb") + ); + } + + @Test + public void testSuffixEqualsIgnoreCaseWithExistingPrefixEqualsIgnoreCaseMatch() { + String[] noMatches = new String[] { "", "ab", "bcȺ", "bcⱥ" }; + testPatternPermutations(noMatches, + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("Ⱥcba"), + "ⱥcba", "Ⱥcba", "Ⱥcba", "ⱥcbaaa", "Ⱥcbabb", "ⱥcbaabcȺ", "ⱥcbaabcⱥ"), + new PatternMatch(Patterns.prefixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "AbcȺ", "abcⱥaa", "abcȺbb"), + new PatternMatch(Patterns.suffixEqualsIgnoreCaseMatch("abcȺ"), + "abcⱥ", "abcȺ", "AbcȺ", "aaabcⱥ", "bbabcȺ", "ⱥcbaabcȺ", "ⱥcbaabcⱥ") + ); + } + @Test public void testWildcardSingleWildcardCharacter() { testPatternPermutations( diff --git a/src/test/software/amazon/event/ruler/JsonRuleCompilerTest.java b/src/test/software/amazon/event/ruler/JsonRuleCompilerTest.java index f8b14c4..e7198d3 100644 --- a/src/test/software/amazon/event/ruler/JsonRuleCompilerTest.java +++ b/src/test/software/amazon/event/ruler/JsonRuleCompilerTest.java @@ -30,6 +30,18 @@ public void testBigNumbers() throws Exception { assertEquals(1, m.rulesForJSONEvent(event).size()); } + @Test + public void testPrefixEqualsIgnoreCaseCompile() { + String json = "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"child\" } } ] }"; + assertNull("Good prefix equals-ignore-case should parse", JsonRuleCompiler.check(json)); + } + + @Test + public void testSuffixEqualsIgnoreCaseCompile() { + String json = "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"child\" } } ] }"; + assertNull("Good suffix equals-ignore-case should parse", JsonRuleCompiler.check(json)); + } + @Test public void testVariantForms() throws Exception { Machine m = new Machine(); @@ -172,6 +184,18 @@ public void testCompile() throws Exception { "{\"a\": [ { \"anything-but\": {\"equals-ignore-case\": [1, 2, 3] } } ] }", // no numbers "{\"a\": [ { \"equals-ignore-case\": 5 } ] }", "{\"a\": [ { \"equals-ignore-case\": [ \"abc\" ] } ] }", + "{\"a\": [ { \"prefix\": { \"invalid-expression\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": 5 } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"abc\", \"test\": \"def\" } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"abc\" }, \"test\": \"def\" } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": [ 1, 2 3 ] } } ] }", + "{\"a\": [ { \"suffix\": { \"invalid-expression\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": 5 } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"abc\", \"test\": \"def\" } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"abc\" }, \"test\": \"def\" } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": [ 1, 2 3 ] } } ] }", "{\"a\": [ { \"wildcard\": 5 } ] }", "{\"a\": [ { \"wildcard\": [ \"abc\" ] } ] }" }; diff --git a/src/test/software/amazon/event/ruler/RuleCompilerTest.java b/src/test/software/amazon/event/ruler/RuleCompilerTest.java index d569ede..8946f06 100644 --- a/src/test/software/amazon/event/ruler/RuleCompilerTest.java +++ b/src/test/software/amazon/event/ruler/RuleCompilerTest.java @@ -40,6 +40,18 @@ public void testBigNumbers() throws Exception { assertEquals(1, m.rulesForJSONEvent(event).size()); } + @Test + public void testPrefixEqualsIgnoreCaseCompile() { + String json = "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"child\" } } ] }"; + assertNull("Good prefix equals-ignore-case should parse", RuleCompiler.check(json)); + } + + @Test + public void testSuffixEqualsIgnoreCaseCompile() { + String json = "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"child\" } } ] }"; + assertNull("Good suffix equals-ignore-case should parse", RuleCompiler.check(json)); + } + @Test public void testVariantForms() throws Exception { Machine m = new Machine(); @@ -181,6 +193,18 @@ public void testCompile() throws Exception { "{\"a\": [ { \"anything-but\": {\"equals-ignore-case\": [1, 2, 3] } } ] }", // no numbers allowed "{\"a\": [ { \"equals-ignore-case\": 5 } ] }", "{\"a\": [ { \"equals-ignore-case\": [ \"abc\" ] } ] }", + "{\"a\": [ { \"prefix\": { \"invalid-expression\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": 5 } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"abc\", \"test\": \"def\" } } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": \"abc\" }, \"test\": \"def\" } ] }", + "{\"a\": [ { \"prefix\": { \"equals-ignore-case\": [ 1, 2 3 ] } } ] }", + "{\"a\": [ { \"suffix\": { \"invalid-expression\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": 5 } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": [ \"abc\" ] } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"abc\", \"test\": \"def\" } } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": \"abc\" }, \"test\": \"def\" } ] }", + "{\"a\": [ { \"suffix\": { \"equals-ignore-case\": [ 1, 2 3 ] } } ] }", "{\"a\": [ { \"wildcard\": 5 } ] }", "{\"a\": [ { \"wildcard\": [ \"abc\" ] } ] }" }; diff --git a/src/test/software/amazon/event/ruler/RulerTest.java b/src/test/software/amazon/event/ruler/RulerTest.java index 3645ee1..a673fd2 100644 --- a/src/test/software/amazon/event/ruler/RulerTest.java +++ b/src/test/software/amazon/event/ruler/RulerTest.java @@ -110,6 +110,21 @@ public void WHEN_RulesFromReadmeAreTried_THEN_TheyWork() throws Exception { " \"state\": [ { \"anything-but\": { \"prefix\": \"init\" } } ]\n" + " }\n" + "}", + "{\n" + + " \"detail\": {\n" + + " \"state\": [ { \"prefix\": { \"equals-ignore-case\": \"RuNn\" } } ]\n" + + " }\n" + + "}", + "{\n" + + " \"detail\": {\n" + + " \"state\": [ { \"suffix\": \"ning\" } ]\n" + + " }\n" + + "}", + "{\n" + + " \"detail\": {\n" + + " \"state\": [ { \"suffix\": { \"equals-ignore-case\": \"nInG\" } } ]\n" + + " }\n" + + "}", "{\n" + " \"detail\": {\n" + " \"source-ip\": [ { \"cidr\": \"10.0.0.0/24\" } ]\n" + @@ -151,6 +166,21 @@ public void WHEN_WeWriteRulesToMatchVariousFieldCombos_THEN_TheyWork() throws Ex " }\n" + " }\n" + " }", + " {\n" + + " \"Image\": {\n" + + " \"Title\": [ { \"prefix\": { \"equals-ignore-case\": \"VIeW\" } } ]\n" + + " }\n" + + " }", + " {\n" + + " \"Image\": {\n" + + " \"Title\": [ { \"suffix\": { \"equals-ignore-case\": \"LoOr\" } } ]\n" + + " }\n" + + " }", + " {\n" + + " \"Image\": {\n" + + " \"Title\": [ { \"suffix\": \"loor\" } ]\n" + + " }\n" + + " }", " {\n" + " \"Image\": {\n" + " \"Width\": [ { \"numeric\": [ \"<\", 1000 ] } ],\n" + diff --git a/src/test/software/amazon/event/ruler/input/ParserTest.java b/src/test/software/amazon/event/ruler/input/ParserTest.java index f69e299..173142f 100644 --- a/src/test/software/amazon/event/ruler/input/ParserTest.java +++ b/src/test/software/amazon/event/ruler/input/ParserTest.java @@ -25,14 +25,14 @@ public void testParseString() { @Test public void testOtherMatchTypes() { - final int[] parserInvokedCount = { 0, 0, 0 }; + final int[] parserInvokedCount = { 0, 0, 0, 0 }; DefaultParser parser = new DefaultParser( new WildcardParser() { - @Override - public InputCharacter[] parse(String value) { - parserInvokedCount[0] +=1; - return null; - } + @Override + public InputCharacter[] parse(String value) { + parserInvokedCount[0] +=1; + return null; + } }, new EqualsIgnoreCaseParser() { @Override @@ -47,6 +47,13 @@ public InputCharacter[] parse(String value) { parserInvokedCount[2] += 1; return null; } + }, + new SuffixEqualsIgnoreCaseParser() { + @Override + public InputCharacter[] parse(String value) { + parserInvokedCount[3] += 1; + return null; + } } ); @@ -54,15 +61,24 @@ public InputCharacter[] parse(String value) { assertEquals(parserInvokedCount[0], 1); assertEquals(parserInvokedCount[1], 0); assertEquals(parserInvokedCount[2], 0); + assertEquals(parserInvokedCount[3], 0); assertNull(parser.parse(MatchType.EQUALS_IGNORE_CASE, "abc")); assertEquals(parserInvokedCount[0], 1); assertEquals(parserInvokedCount[1], 1); assertEquals(parserInvokedCount[2], 0); + assertEquals(parserInvokedCount[3], 0); assertNull(parser.parse(MatchType.SUFFIX, "abc")); assertEquals(parserInvokedCount[0], 1); assertEquals(parserInvokedCount[1], 1); assertEquals(parserInvokedCount[2], 1); + assertEquals(parserInvokedCount[3], 0); + + assertNull(parser.parse(MatchType.SUFFIX_EQUALS_IGNORE_CASE, "abc")); + assertEquals(parserInvokedCount[0], 1); + assertEquals(parserInvokedCount[1], 1); + assertEquals(parserInvokedCount[2], 1); + assertEquals(parserInvokedCount[3], 1); } } diff --git a/src/test/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParserTest.java b/src/test/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParserTest.java new file mode 100644 index 0000000..3be900b --- /dev/null +++ b/src/test/software/amazon/event/ruler/input/SuffixEqualsIgnoreCaseParserTest.java @@ -0,0 +1,57 @@ +package software.amazon.event.ruler.input; + +import org.junit.Before; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; + +import static org.junit.Assert.assertArrayEquals; + +public class SuffixEqualsIgnoreCaseParserTest { + + private SuffixEqualsIgnoreCaseParser parser; + + @Before + public void setup() { + parser = new SuffixEqualsIgnoreCaseParser(); + } + + @Test + public void testParseSimpleString() { + assertArrayEquals(new InputCharacter[] { + set(new MultiByte((byte) 97), new MultiByte((byte) 65)), + set(new MultiByte((byte) 98), new MultiByte((byte) 66)), + set(new MultiByte((byte) 99), new MultiByte((byte) 67)), + }, parser.parse("aBc")); + } + + @Test + public void testParseSimpleStringWithNonLetters() { + assertArrayEquals(new InputCharacter[] { + set(new MultiByte((byte) 97), new MultiByte((byte) 65)), + new InputByte((byte) 49), + set(new MultiByte((byte) 98), new MultiByte((byte) 66)), + new InputByte((byte) 50), + set(new MultiByte((byte) 99), new MultiByte((byte) 67)), + new InputByte((byte) 33), + }, parser.parse("a1B2c!")); + } + + @Test + public void testParseStringWithSingleBytesMultiBytesCharactersNonCharactersAndDifferingLengthMultiBytes() { + assertArrayEquals(new InputCharacter[] { + new InputByte((byte) 49), + set(new MultiByte((byte) 97), new MultiByte((byte) 65)), + set(new MultiByte((byte) 97), new MultiByte((byte) 65)), + new InputByte((byte) 42), + new InputByte((byte) -96), new InputByte((byte) -128), new InputByte((byte) -30), + set(new MultiByte((byte) -119, (byte) -61), new MultiByte((byte) -87, (byte) -61)), + set(new MultiByte((byte) -70, (byte) -56), new MultiByte((byte) -91, (byte) -79, (byte) -30)), + }, parser.parse("1aA*†Éⱥ")); + } + + private static InputMultiByteSet set(MultiByte ... multiBytes) { + return new InputMultiByteSet(new HashSet<>(Arrays.asList(multiBytes))); + } +} \ No newline at end of file