Skip to content
Merged
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
10 changes: 10 additions & 0 deletions src/main/java/org/codejive/properties/Cursor.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,16 @@ public int prevCount(Predicate<PropertiesParser.Token> accept) {
return cnt;
}

/**
* Skips to the start of the line.
*
* @return a Cursor pointing to the start of the line
*/
public Cursor home() {
prevWhile(tk -> !tk.isEol());
return this;
}

/**
* Inserts a token at the current position, shifting the current token (if any) and any
* subsequent tokens to the right (adds one to their indices). <br>
Expand Down
187 changes: 133 additions & 54 deletions src/main/java/org/codejive/properties/Properties.java
Original file line number Diff line number Diff line change
Expand Up @@ -455,22 +455,9 @@ private void replaceValue(String key, String rawValue, String value) {

// Add new tokens to the end of the list of tokens
private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String value) {
// Track back from end until we encounter the last VALUE token (if any)
Cursor pos = last();
while (pos.isType(PropertiesParser.Type.WHITESPACE, PropertiesParser.Type.COMMENT)) {
pos.prev();
}
// 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);
Cursor pos = afterLastProperty();
// Add a newline whitespace token if necessary
if (pos.hasToken()) {
pos.next();
if (!pos.atStart()) {
if (pos.isEol()) {
pos.next().addEol(eolType).prev();
} else {
Expand All @@ -481,7 +468,7 @@ private Cursor addNewKeyValue(String rawKey, String key, String rawValue, String
// but there might be comments, so we move forward again,
// skipping any header comments
// (*) = we'll always skip past the final comment's EOL
pos = determineAddNewInsertionPoint();
pos = afterHeader();
if (!pos.atStart()) {
// We have to make sure there are at least 2 EOLs after the last comment
if (pos.atEnd()) {
Expand Down Expand Up @@ -1007,9 +994,13 @@ public void store(OutputStream out, String... comment) throws IOException {
* @throws IOException Thrown when any IO error occurs during operation
*/
public void store(Writer writer, String... comment) throws IOException {
Cursor pos = first();
Cursor pos;
if (comment.length > 0) {
pos = determineStoreInsertionPoint();
pos = afterHeader();
if (!pos.atStart()) {
// Skip any following empty lines
pos.nextWhile(PropertiesParser.Token::isEol);
}
List<String> newcs = normalizeComments(Arrays.asList(comment), "# ");
for (String c : newcs) {
writer.write(new PropertiesParser.Token(PropertiesParser.Type.COMMENT, c).getRaw());
Expand All @@ -1018,6 +1009,8 @@ public void store(Writer writer, String... comment) throws IOException {
// We write an extra empty line so this comment won't be taken as part of the first
// property
writer.write(eolType.text);
} else {
pos = first();
}
while (pos.hasToken()) {
writer.write(pos.raw());
Expand Down Expand Up @@ -1055,61 +1048,147 @@ Cursor.EolType determineEol() {
}
}

private Cursor determineStoreInsertionPoint() {
Cursor pos = skipHeaderCommentLines();
if (pos.isType(PropertiesParser.Type.KEY)) {
// We found a comment attached to a property, not a header comment
pos = first();
Cursor index(int index) {
return Cursor.index(tokens, index);
}

/**
* @return a Cursor pointing to the first token, or to {@code -1} if thw `Properties` object is
* empty.
*/
public Cursor first() {
return Cursor.first(tokens);
}

/**
* @return a Cursor pointing to the last token, or to {@code -1} if thw `Properties` object is
* empty.
*/
public Cursor last() {
return Cursor.last(tokens);
}

/**
* Returns a Cursor pointing to the position right before the property with the given key. If no
* such property exists, {@code null} will be returned. If the property was found the position
* will take into account any leading whitespace and will be positioned at the start of the
* line.
*
* @param key The name of property to look for
* @return a Cursor pointing to the right position or {@code null} if not found
*/
public Cursor beforeProperty(String key) {
Cursor pos =
first().nextWhile(
tk ->
tk.getType() == PropertiesParser.Type.KEY
&& !tk.getText().equals(key));
if (pos.atEnd()) {
return null;
} else {
// Skip any following empty lines
pos.nextWhile(PropertiesParser.Token::isEol);
return pos.home();
}
return pos;
}

private Cursor determineAddNewInsertionPoint() {
Cursor pos = skipHeaderCommentLines();
return skipHome(pos);
/**
* Returns a Cursor pointing to the position right after the property with the given key. If no
* such property exists, {@code null} will be returned. If the property was found, the position
* will be at the start of the next line, if it exists, if not it will be at the end.
*
* @param key The name of property to look for
* @return a Cursor pointing to the right position or {@code null} if not found
*/
public Cursor afterProperty(String key) {
Cursor pos =
last().prevWhile(
tk ->
tk.getType() == PropertiesParser.Type.KEY
&& !tk.getText().equals(key));
if (pos.atStart()) {
return null;
} else {
if (pos.next().isEol()) {
pos.next();
}
return pos;
}
}

private Cursor skipHeaderCommentLines() {
/**
* Returns a Cursor pointing to the position right before the first property, or to {@code -1}
* if the `Properties` object is empty.
*
* @return a Cursor pointing to the right position or {@code -1} if not found
*/
public Cursor beforeFirstProperty() {
Cursor pos = first();
// Skip a single following whitespace if it is NOT an EOL token
pos.nextIf(PropertiesParser.Token::isWs);
// Skip all consecutive comments
while (pos.nextIf(PropertiesParser.Type.COMMENT)) {
// Skip a single following whitespace if it IS an EOL token
pos.nextIf(PropertiesParser.Token::isEol);
// Skip a single following whitespace if it is NOT an EOL token
pos.nextIf(PropertiesParser.Token::isWs);
while (pos.isType(PropertiesParser.Type.WHITESPACE, PropertiesParser.Type.COMMENT)) {
pos.next();
}
return pos;
}

// Skips to start of line
private Cursor skipHome(Cursor pos) {
if (!pos.atStart() && pos.copy().prev().isWhitespace()) {
// Make sure we're either at the end or we've found a property
validate(
pos.atEnd()
|| pos.isType(
PropertiesParser.Type.VALUE,
PropertiesParser.Type.SEPARATOR,
PropertiesParser.Type.KEY),
pos);
if (!pos.atEnd()) {
pos.prev();
}
return pos;
}

Cursor index(int index) {
return Cursor.index(tokens, index);
}

/**
* @return a Cursor pointing to the first token, or to {@code -1} if no tokens have been loaded.
* Returns a Cursor pointing to the position past the last property, or to {@code -1} if the
* `Properties` object is empty.
*
* @return a Cursor pointing to the right position or {@code -1} if not found
*/
public Cursor first() {
return Cursor.first(tokens);
public Cursor afterLastProperty() {
// Track back from end until we encounter the last VALUE token (if any)
Cursor pos = last();
while (pos.isType(PropertiesParser.Type.WHITESPACE, PropertiesParser.Type.COMMENT)) {
pos.prev();
}
// 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);
if (!pos.atStart()) {
pos.next();
}
return pos;
}

/**
* @return a Cursor pointing to the last token, or to {@code -1} if no tokens have been loaded.
* Returns a Cursor pointing to the position right after the last comment in the header. If no
* header comments are found, the cursor will point to the start of the file.
*
* @return a Cursor pointing to the right position
*/
public Cursor last() {
return Cursor.last(tokens);
public Cursor afterHeader() {
Cursor pos = first();
// Skip a single following whitespace if it is NOT an EOL token
pos.nextIf(PropertiesParser.Token::isWs);
// Skip all consecutive comments
while (pos.nextIf(PropertiesParser.Type.COMMENT)) {
// Skip a single following EOL token
pos.nextIf(PropertiesParser.Token::isEol);
// Skip a single following whitespace (not EOL) token
pos.nextIf(PropertiesParser.Token::isWs);
}
if (pos.isType(PropertiesParser.Type.KEY)) {
// We found a comment attached to a property, not a header comment
pos = first();
} else {
pos.home();
}
return pos;
}

private void validate(boolean ok, Cursor cursor) {
Expand Down
Loading