Skip to content
Closed
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
31 changes: 30 additions & 1 deletion src/main/java/org/codejive/properties/Cursor.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,29 @@ private Cursor(List<PropertiesParser.Token> tokens, int index) {
this.index = index;
}

/**
* @return {@code true} if the Cursor is at the start of the document, before the first token.
*/
public boolean atStart() {
return index < 0;
}

/**
* @return {@code true} if the Cursor is at the end of the document, after the last token.
*/
public boolean atEnd() {
return index >= tokens.size();
}

public int position() {
return index;
}

public boolean hasToken() {
return hasToken(index);
}

private boolean hasToken(int index) {
return index >= 0 && index < tokens.size();
}

Expand Down Expand Up @@ -141,17 +155,32 @@ public int prevCount(Predicate<PropertiesParser.Token> accept) {
return cnt;
}

/**
* Inserts a token at the current position, pushing the current token (if any) forwards.
* <br/>
* This method advances the cursor by one token (causing it to point at the initial token again).
* @param token the token to insert.
* @return {@code this}
*/
public Cursor add(PropertiesParser.Token token) {
if (index < 0)
index = 0;

addToken(index++, token);
return this;
}

/**
* Inserts an EOL Token at the current position.
* @see #add(PropertiesParser.Token)
* @return {@code this}
*/
public Cursor addEol() {
return add(PropertiesParser.Token.EOL);
}

private void addToken(int index, PropertiesParser.Token token) {
if (hasToken()) {
if (hasToken(index)) {
tokens.add(index, token);
} else {
tokens.add(token);
Expand Down
24 changes: 16 additions & 8 deletions src/main/java/org/codejive/properties/Properties.java
Original file line number Diff line number Diff line change
Expand Up @@ -393,27 +393,30 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
while (pos.isType(PropertiesParser.Type.WHITESPACE, PropertiesParser.Type.COMMENT)) {
pos.prev();
}
// Make sure we're either at the start or we've found a VALUE
validate(pos.atStart() || pos.isType(PropertiesParser.Type.VALUE), pos);
// 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);
// Add a newline whitespace token if necessary
if (pos.hasToken()) {
pos.next();
if (pos.isEol()) {
pos.next().addEol().prev();
} else {
pos.addEol();
}
pos.addEol();
} else {
// 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
pos = skipHeaderCommentLines();
if (pos.position() > 0) {
// We have to make sure there are at least 2 EOLs after the last comment
pos.prev(); // move cursor back onto the last eol (otherwise prevCount fails)
int eols = pos.prevCount(t -> t.isEol());
for (int i = 0; i < 2 - eols; i++) {
pos.skip(eols); // return to the position from before 'prevCount' was called
pos.next(); // move cursor past the last eol
int numEolsToAdd = Math.max(0, 2 - eols);
for (int i = 0; i < numEolsToAdd; i++) {
pos.addEol();
}
// if there is another comment following this token, push it to a new line below this property.
if (!pos.atEnd())
pos.addEol().prev();
}
}
// Add tokens for key, separator and value
Expand Down Expand Up @@ -858,11 +861,16 @@ private Properties load(List<PropertiesParser.Token> ts) {
String key = null;
for (PropertiesParser.Token token : tokens) {
if (token.type == PropertiesParser.Type.KEY) {
if (key != null)
values.put(key, "");
key = token.getText();
} else if (token.type == PropertiesParser.Type.VALUE) {
values.put(key, token.getText());
key = null;
}
}
if (key != null)
values.put(key, "");
return this;
}

Expand Down
57 changes: 57 additions & 0 deletions src/test/java/org/codejive/properties/TestCursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.codejive.properties;

import static org.assertj.core.api.Assertions.*;

import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;

public class TestCursor {
@Test
void addTokenToEmptyDocument() throws IOException {
Properties p = Properties.loadProperties(new StringReader(""));
Cursor c = p.first();

// expect no tokens to be in the document
assertThat(c.hasToken()).isEqualTo(false);

// expect that add should not throw
PropertiesParser.Token addedToken = new PropertiesParser.Token(PropertiesParser.Type.KEY, "test", "test");
c.add(addedToken);

// expect that add skips the added token
assertThat(c.hasToken()).isEqualTo(false);
c.prev();
assertThat(c.hasToken()).isEqualTo(true);
assertThat(c.token()).isEqualTo(addedToken);
}

@Test
void addTokensToDocument() throws IOException {
Properties p = Properties.loadProperties(new StringReader("key=value"
+ "\nkey2=value2"));

Cursor c = p.first();
c.add(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, "# beginning"));
c.addEol();

c = p.indexOf("key2");
c.add(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, "# middle"));
c.addEol();

c = p.last();
c.next();
c.addEol();
c.add(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, "# end"));

ByteArrayOutputStream os = new ByteArrayOutputStream();
p.store(os);
assertThat(os.toString()).isEqualTo("# beginning" +
"\nkey=value" +
"\n# middle" +
"\nkey2=value2" +
"\n# end");
}
}
140 changes: 135 additions & 5 deletions src/test/java/org/codejive/properties/TestProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.io.*;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand Down Expand Up @@ -62,6 +63,7 @@ void testLoad() throws IOException, URISyntaxException {
new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234"));
}

@Test
void testLoadCrLf() throws IOException, URISyntaxException {
Properties p = Properties.loadProperties(getResource("/testcrlf.properties"));
assertThat(p).size().isEqualTo(7);
Expand All @@ -87,7 +89,7 @@ void testLoadCrLf() throws IOException, URISyntaxException {
"and escapes\\n\\t\\r\\f",
"everywhere ",
"value",
"one \\\n two \\\n\tthree",
"one \\\r\n two \\\r\n\tthree",
"\\u1234\u1234");
assertThat(p.entrySet())
.containsExactly(
Expand All @@ -105,7 +107,7 @@ void testLoadCrLf() throws IOException, URISyntaxException {
new AbstractMap.SimpleEntry<>("three", "and escapes\\n\\t\\r\\f"),
new AbstractMap.SimpleEntry<>("\\ with\\ spaces", "everywhere "),
new AbstractMap.SimpleEntry<>("altsep", "value"),
new AbstractMap.SimpleEntry<>("multiline", "one \\\n two \\\n\tthree"),
new AbstractMap.SimpleEntry<>("multiline", "one \\\r\n two \\\r\n\tthree"),
new AbstractMap.SimpleEntry<>("key.4", "\\u1234\u1234"));
}

Expand Down Expand Up @@ -467,6 +469,55 @@ void testPutFirstWithHeader() throws IOException, URISyntaxException {
}
}

@Test
void testPutFirstWithHeader1Eol() throws IOException, URISyntaxException {
try (StringReader sr = new StringReader("# A header comment\n")) {
Properties p = Properties.loadProperties(sr);
p.put("first", "dummy");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString())
.isEqualTo(readAll(getResource("/test-putfirstwithheader.properties")));
}
}

