Skip to content

FileChangeTracker

Basti edited this page Sep 27, 2026 · 1 revision

FileChangeTracker remembers the state of your plugin's data files and tells you what changed since the last time you looked, e.g. after /yourplugin reload or an automatic reload. YAML files are compared setting by setting, folders like lang/ file by file. The wording and logging stay with your plugin, so the list can be shown in every language you ship.

Why Use It? 🎯

  • Admins see at a glance what a reload actually picked up ("Detected file changes: config.yml (max_blocks=300)").
  • An unchanged reload can say so ("No configuration changes detected.") instead of staying silent.
  • Two plugins show the same list without copying the comparison code.

Highlights ✨

  • Setting-level diff for YAML files: changed and new settings show their new value, removed ones show up as removed. Comments, blank lines, indentation and quoting don't count as changes.
  • File-level diff for folders: changed, added and removed files, by content (CRC32 plus size), not by timestamp.
  • Read errors never pass silently: a file that can't be read, or isn't valid YAML, logs one English WARNING, is listed in unreadableFiles(), and keeps its previous snapshot. isEmpty() is then false, so a reload never reports "no changes" for a file it couldn't read.
  • Old files saved as ANSI instead of UTF-8 are read as Windows-1252 (see Limitations).

Limitations ⚠️

  • Memory only: after a restart or with a new tracker, the first call only takes a snapshot, so edits made while the server was off are never listed.
  • Reads everything: each call reads every tracked YAML file and every matching folder file on the calling thread, so large folders take time. It isn't thread-safe.
  • Values compared as text: a type change such as enabled: false β†’ enabled: "false" isn't listed, although Bukkit's getBoolean then returns the default. Lists are joined with , , so [a, b], ["a, b"] and a, b look the same, and an empty list equals "".
  • Sections: adding or removing an empty section (x: {}) isn't listed, a key: without a value shows up as removed, and keys containing a dot mix with nested paths.
  • Values in full: nothing is masked or shortened, so a password or token in a tracked file ends up in the log if you log describe().
  • Unreadable files: they keep their old snapshot and log a WARNING on every call while they stay broken. Their changes can't be listed, so isEmpty() and hasListedChanges() are both false when nothing else changed. A file that was already broken at the first snapshot lists all its settings as new once it's fixed.
  • Missing YAML files count as empty, without a warning.
  • Folders: one level deep, compared as whole files; a renamed file shows up as one removed and one added file.
  • Encodings: bytes that aren't valid UTF-8 are read as Windows-1252, while Bukkit reads the same file as UTF-8 with replacement characters. The list can then show BΓ€ume while the plugin loaded BοΏ½ume, and saving the same text again as UTF-8 isn't listed.
  • Order: only the file lists are sorted. keyChanges() follows the order of yamlFile(...) calls, unreadableFiles() that order plus the file system's, and yamlFile paths are reported as you passed them.
  • Warnings are in English.

Quick Start πŸš€

  1. Build a tracker in onEnable() with the files and folders you want to watch.
  2. Call compareAndUpdate() at the end of onEnable(). The first call only takes the snapshot.
  3. Call it again at the end of every reload and log the result through your MessageService.

References πŸ“š

Method Purpose
FileChangeTracker.builder(File dataFolder, Logger logger) Starts a tracker for the plugin's data folder. logger may be null (read errors aren't logged then).
yamlFile(String relativePath) Adds a YAML file compared setting by setting, e.g. config.yml.
folder(String relativePath, String suffix) Adds a folder whose files ending in suffix (case-insensitive, not recursive) are compared by content, e.g. lang, .yml. Hidden files (name starts with .) are skipped.
compareAndUpdate() Compares with the last snapshot, stores the current state as the new snapshot, and returns the Changes.
hasSnapshot() true once compareAndUpdate() has run at least once.
Changes member Contents
firstSnapshot() true on the first call: only the snapshot was taken, nothing is compared. Don't log anything then.
isEmpty() true if nothing changed and every file could be read, so "no changes" is safe to report.
hasListedChanges() true if keyChanges() or the folder file lists below have something to list.
keyChanges() One FileKeyChanges(file, changes) per YAML file with changes; changes holds KeyChange(key, newValue), sorted by key.
changedFiles() Folder files whose content changed.
addedFiles() Folder files that are new since the last snapshot.
removedFiles() Folder files that were deleted since the last snapshot.
allChangedFiles() The three lists above together, sorted.
unreadableFiles() Files that couldn't be read or parsed this time; they count as unchanged.
  • KeyChange#newValue() is null for a removed setting. KeyChange#describe() gives key=value or key=<removed>; FileKeyChanges#describe() joins them with , .
  • Lists are shown as their items joined with , (e.g. saplings=OAK_SAPLING, BIRCH_SAPLING).
  • Folder file paths are relative to the data folder, with / separators (e.g. lang/de_DE.yml), and sorted.
  • A deleted YAML file lists all its settings as removed; a YAML file that didn't exist yet lists all its settings as new.

Examples πŸ§ͺ

import net.kroet.turtlelib.helper.FileChangeTracker;

private FileChangeTracker changeTracker;

@Override
public void onEnable() {
    // ... load config and messages ...
    changeTracker = FileChangeTracker.builder(getDataFolder(), getLogger())
        .yamlFile("config.yml")
        .folder("lang", ".yml")
        .build();
    changeTracker.compareAndUpdate(); // first snapshot
}

public void reload() {
    // ... reload config and messages ...
    FileChangeTracker.Changes changes = changeTracker.compareAndUpdate();
    if (changes.firstSnapshot()) {
        return;
    }
    if (changes.isEmpty()) {
        messages.log(getLogger(), Level.INFO, "log.reload_changes_none");
        return;
    }
    if (!changes.hasListedChanges()) {
        return; // only unreadable files, already logged as a WARNING
    }
    messages.log(getLogger(), Level.INFO, "log.reload_changes_header");
    for (FileChangeTracker.FileKeyChanges file : changes.keyChanges()) {
        messages.log(getLogger(), Level.INFO, "log.reload_changes_line",
            Map.of("file", file.file(), "changes", file.describe()));
    }
    for (String file : changes.allChangedFiles()) {
        messages.log(getLogger(), Level.INFO, "log.reload_changes_locale_line", Map.of("file", file));
    }
}

Tips & Notes πŸ’‘

  • Take the snapshot after everything that writes files during startup or reload (key renames, new default keys, language syncing), otherwise those writes show up as the admin's changes on the next reload.
  • The tracker is meant for the main thread; it keeps its snapshot in memory only, so a server restart starts over with firstSnapshot().
  • It doesn't watch files by itself. Pair it with ConfigWatcher if reloads should happen automatically.

Relationships to Other Helpers πŸ”—

Clone this wiki locally