Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea/
.vscode/
target/
11 changes: 8 additions & 3 deletions src/main/java/org/codejive/properties/Cursor.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ private Cursor(List<PropertiesParser.Token> tokens, int index) {
}

public boolean atStart() {
return index < 0;
return tokens.isEmpty() || index < 0;
}

public boolean atEnd() {
return index >= tokens.size();
return tokens.isEmpty() || index >= tokens.size();
}

public int position() {
Expand Down Expand Up @@ -174,7 +174,12 @@ public int prevCount(Predicate<PropertiesParser.Token> accept) {
* @return a Cursor pointing to the start of the line
*/
public Cursor home() {
prevWhile(tk -> !tk.isEol());
if (atEnd()) {
return this;
}
while (index > 0 && !tokens.get(index - 1).isEol()) {
prev();
}
return this;
}

Expand Down
69 changes: 55 additions & 14 deletions src/main/java/org/codejive/properties/Properties.java
Original file line number Diff line number Diff line change
Expand Up @@ -437,17 +437,10 @@ private void replaceValue(String key, String rawValue, String value) {
pos.replace(new PropertiesParser.Token(PropertiesParser.Type.VALUE, rawValue, value));
}

// Add new tokens to the end of the list of tokens
// Add a new property to the end of the list of tokens
private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String value) {
Cursor pos = afterLastProperty();
// Add a newline whitespace token if necessary
if (!pos.atStart()) {
if (pos.isEol()) {
pos.next().addEol(eolType).prev();
} else {
pos.addEol(eolType);
}
} else {
if (pos.atStart()) {
// We're at the start, meaning there are no properties yet,
// but there might be comments, so we move forward again,
// skipping any header comments
Expand All @@ -472,13 +465,55 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
}
}
}
return addNewKeyValue(pos, rawKey, key, rawValue, value);
}

// Add a new property at the given position in the list of tokens
// NB: this method will make sure to move the cursor to a legal insertion point
private Cursor addNewKeyValue(
Cursor pos, String rawKey, String key, String rawValue, String value) {
if (pos == null) {
pos = afterLastProperty();
}

// Add a newline whitespace token if necessary
pos = prepareNewLine(pos);

// Add tokens for key, separator and value
pos.add(new PropertiesParser.Token(PropertiesParser.Type.KEY, rawKey, key));
pos.add(new PropertiesParser.Token(PropertiesParser.Type.SEPARATOR, "="));
pos.add(new PropertiesParser.Token(PropertiesParser.Type.VALUE, rawValue, value));
return pos;
}

// Prepare the list for adding a new line at the given position. It will make sure the cursor
// is at a valid insertion point and that any required EOLs are added.
private Cursor prepareNewLine(Cursor pos) {
if (!isEmpty()) {
if (pos.atEnd()) {
// We're at the end of the list and we want to maintain the current structure.
// Meaning that if currently the last token is an EOL, we want to maintain that
// and make sure that after adding a new line, we still end with an EOL. But if
// there is no EOL, we add one because we need a separation between the last line
// and the new line, but we won't add an extra EOL at the end.
if (pos.copy().prev().isEol()) {
pos.addEol(eolType).prev();
} else {
pos.addEol(eolType);
}
} else if (pos.atStart()) {
// If we're at the start we always add a new line at the beginning. Even if there
// already was an EOL, because if it was there it was most likely meant as spacing.
pos.addEol(eolType).prev();
} else {
// If we're at any other position, we add a new line at the beginning of the line
// to maintain separation and move our insertion point right in front of it.
pos.home().addEol(eolType).prev();
}
}
return pos;
}

