This is an automated email from the ASF dual-hosted git repository.
lidongdai pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new 4c9efd0cc3 [Improve][Core] Support parse quote as key (#8975)
4c9efd0cc3 is described below
commit 4c9efd0cc37f19f1fa88e6036cabf439e3f4c2c1
Author: Jia Fan <[email protected]>
AuthorDate: Sat Mar 15 11:57:21 2025 +0800
[Improve][Core] Support parse quote as key (#8975)
---
seatunnel-config/seatunnel-config-base/pom.xml | 3 +
.../shade/com/typesafe/config/impl/PathParser.java | 3 +-
.../shade/com/typesafe/config/impl/Tokenizer.java | 758 +++++++++++++++++++++
.../org/apache/seatunnel/config/ConfigTest.java | 19 +
.../resources/seatunnel/configWithSpecialKey.conf | 20 +
5 files changed, 802 insertions(+), 1 deletion(-)
diff --git a/seatunnel-config/seatunnel-config-base/pom.xml
b/seatunnel-config/seatunnel-config-base/pom.xml
index 5610cab85e..d5664b690e 100644
--- a/seatunnel-config/seatunnel-config-base/pom.xml
+++ b/seatunnel-config/seatunnel-config-base/pom.xml
@@ -92,6 +92,9 @@
<exclude>com/typesafe/config/impl/ConfigImpl$LoaderCache.class</exclude>
<exclude>com/typesafe/config/impl/ConfigImpl$LoaderCacheHolder.class</exclude>
<exclude>com/typesafe/config/impl/ConfigImpl$SystemPropertiesHolder.class</exclude>
+
<exclude>com/typesafe/config/impl/Tokenizer.class</exclude>
+
<exclude>com/typesafe/config/impl/Tokenizer$TokenIterator.class</exclude>
+
<exclude>com/typesafe/config/impl/Tokenizer$ProblemException.class</exclude>
</excludes>
</filter>
</filters>
diff --git
a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/PathParser.java
b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/PathParser.java
index a0cb50b66f..c0549153e5 100644
---
a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/PathParser.java
+++
b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/PathParser.java
@@ -39,7 +39,8 @@ final class PathParser {
return speculated;
}
try (StringReader reader = new StringReader(path)) {
- Iterator<Token> tokens = Tokenizer.tokenize(API_ORIGIN, reader,
ConfigSyntax.CONF);
+ Iterator<Token> tokens =
+ Tokenizer.tokenize(API_ORIGIN, reader, ConfigSyntax.CONF,
true);
tokens.next(); // drop START
return parsePathExpression(tokens, API_ORIGIN, path);
}
diff --git
a/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/Tokenizer.java
b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/Tokenizer.java
new file mode 100644
index 0000000000..3ecab88b5d
--- /dev/null
+++
b/seatunnel-config/seatunnel-config-shade/src/main/java/org/apache/seatunnel/shade/com/typesafe/config/impl/Tokenizer.java
@@ -0,0 +1,758 @@
+/** Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com> */
+package org.apache.seatunnel.shade.com.typesafe.config.impl;
+
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigException;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigOrigin;
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigSyntax;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+final class Tokenizer {
+ // this exception should not leave this file
+ private static class ProblemException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private final Token problem;
+
+ ProblemException(Token problem) {
+ this.problem = problem;
+ }
+
+ Token problem() {
+ return problem;
+ }
+ }
+
+ private static String asString(int codepoint) {
+ if (codepoint == '\n') {
+ return "newline";
+ } else if (codepoint == '\t') {
+ return "tab";
+ } else if (codepoint == -1) {
+ return "end of file";
+ } else if (ConfigImplUtil.isC0Control(codepoint)) {
+ return String.format("control character 0x%x", codepoint);
+ } else {
+ return String.format("%c", codepoint);
+ }
+ }
+
+ /**
+ * Tokenizes a Reader. Does not close the reader; you have to arrange to
do that after you're
+ * done with the returned iterator.
+ */
+ static Iterator<Token> tokenize(ConfigOrigin origin, Reader input,
ConfigSyntax flavor) {
+ return new TokenIterator(origin, input, flavor != ConfigSyntax.JSON);
+ }
+
+ // Add from SeaTunnel
+ static Iterator<Token> tokenize(
+ ConfigOrigin origin, Reader input, ConfigSyntax flavor, boolean
acceptSpecialText) {
+ return new TokenIterator(origin, input, flavor != ConfigSyntax.JSON,
acceptSpecialText);
+ }
+ // End Add from SeaTunnel
+
+ static String render(Iterator<Token> tokens) {
+ StringBuilder renderedText = new StringBuilder();
+ while (tokens.hasNext()) {
+ renderedText.append(tokens.next().tokenText());
+ }
+ return renderedText.toString();
+ }
+
+ private static class TokenIterator implements Iterator<Token> {
+
+ private static class WhitespaceSaver {
+ // has to be saved inside value concatenations
+ private StringBuilder whitespace;
+ // may need to value-concat with next value
+ private boolean lastTokenWasSimpleValue;
+
+ WhitespaceSaver() {
+ whitespace = new StringBuilder();
+ lastTokenWasSimpleValue = false;
+ }
+
+ void add(int c) {
+ whitespace.appendCodePoint(c);
+ }
+
+ Token check(Token t, ConfigOrigin baseOrigin, int lineNumber) {
+ if (isSimpleValue(t)) {
+ return nextIsASimpleValue(baseOrigin, lineNumber);
+ } else {
+ return nextIsNotASimpleValue(baseOrigin, lineNumber);
+ }
+ }
+
+ // called if the next token is not a simple value;
+ // discards any whitespace we were saving between
+ // simple values.
+ private Token nextIsNotASimpleValue(ConfigOrigin baseOrigin, int
lineNumber) {
+ lastTokenWasSimpleValue = false;
+ return createWhitespaceTokenFromSaver(baseOrigin, lineNumber);
+ }
+
+ // called if the next token IS a simple value,
+ // so creates a whitespace token if the previous
+ // token also was.
+ private Token nextIsASimpleValue(ConfigOrigin baseOrigin, int
lineNumber) {
+ Token t = createWhitespaceTokenFromSaver(baseOrigin,
lineNumber);
+ if (!lastTokenWasSimpleValue) {
+ lastTokenWasSimpleValue = true;
+ }
+ return t;
+ }
+
+ private Token createWhitespaceTokenFromSaver(ConfigOrigin
baseOrigin, int lineNumber) {
+ if (whitespace.length() > 0) {
+ Token t;
+ if (lastTokenWasSimpleValue) {
+ t =
+ Tokens.newUnquotedText(
+ lineOrigin(baseOrigin, lineNumber),
whitespace.toString());
+ } else {
+ t =
+ Tokens.newIgnoredWhitespace(
+ lineOrigin(baseOrigin, lineNumber),
whitespace.toString());
+ }
+ whitespace.setLength(0); // reset
+ return t;
+ }
+ return null;
+ }
+ }
+
+ private final SimpleConfigOrigin origin;
+ private final Reader input;
+ private final LinkedList<Integer> buffer;
+ private int lineNumber;
+ private ConfigOrigin lineOrigin;
+ private final Queue<Token> tokens;
+ private final WhitespaceSaver whitespaceSaver;
+ private final boolean allowComments;
+ private boolean acceptSpecialText = false;
+
+ TokenIterator(ConfigOrigin origin, Reader input, boolean
allowComments) {
+ this.origin = (SimpleConfigOrigin) origin;
+ this.input = input;
+ this.allowComments = allowComments;
+ this.buffer = new LinkedList<Integer>();
+ lineNumber = 1;
+ lineOrigin = this.origin.withLineNumber(lineNumber);
+ tokens = new LinkedList<Token>();
+ tokens.add(Tokens.START);
+ whitespaceSaver = new WhitespaceSaver();
+ }
+
+ // Add from SeaTunnel
+ TokenIterator(
+ ConfigOrigin origin,
+ Reader input,
+ boolean allowComments,
+ boolean acceptSpecialText) {
+ this(origin, input, allowComments);
+ this.acceptSpecialText = acceptSpecialText;
+ }
+ // End Add from SeaTunnel
+
+ // this should ONLY be called from nextCharSkippingComments
+ // or when inside a quoted string, or when parsing a sequence
+ // like ${ or +=, everything else should use
+ // nextCharSkippingComments().
+ private int nextCharRaw() {
+ if (buffer.isEmpty()) {
+ try {
+ return input.read();
+ } catch (IOException e) {
+ throw new ConfigException.IO(origin, "read error: " +
e.getMessage(), e);
+ }
+ } else {
+ int c = buffer.pop();
+ return c;
+ }
+ }
+
+ private void putBack(int c) {
+ if (buffer.size() > 2) {
+ throw new ConfigException.BugOrBroken(
+ "bug: putBack() three times, undesirable look-ahead");
+ }
+ buffer.push(c);
+ }
+
+ static boolean isWhitespace(int c) {
+ return ConfigImplUtil.isWhitespace(c);
+ }
+
+ static boolean isWhitespaceNotNewline(int c) {
+ return c != '\n' && ConfigImplUtil.isWhitespace(c);
+ }
+
+ private boolean startOfComment(int c) {
+ if (c == -1) {
+ return false;
+ } else {
+ if (allowComments) {
+ if (c == '#') {
+ return true;
+ } else if (c == '/') {
+ int maybeSecondSlash = nextCharRaw();
+ // we want to predictably NOT consume any chars
+ putBack(maybeSecondSlash);
+ if (maybeSecondSlash == '/') {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ }
+
+ // get next char, skipping non-newline whitespace
+ private int nextCharAfterWhitespace(WhitespaceSaver saver) {
+ for (; ; ) {
+ int c = nextCharRaw();
+
+ if (c == -1) {
+ return -1;
+ } else {
+ if (isWhitespaceNotNewline(c)) {
+ saver.add(c);
+ continue;
+ } else {
+ return c;
+ }
+ }
+ }
+ }
+
+ private ProblemException problem(String message) {
+ return problem("", message, null);
+ }
+
+ private ProblemException problem(String what, String message) {
+ return problem(what, message, null);
+ }
+
+ private ProblemException problem(String what, String message, boolean
suggestQuotes) {
+ return problem(what, message, suggestQuotes, null);
+ }
+
+ private ProblemException problem(String what, String message,
Throwable cause) {
+ return problem(lineOrigin, what, message, cause);
+ }
+
+ private ProblemException problem(
+ String what, String message, boolean suggestQuotes, Throwable
cause) {
+ return problem(lineOrigin, what, message, suggestQuotes, cause);
+ }
+
+ private static ProblemException problem(
+ ConfigOrigin origin, String what, String message, Throwable
cause) {
+ return problem(origin, what, message, false, cause);
+ }
+
+ private static ProblemException problem(
+ ConfigOrigin origin,
+ String what,
+ String message,
+ boolean suggestQuotes,
+ Throwable cause) {
+ if (what == null || message == null) {
+ throw new ConfigException.BugOrBroken(
+ "internal error, creating bad ProblemException");
+ }
+ return new ProblemException(
+ Tokens.newProblem(origin, what, message, suggestQuotes,
cause));
+ }
+
+ private static ProblemException problem(ConfigOrigin origin, String
message) {
+ return problem(origin, "", message, null);
+ }
+
+ private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin, int
lineNumber) {
+ return ((SimpleConfigOrigin)
baseOrigin).withLineNumber(lineNumber);
+ }
+
+ // ONE char has always been consumed, either the # or the first /, but
+ // not both slashes
+ private Token pullComment(int firstChar) {
+ boolean doubleSlash = false;
+ if (firstChar == '/') {
+ int discard = nextCharRaw();
+ if (discard != '/') {
+ throw new ConfigException.BugOrBroken("called pullComment
but // not seen");
+ }
+ doubleSlash = true;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ int c = nextCharRaw();
+ if (c == -1 || c == '\n') {
+ putBack(c);
+ if (doubleSlash) {
+ return Tokens.newCommentDoubleSlash(lineOrigin,
sb.toString());
+ } else {
+ return Tokens.newCommentHash(lineOrigin,
sb.toString());
+ }
+ } else {
+ sb.appendCodePoint(c);
+ }
+ }
+ }
+
+ // chars JSON allows a number to start with
+ static final String firstNumberChars = "0123456789-";
+ // chars JSON allows to be part of a number
+ static final String numberChars = "0123456789eE+-.";
+ // chars that stop an unquoted string
+ static final String notInUnquotedText = "$\"{}[]:=,+#`^?!@*&\\";
+
+ // The rules here are intended to maximize convenience while
+ // avoiding confusion with real valid JSON. Basically anything
+ // that parses as JSON is treated the JSON way and otherwise
+ // we assume it's a string and let the parser sort it out.
+ private Token pullUnquotedText() {
+ ConfigOrigin origin = lineOrigin;
+ StringBuilder sb = new StringBuilder();
+ int c = nextCharRaw();
+ while (true) {
+ if (c == -1) {
+ break;
+ } else if (notInUnquotedText.indexOf(c) >= 0) {
+ break;
+ } else if (isWhitespace(c)) {
+ break;
+ } else if (startOfComment(c)) {
+ break;
+ } else {
+ sb.appendCodePoint(c);
+ }
+
+ // we parse true/false/null tokens as such no matter
+ // what is after them, as long as they are at the
+ // start of the unquoted token.
+ if (sb.length() == 4) {
+ String s = sb.toString();
+ if (s.equals("true")) {
+ return Tokens.newBoolean(origin, true);
+ } else if (s.equals("null")) {
+ return Tokens.newNull(origin);
+ }
+ } else if (sb.length() == 5) {
+ String s = sb.toString();
+ if (s.equals("false")) {
+ return Tokens.newBoolean(origin, false);
+ }
+ }
+
+ c = nextCharRaw();
+ }
+
+ // put back the char that ended the unquoted text
+ putBack(c);
+
+ String s = sb.toString();
+ return Tokens.newUnquotedText(origin, s);
+ }
+
+ private Token pullNumber(int firstChar) throws ProblemException {
+ StringBuilder sb = new StringBuilder();
+ sb.appendCodePoint(firstChar);
+ boolean containedDecimalOrE = false;
+ int c = nextCharRaw();
+ while (c != -1 && numberChars.indexOf(c) >= 0) {
+ if (c == '.' || c == 'e' || c == 'E') {
+ containedDecimalOrE = true;
+ }
+ sb.appendCodePoint(c);
+ c = nextCharRaw();
+ }
+ // the last character we looked at wasn't part of the number, put
it
+ // back
+ putBack(c);
+ String s = sb.toString();
+ try {
+ if (containedDecimalOrE) {
+ // force floating point representation
+ return Tokens.newDouble(lineOrigin, Double.parseDouble(s),
s);
+ } else {
+ // this should throw if the integer is too large for Long
+ return Tokens.newLong(lineOrigin, Long.parseLong(s), s);
+ }
+ } catch (NumberFormatException e) {
+ // not a number after all, see if it's an unquoted string.
+ for (char u : s.toCharArray()) {
+ if (notInUnquotedText.indexOf(u) >= 0) {
+ throw problem(
+ asString(u),
+ "Reserved character '"
+ + asString(u)
+ + "' is not allowed outside quotes",
+ true /* suggestQuotes */);
+ }
+ }
+ // no evil chars so we just decide this was a string and
+ // not a number.
+ return Tokens.newUnquotedText(lineOrigin, s);
+ }
+ }
+
+ private void pullEscapeSequence(StringBuilder sb, StringBuilder sbOrig)
+ throws ProblemException {
+ int escaped = nextCharRaw();
+ if (escaped == -1) {
+ throw problem("End of input but backslash in string had
nothing after it");
+ }
+
+ // This is needed so we return the unescaped escape characters
back out when rendering
+ // the token
+ sbOrig.appendCodePoint('\\');
+ sbOrig.appendCodePoint(escaped);
+
+ switch (escaped) {
+ case '"':
+ sb.append('"');
+ break;
+ case '\\':
+ sb.append('\\');
+ break;
+ case '/':
+ sb.append('/');
+ break;
+ case 'b':
+ sb.append('\b');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 't':
+ sb.append('\t');
+ break;
+ case 'u':
+ {
+ // kind of absurdly slow, but screw it for now
+ char[] a = new char[4];
+ for (int i = 0; i < 4; ++i) {
+ int c = nextCharRaw();
+ if (c == -1) {
+ throw problem(
+ "End of input but expecting 4 hex
digits for \\uXXXX escape");
+ }
+ a[i] = (char) c;
+ }
+ String digits = new String(a);
+ sbOrig.append(a);
+ try {
+ sb.appendCodePoint(Integer.parseInt(digits, 16));
+ } catch (NumberFormatException e) {
+ throw problem(
+ digits,
+ String.format(
+ "Malformed hex digits after \\u
escape in string: '%s'",
+ digits),
+ e);
+ }
+ }
+ break;
+ default:
+ throw problem(
+ asString(escaped),
+ String.format(
+ "backslash followed by '%s', this is not a
valid escape sequence (quoted strings use JSON escaping, so use
double-backslash \\\\ for literal backslash)",
+ asString(escaped)));
+ }
+ }
+
+ private void appendTripleQuotedString(StringBuilder sb, StringBuilder
sbOrig)
+ throws ProblemException {
+ // we are after the opening triple quote and need to consume the
+ // close triple
+ int consecutiveQuotes = 0;
+ for (; ; ) {
+ int c = nextCharRaw();
+
+ if (c == '"') {
+ consecutiveQuotes += 1;
+ } else if (consecutiveQuotes >= 3) {
+ // the last three quotes end the string and the others are
+ // kept.
+ sb.setLength(sb.length() - 3);
+ putBack(c);
+ break;
+ } else {
+ consecutiveQuotes = 0;
+ if (c == -1) {
+ throw problem("End of input but triple-quoted string
was still open");
+ } else if (c == '\n') {
+ // keep the line number accurate
+ lineNumber += 1;
+ lineOrigin = origin.withLineNumber(lineNumber);
+ }
+ }
+
+ sb.appendCodePoint(c);
+ sbOrig.appendCodePoint(c);
+ }
+ }
+
+ private Token pullQuotedString() throws ProblemException {
+ // the open quote has already been consumed
+ StringBuilder sb = new StringBuilder();
+
+ // We need a second string builder to keep track of escape
characters.
+ // We want to return them exactly as they appeared in the original
text,
+ // which means we will need a new StringBuilder to escape escape
characters
+ // so we can also keep the actual value of the string. This is
gross.
+ StringBuilder sbOrig = new StringBuilder();
+ sbOrig.appendCodePoint('"');
+
+ while (true) {
+ int c = nextCharRaw();
+ if (c == -1) {
+ if (!acceptSpecialText) {
+ throw problem("End of input but string quote was still
open");
+ } else {
+ return Tokens.newString(lineOrigin, sbOrig.toString(),
sbOrig.toString());
+ }
+ }
+
+ if (c == '\\') {
+ pullEscapeSequence(sb, sbOrig);
+ } else if (c == '"') {
+ if (acceptSpecialText) {
+ // we should append '"' twice because we want to keep
the original value
+ sb.appendCodePoint(c);
+ sb.appendCodePoint(c);
+ }
+ sbOrig.appendCodePoint(c);
+ break;
+ } else if (ConfigImplUtil.isC0Control(c)) {
+ throw problem(
+ asString(c),
+ "JSON does not allow unescaped "
+ + asString(c)
+ + " in quoted strings, use a backslash
escape");
+ } else {
+ sb.appendCodePoint(c);
+ sbOrig.appendCodePoint(c);
+ }
+ }
+
+ // maybe switch to triple-quoted string, sort of hacky...
+ if (sb.length() == 0) {
+ int third = nextCharRaw();
+ if (third == '"') {
+ sbOrig.appendCodePoint(third);
+ appendTripleQuotedString(sb, sbOrig);
+ } else {
+ putBack(third);
+ }
+ }
+ return Tokens.newString(lineOrigin, sb.toString(),
sbOrig.toString());
+ }
+
+ private Token pullPlusEquals() throws ProblemException {
+ // the initial '+' has already been consumed
+ int c = nextCharRaw();
+ if (c != '=') {
+ throw problem(
+ asString(c),
+ "'+' not followed by =, '" + asString(c) + "' not
allowed after '+'",
+ true /* suggestQuotes */);
+ }
+ return Tokens.PLUS_EQUALS;
+ }
+
+ private Token pullSubstitution() throws ProblemException {
+ // the initial '$' has already been consumed
+ ConfigOrigin origin = lineOrigin;
+ int c = nextCharRaw();
+ if (c != '{') {
+ throw problem(
+ asString(c),
+ "'$' not followed by {, '" + asString(c) + "' not
allowed after '$'",
+ true /* suggestQuotes */);
+ }
+
+ boolean optional = false;
+ c = nextCharRaw();
+ if (c == '?') {
+ optional = true;
+ } else {
+ putBack(c);
+ }
+
+ WhitespaceSaver saver = new WhitespaceSaver();
+ List<Token> expression = new ArrayList<Token>();
+
+ Token t;
+ do {
+ t = pullNextToken(saver);
+
+ // note that we avoid validating the allowed tokens inside
+ // the substitution here; we even allow nested substitutions
+ // in the tokenizer. The parser sorts it out.
+ if (t == Tokens.CLOSE_CURLY) {
+ // end the loop, done!
+ break;
+ } else if (t == Tokens.END) {
+ throw problem(origin, "Substitution ${ was not closed with
a }");
+ } else {
+ Token whitespace = saver.check(t, origin, lineNumber);
+ if (whitespace != null) {
+ expression.add(whitespace);
+ }
+ expression.add(t);
+ }
+ } while (true);
+
+ return Tokens.newSubstitution(origin, optional, expression);
+ }
+
+ private Token pullNextToken(WhitespaceSaver saver) throws
ProblemException {
+ int c = nextCharAfterWhitespace(saver);
+ if (c == -1) {
+ return Tokens.END;
+ } else if (c == '\n') {
+ // newline tokens have the just-ended line number
+ Token line = Tokens.newLine(lineOrigin);
+ lineNumber += 1;
+ lineOrigin = origin.withLineNumber(lineNumber);
+ return line;
+ } else {
+ Token t;
+ if (startOfComment(c)) {
+ t = pullComment(c);
+ } else {
+ switch (c) {
+ case '"':
+ t = pullQuotedString();
+ break;
+ case '$':
+ t = pullSubstitution();
+ break;
+ case ':':
+ t = Tokens.COLON;
+ break;
+ case ',':
+ t = Tokens.COMMA;
+ break;
+ case '=':
+ t = Tokens.EQUALS;
+ break;
+ case '{':
+ t = Tokens.OPEN_CURLY;
+ break;
+ case '}':
+ t = Tokens.CLOSE_CURLY;
+ break;
+ case '[':
+ t = Tokens.OPEN_SQUARE;
+ break;
+ case ']':
+ t = Tokens.CLOSE_SQUARE;
+ break;
+ case '+':
+ t = pullPlusEquals();
+ break;
+ default:
+ t = null;
+ break;
+ }
+
+ if (t == null) {
+ if (firstNumberChars.indexOf(c) >= 0) {
+ t = pullNumber(c);
+ } else if (notInUnquotedText.indexOf(c) >= 0) {
+ if (acceptSpecialText) {
+ t = Tokens.newUnquotedText(lineOrigin,
asString(c));
+ } else {
+ throw problem(
+ asString(c),
+ "Reserved character '"
+ + asString(c)
+ + "' is not allowed outside
quotes",
+ true /* suggestQuotes */);
+ }
+ } else {
+ putBack(c);
+ t = pullUnquotedText();
+ }
+ }
+ }
+
+ if (t == null) {
+ throw new ConfigException.BugOrBroken("bug: failed to
generate next token");
+ }
+
+ return t;
+ }
+ }
+
+ private static boolean isSimpleValue(Token t) {
+ if (Tokens.isSubstitution(t) || Tokens.isUnquotedText(t) ||
Tokens.isValue(t)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void queueNextToken() throws ProblemException {
+ Token t = pullNextToken(whitespaceSaver);
+ Token whitespace = whitespaceSaver.check(t, origin, lineNumber);
+ if (whitespace != null) {
+ tokens.add(whitespace);
+ }
+
+ tokens.add(t);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !tokens.isEmpty();
+ }
+
+ @Override
+ public Token next() {
+ Token t = tokens.remove();
+ if (tokens.isEmpty() && t != Tokens.END) {
+ try {
+ queueNextToken();
+ } catch (ProblemException e) {
+ tokens.add(e.problem());
+ }
+ if (tokens.isEmpty()) {
+ throw new ConfigException.BugOrBroken(
+ "bug: tokens queue should not be empty here");
+ }
+ }
+ return t;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Does not make sense to remove items from token stream");
+ }
+ }
+}
diff --git
a/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java
b/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java
index 6d8eb73ffa..931bb06183 100644
---
a/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java
+++
b/seatunnel-config/seatunnel-config-shade/src/test/java/org/apache/seatunnel/config/ConfigTest.java
@@ -27,6 +27,8 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
public class ConfigTest {
@@ -40,4 +42,21 @@ public class ConfigTest {
FileUtils.getFileFromResources("/seatunnel/serialize.conf"));
Assertions.assertEquals(expected,
config.root().render(ConfigRenderOptions.concise()));
}
+
+ @Test
+ public void testQuoteAsKey() throws URISyntaxException {
+ Config config =
+ ConfigFactory.parseFile(
+
FileUtils.getFileFromResources("/seatunnel/configWithSpecialKey.conf"));
+ List<String> keys = new
ArrayList<>(config.getObject("object").keySet());
+ Assertions.assertEquals("\"", keys.get(0));
+ Assertions.assertEquals("\"\"", keys.get(1));
+ Assertions.assertEquals("\\\"", keys.get(2));
+
+ Assertions.assertEquals("\\\"",
config.getObject("object").toConfig().getString("\""));
+ Assertions.assertEquals(
+ "\\\"\\\"",
config.getObject("object").toConfig().getString("\"\""));
+ Assertions.assertEquals(
+ "\\\\\\\"",
config.getObject("object").toConfig().getString("\\\""));
+ }
}
diff --git
a/seatunnel-config/seatunnel-config-shade/src/test/resources/seatunnel/configWithSpecialKey.conf
b/seatunnel-config/seatunnel-config-shade/src/test/resources/seatunnel/configWithSpecialKey.conf
new file mode 100644
index 0000000000..b463009b33
--- /dev/null
+++
b/seatunnel-config/seatunnel-config-shade/src/test/resources/seatunnel/configWithSpecialKey.conf
@@ -0,0 +1,20 @@
+# 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.
+
+object {
+ "\""="\\\""
+ "\"\""="\\\"\\\""
+ "\\\""="\\\\\\\""
+}
\ No newline at end of file