From 232d2007b82555770bcd7ea4083098e7ac70f077 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 31 Jan 2024 22:13:37 +1300 Subject: [PATCH] YamlSimpleLoader resets state per load() --- .../io/avaje/config/YamlLoaderSimple.java | 492 +++++++++--------- .../java/io/avaje/config/YamlParserTest.java | 12 + 2 files changed, 261 insertions(+), 243 deletions(-) diff --git a/avaje-config/src/main/java/io/avaje/config/YamlLoaderSimple.java b/avaje-config/src/main/java/io/avaje/config/YamlLoaderSimple.java index 087b0e4..a54ec5a 100644 --- a/avaje-config/src/main/java/io/avaje/config/YamlLoaderSimple.java +++ b/avaje-config/src/main/java/io/avaje/config/YamlLoaderSimple.java @@ -8,307 +8,313 @@ */ final class YamlLoaderSimple implements YamlLoader { - enum MultiLineTrim { - Clip, - Strip, - Keep, - Implicit - } - - enum State { - RequireKey, - MultiLine, - KeyOrValue, - RequireTopKey + @Override + public Map load(InputStream is) { + return new Load().load(is); } - private final Map keyValues = new LinkedHashMap<>(); - private final Stack keyStack = new Stack<>(); - private final List multiLines = new ArrayList<>(); + private static class Load { + enum MultiLineTrim { + Clip, + Strip, + Keep, + Implicit + } - private State state = State.RequireKey; - private MultiLineTrim multiLineTrim = MultiLineTrim.Clip; - private int currentLine; - private int currentIndent; - private int multiLineIndent; + enum State { + RequireKey, + MultiLine, + KeyOrValue, + RequireTopKey + } - @Override - public Map load(InputStream is) { - try (LineNumberReader lineReader = new LineNumberReader(new InputStreamReader(is))) { - String line; - do { - line = lineReader.readLine(); - processLine(line); - } while (line != null); - - return keyValues; - } catch (IOException e) { - throw new UncheckedIOException(e); + private final Map keyValues = new LinkedHashMap<>(); + private final Stack keyStack = new Stack<>(); + private final List multiLines = new ArrayList<>(); + + private State state = State.RequireKey; + private MultiLineTrim multiLineTrim = MultiLineTrim.Clip; + private int currentLine; + private int currentIndent; + private int multiLineIndent; + + private Map load(InputStream is) { + try (LineNumberReader lineReader = new LineNumberReader(new InputStreamReader(is))) { + String line; + do { + line = lineReader.readLine(); + processLine(line); + } while (line != null); + + return keyValues; + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - } - private void processLine(String line) { - if (line == null) { - checkFinalMultiLine(); - } else { - currentLine++; - readIndent(line); - if (state == State.MultiLine) { - processMultiLine(line); + private void processLine(String line) { + if (line == null) { + checkFinalMultiLine(); } else { - processNext(line); + currentLine++; + readIndent(line); + if (state == State.MultiLine) { + processMultiLine(line); + } else { + processNext(line); + } } } - } - private void checkFinalMultiLine() { - if (state == State.MultiLine) { - addKeyVal(multiLineValue()); + private void checkFinalMultiLine() { + if (state == State.MultiLine) { + addKeyVal(multiLineValue()); + } } - } - private void processMultiLine(String line) { - if (multiLineIndent == 0) { - if (currentIndent == 0 && !line.trim().isEmpty()) { + private void processMultiLine(String line) { + if (multiLineIndent == 0) { + if (currentIndent == 0 && !line.trim().isEmpty()) { + multiLineEnd(line); + return; + } + // first multiLine + multiLineIndent = currentIndent; + multiLines.add(line); + } else if (currentIndent >= multiLineIndent || line.trim().isEmpty()) { + multiLines.add(line); + } else { + // end of multiLine multiLineEnd(line); - return; } - // first multiLine - multiLineIndent = currentIndent; - multiLines.add(line); - } else if (currentIndent >= multiLineIndent || line.trim().isEmpty()) { - multiLines.add(line); - } else { - // end of multiLine - multiLineEnd(line); } - } - - private void multiLineEnd(String line) { - addKeyVal(multiLineValue()); - processNext(line); - } - private String multiLineValue() { - if (multiLines.isEmpty()) { - return ""; - } - if (multiLineTrim != MultiLineTrim.Keep) { - multiLineTrimTrailing(); + private void multiLineEnd(String line) { + addKeyVal(multiLineValue()); + processNext(line); } - String join = (multiLineTrim == MultiLineTrim.Implicit) ? " " : "\n"; - StringBuilder sb = new StringBuilder(); - int lastIndex = multiLines.size() - 1; - for (int i = 0; i <= lastIndex; i++) { - String line = multiLines.get(i); - if (line.length() < multiLineIndent) { - // empty line whitespace - sb.append("\n"); - } else { - line = line.substring(multiLineIndent); - if (i == lastIndex && (multiLineTrim == MultiLineTrim.Strip || multiLineTrim == MultiLineTrim.Implicit)) { - sb.append(line); + + private String multiLineValue() { + if (multiLines.isEmpty()) { + return ""; + } + if (multiLineTrim != MultiLineTrim.Keep) { + multiLineTrimTrailing(); + } + String join = (multiLineTrim == MultiLineTrim.Implicit) ? " " : "\n"; + StringBuilder sb = new StringBuilder(); + int lastIndex = multiLines.size() - 1; + for (int i = 0; i <= lastIndex; i++) { + String line = multiLines.get(i); + if (line.length() < multiLineIndent) { + // empty line whitespace + sb.append("\n"); } else { - sb.append(line).append(join); + line = line.substring(multiLineIndent); + if (i == lastIndex && (multiLineTrim == MultiLineTrim.Strip || multiLineTrim == MultiLineTrim.Implicit)) { + sb.append(line); + } else { + sb.append(line).append(join); + } } } + multiLineEnd(); + return sb.toString(); } - multiLineEnd(); - return sb.toString(); - } - private void multiLineTrimTrailing() { - for (int i = multiLines.size(); i-- > 0; ) { - if (!multiLines.get(i).trim().isEmpty()) { - break; - } else { - multiLines.remove(i); + private void multiLineTrimTrailing() { + for (int i = multiLines.size(); i-- > 0; ) { + if (!multiLines.get(i).trim().isEmpty()) { + break; + } else { + multiLines.remove(i); + } } } - } - - void readIndent(String line) { - currentIndent = indent(line); - } - private void processNext(String line) { - if (newDocument(line) || ignoreLine(line)) { - return; + void readIndent(String line) { + currentIndent = indent(line); } - final int pos = line.indexOf(':'); - if (pos == -1) { - // value on another line - processNonKey(line); - return; - } - if (state == State.RequireTopKey && currentIndent > 0) { - throw new IllegalStateException("Require top level key at line:" + currentLine + " [" + line + "]"); - } + private void processNext(String line) { + if (newDocument(line) || ignoreLine(line)) { + return; + } - // must be a key - would expect explicit multiline otherwise - final Key key = new Key(currentIndent, trimKey(line.substring(0, pos))); - popKeys(currentIndent); - keyStack.push(key); - - // look at the remainder of the line - final String remaining = line.substring(pos + 1); - final String trimmedValue = remaining.trim(); - if (trimmedValue.startsWith("|")) { - multilineStart(multiLineTrimMode(trimmedValue)); - - } else if (trimmedValue.isEmpty() || trimmedValue.startsWith("#")) { - // empty or comment - state = State.KeyOrValue; - } else { - // simple key value - addKeyVal(trimValue(remaining.trim())); - } - } + final int pos = line.indexOf(':'); + if (pos == -1) { + // value on another line + processNonKey(line); + return; + } + if (state == State.RequireTopKey && currentIndent > 0) { + throw new IllegalStateException("Require top level key at line:" + currentLine + " [" + line + "]"); + } - private MultiLineTrim multiLineTrimMode(String trimmedValue) { - if (trimmedValue.length() == 1) { - return MultiLineTrim.Clip; - } - final char ch = trimmedValue.charAt(1); - switch (ch) { - case '-': - // the final line break and any trailing empty lines are excluded - return MultiLineTrim.Strip; - case '+': - // the final line break and any trailing empty lines are included - return MultiLineTrim.Keep; - default: - // the final line break character is included and trailing empty lines are excluded - return MultiLineTrim.Clip; - } - } + // must be a key - would expect explicit multiline otherwise + final Key key = new Key(currentIndent, trimKey(line.substring(0, pos))); + popKeys(currentIndent); + keyStack.push(key); - private void addKeyVal(String value) { - keyValues.put(fullKey(), value); - keyStack.pop(); - state = State.RequireKey; - } + // look at the remainder of the line + final String remaining = line.substring(pos + 1); + final String trimmedValue = remaining.trim(); + if (trimmedValue.startsWith("|")) { + multilineStart(multiLineTrimMode(trimmedValue)); - private void processNonKey(String line) { - if (state == State.RequireKey) { - state = State.RequireTopKey; - // drop this value line - return; - } - if (keyStack.isEmpty()) { - throw new IllegalStateException("Reading a value but no key at line: " + currentLine + " line[" + line + "]"); - } - final int keyIndent = keyStack.peek().indent; - if (currentIndent <= keyIndent) { - throw new IllegalStateException("Value not indented enough for key " + fullKey() + " at line: " + currentLine + " line[" + line + "]"); + } else if (trimmedValue.isEmpty() || trimmedValue.startsWith("#")) { + // empty or comment + state = State.KeyOrValue; + } else { + // simple key value + addKeyVal(trimValue(remaining.trim())); + } } - multilineStart(MultiLineTrim.Implicit); - multiLineIndent = currentIndent; - multiLines.add(line); - } - private void multilineStart(MultiLineTrim trim) { - state = State.MultiLine; - multiLineIndent = 0; - multiLineTrim = trim; - } + private MultiLineTrim multiLineTrimMode(String trimmedValue) { + if (trimmedValue.length() == 1) { + return MultiLineTrim.Clip; + } + final char ch = trimmedValue.charAt(1); + switch (ch) { + case '-': + // the final line break and any trailing empty lines are excluded + return MultiLineTrim.Strip; + case '+': + // the final line break and any trailing empty lines are included + return MultiLineTrim.Keep; + default: + // the final line break character is included and trailing empty lines are excluded + return MultiLineTrim.Clip; + } + } - private void multiLineEnd() { - state = State.RequireKey; - multiLineIndent = 0; - multiLines.clear(); - } + private void addKeyVal(String value) { + keyValues.put(fullKey(), value); + keyStack.pop(); + state = State.RequireKey; + } - private boolean newDocument(String line) { - if (line.startsWith("---")) { - keyStack.clear(); - return true; + private void processNonKey(String line) { + if (state == State.RequireKey) { + state = State.RequireTopKey; + // drop this value line + return; + } + if (keyStack.isEmpty()) { + throw new IllegalStateException("Reading a value but no key at line: " + currentLine + " line[" + line + "]"); + } + final int keyIndent = keyStack.peek().indent; + if (currentIndent <= keyIndent) { + throw new IllegalStateException("Value not indented enough for key " + fullKey() + " at line: " + currentLine + " line[" + line + "]"); + } + multilineStart(MultiLineTrim.Implicit); + multiLineIndent = currentIndent; + multiLines.add(line); } - return false; - } - private boolean ignoreLine(String line) { - final String trimmed = line.trim(); - return trimmed.isEmpty() || trimmed.startsWith("#"); - } + private void multilineStart(MultiLineTrim trim) { + state = State.MultiLine; + multiLineIndent = 0; + multiLineTrim = trim; + } - private String trimValue(String value) { - if (value.startsWith("'")) { - return unquoteValue('\'', value); + private void multiLineEnd() { + state = State.RequireKey; + multiLineIndent = 0; + multiLines.clear(); } - if (value.startsWith("\"")) { - return unquoteValue('"', value); + + private boolean newDocument(String line) { + if (line.startsWith("---")) { + keyStack.clear(); + return true; + } + return false; } - int commentPos = value.indexOf('#'); - if (commentPos > -1) { - return value.substring(0, commentPos).trim(); + + private boolean ignoreLine(String line) { + final String trimmed = line.trim(); + return trimmed.isEmpty() || trimmed.startsWith("#"); } - return value; - } - private String unquoteValue(char quoteChar, String value) { - final int pos = value.lastIndexOf(quoteChar); - return value.substring(1, pos); - } + private String trimValue(String value) { + if (value.startsWith("'")) { + return unquoteValue('\'', value); + } + if (value.startsWith("\"")) { + return unquoteValue('"', value); + } + int commentPos = value.indexOf('#'); + if (commentPos > -1) { + return value.substring(0, commentPos).trim(); + } + return value; + } - private String fullKey() { - StringJoiner fullKey = new StringJoiner("."); - for (Key next : keyStack) { - fullKey.add(next.key()); + private String unquoteValue(char quoteChar, String value) { + final int pos = value.lastIndexOf(quoteChar); + return value.substring(1, pos); } - return fullKey.toString(); - } - private void popKeys(int indent) { - while (!keyStack.isEmpty()) { - if (keyStack.peek().indent() < indent) { - break; - } else { - keyStack.pop(); + private String fullKey() { + StringJoiner fullKey = new StringJoiner("."); + for (Key next : keyStack) { + fullKey.add(next.key()); } + return fullKey.toString(); } - } - private String trimKey(String indentKey) { - return unquoteKey(indentKey.trim()); - } + private void popKeys(int indent) { + while (!keyStack.isEmpty()) { + if (keyStack.peek().indent() < indent) { + break; + } else { + keyStack.pop(); + } + } + } - private String unquoteKey(String value) { - if (value.startsWith("'") && value.endsWith("'")) { - return value.substring(1, value.length() - 1); + private String trimKey(String indentKey) { + return unquoteKey(indentKey.trim()); } - if (value.startsWith("\"") && value.endsWith("\"")) { - return value.substring(1, value.length() - 1); + + private String unquoteKey(String value) { + if (value.startsWith("'") && value.endsWith("'")) { + return value.substring(1, value.length() - 1); + } + if (value.startsWith("\"") && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } + return value; } - return value; - } - private int indent(String line) { - final char[] chars = line.toCharArray(); - for (int i = 0; i < chars.length; i++) { - if (!Character.isWhitespace(chars[i])) { - return i; + private int indent(String line) { + final char[] chars = line.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if (!Character.isWhitespace(chars[i])) { + return i; + } } + return 0; } - return 0; - } - private static class Key { - private final int indent; - private final String key; + private static class Key { + private final int indent; + private final String key; - Key(int indent, String key) { - this.indent = indent; - this.key = key; - } + Key(int indent, String key) { + this.indent = indent; + this.key = key; + } - int indent() { - return indent; - } + int indent() { + return indent; + } - String key() { - return key; + String key() { + return key; + } } } } diff --git a/avaje-config/src/test/java/io/avaje/config/YamlParserTest.java b/avaje-config/src/test/java/io/avaje/config/YamlParserTest.java index e6ef541..536d0d0 100644 --- a/avaje-config/src/test/java/io/avaje/config/YamlParserTest.java +++ b/avaje-config/src/test/java/io/avaje/config/YamlParserTest.java @@ -12,6 +12,18 @@ class YamlParserTest { private final YamlLoaderSnake load = new YamlLoaderSnake(); + @Test + void simpleYamlParserMultiLoad() { + YamlLoaderSimple parser = new YamlLoaderSimple(); + Map load1 = parser.load(res("/yaml/basic.yaml")); + assertThat(load1).hasSize(5); + basic(load1); + + Map load2 = parser.load(res("/yaml/key-comment.yaml")); + assertThat(load2).hasSize(4); + assertThat(load2).containsOnlyKeys("k1","k2","k3", "k4"); + } + @Test void basic() { basic(parseYaml2("/yaml/basic.yaml"));