@@ -455,22 +455,9 @@ private void replaceValue(String key, String rawValue, String value) {
455455
456456 // Add new tokens to the end of the list of tokens
457457 private Cursor addNewKeyValue (String rawKey , String key , String rawValue , String value ) {
458- // Track back from end until we encounter the last VALUE token (if any)
459- Cursor pos = last ();
460- while (pos .isType (PropertiesParser .Type .WHITESPACE , PropertiesParser .Type .COMMENT )) {
461- pos .prev ();
462- }
463- // Make sure we're either at the start or we've found a property
464- validate (
465- pos .atStart ()
466- || pos .isType (
467- PropertiesParser .Type .VALUE ,
468- PropertiesParser .Type .SEPARATOR ,
469- PropertiesParser .Type .KEY ),
470- pos );
458+ Cursor pos = afterLastProperty ();
471459 // Add a newline whitespace token if necessary
472- if (pos .hasToken ()) {
473- pos .next ();
460+ if (!pos .atStart ()) {
474461 if (pos .isEol ()) {
475462 pos .next ().addEol (eolType ).prev ();
476463 } else {
@@ -481,7 +468,7 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
481468 // but there might be comments, so we move forward again,
482469 // skipping any header comments
483470 // (*) = we'll always skip past the final comment's EOL
484- pos = determineAddNewInsertionPoint ();
471+ pos = afterHeader ();
485472 if (!pos .atStart ()) {
486473 // We have to make sure there are at least 2 EOLs after the last comment
487474 if (pos .atEnd ()) {
@@ -1007,9 +994,13 @@ public void store(OutputStream out, String... comment) throws IOException {
1007994 * @throws IOException Thrown when any IO error occurs during operation
1008995 */
1009996 public void store (Writer writer , String ... comment ) throws IOException {
1010- Cursor pos = first () ;
997+ Cursor pos ;
1011998 if (comment .length > 0 ) {
1012- pos = determineStoreInsertionPoint ();
999+ pos = afterHeader ();
1000+ if (!pos .atStart ()) {
1001+ // Skip any following empty lines
1002+ pos .nextWhile (PropertiesParser .Token ::isEol );
1003+ }
10131004 List <String > newcs = normalizeComments (Arrays .asList (comment ), "# " );
10141005 for (String c : newcs ) {
10151006 writer .write (new PropertiesParser .Token (PropertiesParser .Type .COMMENT , c ).getRaw ());
@@ -1018,6 +1009,8 @@ public void store(Writer writer, String... comment) throws IOException {
10181009 // We write an extra empty line so this comment won't be taken as part of the first
10191010 // property
10201011 writer .write (eolType .text );
1012+ } else {
1013+ pos = first ();
10211014 }
10221015 while (pos .hasToken ()) {
10231016 writer .write (pos .raw ());
@@ -1055,61 +1048,147 @@ Cursor.EolType determineEol() {
10551048 }
10561049 }
10571050
1058- private Cursor determineStoreInsertionPoint () {
1059- Cursor pos = skipHeaderCommentLines ();
1060- if (pos .isType (PropertiesParser .Type .KEY )) {
1061- // We found a comment attached to a property, not a header comment
1062- pos = first ();
1051+ Cursor index (int index ) {
1052+ return Cursor .index (tokens , index );
1053+ }
1054+
1055+ /**
1056+ * @return a Cursor pointing to the first token, or to {@code -1} if thw `Properties` object is
1057+ * empty.
1058+ */
1059+ public Cursor first () {
1060+ return Cursor .first (tokens );
1061+ }
1062+
1063+ /**
1064+ * @return a Cursor pointing to the last token, or to {@code -1} if thw `Properties` object is
1065+ * empty.
1066+ */
1067+ public Cursor last () {
1068+ return Cursor .last (tokens );
1069+ }
1070+
1071+ /**
1072+ * Returns a Cursor pointing to the position right before the property with the given key. If no
1073+ * such property exists, {@code null} will be returned. If the property was found the position
1074+ * will take into account any leading whitespace and will be positioned at the start of the
1075+ * line.
1076+ *
1077+ * @param key The name of property to look for
1078+ * @return a Cursor pointing to the right position or {@code null} if not found
1079+ */
1080+ public Cursor beforeProperty (String key ) {
1081+ Cursor pos =
1082+ first ().nextWhile (
1083+ tk ->
1084+ tk .getType () == PropertiesParser .Type .KEY
1085+ && !tk .getText ().equals (key ));
1086+ if (pos .atEnd ()) {
1087+ return null ;
10631088 } else {
1064- // Skip any following empty lines
1065- pos .nextWhile (PropertiesParser .Token ::isEol );
1089+ return pos .home ();
10661090 }
1067- return pos ;
10681091 }
10691092
1070- private Cursor determineAddNewInsertionPoint () {
1071- Cursor pos = skipHeaderCommentLines ();
1072- return skipHome (pos );
1093+ /**
1094+ * Returns a Cursor pointing to the position right after the property with the given key. If no
1095+ * such property exists, {@code null} will be returned. If the property was found, the position
1096+ * will be at the start of the next line, if it exists, if not it will be at the end.
1097+ *
1098+ * @param key The name of property to look for
1099+ * @return a Cursor pointing to the right position or {@code null} if not found
1100+ */
1101+ public Cursor afterProperty (String key ) {
1102+ Cursor pos =
1103+ last ().prevWhile (
1104+ tk ->
1105+ tk .getType () == PropertiesParser .Type .KEY
1106+ && !tk .getText ().equals (key ));
1107+ if (pos .atStart ()) {
1108+ return null ;
1109+ } else {
1110+ if (pos .next ().isEol ()) {
1111+ pos .next ();
1112+ }
1113+ return pos ;
1114+ }
10731115 }
10741116
1075- private Cursor skipHeaderCommentLines () {
1117+ /**
1118+ * Returns a Cursor pointing to the position right before the first property, or to {@code -1}
1119+ * if the `Properties` object is empty.
1120+ *
1121+ * @return a Cursor pointing to the right position or {@code -1} if not found
1122+ */
1123+ public Cursor beforeFirstProperty () {
10761124 Cursor pos = first ();
1077- // Skip a single following whitespace if it is NOT an EOL token
1078- pos .nextIf (PropertiesParser .Token ::isWs );
1079- // Skip all consecutive comments
1080- while (pos .nextIf (PropertiesParser .Type .COMMENT )) {
1081- // Skip a single following whitespace if it IS an EOL token
1082- pos .nextIf (PropertiesParser .Token ::isEol );
1083- // Skip a single following whitespace if it is NOT an EOL token
1084- pos .nextIf (PropertiesParser .Token ::isWs );
1125+ while (pos .isType (PropertiesParser .Type .WHITESPACE , PropertiesParser .Type .COMMENT )) {
1126+ pos .next ();
10851127 }
1086- return pos ;
1087- }
1088-
1089- // Skips to start of line
1090- private Cursor skipHome (Cursor pos ) {
1091- if (!pos .atStart () && pos .copy ().prev ().isWhitespace ()) {
1128+ // Make sure we're either at the end or we've found a property
1129+ validate (
1130+ pos .atEnd ()
1131+ || pos .isType (
1132+ PropertiesParser .Type .VALUE ,
1133+ PropertiesParser .Type .SEPARATOR ,
1134+ PropertiesParser .Type .KEY ),
1135+ pos );
1136+ if (!pos .atEnd ()) {
10921137 pos .prev ();
10931138 }
10941139 return pos ;
10951140 }
10961141
1097- Cursor index (int index ) {
1098- return Cursor .index (tokens , index );
1099- }
1100-
11011142 /**
1102- * @return a Cursor pointing to the first token, or to {@code -1} if no tokens have been loaded.
1143+ * Returns a Cursor pointing to the position past the last property, or to {@code -1} if the
1144+ * `Properties` object is empty.
1145+ *
1146+ * @return a Cursor pointing to the right position or {@code -1} if not found
11031147 */
1104- public Cursor first () {
1105- return Cursor .first (tokens );
1148+ public Cursor afterLastProperty () {
1149+ // Track back from end until we encounter the last VALUE token (if any)
1150+ Cursor pos = last ();
1151+ while (pos .isType (PropertiesParser .Type .WHITESPACE , PropertiesParser .Type .COMMENT )) {
1152+ pos .prev ();
1153+ }
1154+ // Make sure we're either at the start or we've found a property
1155+ validate (
1156+ pos .atStart ()
1157+ || pos .isType (
1158+ PropertiesParser .Type .VALUE ,
1159+ PropertiesParser .Type .SEPARATOR ,
1160+ PropertiesParser .Type .KEY ),
1161+ pos );
1162+ if (!pos .atStart ()) {
1163+ pos .next ();
1164+ }
1165+ return pos ;
11061166 }
11071167
11081168 /**
1109- * @return a Cursor pointing to the last token, or to {@code -1} if no tokens have been loaded.
1169+ * Returns a Cursor pointing to the position right after the last comment in the header. If no
1170+ * header comments are found, the cursor will point to the start of the file.
1171+ *
1172+ * @return a Cursor pointing to the right position
11101173 */
1111- public Cursor last () {
1112- return Cursor .last (tokens );
1174+ public Cursor afterHeader () {
1175+ Cursor pos = first ();
1176+ // Skip a single following whitespace if it is NOT an EOL token
1177+ pos .nextIf (PropertiesParser .Token ::isWs );
1178+ // Skip all consecutive comments
1179+ while (pos .nextIf (PropertiesParser .Type .COMMENT )) {
1180+ // Skip a single following EOL token
1181+ pos .nextIf (PropertiesParser .Token ::isEol );
1182+ // Skip a single following whitespace (not EOL) token
1183+ pos .nextIf (PropertiesParser .Token ::isWs );
1184+ }
1185+ if (pos .isType (PropertiesParser .Type .KEY )) {
1186+ // We found a comment attached to a property, not a header comment
1187+ pos = first ();
1188+ } else {
1189+ pos .home ();
1190+ }
1191+ return pos ;
11131192 }
11141193
11151194 private void validate (boolean ok , Cursor cursor ) {
0 commit comments