Skip to content

Commit cef6553

Browse files
committed
chore: added some more cursor navigatrion helper functions
And made the existing code use them.
1 parent e3c674b commit cef6553

2 files changed

Lines changed: 143 additions & 54 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,16 @@ public int prevCount(Predicate<PropertiesParser.Token> accept) {
168168
return cnt;
169169
}
170170

171+
/**
172+
* Skips to the start of the line.
173+
*
174+
* @return a Cursor pointing to the start of the line
175+
*/
176+
public Cursor home() {
177+
prevWhile(tk -> !tk.isEol());
178+
return this;
179+
}
180+
171181
/**
172182
* Inserts a token at the current position, shifting the current token (if any) and any
173183
* subsequent tokens to the right (adds one to their indices). <br>

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

Lines changed: 133 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)