Skip to content

Commit

Permalink
[Bug][Seatunnel-web] Escape seatunnel-web placeholders
Browse files Browse the repository at this point in the history
1. An option to escape placeholders in the configuration has been provided.
2. If a placeholder is escaped by adding a backslash (e.g., \${valueParam:defaultValue}), it is passed to the Seatunnel engine as-is without value being replaced in Seatunnel-web.
3. laceholders configured as default values in connectors are escaped before being displayed in the UI.
  • Loading branch information
arshadmohammad committed Oct 10, 2024
1 parent 2515e2b commit bb35af1
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.seatunnel.app.dynamicforms.FormOptionBuilder;
import org.apache.seatunnel.app.dynamicforms.FormStructure;
import org.apache.seatunnel.app.dynamicforms.FormStructureBuilder;
import org.apache.seatunnel.app.dynamicforms.PlaceholderUtil;
import org.apache.seatunnel.app.dynamicforms.validate.ValidateBuilder;
import org.apache.seatunnel.common.constants.PluginType;

Expand Down Expand Up @@ -97,6 +98,17 @@ public static FormStructure wrapper(
return FormOptionSort.sortFormStructure(formStructureBuilder.build());
}

private static Object getDefaultValue(Option<?> option) {
Object defValue = option.defaultValue();
if (defValue == null) {
return null;
}
if (String.class.equals(option.typeReference().getType())) {
return PlaceholderUtil.escapePlaceholders(defValue.toString());
}
return defValue;
}

