Skip to content

Commit 2bc6fd1

Browse files
authored
fix: added possibilitry for setting the EOL type of a Properties object (#79)
This allows us to control the line endings of the properties file. It's also used when reading from files, to ensure that any new proprties added will use the correct line endings as well. Fixes #74
1 parent f2e8837 commit 2bc6fd1

4 files changed

Lines changed: 87 additions & 31 deletions

File tree

‎src/main/java/org/codejive/properties/Cursor.java‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,25 @@ public class Cursor {
77
private final List<PropertiesParser.Token> tokens;
88
private int index;
99

10+
public enum EolType {
11+
LF("\n", EOL_LF),
12+
CRLF("\r\n", EOL_CRLF),
13+
SYSTEM(System.lineSeparator(), System.lineSeparator().equals("\r\n") ? EOL_CRLF : EOL_LF);
14+
15+
public final String text;
16+
public final PropertiesParser.Token token;
17+
18+
EolType(String text, PropertiesParser.Token token) {
19+
this.text = text;
20+
this.token = token;
21+
}
22+
}
23+
24+
private static final PropertiesParser.Token EOL_LF =
25+
new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\n");
26+
private static final PropertiesParser.Token EOL_CRLF =
27+
new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\r\n");
28+
1029
public static Cursor index(List<PropertiesParser.Token> tokens, int index) {
1130
return new Cursor(tokens, index);
1231
}
@@ -155,8 +174,8 @@ public Cursor add(PropertiesParser.Token token) {
155174
return this;
156175
}
157176

158-
public Cursor addEol() {
159-
return add(PropertiesParser.Token.EOL);
177+
public Cursor addEol(EolType eolType) {
178+
return add(eolType.token);
160179
}
161180

162181
private void addToken(int index, PropertiesParser.Token token) {

‎src/main/java/org/codejive/properties/Properties.java‎

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,56 @@ public class Properties extends AbstractMap<String, String> {
2626
private final LinkedHashMap<String, String> values;
2727
private final List<PropertiesParser.Token> tokens;
2828
private final Properties defaults;
29+
private Cursor.EolType eolType;
2930

3031
public Properties() {
3132
this((Properties) null);
3233
}
3334

35+
public Properties(Cursor.EolType eolType) {
36+
this((Properties) null);
37+
this.eolType = eolType;
38+
}
39+
3440
public Properties(Properties defaults) {
3541
this.defaults = defaults;
3642
values = new LinkedHashMap<>();
3743
tokens = new ArrayList<>();
44+
eolType = defaults != null ? defaults.eolType : Cursor.EolType.SYSTEM;
3845
}
3946

4047
private Properties(Properties defaults, List<PropertiesParser.Token> tokens) {
4148
this.defaults = defaults;
4249
values = new LinkedHashMap<>();
4350
this.tokens = tokens;
51+
eolType = defaults != null ? defaults.eolType : Cursor.EolType.SYSTEM;
4452
rawEntrySet()
4553
.forEach(
4654
e -> {
4755
values.put(unescape(e.getKey()), unescape(e.getValue()));
4856
});
4957
}
5058

59+
/**
60+
* Returns the EOL type currently used by this properties object. This is what will be used when
61+
* adding new lines to the properties object.
62+
*
63+
* @return the EOL type, LF, CRLF or SYSTEM
64+
*/
65+
Cursor.EolType getEolType() {
66+
return eolType;
67+
}
68+
69+
/**
70+
* Sets the EOL type to be used by this properties object. The new value will only be applied to
71+
* future lines added to the properties object.
72+
*
73+
* @param eolType the EOL type, LF, CRLF or SYSTEM
74+
*/
75+
void setEolType(Cursor.EolType eolType) {
76+
this.eolType = eolType;
77+
}
78+
5179
/**
5280
* Searches for the property with the specified key in this property list. If the key is not
5381
* found in this property list, the default property list, and its defaults, recursively, are
@@ -216,7 +244,7 @@ public Properties flatten() {
216244
* @return a <code>Properties</code> object
217245
*/
218246
public Properties flattened() {
219-
Properties result = new Properties();
247+
Properties result = new Properties(eolType);
220248
flatten(result);
221249
return result;
222250
}
@@ -437,9 +465,9 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
437465
if (pos.hasToken()) {
438466
pos.next();
439467
if (pos.isEol()) {
440-
pos.next().addEol().prev();
468+
pos.next().addEol(eolType).prev();
441469
} else {
442-
pos.addEol();
470+
pos.addEol(eolType);
443471
}
444472
} else {
445473
// We're at the start, meaning there are no properties yet,
@@ -453,11 +481,11 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
453481
Cursor pp = last();
454482
if (pp.isType(PropertiesParser.Type.COMMENT)) {
455483
// If the last token is a comment, we're short two EOLs
456-
pos.addEol();
457-
pos.addEol();
484+
pos.addEol(eolType);
485+
pos.addEol(eolType);
458486
} else if (pp.isEol()) {
459487
// If the last element is an EOL we still need one (*)
460-
pos.addEol();
488+
pos.addEol(eolType);
461489
}
462490
} else if (pos.isEol()) {
463491
// If the current element is an EOL we know there are at least 2 (*),
@@ -590,7 +618,7 @@ private Cursor setComment(List<Integer> indices, Cursor pos, List<String> commen
590618
// Add any additional lines (when there are more new lines than old ones)
591619
for (int j = i; j < newcs.size(); j++) {
592620
pos.add(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, newcs.get(j)));
593-
pos.addEol();
621+
pos.addEol(eolType);
594622
}
595623

596624
return pos;
@@ -877,6 +905,7 @@ public void load(Reader reader) throws IOException {
877905

878906
private Properties load(List<PropertiesParser.Token> ts) {
879907
tokens.addAll(ts);
908+
eolType = determineEol();
880909
String key = null;
881910
for (PropertiesParser.Token token : tokens) {
882911
if (token.type == PropertiesParser.Type.KEY) {
@@ -974,15 +1003,14 @@ public void store(Writer writer, String... comment) throws IOException {
9741003
Cursor pos = first();
9751004
if (comment.length > 0) {
9761005
pos = determineStoreInsertionPoint();
977-
String nl = determineNewline();
9781006
List<String> newcs = normalizeComments(Arrays.asList(comment), "# ");
9791007
for (String c : newcs) {
9801008
writer.write(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, c).getRaw());
981-
writer.write(nl);
1009+
writer.write(eolType.text);
9821010
}
9831011
// We write an extra empty line so this comment won't be taken as part of the first
9841012
// property
985-
writer.write(nl);
1013+
writer.write(eolType.text);
9861014
}
9871015
while (pos.hasToken()) {
9881016
writer.write(pos.raw());
@@ -999,7 +1027,7 @@ public void store(Writer writer, String... comment) throws IOException {
9991027
*
10001028
* @return A string containing the line ending to use
10011029
*/
1002-
String determineNewline() {
1030+
Cursor.EolType determineEol() {
10031031
boolean lf = false;
10041032
boolean crlf = false;
10051033
for (PropertiesParser.Token token : tokens) {
@@ -1012,11 +1040,11 @@ String determineNewline() {
10121040
}
10131041
}
10141042
if (lf && crlf) {
1015-
return System.lineSeparator();
1043+
return Cursor.EolType.SYSTEM;
10161044
} else if (crlf) {
1017-
return "\r\n";
1045+
return Cursor.EolType.CRLF;
10181046
} else {
1019-
return "\n";
1047+
return Cursor.EolType.LF;
10201048
}
10211049
}
10221050

‎src/main/java/org/codejive/properties/PropertiesParser.java‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,6 @@ public static class Token {
5050
final String raw;
5151
final String text;
5252

53-
public static final Token EOL =
54-
new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\n");
55-
5653
/**
5754
* Constructor for tokens where the raw value and the text value are exactly the same.
5855
*

‎src/test/java/org/codejive/properties/TestProperties.java‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
import java.io.*;
66
import java.net.URISyntaxException;
7-
import java.nio.charset.StandardCharsets;
87
import java.net.URL;
8+
import java.nio.charset.StandardCharsets;
99
import java.nio.file.Files;
1010
import java.nio.file.Path;
1111
import java.nio.file.Paths;
@@ -400,14 +400,14 @@ void testStoreTest2() throws IOException, URISyntaxException {
400400
void testLf() throws IOException, URISyntaxException {
401401
Path f = getResource("/test.properties");
402402
Properties p = Properties.loadProperties(f);
403-
assertThat(p.determineNewline()).isEqualTo("\n");
403+
assertThat(p.determineEol()).isEqualTo(Cursor.EolType.LF);
404404
}
405405

406406
@Test
407407
void testCrLf() throws IOException, URISyntaxException {
408408
Path f = getResource("/testcrlf.properties");
409409
Properties p = Properties.loadProperties(f);
410-
assertThat(p.determineNewline()).isEqualTo("\r\n");
410+
assertThat(p.determineEol()).isEqualTo(Cursor.EolType.CRLF);
411411
}
412412

413413
@Test
@@ -544,7 +544,7 @@ void testSetCommentNonExistent() throws IOException, URISyntaxException {
544544

545545
@Test
546546
void testPut() throws IOException, URISyntaxException {
547-
Properties p = new Properties();
547+
Properties p = new Properties(Cursor.EolType.LF);
548548
p.put("one", "simple");
549549
p.put("two", "value containing spaces");
550550
p.put("three", "and escapes\n\t\r\f");
@@ -635,7 +635,7 @@ void testPut() throws IOException, URISyntaxException {
635635

636636
@Test
637637
void testSetProperty() throws IOException, URISyntaxException {
638-
Properties p = new Properties();
638+
Properties p = new Properties(Cursor.EolType.LF);
639639
p.setProperty("one", "simple", "! comment3");
640640
p.setProperty("two", "value containing spaces");
641641
p.setProperty(
@@ -654,7 +654,7 @@ void testSetProperty() throws IOException, URISyntaxException {
654654

655655
@Test
656656
void testPutRaw() throws IOException, URISyntaxException {
657-
Properties p = new Properties();
657+
Properties p = new Properties(Cursor.EolType.LF);
658658
p.putRaw("one", "simple");
659659
p.putRaw("two", "value containing spaces");
660660
p.putRaw("three", "and escapes\\n\\t\\r\\f");
@@ -746,7 +746,7 @@ void testPutRaw() throws IOException, URISyntaxException {
746746
@SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test
747747
@Test
748748
void testPutReplaceFirst() throws IOException, URISyntaxException {
749-
Properties p = new Properties();
749+
Properties p = new Properties(Cursor.EolType.LF);
750750
p.put("one", "simple");
751751
p.put("two", "value containing spaces");
752752
p.put("three", "and escapes\n\t\r\f");
@@ -760,7 +760,7 @@ void testPutReplaceFirst() throws IOException, URISyntaxException {
760760
@SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test
761761
@Test
762762
void testPutReplaceMiddle() throws IOException, URISyntaxException {
763-
Properties p = new Properties();
763+
Properties p = new Properties(Cursor.EolType.LF);
764764
p.put("one", "simple");
765765
p.put("two", "value containing spaces");
766766
p.put("three", "and escapes\n\t\r\f");
@@ -774,7 +774,7 @@ void testPutReplaceMiddle() throws IOException, URISyntaxException {
774774
@SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test
775775
@Test
776776
void testPutReplaceLast() throws IOException, URISyntaxException {
777-
Properties p = new Properties();
777+
Properties p = new Properties(Cursor.EolType.LF);
778778
p.put("one", "simple");
779779
p.put("two", "value containing spaces");
780780
p.put("three", "and escapes\n\t\r\f");
@@ -887,7 +887,7 @@ void testPutFirstWithHeader3Eol() throws IOException, URISyntaxException {
887887

888888
@Test
889889
void testPutNull() {
890-
Properties p = new Properties();
890+
Properties p = new Properties(Cursor.EolType.LF);
891891
assertThatThrownBy(() -> p.put("one", null)).isInstanceOf(NullPointerException.class);
892892
assertThatThrownBy(() -> p.setProperty("one", null))
893893
.isInstanceOf(NullPointerException.class);
@@ -898,7 +898,7 @@ void testPutNull() {
898898

899899
@Test
900900
void testPutUnicode() throws IOException, URISyntaxException {
901-
Properties p = new Properties();
901+
Properties p = new Properties(Cursor.EolType.LF);
902902
p.putRaw("encoded", "\\u0627\\u0644\\u0623\\u0644\\u0628\\u0627\\u0646\\u064a\\u0629");
903903
p.put("text", "\u0627\u0644\u0623\u0644\u0628\u0627\u0646\u064a\u0629");
904904
StringWriter sw = new StringWriter();
@@ -1140,13 +1140,25 @@ void testMultiDelim() throws IOException, URISyntaxException {
11401140

11411141
@Test
11421142
void testPutAll() {
1143-
Properties p = new Properties();
1143+
Properties p = new Properties(Cursor.EolType.LF);
11441144
java.util.Properties ju = new java.util.Properties();
11451145
ju.setProperty("foo", "bar");
11461146
p.putAll(ju);
11471147
assertThat(p.getProperty("foo")).isEqualTo("bar");
11481148
}
11491149

1150+
@Test
1151+
void testPutCrLf() throws IOException {
1152+
final String given = "first=line\r\n# trailer";
1153+
final String expected = "first=line\r\nsecond=line\r\n# trailer";
1154+
1155+
Properties p = Properties.loadProperties(new StringReader(given));
1156+
p.put("second", "line");
1157+
StringWriter sw = new StringWriter();
1158+
p.store(sw);
1159+
assertThat(sw.toString()).isEqualTo(expected);
1160+
}
1161+
11501162
private Path getResource(String name) throws URISyntaxException {
11511163
URL resource = getClass().getResource(name);
11521164
if (resource == null)

0 commit comments

Comments
 (0)