Files
RegistryLib/src/main/java/io/github/axolotsh/registrylib/RegistryLoader.java
2026-04-30 12:59:36 +03:00

214 lines
7.2 KiB
Java

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 <T> A type extending {@link Keyed}.
* @return The loaded object, or {@code null} if loading
* fails.
*/
@Nullable
public <T extends Keyed> T loadFile(Path filePath, String type, Class<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> 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 <T> A type extending {@link Keyed}.
* @return A collection of loaded objects.
*/
public <T extends Keyed> Collection<T> loadAll(String type, Class<T> classType) {
if (!Files.exists(rootPath))
return Collections.emptyList();
List<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void loadAll(Registry<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void saveAll(Collection<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void saveAll(Registry<T> registry) {
registry.getAll().stream()
.map(registry::onBeforeSave)
.filter(Objects::nonNull)
.forEach(x -> saveFile(x, registry.type()));
}
}