Repository navigation
ConfigKeyMigrator
Basti edited this page Sep 27, 2026
·
1 revision
ConfigKeyMigrator renames keys in config.yml or lang/*.yml directly in the file's raw text. Unlike a YamlConfiguration load/save round-trip, comments, blank lines, key order, quoting style, and line endings all stay exactly as the admin left them.
- You renamed config or message keys between releases (e.g.
kebab-caseβsnake_case) and don't want admins to lose their customized values. - A setting moved into another section, and the admin's value should move with it.
- A regular
YamlConfiguration#savewould normalize quoting and indentation and wrap long lines throughout the file. - Admins should get one clear console note listing what was renamed.
- Two ways to rename:
-
By key name with
rewriteLegacyKeysInFile, for style renames such askebab-caseβsnake_case. Dotted paths are zipped segment by segment, and every changed segment is renamed as a key name at any depth:update-check.interval-hoursβupdate_check.interval_hoursrenames bothupdate-check:andinterval-hours:, in every section. -
By path with
rewriteKeyPathsInFile: only the named path is renamed. A new parent moves the key with its whole block and the comments directly above it, also to another depth; missing sections on the new path are created.
-
By key name with
- Reads the file's structure first: every YAML key counts (
Settings:,maxBlocks:,grΓΆΓe:, quoted keys), and text inside values (block scalars|and>, multi-line strings, flow collections) is never touched. - Checks the result before writing: Bukkit has to read every value back under its new path, otherwise the file stays unchanged.
- Logs a single
INFOline such asRenamed 2 legacy key(s) directly in config.yml (comments and formatting preserved): .... - Writes a hidden temporary file next to the file, flushes it to disk and then replaces the file with it, so a crash while writing normally doesn't leave it half written (see Limitations).
-
Rename lists are checked: both methods throw an
IllegalArgumentExceptionfor a list whose result would depend on the order of its entries. By key name: one old name with different new names, a new name that is itself renamed (a chain likea.max-radiusβa.radiusandb.radiusβb.range), or an entry that changes the nesting depth (userewriteKeyPathsInFilefor moves). By path: overlapping old paths (a section and a key inside it), a new path overlapping another entry's old path, a new path inside another entry's new path, or a path renamed into itself. The check runs before the file is read, so a unit test finds a bad list even without a file. -
By key name means everywhere:
rewriteLegacyKeysInFilerenames every key with an old name, at any depth, also inside list items and in sections the admin added.database.hostβdatabase.hostnamerenamesredis.hosttoo; userewriteKeyPathsInFilefor single keys. -
What a path can reach:
rewriteKeyPathsInFilefinds keys by their dotted path, so keys inside list items and keys whose name contains a dot can't be renamed with it. -
Moves need block sections: every existing section on the new path must be written one key per line. If one is a plain value (
storage: none), inline ({...}) or an alias, the key isn't moved, and oneWARNINGper file asks to move it by hand. - Where moved keys go: a moved key goes to the end of its new section. A new top-level section is inserted right after the old key's top-level section, or in the old key's place if that key was at the top level.
-
Emptied sections stay: a section a move leaves without keys stays as an empty
section:line, which Bukkit reads as unset. Comments that weren't directly above the moved key stay where they were. Move the whole section to take everything along. - Moved text: the moved lines get the file's line ending (taken from its first line) and indentation made of spaces.
-
Invalid YAML: a file that isn't valid YAML is left unchanged, with a
WARNINGnaming the line. -
Result check: if Bukkit wouldn't read every value back at its new path, nothing is renamed, with a
WARNINGon every call. This happens for a key whose name contains a dot (Bukkit splits it into sections) when one of its parts is renamed. -
Encodings: every byte outside the renamed keys and moved lines survives. The file is read as UTF-8, or as Windows-1252 if it isn't valid UTF-8, with or without a BOM. A new name the file's encoding can't store is written in double quotes with
\uescapes. -
Writing: the file is replaced through a temporary file next to it, which is flushed to disk first. If the replace fails (e.g. a file lock on Windows), the file is written in place, with a
WARNING. A symlink becomes a regular file, and only POSIX permissions are kept. - Threads: nothing is locked. Don't run it on the same file from several threads at once; an admin saving the file at the same moment can lose one of the two changes.
- Collect your renames in a
Map<String, String>of old dotted path β new dotted path. - Call
ConfigKeyMigrator.rewriteLegacyKeysInFile(...)for style renames, orrewriteKeyPathsInFile(...)for single keys and moves, inonEnable()(and on reload) before anything reads the config. - Load the config afterwards (e.g.
getConfig()/reloadConfig()) so it sees the renamed keys.
| Method | Purpose |
|---|---|
rewriteLegacyKeysInFile(File file, Logger logger, String fileLabel, Map<String, String> oldToNew) |
Renames by key name: every key whose name is an old segment, at any depth. Returns true if the file was changed and rewritten. |
rewriteLegacyKeysInFile(File file, Logger logger, String fileLabel, Map<String, String> oldToNew, InputStream legacyDefaults) |
Same, plus the old version's default file (UTF-8, may be null): an old key left next to its renamed key is also removed while it still has that default. The caller closes the stream. |
rewriteKeyPathsInFile(File file, Logger logger, String fileLabel, Map<String, String> oldToNew) |
Renames exactly the given paths and moves keys whose parent changes. Returns true if the file was changed and rewritten. |
rewriteKeyPathsInFile(File file, Logger logger, String fileLabel, Map<String, String> oldToNew, InputStream legacyDefaults) |
Same, with the old version's default file as above. |
-
loggermay benull(no logging). Read and write failures, and any unexpected error while renaming, log atWARNINGand returnfalse; the file stays unchanged. Only a rejected rename list throws. -
fileLabelis only used in log messages (e.g."config.yml"). - Returns
falsewithout changing the file if it doesn't exist, the map is empty, the file isn't valid YAML or nothing is renamed. The return value only covers the file itself: the state file.seen-defaults.ymlnext to it can be written either way (see Tips & Notes).
import net.kroet.turtlelib.helper.ConfigKeyMigrator;
public final class ExamplePlugin extends JavaPlugin {
private static final Map<String, String> LEGACY_CONFIG_KEYS = Map.of(
"chat-prefix-label", "chat_prefix_label",
"update-check.interval-hours", "update_check.interval_hours");
private static final Map<String, String> MOVED_CONFIG_KEYS = Map.of(
"database.host", "storage.database.host",
"maxBlocks", "limits.max_blocks");
@Override
public void onEnable() {
saveDefaultConfig();
File config = new File(getDataFolder(), "config.yml");
ConfigKeyMigrator.rewriteLegacyKeysInFile(config, getLogger(), "config.yml", LEGACY_CONFIG_KEYS);
ConfigKeyMigrator.rewriteKeyPathsInFile(config, getLogger(), "config.yml", MOVED_CONFIG_KEYS);
reloadConfig(); // now reads the renamed keys
}
}For locale files you usually don't call it directly: pass the renames to MessageService#setLegacyKeyMigrations(...) or LangLoader.syncLocale(plugin, locale, renames) and they run the rename by key name for you before merging new defaults. MessageService#setLegacyDefaultsRoot(...) supplies the old lang files as legacyDefaults.
- For renames by key name, avoid old names that also appear elsewhere in the file with a different meaning (see Limitations).
- If a key's new name already exists in the same section (e.g. a downgrade to the old version added
update-check:back next toupdate_check:), or withrewriteKeyPathsInFilethe new path exists, the old key and its whole block are removed when its value equals the new key's value or, withlegacyDefaults, the old default. OneINFOline lists them. Only those lines go; comments and everything else stay. Keys under different list items (- name: ...) count as separate sections. - If the admin had changed the old key, both stay: the old key and everything inside it keep their old names, and one
WARNINGasks to keep one of them. It appears once per file and key: the key is recorded in.seen-defaults.ymlnext to the file, the state file ConfigDefaultsInserter uses too. - Several old names for one new key (e.g.
usage-adminandusage_adminβusage): the first one in the file is renamed, the others count as duplicates of it. - It is meant to run on every startup and reload: once the keys are renamed, a later run changes nothing.
-
LangLoader.syncLocale(...)/loadLocale(...)overloads with aMapcallrewriteLegacyKeysInFilebefore merging missing keys. -
MessageService#setLegacyKeyMigrations(...)forwards the renames toLangLoaderon everyload/syncLocaleFile. -
LegacyDataUpgrade takes the same maps:
carryOverValuesrenames by key name,carryOverValuesByPathby path. - Pair with
ConfigWatcherso externally edited files are migrated on the next reload too.