Skip to content

Commit b87f622

Browse files
author
Spexx
committed
stable release 1.0.1
1 parent 29f7d7d commit b87f622

4 files changed

Lines changed: 166 additions & 14 deletions

File tree

src/main/java/dev/spexx/configurationAPI/ConfigurationAPI.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dev.spexx.configurationAPI;
22

33
import dev.spexx.configurationAPI.config.YamlConfig;
4+
import dev.spexx.configurationAPI.difference.ConfigLineDiff;
45
import dev.spexx.configurationAPI.events.ConfigReloadedEvent;
56
import dev.spexx.configurationAPI.manager.ConfigManager;
67
import org.bukkit.event.EventHandler;
@@ -60,6 +61,26 @@ public void onDisable() { }
6061
*/
6162
@EventHandler
6263
public void onReload(@NotNull ConfigReloadedEvent event) {
64+
65+
for (ConfigLineDiff diff : event.getDiffs()) {
66+
67+
int delta = diff.getCharDelta();
68+
String sign = delta > 0 ? "+" : "";
69+
70+
String type =
71+
diff.getOldLine().isEmpty() ? "ADDED" :
72+
diff.getNewLine().isEmpty() ? "REMOVED" :
73+
diff.isOnlyWhitespaceChange() ? "WHITESPACE" :
74+
"MODIFIED";
75+
76+
getLogger().info(() ->
77+
"[ConfigWatcher] file=" + event.getConfig().file().getName()
78+
+ " line=" + diff.getLineNumber()
79+
+ " type=" + type
80+
+ " delta=" + sign + delta
81+
);
82+
}
83+
6384
getLogger().info("Config reloaded: " +
6485
event.getConfig().file().getName());
6586

src/main/java/dev/spexx/configurationAPI/difference/ConfigLineDiff.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,48 @@ public ConfigLineDiff(int lineNumber,
7171
this.charDelta = newLine.length() - oldLine.length();
7272
}
7373

74+
/**
75+
* Determines whether the difference between the old and new line
76+
* consists solely of whitespace changes.
77+
*
78+
* <p>This method ignores all whitespace characters (spaces, tabs,
79+
* and other Unicode whitespace) and compares the normalized content
80+
* of both lines.</p>
81+
*
82+
* <p>Examples:</p>
83+
* <ul>
84+
* <li>{@code "value: 1"} vs {@code "value: 1 "} → {@code true}</li>
85+
* <li>{@code "value: 1"} vs {@code "value: 1"} → {@code true}</li>
86+
* <li>{@code "value: 1"} vs {@code "value: 2"} → {@code false}</li>
87+
* </ul>
88+
*
89+
* @return {@code true} if only whitespace differs, {@code false} otherwise
90+
*
91+
* @apiNote
92+
* This method performs a lightweight normalization by removing all
93+
* whitespace characters before comparison.
94+
*
95+
* @implSpec
96+
* The comparison is based on {@code String.replaceAll("\\s+", "")}.
97+
*
98+
* @implNote
99+
* This method does not perform semantic or structural comparison.
100+
* It is intended for quick detection of formatting-only changes.
101+
*
102+
* @since 1.0.1
103+
*/
104+
public boolean isOnlyWhitespaceChange() {
105+
return !oldLine.equals(newLine)
106+
&& normalize(oldLine).equals(normalize(newLine));
107+
}
108+
109+
/**
110+
* Normalizes a line by removing all whitespace characters.
111+
*/
112+
private static @NotNull String normalize(@NotNull String input) {
113+
return input.replaceAll("\\s+", "");
114+
}
115+
74116
/**
75117
* Returns the line number where the change occurred.
76118
*

src/main/java/dev/spexx/configurationAPI/events/ConfigReloadedEvent.java

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.bukkit.event.HandlerList;
77
import org.jetbrains.annotations.NotNull;
88

9+
import java.util.ArrayList;
10+
import java.util.Collection;
911
import java.util.List;
1012

1113
/**
@@ -52,9 +54,9 @@ public final class ConfigReloadedEvent extends Event {
5254
* @param changedLines number of modified lines
5355
* @param addedLines number of added lines
5456
* @param removedLines number of removed lines
55-
* @param diffs list of line-level differences
57+
* @param diffs list of line-level differences, must not be {@code null}
5658
*
57-
* @since 1.0
59+
* @since 1.0.0
5860
*/
5961
public ConfigReloadedEvent(
6062
@NotNull YamlConfig config,
@@ -71,7 +73,7 @@ public ConfigReloadedEvent(
7173
this.changedLines = changedLines;
7274
this.addedLines = addedLines;
7375
this.removedLines = removedLines;
74-
this.diffs = diffs;
76+
this.diffs = List.copyOf(new ArrayList<>(diffs));
7577
}
7678

7779
/**
@@ -143,14 +145,58 @@ public int getRemovedLines() {
143145
/**
144146
* Returns the list of detected line-level differences.
145147
*
146-
* @return immutable diff list
148+
* <p>The returned list is immutable and reflects the exact state at the time
149+
* the event was created.</p>
147150
*
148-
* @since 1.0
151+
* @return immutable list of diffs, never {@code null}
152+
*
153+
* @apiNote
154+
* The returned list is safe for concurrent iteration without additional synchronization.
155+
*
156+
* @implSpec
157+
* Backed by {@link List#copyOf(Collection)} to guarantee immutability.
158+
*
159+
* @since 1.0.0
149160
*/
150161
public @NotNull List<ConfigLineDiff> getDiffs() {
151162
return diffs;
152163
}
153164

165+
/**
166+
* Indicates whether any changes were detected during the reload operation.
167+
*
168+
* <p>This is a convenience method that aggregates the individual change counters
169+
* exposed by this event.</p>
170+
*
171+
* <p>A configuration is considered changed if at least one of the following is true:</p>
172+
* <ul>
173+
* <li>One or more existing lines were modified</li>
174+
* <li>One or more lines were added</li>
175+
* <li>One or more lines were removed</li>
176+
* </ul>
177+
*
178+
* @apiNote
179+
* This method provides a fast and expressive way to determine whether meaningful
180+
* changes occurred without inspecting individual counters.
181+
*
182+
* @implSpec
183+
* This method returns {@code true} if any of the internal counters
184+
* ({@code changedLines}, {@code addedLines}, {@code removedLines})
185+
* is greater than zero.
186+
*
187+
* @implNote
188+
* In normal operation, this method should always return {@code true} for fired events,
189+
* as events are only emitted when a checksum difference is detected. It is provided
190+
* for defensive usage and future extensibility.
191+
*
192+
* @return {@code true} if changes were detected, {@code false} otherwise
193+
*
194+
* @since 1.0.1
195+
*/
196+
public boolean hasChanges() {
197+
return changedLines > 0 || addedLines > 0 || removedLines > 0;
198+
}
199+
154200
@Override
155201
public @NotNull HandlerList getHandlers() {
156202
return HANDLERS;

src/main/java/dev/spexx/configurationAPI/watcher/YamlConfigWatcher.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,17 +226,42 @@ private void run() {
226226
}
227227

228228
/**
229-
* Performs checksum validation, computes a diff snapshot, and triggers
230-
* a configuration reload if the file content has changed.
229+
* Performs checksum validation and triggers a reload if the file content has changed.
231230
*
232-
* <p>The reload operation is executed on the main server thread and followed
233-
* by dispatching a {@link ConfigReloadedEvent} containing diff metadata.</p>
231+
* <p>This method includes safeguards against transient file system states,
232+
* such as temporary deletion or recreation of the file during write operations.</p>
233+
*
234+
* @apiNote
235+
* If the file does not exist at the time of invocation, the reload is skipped
236+
* to prevent unnecessary errors and log spam.
237+
*
238+
* @implSpec
239+
* Reload is only performed if:
240+
* <ul>
241+
* <li>The file exists</li>
242+
* <li>The computed checksum differs from the previous value</li>
243+
* </ul>
244+
*
245+
* @implNote
246+
* Some editors replace files atomically (delete + recreate). This guard prevents
247+
* false-positive reload attempts during such operations.
248+
*
249+
* @since 1.0.1
234250
*/
235251
private void reload() {
236252

237253
File file = yamlConfig.file();
238254

255+
// 🔥 NEW: existence guard
256+
if (!file.exists()) {
257+
plugin.getLogger().fine(() ->
258+
"[ConfigWatcher] File missing, skipping reload: " + file.getName()
259+
);
260+
return;
261+
}
262+
239263
String newChecksum = FileChecksum.sha256(file);
264+
240265
if (newChecksum.equals(lastChecksum)) return;
241266

242267
String oldChecksum = lastChecksum;
@@ -276,6 +301,7 @@ private void reload() {
276301
lastChecksum = newChecksum;
277302
lastContent = newContent;
278303

304+
// snapshot values for lambda (effectively final)
279305
final int finalChanged = changed;
280306
final int finalAdded = added;
281307
final int finalRemoved = removed;
@@ -302,17 +328,34 @@ private void reload() {
302328
}
303329

304330
/**
305-
* Reads the full content of a file as a string.
331+
* Reads the full content of the specified file as a string.
332+
*
333+
* <p>If an error occurs during reading, an empty string is returned and
334+
* a warning is logged to the plugin logger.</p>
335+
*
336+
* @param file the file to read, must not be {@code null}
337+
* @return file content as a string, or an empty string if reading fails
306338
*
307-
* <p>If the file cannot be read, an empty string is returned.</p>
339+
* @apiNote
340+
* This method is intentionally fail-safe and does not propagate exceptions,
341+
* as it is used within a file-watching context where stability is preferred.
308342
*
309-
* @param file the file to read
310-
* @return file content, or empty string if reading fails
343+
* @implSpec
344+
* File content is read using {@link Files#readString(Path)}.
345+
*
346+
* @implNote
347+
* Returning an empty string ensures that diff computation can proceed
348+
* without interruption, even if file access temporarily fails.
349+
*
350+
* @since 1.0.1
311351
*/
312-
private @NotNull String readContentSafe(File file) {
352+
private @NotNull String readContentSafe(@NotNull File file) {
313353
try {
314354
return Files.readString(file.toPath());
315355
} catch (Exception e) {
356+
plugin.getLogger().warning(
357+
"[ConfigWatcher] Failed to read config file: " + file.getAbsolutePath()
358+
);
316359
return "";
317360
}
318361
}

0 commit comments

Comments
 (0)