package io.github.axolotsh.registrylib; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import lombok.Builder; import lombok.Getter; import lombok.experimental.Accessors; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; /** * Provides utility methods for loading and saving {@link Keyed} registry * objects from the file system. */ @Builder @Accessors(chain = true, fluent = true) public class RegistryLoader { /** * The logger used to report IO errors and serialization failures. * * @return The current logger instance. */ @Getter private Logger logger; /** * The base directory where registry files are stored. * * @return The current base directory. */ @Getter private Path rootPath; private static final Gson gson = new GsonBuilder() .registerTypeAdapter(Key.class, new KeyAdapter()) .setPrettyPrinting() .create(); /** * Loads a single JSON file and converts it into a keyed object. * * @param filePath The path to the JSON file. * @param type The registry type category (e.g., "items", "quests"). * @param classType The class to deserialize into. * @param A type extending {@link Keyed}. * @return The loaded object, or {@code null} if loading * fails. */ @Nullable public T loadFile(Path filePath, String type, Class classType) { try (var reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { var result = gson.fromJson(reader, classType); injectKey(result, getFileKey(filePath, type)); return result; } catch (Exception e) { logger.error("Failed to load registry file {}: {}", filePath, e.getMessage()); return null; } } private void injectKey(Object target, Key key) throws Exception { var current = target.getClass(); while (current != null) { try { var field = current.getDeclaredField("key"); field.setAccessible(true); field.set(target, key); return; } catch (NoSuchFieldException e) { current = current.getSuperclass(); } } throw new NoSuchFieldException("Field 'key' not found in class hierarchy"); } /** * Saves a keyed object to its corresponding JSON file location. * * @param value The object to save. * @param type The registry type category (e.g., "items", "quests"). * @param A type extending {@link Keyed}. */ public void saveFile(T value, String type) { var filePath = getKeyFile(value.key(), type); try { Files.createDirectories(filePath.getParent()); try (var writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) { gson.toJson(value, writer); } } catch (IOException e) { logger.error("Failed to save registry file {}: {}", filePath, e.getMessage()); } } /** * Determines a {@link Key} based on a file's location relative to the * {@code rootPath}. * * @param filePath The path to the JSON file. * @param type The registry type category (e.g., "items", "quests"). * @return A {@link Key} representing the namespace and internal path. */ public Key getFileKey(Path filePath, String type) { var relative = rootPath.relativize(filePath); var namespace = relative.getName(0).toString(); var valuePath = relative.subpath(2, relative.getNameCount()).toString() .replace('\\', '/') .replace(".json", ""); return Key.key(namespace, valuePath); } /** * Determines a {@link Path} based on a provided key relative to the * {@code rootPath}. * * @param key The key identifier. * @param type The registry type category (e.g., "items", "quests"). * @return A {@link Path} representing the key. */ public Path getKeyFile(Key key, String type) { return rootPath.resolve(key.namespace()) .resolve(type) .resolve(key.value() + ".json"); } /** * Loads all objects of a specific type category. * * @param type The registry type category (e.g., "items", "quests"). * @param classType The class to deserialize into. * @param A type extending {@link Keyed}. * @return A collection of loaded objects. */ public Collection loadAll(String type, Class classType) { if (!Files.exists(rootPath)) return Collections.emptyList(); List loaded = new ArrayList<>(); try (var namespaces = Files.list(rootPath)) { for (var nsPath : namespaces.filter(Files::isDirectory).toList()) { var typePath = nsPath.resolve(type); if (!Files.exists(typePath)) continue; try (var files = Files.walk(typePath)) { var results = files .filter(Files::isRegularFile) .filter(path -> path.toString().endsWith(".json")) .map(path -> loadFile(path, type, classType)) .filter(Objects::nonNull) .toList(); loaded.addAll(results); } } } catch (IOException e) { logger.error("Error while walking through registry: {}", e.getMessage()); } return loaded; } /** * Loads all objects of a specific type category and add them into provided * registry. * * @param registry The registry to add loaded object into. * @param A type extending {@link Keyed}. */ public void loadAll(Registry registry) { registry.addAll(loadAll(registry.type(), registry.typeClass())); } /** * Saves a collection of objects to the filesystem. * * @param values The values collection to save. * @param type The registry type category (e.g., "items", "quests"). * @param A type extending {@link Keyed}. */ public void saveAll(Collection values, String type) { values.forEach(x -> saveFile(x, type)); } /** * Saves all objects from a registry, invoking * {@link Registry#onBeforeSave(Object)} * on each item before writing to disk. * * @param registry The registry to save. * @param A type extending {@link Keyed}. */ public void saveAll(Registry registry) { registry.getAll().stream() .map(registry::onBeforeSave) .filter(Objects::nonNull) .forEach(x -> saveFile(x, registry.type())); } }