private static List<AbstractFormOption> wrapperOptionOptions(
@NonNull String connectorName, @NonNull List<Option<?>> optionList, FormLocale locale) {
return optionList.stream()
Expand Down Expand Up @@ -401,10 +413,7 @@ private static AbstractFormOption selectInput(
AbstractFormOption abstractFormOption =
staticSelectOptionBuilder
.formStaticSelectOption()
.withDefaultValue(
option.defaultValue() == null
? null
: option.defaultValue().toString());
.withDefaultValue(getDefaultValue(option));

String placeholderI18nOptionKey = i18nOptionKey + "_description";
if (enableLabelI18n(connectorName, placeholderI18nOptionKey, locale)) {
Expand Down Expand Up @@ -457,7 +466,7 @@ private static AbstractFormOption textInput(
builder.withField(option.key())
.inputOptionBuilder()
.formTextInputOption()
.withDefaultValue(option.defaultValue());
.withDefaultValue(getDefaultValue(option));
if (enableLabelI18n(connectorName, placeholderI18nOptionKey, locale)) {
abstractFormOption = abstractFormOption.withI18nPlaceholder(placeholderI18nOptionKey);
} else {
Expand All @@ -484,7 +493,7 @@ private static AbstractFormOption textareaInput(
.inputOptionBuilder()
.formTextareaInputOption()
.withClearable()
.withDefaultValue(option.defaultValue());
.withDefaultValue(getDefaultValue(option));
if (enableLabelI18n(connectorName, placeholderI18nOptionKey, locale)) {
abstractFormOption = abstractFormOption.withI18nPlaceholder(placeholderI18nOptionKey);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class JobUtils {

// The maximum length of the job execution error message, 4KB
private static final int ERROR_MESSAGE_MAX_LENGTH = 4096;
private static final Pattern placeholderPattern = Pattern.compile("\\$\\{(\\w+)(?::(.*?))?\\}");
private static final Pattern placeholderPattern =
Pattern.compile("(\\\\{0,2})\\$\\{(\\w+)(?::(.*?))?\\}");

public static String getJobInstanceErrorMessage(String message) {
if (message == null) {
Expand Down Expand Up @@ -75,18 +76,26 @@ public static String replaceJobConfigPlaceholders(
(jobExecParam != null && jobExecParam.getPlaceholderValues() != null)
? jobExecParam.getPlaceholderValues()
: Collections.emptyMap();

Matcher matcher = placeholderPattern.matcher(jobConfigString);
StringBuffer result = new StringBuffer();

while (matcher.find()) {
String placeholderName = matcher.group(1);
String replacement = placeholderValues.getOrDefault(placeholderName, matcher.group(2));
String escapeCharacter = matcher.group(1);
String placeholderName = matcher.group(2);

if (escapeCharacter != null && !escapeCharacter.isEmpty()) {
String withoutEscape =
matcher.group().replace("\\\\${", "${").replace("\\${", "${");
matcher.appendReplacement(result, Matcher.quoteReplacement(withoutEscape));
// remove the escape character and continue
continue;
}
String replacement = placeholderValues.getOrDefault(placeholderName, matcher.group(3));
if (replacement == null) {
throw new SeatunnelException(
SeatunnelErrorEnum.JOB_NO_VALUE_FOUND_FOR_PLACEHOLDER, placeholderName);
}
matcher.appendReplacement(result, replacement);
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
}

matcher.appendTail(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ public void testParseConfigWithPlaceHolders() {
assertNotNull(config);
}

@Test
public void testEscapedPlaceholderValuesNotReplaced() {
String jobConfigContent =
"job.mode=\\${jobModeParam:BATCH}\ncheckpoint.interval=\\\\${checkParam:30}\njob.name=${jobNameParam}";
Map<String, String> paramValues = new HashMap<>();
paramValues.put("jobModeParam", "STREAMING");
paramValues.put("jobNameParam", "newJob");
JobExecParam jobExecParam = getJobExecParam(paramValues);

String expected =
"job.mode=${jobModeParam:BATCH}\ncheckpoint.interval=${checkParam:30}\njob.name=newJob";
String actual = JobUtils.replaceJobConfigPlaceholders(jobConfigContent, jobExecParam);

assertEquals(expected, actual);
}

private JobExecParam getJobExecParam(Map<String, String> paramValues) {
JobExecParam jobExecParam = new JobExecParam();
jobExecParam.setPlaceholderValues(paramValues);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.seatunnel.app.dynamicforms;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PlaceholderUtil {

private static final Pattern placeholderPattern =
Pattern.compile("(?<!\\\\)\\$\\{(\\w+)(?::(.*?))?\\}");

/**
* To replace ${paramName:defaultValue} or ${paramName} with \${paramName:defaultValue} and
* \${paramName} respectively.
*
* @param input the input string
* @return the input string with placeholders escaped
*/
public static String escapePlaceholders(String input) {
Matcher matcher = placeholderPattern.matcher(input);
StringBuffer result = new StringBuffer();

while (matcher.find()) {
String placeholder = matcher.group();
matcher.appendReplacement(result, Matcher.quoteReplacement("\\" + placeholder));
}

matcher.appendTail(result);
return result.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.seatunnel.app.dynamicforms;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PlaceholderUtilTest {

@Test
public void testEscapePlaceholders() {
String input = "This is a test string with ${paramName:defaultValue} and ${paramName}.";
String expectedOutput =
"This is a test string with \\${paramName:defaultValue} and \\${paramName}.";
String actualOutput = PlaceholderUtil.escapePlaceholders(input);
assertEquals(expectedOutput, actualOutput);
}

@Test
public void testEscapePlaceholdersWithNoPlaceholders() {
String input = "This is a test string with no placeholders.";
String expectedOutput = "This is a test string with no placeholders.";
String actualOutput = PlaceholderUtil.escapePlaceholders(input);
assertEquals(expectedOutput, actualOutput);
}

@Test
public void testEscapePlaceholdersWithEscapedPlaceholders() {
String input = "This is a test string with \\${paramName:defaultValue} and \\${paramName}.";
String expectedOutput =
"This is a test string with \\${paramName:defaultValue} and \\${paramName}.";
String actualOutput = PlaceholderUtil.escapePlaceholders(input);
assertEquals(expectedOutput, actualOutput);
}

@Test
public void testEscapePlaceholdersWithMixedPlaceholders() {
String input =
"This is a test string with ${paramName:defaultValue}, \\${paramName}, and ${anotherParam}.";
String expectedOutput =
"This is a test string with \\${paramName:defaultValue}, \\${paramName}, and \\${anotherParam}.";
String actualOutput = PlaceholderUtil.escapePlaceholders(input);
assertEquals(expectedOutput, actualOutput);
}
}

0 comments on commit bb35af1

Please sign in to comment.