From 1822612da93d32141fac4c3d729ae00db016dfa7 Mon Sep 17 00:00:00 2001 From: schlathoeltt Date: Fri, 8 Sep 2023 14:44:26 +0200 Subject: [PATCH] fix(#341): Allow comma and brackets in matcher expressions --- .../hamcrest/HamcrestValidationMatcher.java | 98 +++++++++++++++++-- .../HamcrestValidationMatcherTest.java | 14 ++- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java b/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java index fd69b14474..9efb2b05ca 100644 --- a/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java +++ b/validation/citrus-validation-hamcrest/src/main/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcher.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.Optional; import java.util.Properties; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.citrusframework.context.TestContext; @@ -84,11 +85,9 @@ public void validate(String fieldName, String value, List controlParamet } String matcherName = matcherExpression.trim().substring(0, matcherExpression.trim().indexOf("(")); - String[] matcherParameter = matcherExpression.trim().substring(matcherName.length() + 1, matcherExpression.trim().length() - 1).split(","); - for (int i = 0; i < matcherParameter.length; i++) { - matcherParameter[i] = VariableUtils.cutOffSingleQuotes(matcherParameter[i].trim()); - } + String[] matcherParameter = determineNestedMatcherParameters(matcherExpression.trim() + .substring(matcherName.length() + 1, matcherExpression.trim().length() - 1)); try { Matcher matcher = getMatcher(matcherName, matcherParameter, context); @@ -174,8 +173,11 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes List> nestedMatchers = new ArrayList<>(); for (String matcherExpression : matcherParameter) { String nestedMatcherName = matcherExpression.trim().substring(0, matcherExpression.trim().indexOf("(")); - String nestedMatcherParameter = matcherExpression.trim().substring(nestedMatcherName.length() + 1, matcherExpression.trim().length() - 1); - nestedMatchers.add(getMatcher(nestedMatcherName, new String[] { nestedMatcherParameter }, context)); + String[] nestedMatcherParameters = determineNestedMatcherParameters( + matcherExpression.trim().substring(nestedMatcherName.length() + 1, + matcherExpression.trim().length() - 1)); + + nestedMatchers.add(getMatcher(nestedMatcherName, nestedMatcherParameters, context)); } return (Matcher) matcherMethod.invoke(null, nestedMatchers); @@ -362,6 +364,35 @@ public List extractControlValues(String controlExpression, Character del } } + /** + * Extracts parameters for a matcher from the raw parameter expression. + * Parameters refer to the contained parameters and matchers (first level), + * excluding nested ones. + *

+ * For example, given the expression:
+ * {@code "oneOf(greaterThan(5.0), allOf(lessThan(-1.0), greaterThan(-2.0)))"} + *

+ * The extracted parameters are:
+ * {@code "greaterThan(5.0)", "allOf(lessThan(-1.0), greaterThan(-2.0))"}. + *

+ * Note that nested container expressions "allOf(lessThan(-1.0), greaterThan(-2.0))" in + * the second parameter are treated as a single expression. They need to be treated + * separately in a recursive call to this method, when the parameters for the + * respective allOf() expression are extracted. + * + * @param rawExpression the full parameter expression of a container matcher + */ + public String[] determineNestedMatcherParameters(final String rawExpression) { + if (!StringUtils.hasText(rawExpression)) { + return new String[0]; + } + + Tokenizer tokenizer = new Tokenizer(); + String tokenizedExpression = tokenizer.tokenize(rawExpression); + return tokenizer.restoreInto(tokenizedExpression.split(",")); + + } + /** * Numeric value comparable automatically converts types to numeric values for * comparison. @@ -418,4 +449,59 @@ public String toString() { } } + /** + * Class that provides functionality to replace expressions that match + * {@link Tokenizer#TEXT_PARAMETER_PATTERN} with simple tokens of the form $$1$$. + * The reason for this is, that complex nested expressions + * may contain characters that interfere with further processing - e.g. ''', '(' and ')' + */ + private static class Tokenizer { + + + private static final Pattern TEXT_PARAMETER_PATTERN = Pattern.compile("\\(('[^']*')\\)|('[^']*')"); + + private final List params = new ArrayList<>(); + + /** + * Tokenize the given raw expression + * + * @param rawExpression + * @return the expression with all relevant subexpressions replaced by tokens + */ + public String tokenize(String rawExpression) { + String tokenizedExpression = rawExpression; + java.util.regex.Matcher matcher = TEXT_PARAMETER_PATTERN.matcher(rawExpression); + while (matcher.find()) { + String param = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + params.add(param); + tokenizedExpression = tokenizedExpression.replace(param, "$$" + params.size() + "$$"); + } + + return tokenizedExpression; + } + + /** + * Restore the tokens back into the given expressions. + * + * @param expressions containing strings with tokens, generated by this tokenizer. + * @return expressions with the tokens being replaced with their original values. + */ + public String[] restoreInto(String[] expressions) { + + // Replace tokens with real text. + for (int i = 0; i < expressions.length; i++) { + expressions[i] = VariableUtils.cutOffSingleQuotes( + replaceTokens(expressions[i], params).trim()); + } + + return expressions; + } + + private String replaceTokens(String expression, List params) { + for (int i = 0; i < params.size(); i++) { + expression = expression.replace("$$" + (i + 1) + "$$", params.get(i)); + } + return expression; + } + } } diff --git a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java index d850fe748b..570e8aa63e 100644 --- a/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java +++ b/validation/citrus-validation-hamcrest/src/test/java/org/citrusframework/validation/matcher/hamcrest/HamcrestValidationMatcherTest.java @@ -49,7 +49,7 @@ public String getName() { @Override public Matcher provideMatcher(String predicate) { - return new CustomMatcher(String.format("path matching %s", predicate)) { + return new CustomMatcher<>(String.format("path matching %s", predicate)) { @Override public boolean matches(Object item) { return ((item instanceof String) && new AntPathMatcher().match(predicate, (String) item)); @@ -63,7 +63,7 @@ public boolean matches(Object item) { } @Test(dataProvider = "testData") - public void testValidate(String path, String value, List params) throws Exception { + public void testValidate(String path, String value, List params) { validationMatcher.validate( path, value, params, context); } @@ -127,12 +127,18 @@ public Object[][] testData() { new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("contains(value1,value2,value3,value4,value5)") }, new Object[]{"foo", "[value1,value2,value3,value4,value5]", Collections.singletonList("containsInAnyOrder(value2,value4,value1,value3,value5)") }, new Object[]{"foo", "[\"unique_value\",\"different_unique_value\"]", Collections.singletonList("hasSize(2)") }, - new Object[]{"foo", "[\"duplicate_value\",\"duplicate_value\"]", Collections.singletonList("hasSize(2)") } + new Object[]{"foo", "[\"duplicate_value\",\"duplicate_value\"]", Collections.singletonList("hasSize(2)") }, + new Object[]{"foo", "text containing a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "", Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", null, Collections.singletonList("anyOf(equalTo('text containing a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "INSERT INTO todo_entries (id, title, description, done) values (1, 'Invite for meeting', 'Invite the group for a lunch meeting', 'false')", Collections.singletonList("allOf(startsWith('INSERT INTO todo_entries (id, title, description, done)'))")}, + + }; } @Test(dataProvider = "testDataFailed", expectedExceptions = ValidationException.class) - public void testValidateFailed(String path, String value, List params) throws Exception { + public void testValidateFailed(String path, String value, List params) { validationMatcher.validate( path, value, params, context); }