Skip to content

Commit

Permalink
fix: parameter list parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Sterchi Daniel committed Mar 6, 2024
1 parent f676b8e commit 480a2bf
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 75 deletions.
9 changes: 9 additions & 0 deletions core/citrus-api/pom.xml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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();
}

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 = "";
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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>
Loading

0 comments on commit 480a2bf

Please sign in to comment.