Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: parameter list parsing #1131

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/citrus-api/pom.xml
Expand Up @@ -13,6 +13,15 @@
<name>Citrus :: Core :: API</name>
<description>Citrus API and basic interfaces</description>

<dependencies>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand Down
@@ -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.
Expand All @@ -16,9 +16,8 @@

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.
Expand All @@ -34,62 +33,94 @@ private FunctionParameterHelper() {}

/**
* Convert a parameter string to a list of parameters.
*
*
* @param parameterString comma separated parameter string.
* @return list of parameters.
*/
public static List<String> getParameterList(String parameterString) {
List<String> parameterList = new ArrayList<>();
return new ParameterParser(parameterString).parse();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The longer I think about that... wouldn't it be easier to just use a CSV-Parser for that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bbortt
Your thoughts on that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm.. I think CSV uses double quotes for concatenation. that's a bit problematic in the context of Java. other than that, I do too think that it's the same parsing logic.

}

public static class ParameterParser {

private final String parameterString;
private final Stack<String> 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<String> 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<String> 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 static String cutOffSingleQuotes(String param) {
if (param.equals("'")) {
return "";
private boolean isNestedSingleQuote(char c) {
return isSingleQuote(c) && isNotWithinSingleQuotes() && !currentParameter.trim().isEmpty();
}

if (param.length() > 1 && param.charAt(0) == '\'' && param.charAt(param.length()-1) == '\'') {
return param.substring(1, param.length()-1);
private boolean isStartingSingleQuote(char c) {
return isSingleQuote(c) && isNotWithinSingleQuotes();
}

return param;
private boolean isParameterSeparatingComma(char c) {
return isComma(c) && isNotWithinSingleQuotes();
}

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 = "";
}
}
}
@@ -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).contains(json);
}

@Test
void shouldConvertMultiLineJson() {
// language=JSON
String json = """
{
"id": 133,
"myValues": [
"O15o3a8",
"PhDjdSruZgG",
"I2qrC1Mu, PmSsd8LPLe"
]
}""";
var result = getParameterList(wrappedInSingleQuotes(json));
assertThat(result).contains(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);
}
}
6 changes: 6 additions & 0 deletions core/citrus-base/pom.xml
Expand Up @@ -53,6 +53,12 @@
<artifactId>groovy-xml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>