From 6c1338d1718119053edfc411ba4c5e10229457bd Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Tue, 1 Jul 2025 15:50:03 +0200 Subject: [PATCH] Added JsonConfiguration --- .../gson/config/JsonConfiguration.java | 270 ++++++++++++++++++ .../gson/config/JsonConfigurationTest.java | 128 +++++++++ 2 files changed, 398 insertions(+) create mode 100644 gson/src/main/java/de/neitzel/gson/config/JsonConfiguration.java create mode 100644 gson/src/test/java/de/neitzel/gson/config/JsonConfigurationTest.java diff --git a/gson/src/main/java/de/neitzel/gson/config/JsonConfiguration.java b/gson/src/main/java/de/neitzel/gson/config/JsonConfiguration.java new file mode 100644 index 0000000..ec43e0b --- /dev/null +++ b/gson/src/main/java/de/neitzel/gson/config/JsonConfiguration.java @@ -0,0 +1,270 @@ +package de.neitzel.gson.config; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import lombok.Builder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a configuration utility class that manages application settings and allows + * loading and saving the configuration in JSON format. This class is built using Gson for + * JSON serialization and deserialization. + *

+ * The class maintains application settings in memory and provides methods to store + * the configuration on the filesystem and retrieve it as needed. + */ +@Builder(builderClassName = "ConfigurationBuilder") +public class JsonConfiguration { + + /** + * The name of the application. + * This variable is used to identify the application and is incorporated into various configurations, + * such as the naming of directories or files associated with the application's settings. + * It is finalized and set during the construction of the {@code JsonConfiguration} instance and cannot be changed afterward. + */ + private final String appName; + + /** + * The filesystem path representing the home directory utilized by the application. + * This directory serves as the location where application-specific configuration files + * and data are stored. The path is initialized when the {@code JsonConfiguration} instance + * is constructed, based on the settings provided through the {@code JsonConfigurationBuilder}. + * It can be customized to a user-specified value or defaults to the user's home directory. + *

+ * The {@code homeDir} variable is integral in determining the location of the main configuration + * file and other related resources used by the application. + */ + private final String homeDir; + + /** + * A map that stores configuration settings as key-value pairs, + * where both the keys and values are represented as strings. + *

+ * This map is used to manage dynamic or runtime settings for the configuration, + * allowing for flexible assignment and retrieval of values associated with specific + * configuration keys. It is initialized as a final instance, ensuring it cannot be + * reassigned after creation. + */ + private final Map settings; + + /** + * A Gson instance used for handling JSON serialization and deserialization within + * the JsonConfiguration class. This instance is immutable and customized with + * registered adapters during the initialization of the JsonConfiguration. + *

+ * The Gson object provides functionality for converting Java objects to JSON + * and vice versa. It supports complex serialization and deserialization + * workflows by leveraging adapters specified during the configuration phase. + *

+ * Adapters can be registered to customize the behavior of (de)serialization + * for specific types. + */ + private final Gson gson; + + /** + * Stores a key-value pair into the configuration settings. + * The value is serialized into a JSON string before being stored. + * + * @param key the key under which the value will be stored + * @param value the object to be associated with the specified key + */ + public void set(String key, Object value) { + settings.put(key, gson.toJson(value)); + } + + /** + * Retrieves a value associated with the specified key from the configuration, + * deserializing it into the specified class type using Gson. + * If the key does not exist in the settings, the method returns {@code null}. + * + * @param the type of the object to be deserialized + * @param key the key corresponding to the value in the configuration + * @param clazz the {@code Class} object representing the expected type of the value + * @return the deserialized object of type {@code T}, or {@code null} + * if the key does not exist + */ + public T get(String key, Class clazz) { + String json = settings.get(key); + return json != null ? gson.fromJson(json, clazz) : null; + } + + /** + * Retrieves a value associated with the given key from the configuration as an object of the specified type. + * If the key does not exist in the configuration, the provided default value is returned. + * + * @param the type of the object to be returned + * @param key the key whose associated value is to be retrieved + * @param clazz the class of the object to deserialize the value into + * @param defaultValue the default value to return if the key does not exist or the value is null + * @return the value associated with the specified key, deserialized into the specified type, + * or the default value if the key does not exist or the value is null + */ + public T get(String key, Class clazz, T defaultValue) { + String json = settings.get(key); + return json != null ? gson.fromJson(json, clazz) : defaultValue; + } + + /** + * Loads the configuration data from the JSON configuration file located at the path + * determined by the {@code getConfigFilePath()} method. + * If the configuration file exists, its content is read and deserialized into a map + * of key-value pairs using Gson. The method clears the current settings and populates + * them with the loaded values. + * + * @throws IOException if an I/O error occurs while reading the configuration file + */ + public void load() throws IOException { + Path configPath = getConfigFilePath(); + if (Files.exists(configPath)) { + try (var reader = Files.newBufferedReader(configPath)) { + Map loaded = gson.fromJson(reader, new TypeToken>() { + }.getType()); + settings.clear(); + settings.putAll(loaded); + } + } + } + + /** + * Constructs the path to the configuration file for the application. + * The configuration file is located in the user's home directory, within a hidden + * folder named after the application, and is named "config.json". + * + * @return the {@code Path} to the application's configuration file + */ + private Path getConfigFilePath() { + return Path.of(homeDir, "." + appName, "config.json"); + } + + /** + * Saves the current configuration to a JSON file. + *

+ * This method serializes the current settings into a JSON format and writes + * them to a file located at the configuration path. If the parent directory + * of the configuration file does not exist, it is created automatically. + *

+ * The configuration file path is determined based on the application name + * and home directory. Any existing content in the file will be overwritten + * during this operation. + * + * @throws IOException if an I/O error occurs while creating the directory, + * opening the file, or writing to the file + */ + public void save() throws IOException { + Path configPath = getConfigFilePath(); + Files.createDirectories(configPath.getParent()); + try (var writer = Files.newBufferedWriter(configPath)) { + gson.toJson(settings, writer); + } + } + + /** + * Builder class for creating instances of {@code JsonConfiguration}. + * The {@code JsonConfigurationBuilder} allows the configuration of application + * name, home directory, and custom Gson adapters before building a {@code JsonConfiguration} object. + */ + public static class JsonConfigurationBuilder { + /** + * A map that holds custom Gson adapters to register with a GsonBuilder. + * The keys represent the classes for which the adapters are applicable, + * and the values are the adapter instances associated with those classes. + *

+ * This variable is used to store user-defined type adapters, allowing + * for customized serialization and deserialization behavior for specific + * classes when constructing a {@code JsonConfiguration}. + *

+ * It is populated using the {@code addGsonAdapter} method in the + * {@code JsonConfigurationBuilder} class and is later passed to a + * {@code GsonBuilder} for registration. + */ + private final Map, Object> gsonAdapters = new HashMap<>(); + + /** + * The name of the application being configured. + * This variable holds a string representation of the application's name and is used + * to identify the application within the context of the {@code JsonConfiguration}. + * It is set during the construction of a {@code JsonConfigurationBuilder} instance. + */ + private String appName; + + /** + * Represents the home directory path of the current user. + * By default, this variable is initialized to the value of the "user.home" system property, + * which typically points to the user's home directory on the filesystem. + *

+ * This field can be customized to point to a different directory via the builder class + * methods when building an instance of {@code JsonConfiguration}. + *

+ * Example system values for "user.home" may include paths such as "~/Users/username" for macOS, + * "C:/Users/username" for Windows, or "/home/username" for Unix/Linux systems. + */ + private String homeDir = System.getProperty("user.home"); + + /** + * Sets the application name to be used in the configuration. + * + * @param appName the name of the application to be set + * @return the current instance of {@code JsonConfigurationBuilder} for chaining + */ + public JsonConfigurationBuilder appName(String appName) { + this.appName = appName; + return this; + } + + /** + * Sets the home directory for the configuration being built. + * This method specifies the directory path where the application's configuration + * files or settings should be stored. + * + * @param homeDir the path to the desired home directory + * @return the current instance of {@code JsonConfigurationBuilder} to enable method chaining + */ + public JsonConfigurationBuilder homeDir(String homeDir) { + this.homeDir = homeDir; + return this; + } + + /** + * Adds a custom Gson adapter to the builder for the specified type. + * This method allows registration of a type-to-adapter mapping that will be applied + * when building the Gson instance within the {@code JsonConfiguration}. + * + * @param type the {@code Class} of the type for which the adapter is being registered + * @param adapter the adapter object to be used for the specified type + * @return the current instance of {@code JsonConfigurationBuilder}, allowing method chaining + */ + public JsonConfigurationBuilder addGsonAdapter(Class type, Object adapter) { + gsonAdapters.put(type, adapter); + return this; + } + + /** + * Builds and returns an instance of {@code JsonConfiguration} with the specified + * settings, including the application name, home directory, and any registered + * custom Gson adapters. + * + * @return a {@code JsonConfiguration} instance configured with the builder's parameters. + */ + public JsonConfiguration build() { + GsonBuilder gsonBuilder = new GsonBuilder(); + for (var entry : gsonAdapters.entrySet()) { + gsonBuilder.registerTypeAdapter(entry.getKey(), entry.getValue()); + } + + Gson gson = gsonBuilder.create(); + + return new JsonConfiguration( + appName, + homeDir, + new HashMap<>(), + gson + ); + } + } +} diff --git a/gson/src/test/java/de/neitzel/gson/config/JsonConfigurationTest.java b/gson/src/test/java/de/neitzel/gson/config/JsonConfigurationTest.java new file mode 100644 index 0000000..b95508b --- /dev/null +++ b/gson/src/test/java/de/neitzel/gson/config/JsonConfigurationTest.java @@ -0,0 +1,128 @@ +package de.neitzel.gson.config; + +import com.google.gson.Gson; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class JsonConfigurationTest { + + /** + * Test class for JsonConfiguration containing unit tests for the `get` method. + * The `get` method fetches the value associated with a given key and converts + * it from JSON representation to the specified class type. + */ + + @Test + void testGetExistingKeyWithValidValue() { + // Arrange + Gson gson = new Gson(); + HashMap settings = new HashMap<>(); + settings.put("testKey", gson.toJson(42)); + JsonConfiguration jsonConfiguration = JsonConfiguration.builder() + .settings(settings) + .gson(gson) + .build(); + + // Act + Integer value = jsonConfiguration.get("testKey", Integer.class); + + // Assert + assertEquals(42, value); + } + + @Test + void testGetNonExistingKey() { + // Arrange + Gson gson = new Gson(); + HashMap settings = new HashMap<>(); + JsonConfiguration jsonConfiguration = JsonConfiguration.builder() + .settings(settings) + .gson(gson) + .build(); + + // Act + String value = jsonConfiguration.get("nonExistentKey", String.class); + + // Assert + assertNull(value); + } + + @Test + void testGetWithDefaultValueWhenKeyExists() { + // Arrange + Gson gson = new Gson(); + HashMap settings = new HashMap<>(); + settings.put("testKey", gson.toJson("Hello World")); + JsonConfiguration jsonConfiguration = JsonConfiguration.builder() + .settings(settings) + .gson(gson) + .build(); + + // Act + String value = jsonConfiguration.get("testKey", String.class, "Default Value"); + + // Assert + assertEquals("Hello World", value); + } + + @Test + void testGetWithDefaultValueWhenKeyDoesNotExist() { + // Arrange + Gson gson = new Gson(); + HashMap settings = new HashMap<>(); + JsonConfiguration jsonConfiguration = JsonConfiguration.builder() + .settings(settings) + .gson(gson) + .build(); + + // Act + String value = jsonConfiguration.get("nonExistentKey", String.class, "Default Value"); + + // Assert + assertEquals("Default Value", value); + } + + @Test + void testGetWithComplexObject() { + // Arrange + Gson gson = new Gson(); + HashMap settings = new HashMap<>(); + TestObject testObject = new TestObject("John Doe", 30); + settings.put("complexKey", gson.toJson(testObject)); + JsonConfiguration jsonConfiguration = JsonConfiguration.builder() + .settings(settings) + .gson(gson) + .build(); + + // Act + TestObject result = jsonConfiguration.get("complexKey", TestObject.class); + + // Assert + assertEquals("John Doe", result.name()); + assertEquals(30, result.age()); + } + + // Helper class for complex object tests + static class TestObject { + private final String name; + + private final int age; + + TestObject(String name, int age) { + this.name = name; + this.age = age; + } + + public String name() { + return name; + } + + public int age() { + return age; + } + } +} \ No newline at end of file