diff --git a/src/main/java/org/codejive/properties/Cursor.java b/src/main/java/org/codejive/properties/Cursor.java index e5dfc60..f9872c3 100644 --- a/src/main/java/org/codejive/properties/Cursor.java +++ b/src/main/java/org/codejive/properties/Cursor.java @@ -7,6 +7,25 @@ public class Cursor { private final List tokens; private int index; + public enum EolType { + LF("\n", EOL_LF), + CRLF("\r\n", EOL_CRLF), + SYSTEM(System.lineSeparator(), System.lineSeparator().equals("\r\n") ? EOL_CRLF : EOL_LF); + + public final String text; + public final PropertiesParser.Token token; + + EolType(String text, PropertiesParser.Token token) { + this.text = text; + this.token = token; + } + } + + private static final PropertiesParser.Token EOL_LF = + new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\n"); + private static final PropertiesParser.Token EOL_CRLF = + new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\r\n"); + public static Cursor index(List tokens, int index) { return new Cursor(tokens, index); } @@ -155,8 +174,8 @@ public Cursor add(PropertiesParser.Token token) { return this; } - public Cursor addEol() { - return add(PropertiesParser.Token.EOL); + public Cursor addEol(EolType eolType) { + return add(eolType.token); } private void addToken(int index, PropertiesParser.Token token) { diff --git a/src/main/java/org/codejive/properties/Properties.java b/src/main/java/org/codejive/properties/Properties.java index a027e80..f887417 100644 --- a/src/main/java/org/codejive/properties/Properties.java +++ b/src/main/java/org/codejive/properties/Properties.java @@ -26,21 +26,29 @@ public class Properties extends AbstractMap { private final LinkedHashMap values; private final List tokens; private final Properties defaults; + private Cursor.EolType eolType; public Properties() { this((Properties) null); } + public Properties(Cursor.EolType eolType) { + this((Properties) null); + this.eolType = eolType; + } + public Properties(Properties defaults) { this.defaults = defaults; values = new LinkedHashMap<>(); tokens = new ArrayList<>(); + eolType = defaults != null ? defaults.eolType : Cursor.EolType.SYSTEM; } private Properties(Properties defaults, List tokens) { this.defaults = defaults; values = new LinkedHashMap<>(); this.tokens = tokens; + eolType = defaults != null ? defaults.eolType : Cursor.EolType.SYSTEM; rawEntrySet() .forEach( e -> { @@ -48,6 +56,26 @@ private Properties(Properties defaults, List tokens) { }); } + /** + * Returns the EOL type currently used by this properties object. This is what will be used when + * adding new lines to the properties object. + * + * @return the EOL type, LF, CRLF or SYSTEM + */ + Cursor.EolType getEolType() { + return eolType; + } + + /** + * Sets the EOL type to be used by this properties object. The new value will only be applied to + * future lines added to the properties object. + * + * @param eolType the EOL type, LF, CRLF or SYSTEM + */ + void setEolType(Cursor.EolType eolType) { + this.eolType = eolType; + } + /** * Searches for the property with the specified key in this property list. If the key is not * found in this property list, the default property list, and its defaults, recursively, are @@ -216,7 +244,7 @@ public Properties flatten() { * @return a Properties object */ public Properties flattened() { - Properties result = new Properties(); + Properties result = new Properties(eolType); flatten(result); return result; } @@ -437,9 +465,9 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String if (pos.hasToken()) { pos.next(); if (pos.isEol()) { - pos.next().addEol().prev(); + pos.next().addEol(eolType).prev(); } else { - pos.addEol(); + pos.addEol(eolType); } } else { // 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 Cursor pp = last(); if (pp.isType(PropertiesParser.Type.COMMENT)) { // If the last token is a comment, we're short two EOLs - pos.addEol(); - pos.addEol(); + pos.addEol(eolType); + pos.addEol(eolType); } else if (pp.isEol()) { // If the last element is an EOL we still need one (*) - pos.addEol(); + pos.addEol(eolType); } } else if (pos.isEol()) { // If the current element is an EOL we know there are at least 2 (*), @@ -590,7 +618,7 @@ private Cursor setComment(List indices, Cursor pos, List commen // Add any additional lines (when there are more new lines than old ones) for (int j = i; j < newcs.size(); j++) { pos.add(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, newcs.get(j))); - pos.addEol(); + pos.addEol(eolType); } return pos; @@ -877,6 +905,7 @@ public void load(Reader reader) throws IOException { private Properties load(List ts) { tokens.addAll(ts); + eolType = determineEol(); String key = null; for (PropertiesParser.Token token : tokens) { if (token.type == PropertiesParser.Type.KEY) { @@ -974,15 +1003,14 @@ public void store(Writer writer, String... comment) throws IOException { Cursor pos = first(); if (comment.length > 0) { pos = determineStoreInsertionPoint(); - String nl = determineNewline(); List newcs = normalizeComments(Arrays.asList(comment), "# "); for (String c : newcs) { writer.write(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, c).getRaw()); - writer.write(nl); + writer.write(eolType.text); } // We write an extra empty line so this comment won't be taken as part of the first // property - writer.write(nl); + writer.write(eolType.text); } while (pos.hasToken()) { writer.write(pos.raw()); @@ -999,7 +1027,7 @@ public void store(Writer writer, String... comment) throws IOException { * * @return A string containing the line ending to use */ - String determineNewline() { + Cursor.EolType determineEol() { boolean lf = false; boolean crlf = false; for (PropertiesParser.Token token : tokens) { @@ -1012,11 +1040,11 @@ String determineNewline() { } } if (lf && crlf) { - return System.lineSeparator(); + return Cursor.EolType.SYSTEM; } else if (crlf) { - return "\r\n"; + return Cursor.EolType.CRLF; } else { - return "\n"; + return Cursor.EolType.LF; } } diff --git a/src/main/java/org/codejive/properties/PropertiesParser.java b/src/main/java/org/codejive/properties/PropertiesParser.java index 06174b2..e361018 100644 --- a/src/main/java/org/codejive/properties/PropertiesParser.java +++ b/src/main/java/org/codejive/properties/PropertiesParser.java @@ -50,9 +50,6 @@ public static class Token { final String raw; final String text; - public static final Token EOL = - new PropertiesParser.Token(PropertiesParser.Type.WHITESPACE, "\n"); - /** * Constructor for tokens where the raw value and the text value are exactly the same. * diff --git a/src/test/java/org/codejive/properties/TestProperties.java b/src/test/java/org/codejive/properties/TestProperties.java index fae4729..f5fa348 100644 --- a/src/test/java/org/codejive/properties/TestProperties.java +++ b/src/test/java/org/codejive/properties/TestProperties.java @@ -4,8 +4,8 @@ import java.io.*; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -400,14 +400,14 @@ void testStoreTest2() throws IOException, URISyntaxException { void testLf() throws IOException, URISyntaxException { Path f = getResource("/test.properties"); Properties p = Properties.loadProperties(f); - assertThat(p.determineNewline()).isEqualTo("\n"); + assertThat(p.determineEol()).isEqualTo(Cursor.EolType.LF); } @Test void testCrLf() throws IOException, URISyntaxException { Path f = getResource("/testcrlf.properties"); Properties p = Properties.loadProperties(f); - assertThat(p.determineNewline()).isEqualTo("\r\n"); + assertThat(p.determineEol()).isEqualTo(Cursor.EolType.CRLF); } @Test @@ -544,7 +544,7 @@ void testSetCommentNonExistent() throws IOException, URISyntaxException { @Test void testPut() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.put("one", "simple"); p.put("two", "value containing spaces"); p.put("three", "and escapes\n\t\r\f"); @@ -635,7 +635,7 @@ void testPut() throws IOException, URISyntaxException { @Test void testSetProperty() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.setProperty("one", "simple", "! comment3"); p.setProperty("two", "value containing spaces"); p.setProperty( @@ -654,7 +654,7 @@ void testSetProperty() throws IOException, URISyntaxException { @Test void testPutRaw() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.putRaw("one", "simple"); p.putRaw("two", "value containing spaces"); p.putRaw("three", "and escapes\\n\\t\\r\\f"); @@ -746,7 +746,7 @@ void testPutRaw() throws IOException, URISyntaxException { @SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test @Test void testPutReplaceFirst() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.put("one", "simple"); p.put("two", "value containing spaces"); p.put("three", "and escapes\n\t\r\f"); @@ -760,7 +760,7 @@ void testPutReplaceFirst() throws IOException, URISyntaxException { @SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test @Test void testPutReplaceMiddle() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.put("one", "simple"); p.put("two", "value containing spaces"); p.put("three", "and escapes\n\t\r\f"); @@ -774,7 +774,7 @@ void testPutReplaceMiddle() throws IOException, URISyntaxException { @SuppressWarnings("OverwrittenKey") // assigning the same key twice is the point of this test @Test void testPutReplaceLast() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.put("one", "simple"); p.put("two", "value containing spaces"); p.put("three", "and escapes\n\t\r\f"); @@ -887,7 +887,7 @@ void testPutFirstWithHeader3Eol() throws IOException, URISyntaxException { @Test void testPutNull() { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); assertThatThrownBy(() -> p.put("one", null)).isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> p.setProperty("one", null)) .isInstanceOf(NullPointerException.class); @@ -898,7 +898,7 @@ void testPutNull() { @Test void testPutUnicode() throws IOException, URISyntaxException { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); p.putRaw("encoded", "\\u0627\\u0644\\u0623\\u0644\\u0628\\u0627\\u0646\\u064a\\u0629"); p.put("text", "\u0627\u0644\u0623\u0644\u0628\u0627\u0646\u064a\u0629"); StringWriter sw = new StringWriter(); @@ -1140,13 +1140,25 @@ void testMultiDelim() throws IOException, URISyntaxException { @Test void testPutAll() { - Properties p = new Properties(); + Properties p = new Properties(Cursor.EolType.LF); java.util.Properties ju = new java.util.Properties(); ju.setProperty("foo", "bar"); p.putAll(ju); assertThat(p.getProperty("foo")).isEqualTo("bar"); } + @Test + void testPutCrLf() throws IOException { + final String given = "first=line\r\n# trailer"; + final String expected = "first=line\r\nsecond=line\r\n# trailer"; + + Properties p = Properties.loadProperties(new StringReader(given)); + p.put("second", "line"); + StringWriter sw = new StringWriter(); + p.store(sw); + assertThat(sw.toString()).isEqualTo(expected); + } + private Path getResource(String name) throws URISyntaxException { URL resource = getClass().getResource(name); if (resource == null)