From e965d39bd01f78d96074a71bc1c683fcd42a2501 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Wed, 22 Jul 2026 16:45:15 +0200 Subject: [PATCH 1/3] feat(file): add file utility class --- .../aves/file/FileHandlerUtil.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/net/theevilreaper/aves/file/FileHandlerUtil.java diff --git a/src/main/java/net/theevilreaper/aves/file/FileHandlerUtil.java b/src/main/java/net/theevilreaper/aves/file/FileHandlerUtil.java new file mode 100644 index 00000000..3eca12c2 --- /dev/null +++ b/src/main/java/net/theevilreaper/aves/file/FileHandlerUtil.java @@ -0,0 +1,41 @@ +package net.theevilreaper.aves.file; + +import net.minestom.server.utils.validate.Check; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Internal utility class for file handling operations. + * + * @author theEvilReaper + * @version 1.0.0 + * @since 1.14.0 + */ +final class FileHandlerUtil { + + private FileHandlerUtil() { + } + + /** + * Prepares a target file path for saving by validating it is not a directory and ensuring parent directories exist. + * + * @param path target path + * @param logger logger instance to log warnings + * @return true if the file does not exist yet, false otherwise + */ + static boolean prepareSavePath(Path path, Logger logger) { + Check.argCondition(Files.isDirectory(path), "Unable to save a directory. Please check the used path"); + boolean isNewFile = !Files.exists(path); + try { + if (path.getParent() != null) { + Files.createDirectories(path.getParent()); + } + } catch (IOException exception) { + logger.warn("Unable to create directories for path: {}", path, exception); + } + return isNewFile; + } +} From f15a0da6c6c260fa46c11063f5c48ad118b639c8 Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Wed, 22 Jul 2026 16:46:11 +0200 Subject: [PATCH 2/3] fix(file): ensure directory creation and preserve generic types --- .../theevilreaper/aves/file/FileHandler.java | 16 +++++-- .../aves/file/GsonFileHandler.java | 43 +++++++++-------- .../aves/file/ModernFileHandler.java | 36 +++++++++----- .../aves/file/ModernGsonFileHandler.java | 47 +++++++++++-------- 4 files changed, 87 insertions(+), 55 deletions(-) diff --git a/src/main/java/net/theevilreaper/aves/file/FileHandler.java b/src/main/java/net/theevilreaper/aves/file/FileHandler.java index 76ad88d5..9878ea13 100644 --- a/src/main/java/net/theevilreaper/aves/file/FileHandler.java +++ b/src/main/java/net/theevilreaper/aves/file/FileHandler.java @@ -9,13 +9,13 @@ import java.util.Optional; /** - * The class represents the base logic to load or save json files. + * Interface representing the base logic to load or save JSON files. * * @author theEvilReaper * @version 1.0.0 * @since 1.0.0 - * * @deprecated This interface is deprecated since version 1.9.0 and will be removed in a future release. Use {@link ModernFileHandler} instead. - **/ + * @deprecated Deprecated since version 1.9.0 and will be removed in a future release. Use {@link ModernFileHandler} instead. + */ @Deprecated(since = "1.9.0", forRemoval = true) public interface FileHandler { @@ -29,6 +29,16 @@ public interface FileHandler { */ Charset UTF_8 = StandardCharsets.UTF_8; + /** + * Prepares a target file path for saving by validating it is not a directory and ensuring parent directories exist. + * + * @param path target path + * @return true if the file does not exist yet, false otherwise + */ + default boolean prepareSavePath(Path path) { + return FileHandlerUtil.prepareSavePath(path, LOGGER); + } + /** * Saves a given object into a file. * diff --git a/src/main/java/net/theevilreaper/aves/file/GsonFileHandler.java b/src/main/java/net/theevilreaper/aves/file/GsonFileHandler.java index c65504c1..7d581075 100644 --- a/src/main/java/net/theevilreaper/aves/file/GsonFileHandler.java +++ b/src/main/java/net/theevilreaper/aves/file/GsonFileHandler.java @@ -10,45 +10,49 @@ import java.util.Optional; /** - * The class represents the implementation of the {@link FileHandler} for the {@link com.google.gson.Gson} library. + * Implementation of the {@link FileHandler} using the {@link Gson} library for JSON serialization and deserialization. + * * @author theEvilReaper * @version 1.0.0 * @since 1.1.0 - * @deprecated This class is deprecated since version 1.9.0 and will be removed in a future release. Use {@link ModernGsonFileHandler} instead. - **/ + * @deprecated Deprecated since version 1.9.0 and will be removed in a future release. Use {@link ModernGsonFileHandler} instead. + */ @Deprecated(since = "1.9.0", forRemoval = true) public final class GsonFileHandler implements FileHandler { private final Gson gson; /** - * Creates a new instance from the file handler. + * Creates a new instance of the file handler with a default {@link Gson} instance. */ public GsonFileHandler() { this.gson = new Gson(); } /** - * Creates a new instance from the file handler. - * @param gson the gson instance to deserialize or serialize data + * Creates a new instance of the file handler. + * + * @param gson the {@link Gson} instance to deserialize or serialize data */ public GsonFileHandler(Gson gson) { this.gson = gson; } /** - * Saves a given object into a file. - * @param path The path where the file is located - * @param object The object to save - * @param A generic type for the object value + * Saves a given object into a file at the specified path. + * Automatically creates parent directories if they do not exist. + * + * @param path path where the file is located + * @param object object to save + * @param generic type for the object value */ @Override public void save(Path path, T object) { - Check.argCondition(Files.isDirectory(path), "Unable to save a directory. Please check the used path"); + boolean isNewFile = prepareSavePath(path); + try (var outputStream = Files.newBufferedWriter(path, UTF_8)) { - if (!Files.exists(path)) { - var file = Files.createFile(path).getFileName(); - LOGGER.info("Created new file: {}", file); + if (isNewFile) { + LOGGER.info("Created new file: {}", path.getFileName()); } gson.toJson(object, TypeToken.get(object.getClass()).getType(), outputStream); } catch (IOException exception) { @@ -57,11 +61,12 @@ public void save(Path path, T object) { } /** - * Load a given file and parse to the give class. - * @param path is the where the file is located - * @param clazz is the generic class object - * @param is generic type for the object value - * @return a {@link Optional} with the object instance + * Loads a given file and deserializes its JSON content to the target class. + * + * @param path path where the file is located + * @param clazz generic class object + * @param generic type for the object value + * @return an {@link Optional} containing the deserialized object, or empty if the file does not exist */ @Override public Optional load(Path path, Class clazz) { diff --git a/src/main/java/net/theevilreaper/aves/file/ModernFileHandler.java b/src/main/java/net/theevilreaper/aves/file/ModernFileHandler.java index 2b2628e5..a49b2a89 100644 --- a/src/main/java/net/theevilreaper/aves/file/ModernFileHandler.java +++ b/src/main/java/net/theevilreaper/aves/file/ModernFileHandler.java @@ -10,12 +10,12 @@ import java.util.Optional; /** - * The class represents the base logic to load or save JSON files. + * Interface representing the base logic to load or save JSON files. * * @author theEvilReaper - * @version 1.0.0 + * @version 1.1.0 * @since 1.9.0 - **/ + */ public interface ModernFileHandler { Logger LOGGER = LoggerFactory.getLogger(ModernFileHandler.class); @@ -23,22 +23,32 @@ public interface ModernFileHandler { Charset UTF_8 = StandardCharsets.UTF_8; /** - * Saves a given object into a file. + * Prepares a target file path for saving by validating it is not a directory and ensuring parent directories exist. * - * @param path The path where the file is located - * @param object The object to save - * @param typeToken the type token to serialize the object - * @param A generic type for the object value + * @param path target path + * @return true if the file does not exist yet, false otherwise + */ + default boolean prepareSavePath(Path path) { + return FileHandlerUtil.prepareSavePath(path, LOGGER); + } + + /** + * Saves a given object into a file at the specified path. + * + * @param path path where the file is located + * @param object object to save + * @param typeToken type token to serialize the object + * @param generic type for the object value */ void save(Path path, T object, TypeToken typeToken); /** - * Load a given file and parse to the give class. + * Loads a given file and deserializes its content to the target type. * - * @param path is the where the file is located - * @param typeToken the type token to deserialize the object - * @param is generic type for the object value - * @return a {@link Optional} with the object instance + * @param path path where the file is located + * @param typeToken type token to deserialize the object + * @param generic type for the object value + * @return an {@link Optional} containing the deserialized object, or empty if the file does not exist */ Optional load(Path path, TypeToken typeToken); } \ No newline at end of file diff --git a/src/main/java/net/theevilreaper/aves/file/ModernGsonFileHandler.java b/src/main/java/net/theevilreaper/aves/file/ModernGsonFileHandler.java index a08367fb..af10e045 100644 --- a/src/main/java/net/theevilreaper/aves/file/ModernGsonFileHandler.java +++ b/src/main/java/net/theevilreaper/aves/file/ModernGsonFileHandler.java @@ -10,9 +10,11 @@ import java.util.Optional; /** - * The class represents the implementation of the {@link ModernFileHandler} for the {@link com.google.gson.Gson} library. + * Implementation of the {@link ModernFileHandler} using the {@link Gson} library for JSON serialization and deserialization. + * * @author TheMeinerLP - * @version 1.0.0 + * @author theEvilReaper + * @version 1.1.0 * @since 1.9.0 */ public class ModernGsonFileHandler implements ModernFileHandler { @@ -20,46 +22,51 @@ public class ModernGsonFileHandler implements ModernFileHandler { private final Gson gson; /** - * Creates a new instance from the file handler. + * Creates a new instance of the file handler with a default {@link Gson} instance. */ public ModernGsonFileHandler() { this.gson = new Gson(); } /** - * Creates a new instance from the file handler. - * @param gson the gson instance to deserialize or serialize data + * Creates a new instance of the file handler. + * + * @param gson the {@link Gson} instance to deserialize or serialize data */ public ModernGsonFileHandler(Gson gson) { this.gson = gson; } /** - * Saves a given object into a file. - * @param path The path where the file is located - * @param object The object to save - * @param A generic type for the object value + * Saves a given object into a file at the specified path. + * Automatically creates parent directories if they do not exist. + * + * @param path path where the file is located + * @param object object to save + * @param typeToken type token to serialize the object + * @param generic type for the object value */ @Override public void save(Path path, T object, TypeToken typeToken) { - Check.argCondition(Files.isDirectory(path), "Unable to save a directory. Please check the used path"); + boolean isNewFile = prepareSavePath(path); + try (var outputStream = Files.newBufferedWriter(path, UTF_8)) { - if (!Files.exists(path)) { - var file = Files.createFile(path).getFileName(); - LOGGER.info("Created new file: {}", file); + if (isNewFile) { + LOGGER.info("Created new file: {}", path.getFileName()); } - gson.toJson(object, typeToken.getRawType(), outputStream); + gson.toJson(object, typeToken.getType(), outputStream); } catch (IOException exception) { LOGGER.warn("Unable to save file", exception); } } /** - * Load a given file and parse to the give class. - * @param path is the where the file is located - * @param typeToken the type token to deserialize the object - * @param is generic type for the object value - * @return a {@link Optional} with the object instance + * Loads a given file and deserializes its JSON content to the target type. + * + * @param path path where the file is located + * @param typeToken type token to deserialize the object + * @param generic type for the object value + * @return an {@link Optional} containing the deserialized object, or empty if the file does not exist */ @Override public Optional load(Path path, TypeToken typeToken) { @@ -69,7 +76,7 @@ public Optional load(Path path, TypeToken typeToken) { } try (var reader = Files.newBufferedReader(path, UTF_8)) { - return Optional.ofNullable(gson.fromJson(reader, typeToken)); + return Optional.ofNullable(gson.fromJson(reader, typeToken.getType())); } catch (IOException exception) { LOGGER.warn("Unable to load file", exception); } From 113d2908f41a3108dd2eb69dc6db8d82392aa9bf Mon Sep 17 00:00:00 2001 From: theEvilReaper Date: Wed, 22 Jul 2026 16:46:27 +0200 Subject: [PATCH 3/3] refactor(file): decouple ModernFileHandlerTest cases --- .../aves/file/ModernFileHandlerTest.java | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/src/test/java/net/theevilreaper/aves/file/ModernFileHandlerTest.java b/src/test/java/net/theevilreaper/aves/file/ModernFileHandlerTest.java index acb4e3f4..cda936db 100644 --- a/src/test/java/net/theevilreaper/aves/file/ModernFileHandlerTest.java +++ b/src/test/java/net/theevilreaper/aves/file/ModernFileHandlerTest.java @@ -3,100 +3,103 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import net.theevilreaper.aves.map.BaseMap; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.io.TempDir; -import java.io.File; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class ModernFileHandlerTest { - private final String testMap = "test.json"; - private final ModernFileHandler fileHandler = new ModernGsonFileHandler(); - @TempDir - static File tempDir; + private final ModernFileHandler fileHandler = new ModernGsonFileHandler(); - @Order(1) @Test void testCharset() { assertSame(StandardCharsets.UTF_8, ModernFileHandler.UTF_8); } - @Order(2) @Test - void testOtherConstructor() { + void testCustomGsonConstructor() { var fileLoader = new ModernGsonFileHandler(new Gson()); assertNotNull(fileLoader); } - @Order(3) + @Test - void testGsonFileHandlerWrite() { - var path = tempDir.toPath().resolve(testMap); + void testSaveAndLoadObject(@TempDir Path tempDir) { + var path = tempDir.resolve("test.json"); var baseMap = new BaseMap("TestMap", null, List.of("Builder1", "Builder2")); + fileHandler.save(path, baseMap, TypeToken.get(BaseMap.class)); assertTrue(Files.exists(path)); + + var loadedMap = fileHandler.load(path, TypeToken.get(BaseMap.class)); + assertTrue(loadedMap.isPresent()); + assertEquals("TestMap", loadedMap.get().name()); + assertEquals(List.of("Builder1", "Builder2"), loadedMap.get().builders()); } - @Order(4) @Test - void testGsonFileHandler() { - var path = tempDir.toPath().resolve(testMap); - var optional = fileHandler.load(path, TypeToken.get(BaseMap.class)); + void testSaveCreatesParentDirectories(@TempDir Path tempDir) { + var nestedPath = tempDir.resolve("maps/lobby/map.json"); + var baseMap = new BaseMap("NestedMap", null, List.of("Builder1")); + + fileHandler.save(nestedPath, baseMap, TypeToken.get(BaseMap.class)); + assertTrue(Files.exists(nestedPath)); - assertTrue(optional.isPresent()); + var loadedMap = fileHandler.load(nestedPath, TypeToken.get(BaseMap.class)); + assertTrue(loadedMap.isPresent()); + assertEquals("NestedMap", loadedMap.get().name()); + } + + @Test + void testGenericListSerialization(@TempDir Path tempDir) { + var path = tempDir.resolve("items.json"); + List items = List.of("item1", "item2", "item3"); + TypeToken> token = new TypeToken<>() {}; - var map = optional.get(); + fileHandler.save(path, items, token); + assertTrue(Files.exists(path)); - assertEquals("TestMap", map.name()); - assertEquals(List.of("Builder1", "Builder2"), map.builders()); + var loadedList = fileHandler.load(path, token); + assertTrue(loadedList.isPresent()); + assertEquals(items, loadedList.get()); } - @Order(5) @Test - void testFileNotExistsRead() { - var file = fileHandler.load(tempDir.toPath().resolve("test3.json"), TypeToken.get(BaseMap.class)); - assertTrue(file::isEmpty); + void testLoadNonExistentFileReturnsEmpty(@TempDir Path tempDir) { + var path = tempDir.resolve("non_existent.json"); + var result = fileHandler.load(path, TypeToken.get(BaseMap.class)); + assertTrue(result.isEmpty()); } - @Order(6) @Test - void testInvalidPathSave() { - var path = tempDir.toPath(); + void testSaveDirectoryThrowsException(@TempDir Path tempDir) { var baseMap = new BaseMap("TestMap", null, null); var typeToken = TypeToken.get(BaseMap.class); + var exception = assertThrows( IllegalArgumentException.class, - () -> fileHandler.save(path, baseMap, typeToken) + () -> fileHandler.save(tempDir, baseMap, typeToken) ); - assertEquals(IllegalArgumentException.class, exception.getClass()); assertEquals("Unable to save a directory. Please check the used path", exception.getMessage()); } - @Order(7) @Test - void testInvalidPathLoad() { - var path = tempDir.toPath(); + void testLoadDirectoryThrowsException(@TempDir Path tempDir) { var typeToken = TypeToken.get(BaseMap.class); + var exception = assertThrows( IllegalArgumentException.class, - () -> fileHandler.load(path, typeToken) + () -> fileHandler.load(tempDir, typeToken) ); - assertEquals(IllegalArgumentException.class, exception.getClass()); assertEquals("Unable to load a directory. Please check the used path", exception.getMessage()); } }