diff --git a/core/citrus-api/pom.xml b/core/citrus-api/pom.xml
index b78b36ff2b..a7d5698b02 100644
--- a/core/citrus-api/pom.xml
+++ b/core/citrus-api/pom.xml
@@ -13,6 +13,15 @@
Citrus :: Core :: API
Citrus API and basic interfaces
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+
diff --git a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionParameterHelper.java b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionParameterHelper.java
index 355f9d3a02..4bcbfc8174 100644
--- a/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionParameterHelper.java
+++ b/core/citrus-api/src/main/java/org/citrusframework/functions/FunctionParameterHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2010 the original author or authors.
+ * Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,80 +16,111 @@
package org.citrusframework.functions;
-import java.util.ArrayList;
import java.util.List;
-import java.util.StringTokenizer;
+import java.util.Stack;
/**
* Helper class parsing a parameter string and converting the tokens to a parameter list.
- *
+ *
* @author Christoph Deppisch
*/
public final class FunctionParameterHelper {
-
+
/**
* Prevent class instantiation.
*/
private FunctionParameterHelper() {}
-
+
/**
* Convert a parameter string to a list of parameters.
- *
+ *
* @param parameterString comma separated parameter string.
* @return list of parameters.
*/
public static List getParameterList(String parameterString) {
- List parameterList = new ArrayList<>();
+ return new ParameterParser(parameterString).parse();
+ }
+
+ public static class ParameterParser {
+
+ private final String parameterString;
+ private final Stack parameterList = new Stack<>();
+ private String currentParameter = "";
+ private int lastQuoteIndex = -1;
+ private boolean isBetweenParams = false;
- StringTokenizer tok = new StringTokenizer(parameterString, ",");
- while (tok.hasMoreElements()) {
- String param = tok.nextToken().trim();
- parameterList.add(cutOffSingleQuotes(param));
+ public ParameterParser(String parameterString) {
+ this.parameterString = parameterString;
}
- List postProcessed = new ArrayList<>();
- for (int i = 0; i < parameterList.size(); i++) {
- int next = i + 1;
-
- String processed = parameterList.get(i);
-
- if (processed.startsWith("'") && !processed.endsWith("'")) {
- while (next < parameterList.size()) {
- if (parameterString.contains(processed + ", " + parameterList.get(next))) {
- processed += ", " + parameterList.get(next);
- } else if (parameterString.contains(processed + "," + parameterList.get(next))) {
- processed += "," + parameterList.get(next);
- } else if (parameterString.contains(processed + " , " + parameterList.get(next))) {
- processed += " , " + parameterList.get(next);
- } else {
- processed += parameterList.get(next);
- }
-
- i++;
- if (parameterList.get(next).endsWith("'")) {
- break;
- } else {
- next++;
- }
- }
+ public List parse() {
+ parameterList.clear();
+ for (int i = 0; i < parameterString.length(); i++) {
+ parseCharacterAt(i);
+ }
+ return parameterList.stream().toList();
+ }
+ private void parseCharacterAt(int i) {
+ char c = parameterString.charAt(i);
+ if (isParameterSeparatingComma(c)) {
+ isBetweenParams = true;
+ addCurrentParamIfNotEmpty();
+ } else if (isNestedSingleQuote(c)) {
+ lastQuoteIndex = i;
+ appendCurrentValueToLastParameter();
+ } else if (isStartingSingleQuote(c)) {
+ isBetweenParams = false;
+ lastQuoteIndex = i;
+ } else if (isSingleQuote(c)) { // closing quote
+ addCurrentParamIfNotEmpty();
+ } else {
+ if (isBetweenParams && !String.valueOf(c).matches("\\s")) isBetweenParams = false;
+ if (!isBetweenParams) currentParameter += c;
+ }
+ if (isLastChar(i)) { // TestFramework!
+ addCurrentParamIfNotEmpty();
}
+ }
- postProcessed.add(cutOffSingleQuotes(processed));
+ private void appendCurrentValueToLastParameter() {
+ currentParameter = "%s'%s'".formatted(parameterList.pop(), currentParameter);
}
- return postProcessed;
- }
+ private boolean isLastChar(int i) {
+ return i == parameterString.length() - 1;
+ }
+
+ private boolean isNestedSingleQuote(char c) {
+ return isSingleQuote(c) && isNotWithinSingleQuotes() && !currentParameter.trim().isEmpty();
+ }
- private static String cutOffSingleQuotes(String param) {
- if (param.equals("'")) {
- return "";
+ private boolean isStartingSingleQuote(char c) {
+ return isSingleQuote(c) && isNotWithinSingleQuotes();
}
- if (param.length() > 1 && param.charAt(0) == '\'' && param.charAt(param.length()-1) == '\'') {
- return param.substring(1, param.length()-1);
+ private boolean isParameterSeparatingComma(char c) {
+ return isComma(c) && isNotWithinSingleQuotes();
}
- return param;
+ private boolean isComma(char c) {
+ return c == ',';
+ }
+
+ private boolean isNotWithinSingleQuotes() {
+ return lastQuoteIndex < 0;
+ }
+
+ private static boolean isSingleQuote(char c) {
+ return c == '\'';
+ }
+
+ private void addCurrentParamIfNotEmpty() {
+ if (!currentParameter.replaceAll("^'|'$", "").isEmpty()) {
+ parameterList.add(currentParameter);
+ }
+ lastQuoteIndex = -1;
+ currentParameter = "";
+ }
}
}
diff --git a/core/citrus-api/src/test/java/org/citrusframework/functions/FunctionParameterHelperTest.java b/core/citrus-api/src/test/java/org/citrusframework/functions/FunctionParameterHelperTest.java
new file mode 100644
index 0000000000..e421f41e5d
--- /dev/null
+++ b/core/citrus-api/src/test/java/org/citrusframework/functions/FunctionParameterHelperTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2006-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.functions;
+
+import org.citrusframework.functions.FunctionParameterHelper.ParameterParser;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.citrusframework.functions.FunctionParameterHelper.getParameterList;
+
+class FunctionParameterHelperTest {
+
+ @Test
+ void shouldOneParam() {
+ var result = getParameterList("lorem");
+ assertThat(result).containsExactly("lorem");
+ }
+
+ @Test
+ void shouldTwoParam() {
+ var result = getParameterList("lorem, ipsum");
+ assertThat(result).containsExactly("lorem", "ipsum");
+ }
+
+ @Test
+ void shouldTwoParam_oneQuoted() {
+ var result = getParameterList("lorem, 'ipsum'");
+ assertThat(result).containsExactly("lorem", "ipsum");
+ }
+
+ @Test
+ void shouldTwoParam_withCommaInParam() {
+ var result = getParameterList("'lorem, dolor', 'ipsum'");
+ assertThat(result).containsExactly("lorem, dolor", "ipsum");
+ }
+
+ @Test
+ void shouldTwoParam_withLinebreak() {
+ var result = getParameterList("'lorem, dolor', 'ipsum\n sit'");
+ assertThat(result).containsExactly("lorem, dolor", "ipsum\n sit");
+ }
+
+ @Test
+ void shouldTwoParam_withLinebreakAfterComma() {
+ var result = getParameterList("'lorem,\n dolor', 'ipsum sit'");
+ assertThat(result).containsExactly("lorem,\n dolor", "ipsum sit");
+ }
+
+ @Test
+ void shouldTwoParam_withWhitespacesAfterComma() {
+ var result = getParameterList("'lorem, dolor', 'ipsum sit'");
+ assertThat(result).containsExactly("lorem, dolor", "ipsum sit");
+ }
+
+ @Test
+ void shouldConvertSingleLineJson() {
+ String json = """
+ {"myValues": ["O15o3a8","PhDjdSruZgG"]}""";
+ var result = getParameterList(wrappedInSingleQuotes(json));
+ assertThat(result).containsExactly(json);
+ }
+
+ @Test
+ void shouldConvertMultiLineJson() {
+ // language=JSON
+ String json = """
+ {
+ "id": 133,
+ "myValues": [
+ "O15o3a8",
+ "PhDjdSruZgG",
+ "I2qrC1Mu, PmSsd8LPLe"
+ ]
+ }""";
+ var result = getParameterList(wrappedInSingleQuotes(json));
+ assertThat(result).containsExactly(json);
+ }
+
+ @Test
+ void shouldConvertNestedSingleQuotedStrings() {
+ // language=JSON
+ String json = """
+ ["part of first param", "also 'part' of first param"]""";
+ var result = getParameterList(wrappedInSingleQuotes(json));
+ assertThat(result).hasSize(1).containsExactly(json);
+ }
+
+ @Test
+ void shouldConvertIdempotent() {
+ // language=JSON
+ String json = """
+ ["part of first param", "also 'part' of first param"]""";
+
+ var parser = new ParameterParser(wrappedInSingleQuotes(json));
+ var result1 = parser.parse();
+ var result2 = parser.parse();
+
+ assertThat(result1).isEqualTo(result2).hasSize(1).containsExactly(json);
+ }
+
+ @Test
+ void cannotConvertSpecialNestedSingleQuotedStrings() {
+ String threeParams = """
+ '["part of first param", "following comma will be missing ',' should also be first param"]', 'lorem', ipsum""";
+ var parser = new ParameterParser(threeParams);
+ var result = parser.parse();
+ assertThat(result).containsExactly(
+ "[\"part of first param\", \"following comma will be missing ",
+ " should also be first param\"]",
+ "lorem",
+ "ipsum"
+ );
+ }
+
+ private static String wrappedInSingleQuotes(String parameterString) {
+ return "'%s'".formatted(parameterString);
+ }
+}
diff --git a/core/citrus-base/pom.xml b/core/citrus-base/pom.xml
index 8535110be8..1b058d6599 100644
--- a/core/citrus-base/pom.xml
+++ b/core/citrus-base/pom.xml
@@ -53,6 +53,12 @@
groovy-xml
test
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
diff --git a/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionUtilsTest.java b/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionUtilsTest.java
index 0612ad58f0..2fb22ac4ec 100644
--- a/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionUtilsTest.java
+++ b/core/citrus-base/src/test/java/org/citrusframework/functions/FunctionUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2010 the original author or authors.
+ * Copyright 2006-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,16 +16,24 @@
package org.citrusframework.functions;
-import java.util.Collections;
-
import org.citrusframework.UnitTestSupport;
import org.citrusframework.exceptions.InvalidFunctionUsageException;
import org.citrusframework.exceptions.NoSuchFunctionException;
import org.citrusframework.exceptions.NoSuchFunctionLibraryException;
import org.citrusframework.functions.core.CurrentDateFunction;
-import org.testng.Assert;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.citrusframework.functions.FunctionUtils.resolveFunction;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
/**
* @author Christoph Deppisch
*/
@@ -33,9 +41,9 @@ public class FunctionUtilsTest extends UnitTestSupport {
@Test
public void testResolveFunction() {
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello', ' TestFramework!')", context), "Hello TestFramework!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('citrus', ':citrus')", context), "citrus:citrus");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('citrus:citrus')", context), "citrus:citrus");
+ assertEquals(resolveFunction("citrus:concat('Hello',' TestFramework!')", context), "Hello TestFramework!");
+ assertEquals(resolveFunction("citrus:concat('citrus', ':citrus')", context), "citrus:citrus");
+ assertEquals(resolveFunction("citrus:concat('citrus:citrus')", context), "citrus:citrus");
}
@Test
@@ -43,15 +51,15 @@ public void testWithVariables() {
context.setVariable("greeting", "Hello");
context.setVariable("text", "TestFramework!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello', ' ', ${text})", context), "Hello TestFramework!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat(${greeting}, ' ', ${text})", context), "Hello TestFramework!");
+ assertEquals(resolveFunction("citrus:concat('Hello', ' ', ${text})", context), "Hello TestFramework!");
+ assertEquals(resolveFunction("citrus:concat(${greeting}, ' ', ${text})", context), "Hello TestFramework!");
}
@Test
public void testWithNestedFunctions() {
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat(citrus:currentDate('yyyy-mm-dd'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context));
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Now is: ', citrus:currentDate('yyyy-mm-dd'))", context), "Now is: " + new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context));
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat(citrus:currentDate('yyyy-mm-dd'), ' ', citrus:concat('Hello', ' TestFramework!'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context) + " Hello TestFramework!");
+ assertEquals(resolveFunction("citrus:concat(citrus:currentDate('yyyy-mm-dd'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context));
+ assertEquals(resolveFunction("citrus:concat('Now is: ', citrus:currentDate('yyyy-mm-dd'))", context), "Now is: " + new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context));
+ assertEquals(resolveFunction("citrus:concat(citrus:currentDate('yyyy-mm-dd'), ' ', citrus:concat('Hello', ' TestFramework!'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context) + " Hello TestFramework!");
}
@Test
@@ -59,41 +67,93 @@ public void testWithNestedFunctionsAndVariables() {
context.setVariable("greeting", "Hello");
context.setVariable("dateFormat", "yyyy-mm-dd");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat(citrus:currentDate('${dateFormat}'), ' ', citrus:concat(${greeting}, ' TestFramework!'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context) + " Hello TestFramework!");
+ assertEquals(resolveFunction("citrus:concat(citrus:currentDate('${dateFormat}'), ' ', citrus:concat(${greeting}, ' TestFramework!'))", context), new CurrentDateFunction().execute(Collections.singletonList("yyyy-mm-dd"), context) + " Hello TestFramework!");
}
@Test
public void testWithCommaValue() {
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat(citrus:upperCase(Yes), ' ', citrus:upperCase(I like Citrus!))", context), "YES I LIKE CITRUS!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Monday, Tuesday, wednesday')", context), "MONDAY, TUESDAY, WEDNESDAY");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Monday, Tuesday', ' Wednesday')", context), "Monday, Tuesday Wednesday");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Yes, I like Citrus!)", context), "'YES, I LIKE CITRUS!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase(''Yes, I like Citrus!)", context), "''YES, I LIKE CITRUS!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase(Yes I like Citrus!')", context), "YES I LIKE CITRUS!'");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Yes, I like Citrus!')", context), "YES, I LIKE CITRUS!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Yes, I like Citrus, and this is great!')", context), "YES, I LIKE CITRUS, AND THIS IS GREAT!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Yes,I like Citrus!')", context), "YES,I LIKE CITRUS!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:upperCase('Yes', 'I like Citrus!')", context), "YES");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes, I like Citrus!')", context), "Hello Yes, I like Citrus!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes,I like Citrus!')", context), "Hello Yes,I like Citrus!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes,I like Citrus, and this is great!')", context), "Hello Yes,I like Citrus, and this is great!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes , I like Citrus!')", context), "Hello Yes , I like Citrus!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes, I like Citrus!', 'Hello Yes,we like Citrus!')", context), "Hello Yes, I like Citrus!Hello Yes,we like Citrus!");
- Assert.assertEquals(FunctionUtils.resolveFunction("citrus:concat('Hello Yes, I like Citrus, and this is great!', 'Hello Yes,we like Citrus, and this is great!')", context), "Hello Yes, I like Citrus, and this is great!Hello Yes,we like Citrus, and this is great!");
+ assertEquals(resolveFunction("citrus:concat(citrus:upperCase(Yes), ' ', citrus:upperCase(I like Citrus!))", context), "YES I LIKE CITRUS!");
+ assertEquals(resolveFunction("citrus:upperCase('Monday, Tuesday, wednesday')", context), "MONDAY, TUESDAY, WEDNESDAY");
+ assertEquals(resolveFunction("citrus:concat('Monday, Tuesday', ' Wednesday')", context), "Monday, Tuesday Wednesday");
+ assertEquals(resolveFunction("citrus:upperCase('Yes, I like Citrus!')", context), "YES, I LIKE CITRUS!");
+ assertEquals(resolveFunction("citrus:upperCase('Yes, I like Citrus, and this is great!')", context), "YES, I LIKE CITRUS, AND THIS IS GREAT!");
+ assertEquals(resolveFunction("citrus:upperCase('Yes,I like Citrus!')", context), "YES,I LIKE CITRUS!");
+ assertEquals(resolveFunction("citrus:upperCase('Yes', 'I like Citrus!')", context), "YES");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes, I like Citrus!')", context), "Hello Yes, I like Citrus!");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes,I like Citrus!')", context), "Hello Yes,I like Citrus!");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes,I like Citrus, and this is great!')", context), "Hello Yes,I like Citrus, and this is great!");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes , I like Citrus!')", context), "Hello Yes , I like Citrus!");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes, I like Citrus!', 'Hello Yes,we like Citrus!')", context), "Hello Yes, I like Citrus!Hello Yes,we like Citrus!");
+ assertEquals(resolveFunction("citrus:concat('Hello Yes, I like Citrus, and this is great!', 'Hello Yes,we like Citrus, and this is great!')", context), "Hello Yes, I like Citrus, and this is great!Hello Yes,we like Citrus, and this is great!");
+
+// assertEquals(resolveFunction("citrus:upperCase(''Yes, I like Citrus!)", context), "''YES, I LIKE CITRUS!");
+// assertEquals(resolveFunction("citrus:upperCase('Yes, I like Citrus!)", context), "'YES, I LIKE CITRUS!");
+// assertEquals(resolveFunction("citrus:upperCase(Yes I like Citrus!')", context), "YES I LIKE CITRUS!'");
}
@Test(expectedExceptions = {InvalidFunctionUsageException.class})
public void testInvalidFunction() {
- FunctionUtils.resolveFunction("citrus:citrus", context);
+ resolveFunction("citrus:citrus", context);
}
@Test(expectedExceptions = {NoSuchFunctionException.class})
public void testUnknownFunction() {
- FunctionUtils.resolveFunction("citrus:functiondoesnotexist()", context);
+ resolveFunction("citrus:functiondoesnotexist()", context);
}
@Test(expectedExceptions = {NoSuchFunctionLibraryException.class})
public void testUnknownFunctionLibrary() {
- FunctionUtils.resolveFunction("doesnotexist:concat('Hello', ' TestFramework!')", context);
+ resolveFunction("doesnotexist:concat('Hello', ' TestFramework!')", context);
+ }
+
+ @DataProvider
+ public static String[][] validParameterLists() {
+ return new String[][]{
+ {
+ "citrus:concat('{\"lorem\": [\"ipsum\", \"other\"]}')",
+ "{\"lorem\": [\"ipsum\", \"other\"]}"
+ },
+ {
+ // has two spaces here ----------------\/
+ "citrus:concat('{\"lorem\": [\"ipsum\", \"other\"]}')",
+ "{\"lorem\": [\"ipsum\", \"other\"]}"
+ },
+ {
+ // has no space here ----------------\/
+ "citrus:concat('{\"lorem\": [\"ipsum\",\"other\"]}')",
+ "{\"lorem\": [\"ipsum\",\"other\"]}"
+ },
+ {
+ // with linebreak after comma
+ """
+ citrus:upperCase('{
+ "myValues": [
+ "O15o3a8",
+ "PhDjdSruZgG"
+ ]
+ }')
+ """,
+ """
+ {
+ "MYVALUES": [
+ "O15O3A8",
+ "PHDJDSRUZGG"
+ ]
+ }
+ """
+ }
+ };
+ }
+
+ @Test(dataProvider = "validParameterLists")
+ void shouldReplaceWithCommasInValue(String given, String expected) {
+ var contextSpy = spy(context);
+ when(contextSpy.getFunctionRegistry()).thenReturn(spy(context.getFunctionRegistry()));
+ List functionLibraries = List.of(new DefaultFunctionLibrary());
+ when(contextSpy.getFunctionRegistry().getFunctionLibraries()).thenReturn(functionLibraries);
+
+ var result = FunctionUtils.replaceFunctionsInString(given, context, false);
+
+ assertThat(result).isEqualTo(expected);
}
}