@Test
void testPutFirstWithHeader2Eol() throws IOException, URISyntaxException {
try (StringReader sr = new StringReader("# A header comment\n\n")) {
Properties p = Properties.loadProperties(sr);
p.put("first", "dummy");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString())
.isEqualTo(readAll(getResource("/test-putfirstwithheader.properties")));
}
}

@Test
void testPutFirstWithHeader3Eol() throws IOException, URISyntaxException {
String expected = "# A header comment\n\n\nfirst=dummy";
try (StringReader sr = new StringReader("# A header comment\n\n\n")) {
Properties p = Properties.loadProperties(sr);
p.put("first", "dummy");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo(expected);
}
}

@Test
void testPutFirstWithHeaderAndTrailer() throws IOException, URISyntaxException {
String given = "# A header comment\n\n\n# A trailer\n";
String expected = "# A header comment\n\n\nfirst=dummy\n# A trailer\n";
try (StringReader sr = new StringReader(given)) {
Properties p = Properties.loadProperties(sr);
p.put("first", "dummy");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo(expected);
}
}

@Test
void testPutNull() throws IOException, URISyntaxException {
Properties p = new Properties();
Expand Down Expand Up @@ -553,7 +604,7 @@ void testRemoveNonExistent() throws IOException, URISyntaxException {
@Test
void testRemoveMiddleIterator() throws IOException, URISyntaxException {
Properties p = Properties.loadProperties(getResource("/test.properties"));
Iterator iter = p.keySet().iterator();
Iterator<String> iter = p.keySet().iterator();
while (iter.hasNext()) {
if (iter.next().equals("three")) {
iter.remove();
Expand Down Expand Up @@ -622,7 +673,7 @@ void testInteropStore() throws IOException, URISyntaxException {
}

@Test
void testInteropPutLoad() throws IOException, URISyntaxException {
void testInteropPutLoad() throws IOException {
java.util.Properties p = new java.util.Properties();
p.put("one", "simple");
p.put("two", "value containing spaces");
Expand Down Expand Up @@ -713,11 +764,90 @@ void testPutAll() {
assertThat(p.getProperty("foo")).isEqualTo("bar");
}

@Test
void testLoadEmptyValue() throws IOException {
String document = "firstline=\n" +
"secondline=";
Properties p = Properties.loadProperties(new StringReader(document));
java.util.Properties ju = new java.util.Properties();
ju.load(new StringReader(document));
assertThat(p.asJUProperties()).isEqualTo(ju);

// also verify that put works
p.put("thirdline", "");
ju.put("thirdline", "");
assertThat(p.asJUProperties()).isEqualTo(ju);

// verify store
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo("firstline=\n" +
"secondline=\n" +
"thirdline=");
}

@Test
void testLoadMissingSeparator() throws IOException {
String document = "firstline\n" +
"secondline";
Properties p = Properties.loadProperties(new StringReader(document));
java.util.Properties ju = new java.util.Properties();
ju.load(new StringReader(document));
assertThat(p.asJUProperties()).isEqualTo(ju);

// also verify that put works
p.put("thirdline", "");
ju.put("thirdline", "");
assertThat(p.asJUProperties()).isEqualTo(ju);

// verify store
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo("firstline\n" +
"secondline\n" +
"thirdline=");
}

@Test
void testPutTrailingSpace() throws IOException {
String document = "foo=x \n";
Properties p = Properties.loadProperties(new StringReader(document));
p.put("bar", "");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo("foo=x \n" +
"bar=\n");
}

@Test
void testPutMissingSeparatorTrailingSpace() throws IOException {
String document = "foo \n";
Properties p = Properties.loadProperties(new StringReader(document));
p.put("bar", "");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo("foo \n" +
"bar=\n");
}

@Test
void testPutMissingSeparatorTrailingComment() throws IOException {
String document = "foo\n" +
"# trailer";
Properties p = Properties.loadProperties(new StringReader(document));
p.put("bar", "");
StringWriter sw = new StringWriter();
p.store(sw);
assertThat(sw.toString()).isEqualTo("foo\n" +
"bar=\n" +
"# trailer");
}

private Path getResource(String name) throws URISyntaxException {
return Paths.get(getClass().getResource(name).toURI());
}

private String readAll(Path f) throws IOException {
return new String(Files.readAllBytes(f));
return new String(Files.readAllBytes(f), StandardCharsets.UTF_8);
}
}
Loading