@Override
public String remove(Object key) {
String skey = key.toString();
Expand Down Expand Up @@ -1054,8 +1089,8 @@ public Cursor beforeProperty(String key) {
Cursor pos =
first().nextWhile(
tk ->
tk.getType() == PropertiesParser.Type.KEY
&& !tk.getText().equals(key));
tk.getType() != PropertiesParser.Type.KEY
|| !tk.getText().equals(key));
if (pos.atEnd()) {
return null;
} else {
Expand All @@ -1075,11 +1110,17 @@ public Cursor afterProperty(String key) {
Cursor pos =
last().prevWhile(
tk ->
tk.getType() == PropertiesParser.Type.KEY
&& !tk.getText().equals(key));
tk.getType() != PropertiesParser.Type.KEY
|| !tk.getText().equals(key));
if (pos.atStart()) {
return null;
} else {
// Skip over the separator and the value
pos.next();
validate(pos.isType(PropertiesParser.Type.SEPARATOR), pos);
pos.next();
validate(pos.isType(PropertiesParser.Type.VALUE), pos);
// Skip over the end-of-line if it exists
if (pos.next().isEol()) {
pos.next();
}
Expand Down Expand Up @@ -1121,7 +1162,7 @@ public Cursor afterLastProperty() {
// Make sure we're either at the start or we've found a property
validate(pos.atStart() || pos.isType(PropertiesParser.Type.VALUE), pos);
if (!pos.atStart()) {
pos.next();
pos.next().nextIf(Token::isEol);
}
return pos;
}
Expand Down
113 changes: 113 additions & 0 deletions src/test/java/org/codejive/properties/TestProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -1109,6 +1111,101 @@ void testUnescaped() throws IOException, URISyntaxException {
assertThat(sw.toString()).isEqualTo(readAll(getResource("/test-unescaped.properties")));
}

@Test
void testBeforeProperty() throws IOException {
Properties p =
Properties.loadProperties(
new StringReader("# header\n\n # attached\n alpha=1\nbeta=2\n"));
Cursor beforeAlpha = p.beforeProperty("alpha");
assertThat(beforeAlpha).isNotNull();
assertThat(beforeAlpha.isWhitespace()).isTrue();
assertThat(beforeAlpha.raw()).isEqualTo(" ");

Cursor beforeBeta = p.beforeProperty("beta");
assertThat(beforeBeta).isNotNull();
assertThat(beforeBeta.isType(PropertiesParser.Type.KEY)).isTrue();
assertThat(beforeBeta.text()).isEqualTo("beta");

Properties noLeadingContent = Properties.loadProperties(new StringReader("alpha=1\n"));
assertThat(noLeadingContent.beforeProperty("alpha").position()).isZero();
assertThat(p.beforeProperty("missing")).isNull();
assertThat(new Properties().beforeProperty("missing")).isNull();

Properties duplicate = Properties.loadProperties(new StringReader("alpha=1\nalpha=2\n"));
assertThat(duplicate.beforeProperty("alpha").position()).isZero();
}

@Test
void testAfterProperty() throws IOException {
Properties p = Properties.loadProperties(new StringReader("alpha=1\nbeta=2\n# trailer\n"));
Cursor afterAlpha = p.afterProperty("alpha");
assertThat(afterAlpha).isNotNull();
assertThat(afterAlpha.isType(PropertiesParser.Type.KEY)).isTrue();
assertThat(afterAlpha.text()).isEqualTo("beta");

Cursor afterBeta = p.afterProperty("beta");
assertThat(afterBeta).isNotNull();
assertThat(afterBeta.isType(PropertiesParser.Type.COMMENT)).isTrue();
assertThat(afterBeta.raw()).isEqualTo("# trailer");

Properties noFinalEol = Properties.loadProperties(new StringReader("alpha=1"));
assertThat(noFinalEol.afterProperty("alpha").atEnd()).isTrue();

Properties keyOnly = Properties.loadProperties(new StringReader("alpha\nbeta=value"));
assertThat(keyOnly.afterProperty("alpha").text()).isEqualTo("beta");
assertThat(keyOnly.afterProperty("missing")).isNull();
assertThat(new Properties().afterProperty("missing")).isNull();

Properties crlf = Properties.loadProperties(new StringReader("alpha=1\r\nbeta=2\r\n"));
assertThat(crlf.afterProperty("alpha").text()).isEqualTo("beta");

Properties duplicate =
Properties.loadProperties(new StringReader("alpha=1\nbeta=2\nalpha=3"));
assertThat(duplicate.afterProperty("alpha").atEnd()).isTrue();
}

@Test
void testBeforeFirstProperty() throws IOException {
Properties withHeader =
Properties.loadProperties(
new StringReader("# header\n\n # attached\n alpha=1\nbeta=2\n"));
Cursor beforeFirst = withHeader.beforeFirstProperty();
assertThat(beforeFirst.isWhitespace()).isTrue();
assertThat(beforeFirst.raw()).isEqualTo(" ");

Properties withoutHeader = Properties.loadProperties(new StringReader("alpha=1\nbeta=2"));
assertThat(withoutHeader.beforeFirstProperty().atStart()).isTrue();

Properties commentsOnly =
Properties.loadProperties(new StringReader("# header\n\n# another\n"));
assertThat(commentsOnly.beforeFirstProperty().atEnd()).isTrue();
assertThat(new Properties().beforeFirstProperty().atStart()).isTrue();
}

@Test
void testAfterLastProperty() throws IOException {
Properties withTrailer =
Properties.loadProperties(
new StringReader("# header\nalpha=1\nbeta=2\n# trailer\n"));
Cursor afterLast = withTrailer.afterLastProperty();
assertThat(afterLast.isType(PropertiesParser.Type.COMMENT)).isTrue();
assertThat(afterLast.raw()).isEqualTo("# trailer");

Properties withBlankTrailer =
Properties.loadProperties(new StringReader("alpha=1\n\n# trailer\n"));
assertThat(withBlankTrailer.afterLastProperty().isEol()).isTrue();

Properties noFinalEol = Properties.loadProperties(new StringReader("alpha=1"));
assertThat(noFinalEol.afterLastProperty().atEnd()).isTrue();

Properties keyOnly = Properties.loadProperties(new StringReader("alpha\n"));
assertThat(keyOnly.afterLastProperty().atEnd()).isTrue();

Properties commentsOnly = Properties.loadProperties(new StringReader("# header\n\n"));
assertThat(commentsOnly.afterLastProperty().atStart()).isTrue();
assertThat(new Properties().afterLastProperty().atStart()).isTrue();
}

@Test
void testCursor() throws IOException, URISyntaxException {
Properties p = Properties.loadProperties(getResource("/test.properties"));
Expand All @@ -1118,6 +1215,22 @@ void testCursor() throws IOException, URISyntaxException {
assertThat(c.prevCount(t -> true)).isEqualTo(p.last().position() + 1);
}

@Test
void testCursorHome() {
List<PropertiesParser.Token> tokens =
Arrays.asList(
new PropertiesParser.Token(PropertiesParser.Type.KEY, "first"),
new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\n"),
new PropertiesParser.Token(PropertiesParser.Type.KEY, "second"),
new PropertiesParser.Token(PropertiesParser.Type.SEPARATOR, "="),
new PropertiesParser.Token(PropertiesParser.Type.VALUE, "value"));

assertThat(Cursor.first(tokens).home().position()).isZero();
assertThat(Cursor.index(tokens, 4).home().position()).isEqualTo(2);
assertThat(Cursor.index(tokens, 2).home().position()).isEqualTo(2);
assertThat(Cursor.last(tokens).next().home().position()).isEqualTo(tokens.size());
}

@Test
void testMissingDelim() throws IOException, URISyntaxException {
Properties p = Properties.loadProperties(getResource("/test-missingdelim.properties"));
Expand Down
Loading