diff --git a/src/main/java/org/codejive/properties/Properties.java b/src/main/java/org/codejive/properties/Properties.java index 91d3a8f..6fb9a6d 100644 --- a/src/main/java/org/codejive/properties/Properties.java +++ b/src/main/java/org/codejive/properties/Properties.java @@ -317,7 +317,7 @@ public Set rawKeySet() { public Collection rawValues() { if (tokens.isEmpty()) return Collections.emptyList(); List result = new ArrayList<>(); - walkProperties((key, value) -> result.add(value != null ? value.getRaw() : "")); + walkProperties((key, value) -> result.add(value.getRaw())); return result; } @@ -330,11 +330,7 @@ public Collection rawValues() { public Set> rawEntrySet() { if (tokens.isEmpty()) return Collections.emptySet(); Set> result = new LinkedHashSet<>(); - walkProperties( - (key, value) -> - result.add( - new SimpleEntry<>( - key.getRaw(), value != null ? value.getRaw() : ""))); + walkProperties((key, value) -> result.add(new SimpleEntry<>(key.getRaw(), value.getRaw()))); return result; } @@ -345,19 +341,9 @@ private void walkProperties(BiConsumer func) { if (c.isType(PropertiesParser.Type.KEY)) { PropertiesParser.Token keyToken = c.token(); c.next(); - if (c.isType(PropertiesParser.Type.SEPARATOR)) { - c.next(); - if (c.isType(PropertiesParser.Type.VALUE)) { - func.accept(keyToken, c.token()); - c.next(); - } else { - // We're dealing with a value-less property - func.accept(keyToken, null); - } - } else { - // We're dealing with a key-only property - func.accept(keyToken, null); - } + c.next(); // Skip the separator + func.accept(keyToken, c.token()); + c.next(); } else { c.next(); } @@ -441,16 +427,14 @@ public String putRaw(String rawKey, String rawValue) { private void replaceValue(String key, String rawValue, String value) { Cursor pos = indexOf(key); validate(pos.nextIf(PropertiesParser.Type.KEY), pos); - if (!pos.isType(PropertiesParser.Type.SEPARATOR)) { - pos.add(new PropertiesParser.Token(PropertiesParser.Type.SEPARATOR, "=")); - } else { - pos.next(); - } - if (pos.isType(PropertiesParser.Type.VALUE)) { - pos.replace(new PropertiesParser.Token(PropertiesParser.Type.VALUE, rawValue, value)); - } else { - pos.add(new PropertiesParser.Token(PropertiesParser.Type.VALUE, rawValue, value)); + validate(pos.isType(PropertiesParser.Type.SEPARATOR), pos); + // A key-only property has an empty separator token; adding a value needs a delimiter. + if (pos.raw().isEmpty()) { + pos.replace(new PropertiesParser.Token(PropertiesParser.Type.SEPARATOR, "=")); } + pos.next(); + validate(pos.isType(PropertiesParser.Type.VALUE), pos); + pos.replace(new PropertiesParser.Token(PropertiesParser.Type.VALUE, rawValue, value)); } // Add new tokens to the end of the list of tokens @@ -904,22 +888,11 @@ private Properties load(List ts) { for (PropertiesParser.Token token : tokens) { if (token.type == PropertiesParser.Type.KEY) { key = token.getText(); - } - if (token.type == PropertiesParser.Type.SEPARATOR && key == null) { - // In case if a name-less property - key = ""; } else if (token.type == PropertiesParser.Type.VALUE) { values.put(key, token.getText()); key = null; - } else if (token.isEol() && key != null) { - // In case of value-less properties - values.put(key, ""); } } - // In case of the last property being value-less - if (key != null) { - values.put(key, ""); - } return this; } @@ -1126,13 +1099,7 @@ public Cursor beforeFirstProperty() { pos.next(); } // Make sure we're either at the end or we've found a property - validate( - pos.atEnd() - || pos.isType( - PropertiesParser.Type.VALUE, - PropertiesParser.Type.SEPARATOR, - PropertiesParser.Type.KEY), - pos); + validate(pos.atEnd() || pos.isType(PropertiesParser.Type.KEY), pos); if (!pos.atEnd()) { pos.prev(); } @@ -1146,19 +1113,13 @@ public Cursor beforeFirstProperty() { * @return a Cursor pointing to the right position or {@code -1} if not found */ public Cursor afterLastProperty() { - // Track back from end until we encounter the last VALUE token (if any) + // Track back from end until we encounter the last VALUE token Cursor pos = last(); while (pos.isType(PropertiesParser.Type.WHITESPACE, PropertiesParser.Type.COMMENT)) { pos.prev(); } // Make sure we're either at the start or we've found a property - validate( - pos.atStart() - || pos.isType( - PropertiesParser.Type.VALUE, - PropertiesParser.Type.SEPARATOR, - PropertiesParser.Type.KEY), - pos); + validate(pos.atStart() || pos.isType(PropertiesParser.Type.VALUE), pos); if (!pos.atStart()) { pos.next(); } diff --git a/src/main/java/org/codejive/properties/PropertiesParser.java b/src/main/java/org/codejive/properties/PropertiesParser.java index e361018..8a37551 100644 --- a/src/main/java/org/codejive/properties/PropertiesParser.java +++ b/src/main/java/org/codejive/properties/PropertiesParser.java @@ -209,7 +209,8 @@ public boolean tryAdvance(Consumer action) { */ public Token nextToken() throws IOException { int ch = peekChar(); - if (isEof(ch)) { + // A property at EOF may still need its empty separator and/or value token. + if (isEof(ch) && state == null) { return null; } Function isValid = (c) -> false; diff --git a/src/test/java/org/codejive/properties/TestPropertiesParser.java b/src/test/java/org/codejive/properties/TestPropertiesParser.java index d2f719b..f967642 100644 --- a/src/test/java/org/codejive/properties/TestPropertiesParser.java +++ b/src/test/java/org/codejive/properties/TestPropertiesParser.java @@ -30,6 +30,7 @@ public class TestPropertiesParser { + " two \\\r\n" + "\tthree\n" + "key.4 = \\u1234\r\n" + + "line-with-missing-value = \n" + "line-with-missing-delim \n" + "multidelim===value\n" + " # final comment"; @@ -83,6 +84,10 @@ void testTokens() throws IOException { new Token(Type.SEPARATOR, " = "), new Token(Type.VALUE, "\\u1234", "\u1234"), new Token(Type.WHITESPACE, "\r\n"), + new Token(Type.KEY, "line-with-missing-value"), + new Token(Type.SEPARATOR, " = "), + new Token(Type.VALUE, ""), + new Token(Type.WHITESPACE, "\n"), new Token(Type.KEY, "line-with-missing-delim"), new Token(Type.SEPARATOR, " "), new Token(Type.VALUE, ""), @@ -102,4 +107,28 @@ void testStringify() throws IOException { String props2 = tokens.map(Token::getRaw).collect(Collectors.joining()); assertThat(props2).isEqualTo(props); } + + @Test + void testEmptySeparatorAndValueAtEof() throws IOException { + List tokens = + PropertiesParser.tokens(new StringReader("foo=")).collect(Collectors.toList()); + + assertThat(tokens) + .containsExactly( + new Token(Type.KEY, "foo"), + new Token(Type.SEPARATOR, "="), + new Token(Type.VALUE, "")); + } + + @Test + void testMissingSeparatorAndValueAtEof() throws IOException { + List tokens = + PropertiesParser.tokens(new StringReader("foo")).collect(Collectors.toList()); + + assertThat(tokens) + .containsExactly( + new Token(Type.KEY, "foo"), + new Token(Type.SEPARATOR, ""), + new Token(Type.VALUE, "")); + } }