refactor(config): split Locales.java into 3 specialized classes
- Created LocaleLoader.java - disk loading and validation - Created LocaleCache.java - thread-safe storage - Locales.java now acts as facade - Clean separation: loading → caching → access - All tests pass, build successful Wave 2/8 complete (260 lines → 3 files)
This commit is contained in:
parent
371e38bfac
commit
0a93c9628f
3 changed files with 205 additions and 132 deletions
70
src/main/java/dev/loki/loparkour/config/LocaleCache.java
Normal file
70
src/main/java/dev/loki/loparkour/config/LocaleCache.java
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package dev.loki.loparkour.config;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thread-safe cache for loaded locale configurations.
|
||||||
|
*/
|
||||||
|
public class LocaleCache {
|
||||||
|
|
||||||
|
private final Map<String, FileConfiguration> locales = new HashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all cached locales with a new map.
|
||||||
|
*/
|
||||||
|
public synchronized void setLocales(@NotNull Map<String, FileConfiguration> newLocales) {
|
||||||
|
locales.clear();
|
||||||
|
locales.putAll(newLocales);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a value from a locale configuration, with fallback.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public synchronized <T> T cachedValue(@NotNull String locale,
|
||||||
|
@NotNull Function<FileConfiguration, T> extractor,
|
||||||
|
@NotNull T defaultValue) {
|
||||||
|
if (locales.isEmpty()) return defaultValue;
|
||||||
|
FileConfiguration config = locales.get(locale);
|
||||||
|
if (config == null) return defaultValue;
|
||||||
|
try {
|
||||||
|
T result = extractor.apply(config);
|
||||||
|
return result != null ? result : defaultValue;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a locale configuration by key.
|
||||||
|
*/
|
||||||
|
public synchronized FileConfiguration getLocale(@NotNull String locale) {
|
||||||
|
return locales.get(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if locales are loaded.
|
||||||
|
*/
|
||||||
|
public synchronized boolean isEmpty() {
|
||||||
|
return locales.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of loaded locales.
|
||||||
|
*/
|
||||||
|
public synchronized int getLocaleCount() {
|
||||||
|
return locales.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all locale keys.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public synchronized Set<String> getLocaleKeys() {
|
||||||
|
return new HashSet<>(locales.keySet());
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/main/java/dev/loki/loparkour/config/LocaleLoader.java
Normal file
107
src/main/java/dev/loki/loparkour/config/LocaleLoader.java
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
package dev.loki.loparkour.config;
|
||||||
|
|
||||||
|
import dev.loki.loparkour.LoParkour;
|
||||||
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads locale files from disk with validation and auto-fix.
|
||||||
|
*/
|
||||||
|
public class LocaleLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all locale files from the locales/ folder.
|
||||||
|
* Returns a map of locale name → FileConfiguration.
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public static Map<String, FileConfiguration> loadAll() {
|
||||||
|
Plugin plugin = LoParkour.getPlugin();
|
||||||
|
try {
|
||||||
|
FileConfiguration embedded = YamlConfiguration.loadConfiguration(
|
||||||
|
new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
return loadFromDisk(embedded);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage());
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static Map<String, FileConfiguration> loadFromDisk(@NotNull FileConfiguration embedded) throws Exception {
|
||||||
|
Map<String, FileConfiguration> result = new HashMap<>();
|
||||||
|
File folder = LoParkour.getInFolder("locales");
|
||||||
|
ensureFolder(folder);
|
||||||
|
|
||||||
|
try (Stream<Path> stream = Files.list(folder.toPath())) {
|
||||||
|
stream.forEach(path -> {
|
||||||
|
File file = path.toFile();
|
||||||
|
if (!file.getName().endsWith(".yml")) return;
|
||||||
|
|
||||||
|
String locale = file.getName().split("\\.")[0];
|
||||||
|
LoParkour.log("Found locale " + locale);
|
||||||
|
|
||||||
|
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||||
|
validateAndFix(embedded, config, file);
|
||||||
|
result.put(locale, config);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureFolder(@NotNull File folder) {
|
||||||
|
if (!folder.exists()) folder.mkdirs();
|
||||||
|
String[] files = folder.list();
|
||||||
|
if (files == null || files.length == 0) {
|
||||||
|
Plugin plugin = LoParkour.getPlugin();
|
||||||
|
plugin.saveResource("locales/en.yml", false);
|
||||||
|
plugin.saveResource("locales/ru.yml", false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a locale file against the reference (en.yml) and auto-fix missing keys.
|
||||||
|
*/
|
||||||
|
private static void validateAndFix(@NotNull FileConfiguration reference,
|
||||||
|
@NotNull FileConfiguration locale,
|
||||||
|
@NotNull File file) {
|
||||||
|
List<String> refNodes = getChildren(reference);
|
||||||
|
List<String> locNodes = getChildren(locale);
|
||||||
|
|
||||||
|
boolean modified = false;
|
||||||
|
for (String node : refNodes) {
|
||||||
|
if (!locNodes.contains(node)) {
|
||||||
|
LoParkour.log("Fixing missing config node %s in %s".formatted(node, file.getName()));
|
||||||
|
locale.set(node, reference.get(node));
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (modified) {
|
||||||
|
try {
|
||||||
|
locale.save(file);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
LoParkour.getPlugin().getLogger().severe(
|
||||||
|
"Error saving fixed locale %s — delete and restart — %s".formatted(file.getName(), ex.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoParkour.log("Validated locale " + file.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private static List<String> getChildren(@NotNull FileConfiguration config) {
|
||||||
|
ConfigurationSection section = config.getConfigurationSection("");
|
||||||
|
return section != null ? new ArrayList<>(section.getKeys(true)) : Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,49 +5,42 @@ import dev.loki.loparkour.menu.ParkourOption;
|
||||||
import dev.loki.loparkour.player.ParkourUser;
|
import dev.loki.loparkour.player.ParkourUser;
|
||||||
import dev.loki.loparkour.util.Item;
|
import dev.loki.loparkour.util.Item;
|
||||||
import dev.loki.loparkour.util.Materials;
|
import dev.loki.loparkour.util.Materials;
|
||||||
import dev.lolib.scheduler.Scheduler;
|
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
|
||||||
import org.bukkit.configuration.file.YamlConfiguration;
|
|
||||||
import org.bukkit.command.CommandSender;
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.Plugin;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locale system: loading, caching, validation, and access.
|
* Facade for locale system: loading, caching, and access.
|
||||||
|
* Delegates to LocaleLoader and LocaleCache.
|
||||||
*/
|
*/
|
||||||
public class Locales {
|
public class Locales {
|
||||||
|
|
||||||
private static final Map<String, FileConfiguration> locales = new HashMap<>();
|
private static final LocaleCache cache = new LocaleCache();
|
||||||
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("%[a-z]");
|
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("%[a-z]");
|
||||||
|
|
||||||
// ── public API ────────────────────────────────────────────────────────────
|
// ── Initialization ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
loadLocalesAsync();
|
Map<String, FileConfiguration> loaded = LocaleLoader.loadAll();
|
||||||
|
cache.setLocales(loaded);
|
||||||
|
LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static String getString(@NotNull Player player, @NotNull String path) {
|
public static String getString(@NotNull Player player, @NotNull String path) {
|
||||||
return getString(getPlayerLocale(player), path);
|
return getString(getPlayerLocale(player), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Player: that player's locale. Console / command blocks: default language from config ({@link Option#OPTIONS_DEFAULTS}), or {@code en}.
|
* Player: that player's locale. Console / command blocks: default language from config, or {@code en}.
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public static String getString(@NotNull CommandSender sender, @NotNull String path) {
|
public static String getString(@NotNull CommandSender sender, @NotNull String path) {
|
||||||
|
|
@ -65,21 +58,21 @@ public class Locales {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static String getString(@NotNull String locale, @NotNull String path) {
|
public static String getString(@NotNull String locale, @NotNull String path) {
|
||||||
return cachedValue(locale, c -> c.getString(path), "");
|
return cache.cachedValue(locale, c -> c.getString(path), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
||||||
return cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getLocaleCount() {
|
public static int getLocaleCount() {
|
||||||
synchronized (locales) { return locales.size(); }
|
return cache.getLocaleCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static Set<String> getLocaleKeys() {
|
public static Set<String> getLocaleKeys() {
|
||||||
synchronized (locales) { return new HashSet<>(locales.keySet()); }
|
return cache.getLocaleKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
|
@ -89,118 +82,20 @@ public class Locales {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public static Item getItem(@NotNull String locale, @NotNull String path, String... replace) {
|
public static Item getItem(@NotNull String locale, @NotNull String path, String... replace) {
|
||||||
synchronized (locales) {
|
if (cache.isEmpty()) {
|
||||||
if (locales.isEmpty()) {
|
LoParkour.getPlugin().getLogger().warning("Locales are empty! Returning STONE item for path: " + path);
|
||||||
LoParkour.getPlugin().getLogger().warning("Locales are empty! Returning STONE item for path: " + path);
|
return new Item(Material.STONE, "");
|
||||||
return new Item(Material.STONE, "");
|
|
||||||
}
|
|
||||||
FileConfiguration config = locales.get(locale);
|
|
||||||
if (config == null) {
|
|
||||||
LoParkour.getPlugin().getLogger().warning("Locale '" + locale + "' not found! Available: " + locales.keySet() + ". Returning STONE for path: " + path);
|
|
||||||
return new Item(Material.STONE, "");
|
|
||||||
}
|
|
||||||
return buildItem(config, path, replace);
|
|
||||||
}
|
}
|
||||||
|
FileConfiguration config = cache.getLocale(locale);
|
||||||
|
if (config == null) {
|
||||||
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
|
"Locale '" + locale + "' not found! Available: " + cache.getLocaleKeys() + ". Returning STONE for path: " + path);
|
||||||
|
return new Item(Material.STONE, "");
|
||||||
|
}
|
||||||
|
return buildItem(config, path, replace);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── loading (was LocaleLoader) ────────────────────────────────────────────
|
// ── Item builder ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private static void loadLocalesAsync() {
|
|
||||||
Plugin plugin = LoParkour.getPlugin();
|
|
||||||
try {
|
|
||||||
FileConfiguration embedded = YamlConfiguration.loadConfiguration(
|
|
||||||
new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
|
|
||||||
|
|
||||||
Map<String, FileConfiguration> loaded = loadFromDisk(embedded);
|
|
||||||
|
|
||||||
synchronized (locales) {
|
|
||||||
locales.clear();
|
|
||||||
locales.putAll(loaded);
|
|
||||||
}
|
|
||||||
LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)");
|
|
||||||
} catch (Exception ex) {
|
|
||||||
plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static Map<String, FileConfiguration> loadFromDisk(@NotNull FileConfiguration embedded) throws Exception {
|
|
||||||
Map<String, FileConfiguration> result = new HashMap<>();
|
|
||||||
File folder = LoParkour.getInFolder("locales");
|
|
||||||
ensureFolder(folder);
|
|
||||||
|
|
||||||
try (Stream<Path> stream = Files.list(folder.toPath())) {
|
|
||||||
stream.forEach(path -> {
|
|
||||||
File file = path.toFile();
|
|
||||||
if (!file.getName().endsWith(".yml")) return;
|
|
||||||
|
|
||||||
String locale = file.getName().split("\\.")[0];
|
|
||||||
LoParkour.log("Found locale " + locale);
|
|
||||||
|
|
||||||
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
|
||||||
validateAndFix(embedded, config, file);
|
|
||||||
result.put(locale, config);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ensureFolder(@NotNull File folder) {
|
|
||||||
if (!folder.exists()) folder.mkdirs();
|
|
||||||
String[] files = folder.list();
|
|
||||||
if (files == null || files.length == 0) {
|
|
||||||
Plugin plugin = LoParkour.getPlugin();
|
|
||||||
plugin.saveResource("locales/en.yml", false);
|
|
||||||
plugin.saveResource("locales/ru.yml", false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── validation (was LocaleValidator) ──────────────────────────────────────
|
|
||||||
|
|
||||||
private static void validateAndFix(@NotNull FileConfiguration reference, @NotNull FileConfiguration locale, @NotNull File file) {
|
|
||||||
List<String> refNodes = getChildren(reference);
|
|
||||||
List<String> locNodes = getChildren(locale);
|
|
||||||
|
|
||||||
boolean modified = false;
|
|
||||||
for (String node : refNodes) {
|
|
||||||
if (!locNodes.contains(node)) {
|
|
||||||
LoParkour.log("Fixing missing config node %s in %s".formatted(node, file.getName()));
|
|
||||||
locale.set(node, reference.get(node));
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (modified) {
|
|
||||||
try { locale.save(file); }
|
|
||||||
catch (IOException ex) {
|
|
||||||
LoParkour.getPlugin().getLogger().severe(
|
|
||||||
"Error saving fixed locale %s — delete and restart — %s".formatted(file.getName(), ex.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LoParkour.log("Validated locale " + file.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static List<String> getChildren(@NotNull FileConfiguration config) {
|
|
||||||
ConfigurationSection section = config.getConfigurationSection("");
|
|
||||||
return section != null ? new ArrayList<>(section.getKeys(true)) : Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── cache access (was LocaleCache) ────────────────────────────────────────
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static <T> T cachedValue(@NotNull String locale, @NotNull Function<FileConfiguration, T> extractor, @NotNull T defaultValue) {
|
|
||||||
synchronized (locales) {
|
|
||||||
if (locales.isEmpty()) return defaultValue;
|
|
||||||
FileConfiguration config = locales.get(locale);
|
|
||||||
if (config == null) return defaultValue;
|
|
||||||
try {
|
|
||||||
T result = extractor.apply(config);
|
|
||||||
return result != null ? result : defaultValue;
|
|
||||||
} catch (Exception e) { return defaultValue; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── item builder ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private static Item buildItem(@NotNull FileConfiguration config, @NotNull String path, String... replace) {
|
private static Item buildItem(@NotNull FileConfiguration config, @NotNull String path, String... replace) {
|
||||||
|
|
@ -221,7 +116,8 @@ public class Locales {
|
||||||
|
|
||||||
Material mat = Materials.parse(material);
|
Material mat = Materials.parse(material);
|
||||||
if (mat == null) {
|
if (mat == null) {
|
||||||
LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE".formatted(material, path));
|
LoParkour.getPlugin().getLogger().warning(
|
||||||
|
"Invalid material '%s' for locale path '%s', using STONE".formatted(material, path));
|
||||||
mat = Material.STONE;
|
mat = Material.STONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue