From c41fe6ff96de6a2b1ae1e005976b2987713b7af0 Mon Sep 17 00:00:00 2001 From: schlathoeltt Date: Fri, 8 Sep 2023 15:01:52 +0200 Subject: [PATCH] fix(#341): Allow comma and brackets in matcher expressions --- src/manual/validation-hamcrest.adoc | 11 ++++++++ .../hamcrest/HamcrestValidationMatcher.java | 25 ++++++++++++++++--- .../HamcrestValidationMatcherTest.java | 21 ++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/manual/validation-hamcrest.adoc b/src/manual/validation-hamcrest.adoc index 48b919db02..8aa7814e77 100644 --- a/src/manual/validation-hamcrest.adoc +++ b/src/manual/validation-hamcrest.adoc @@ -29,6 +29,17 @@ receive("someEndpoint") .expression("node-set:/TestRequest/OrderType", hasSize(3)); ---- +NOTE: If you want to match text containing any of the following characters: ' , ( ) +You need to enclose the respective string in quotation marks when defining your matcher. +If you intend to match an actual single quote, it should be escaped with a backslash (\'). + +For example: +[source,xml] +---- +anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString())) +anyOf(equalTo('text containing a backslash and quote \\\\' and a , (comma) '), anyOf(isEmptyOrNullString())) +---- + .XML [source,xml,indent=0,role="secondary"] ---- 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 c700738fb6..e18b7c5c76 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 @@ -192,7 +192,7 @@ private Matcher getMatcher(String matcherName, String[] matcherParameter, Tes } if (matcherMethod != null) { - return (Matcher) matcherMethod.invoke(null, matcherParameter[0]); + return (Matcher) matcherMethod.invoke(null, matcherParameter[0].replace("\\'","'")); } } @@ -457,7 +457,22 @@ public String toString() { */ private static class Tokenizer { - private static final Pattern TEXT_PARAMETER_PATTERN = Pattern.compile("\\(('[^']*')\\)|('[^']*')"); + /** + * Regular expression with three alternative parts (ORed) to match: + *
    + *
  1. `(sometext)` - Quoted parameter block of a matcher.
  2. + *
  3. 'sometext' - Quoted text used as a parameter to a string matcher.
  4. + *
  5. (unquotedtext) - Unquoted text used as a parameter to a string matcher. This expression is non-greedy, meaning the first closing bracket will terminate the match.
  6. + *
+ *

+ * Please note: + * - 'sometext' may contain an escaped quote. + * - 'unquotedtext' must not contain brackets or commas. + *

+ * To match quotes, commas, or brackets, you must quote the text. To match a quote, it should be escaped with a backslash. + */ + private static final Pattern TEXT_PARAMETER_PATTERN = Pattern.compile("\\('(?((?:[^']|\\\\')*))[^\\\\]'\\)|('(?((?:[^']|\\\\')*))[^\\\\]')|\\((?(([^']|\\\\')*?)[^\\\\])\\)"); + private final List originalTokenValues = new ArrayList<>(); @@ -468,10 +483,13 @@ private static class Tokenizer { * @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); + String param = matcher.group("quoted1"); + param = param != null ? param : matcher.group("quoted2"); + param = param != null ? param : matcher.group("unquoted"); originalTokenValues.add(param); tokenizedExpression = tokenizedExpression.replace(param, "$$" + originalTokenValues.size() + "$$"); } @@ -487,7 +505,6 @@ public String tokenize(String rawExpression) { */ 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], originalTokenValues).trim()); 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 570e8aa63e..973ca7b4fd 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 @@ -131,7 +131,17 @@ public Object[][] testData() { 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)'))")}, + new Object[]{"foo", "text-equalTo(QA, Max", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", null, Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "QA-equalTo(HH), Max", Collections.singletonList("anyOf(equalTo('QA-equalTo(HH), Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "text containing a ' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "text containing a \\' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\\\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "unquoted text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(unquoted text may not include brackets or commas), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "unquoted \\' text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(unquoted \\\\' text may not include brackets or commas), 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)'))")}, }; @@ -196,7 +206,14 @@ public Object[][] testDataFailed() { new Object[]{"foo", "[value1,value2]", Collections.singletonList("hasItem(value5)") }, new Object[]{"foo", "[value1,value2]", Collections.singletonList("hasItems(value1,value2,value5)") }, new Object[]{"foo", "[value1,value2]", Collections.singletonList("contains(value1)") }, - new Object[]{"foo", "[value1,value2]", Collections.singletonList("containsInAnyOrder(value2,value4)") } + new Object[]{"foo", "[value1,value2]", Collections.singletonList("containsInAnyOrder(value2,value4)") }, + new Object[]{"foo", "notext-equalTo(QA, Max", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "aa", Collections.singletonList("anyOf(equalTo('text-equalTo(QA, Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "VA-equalTo(HH), Max", Collections.singletonList("anyOf(equalTo('QA-equalTo(HH), Max'),anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "notext containing a ' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "notext containing a \\' (quote) and a , (comma) ", Collections.singletonList("anyOf(equalTo('text containing a \\\\' (quote) and a , (comma) '), anyOf(isEmptyOrNullString()))")}, + new Object[]{"foo", "nounquoted text may not include brackets or commas", Collections.singletonList("anyOf(equalTo(unquoted text may not include brackets or commas), anyOf(isEmptyOrNullString()))")}, + }; } }