From 5ed5c9978c8bb55a085338fa485a04d214668f72 Mon Sep 17 00:00:00 2001 From: assaf Date: Sun, 27 Oct 2024 12:21:54 +0200 Subject: [PATCH] add docs and tests --- .../ois/core/project/SimulationManifest.java | 25 +- .../ois/core/runner/RunnerConfiguration.java | 64 +- .../org/ois/core/runner/SimulationEngine.java | 58 +- .../java/org/ois/core/state/ErrorState.java | 53 +- src/main/java/org/ois/core/state/IState.java | 41 + .../java/org/ois/core/state/StateManager.java | 120 ++- src/main/java/org/ois/core/tools/Timer.java | 160 +++- .../org/ois/core/utils/ReflectionUtils.java | 17 + src/main/java/org/ois/core/utils/Version.java | 83 +- .../java/org/ois/core/utils/io/FileUtils.java | 56 ++ .../java/org/ois/core/utils/io/ZipUtils.java | 52 ++ .../org/ois/core/utils/io/data/DataNode.java | 455 ++++++++- .../ois/core/utils/io/data/DataObject.java | 19 +- .../utils/io/data/formats/DataFormat.java | 57 ++ .../utils/io/data/formats/JsonFormat.java | 864 ++++-------------- .../java/org/ois/core/utils/log/ILogger.java | 54 ++ .../java/org/ois/core/utils/log/Logger.java | 45 +- .../core/project/SimulationManifestTest.java | 87 ++ .../ois/core/runner/SimulationEngineTest.java | 310 +++++++ .../org/ois/core/state/StateManagerTest.java | 283 ++++++ .../java/org/ois/core/tools/TimerTest.java | 116 +++ .../java/org/ois/core/utils/VersionTest.java | 130 +++ .../ois/core/utils/io/data/DataNodeTest.java | 175 +++- .../utils/io/data/formats/JsonFormatTest.java | 2 +- .../{dataNode => json}/testNode.json | 0 .../{dataNode => json}/testNodeCompact.json | 0 26 files changed, 2556 insertions(+), 770 deletions(-) create mode 100644 src/test/java/org/ois/core/project/SimulationManifestTest.java create mode 100644 src/test/java/org/ois/core/runner/SimulationEngineTest.java create mode 100644 src/test/java/org/ois/core/state/StateManagerTest.java create mode 100644 src/test/java/org/ois/core/tools/TimerTest.java create mode 100644 src/test/java/org/ois/core/utils/VersionTest.java rename src/test/resources/{dataNode => json}/testNode.json (100%) rename src/test/resources/{dataNode => json}/testNodeCompact.json (100%) diff --git a/src/main/java/org/ois/core/project/SimulationManifest.java b/src/main/java/org/ois/core/project/SimulationManifest.java index e1128bb..4aca59d 100644 --- a/src/main/java/org/ois/core/project/SimulationManifest.java +++ b/src/main/java/org/ois/core/project/SimulationManifest.java @@ -25,7 +25,6 @@ public class SimulationManifest implements DataObject { * This map must contain at least one entry. */ private Map states = new Hashtable<>(); - /** The set of supported platforms for the simulation, based on {@link RunnerConfiguration.RunnerType}. */ private Set platforms = new HashSet<>(); /** The width of the screen the simulation needs, in pixels. */ @@ -78,6 +77,30 @@ public DataNode convertToDataNode() { return root; } + // Setters and Getters + + /** + * Sets the initial state of the project. + * + * @param initialState the initial state of the project. + * @return this {@link SimulationManifest} object. + */ + public SimulationManifest setInitialState(String initialState) { + this.initialState = initialState; + return this; + } + + /** + * Sets the states of the simulation. + * + * @param states A map of state keys to the class name of the corresponding `IState` implementation. + * @return this {@link SimulationManifest} object. + */ + public SimulationManifest setStates(Map states) { + this.states = states; + return this; + } + /** * Sets the project title. * diff --git a/src/main/java/org/ois/core/runner/RunnerConfiguration.java b/src/main/java/org/ois/core/runner/RunnerConfiguration.java index cd04277..c692fe7 100644 --- a/src/main/java/org/ois/core/runner/RunnerConfiguration.java +++ b/src/main/java/org/ois/core/runner/RunnerConfiguration.java @@ -10,16 +10,26 @@ /** * The Unified Configuration to be used in the Simulation Engine. - * Passing the specific runner (platform) variables in a unified way + * This class provides a way to set and retrieve configuration parameters for + * running simulations across different platforms. */ @SuppressWarnings("unused") public class RunnerConfiguration { - // The Supported application running platforms by the runners + /** + * Enum representing the supported application running platforms by the runners. + */ public enum RunnerType { Desktop, Html, Android } + /** + * Converts a string representation of a platform to a {@link RunnerType}. + * + * @param platform The string representation of the platform (e.g., "html", "android", "desktop"). + * @return The corresponding {@link RunnerType}. + * @throws RuntimeException if the platform is not supported. + */ public static RunnerType toPlatform(String platform) { switch (platform.trim().toLowerCase()) { case "html": return RunnerType.Html; @@ -32,43 +42,91 @@ public static RunnerType toPlatform(String platform) { private ILogger.Level logLevel; private String[] logTopics; private RunnerType type; - private SimulationManifest simulationManifest; + /** + * Sets the type of the runner configuration. + * + * @param type The runner type to set. + * @return The current instance of {@link RunnerConfiguration} for method chaining. + */ public RunnerConfiguration setType(RunnerType type) { this.type = type; return this; } + /** + * Sets the simulation manifest for this runner configuration. + * + * @param manifest The simulation manifest to set. + * @return The current instance of {@link RunnerConfiguration} for method chaining. + */ public RunnerConfiguration setSimulationManifest(SimulationManifest manifest) { this.simulationManifest = manifest; return this; } + /** + * Sets the log topics for this runner configuration. + * + * @param logTopics An array of log topics to set. + */ public void setLogTopics(String[] logTopics) { this.logTopics = logTopics; } + /** + * Sets the log level for this runner configuration. + * + * @param logLevel The log level to set. + */ public void setLogLevel(ILogger.Level logLevel) { this.logLevel = logLevel; } + /** + * Retrieves the log topics set for this runner configuration. + * + * @return An array of log topics. + */ public String[] getLogTopics() { return logTopics; } + /** + * Retrieves the log level set for this runner configuration. + * + * @return The log level. + */ public ILogger.Level getLogLevel() { return logLevel; } + /** + * Retrieves the runner type set for this configuration. + * + * @return The runner type. + */ public RunnerType getType() { return this.type; } + /** + * Retrieves the simulation manifest associated with this runner configuration. + * + * @return The simulation manifest. + */ public SimulationManifest getSimulationManifest() { return this.simulationManifest; } + /** + * Creates a {@link RunnerConfiguration} instance from a simulation configuration file stream. + * + * @param simulationConfigFileStream The input stream of the simulation configuration file. + * @return A {@link RunnerConfiguration} instance populated with data from the configuration file. + * @throws IOException if an error occurs while reading the configuration file. + */ public static RunnerConfiguration getRunnerConfigurations(InputStream simulationConfigFileStream) throws IOException { return new RunnerConfiguration().setSimulationManifest(JsonFormat.compact().load(new SimulationManifest(), simulationConfigFileStream)); } diff --git a/src/main/java/org/ois/core/runner/SimulationEngine.java b/src/main/java/org/ois/core/runner/SimulationEngine.java index 730fbb9..9569f34 100644 --- a/src/main/java/org/ois/core/runner/SimulationEngine.java +++ b/src/main/java/org/ois/core/runner/SimulationEngine.java @@ -20,24 +20,39 @@ import java.lang.reflect.InvocationTargetException; import java.util.Map; +/** + * The main simulation engine for the OIS project. + * This class manages the application's lifecycle, including loading and managing + * the states of the simulation. + */ public class SimulationEngine extends ApplicationAdapter { private static final Logger log = Logger.get(SimulationEngine.class); - // The Gdx application + /** The Gdx application **/ private Application app; - // The engine runner configuration with information from the dynamic project (Graphic, Meta-data...) + /** The engine runner configuration with information from the dynamic project (Graphic, Meta-data...) **/ private final RunnerConfiguration configuration; - - // The state manager that handles the states of the simulations provided by the project; + /** The state manager that handles the states of the simulations provided by the project; **/ public final StateManager stateManager; + /** The Error state, if the stateManager throws an error, the engine will switch to this state **/ public final ErrorState errorState; + /** + * Constructs a new SimulationEngine with the specified configuration. + * + * @param configuration The configuration to be used by the simulation engine. + */ public SimulationEngine(RunnerConfiguration configuration) { this.configuration = configuration; this.stateManager = new StateManager(); this.errorState = new ErrorState(); } + /** + * Retrieves the runner configuration for this engine. + * + * @return The runner configuration. + */ public RunnerConfiguration getRunnerConfig() { return this.configuration; } @@ -59,7 +74,16 @@ public void create() { } } - private void loadProject() throws ReflectionException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { + /** + * Loads the project manifest if needed and initializes the project states using reflection from the manifest. + * + * @throws ReflectionException if there is an error during reflection. + * @throws InvocationTargetException if a method cannot be invoked. + * @throws NoSuchMethodException if the method to invoke cannot be found. + * @throws InstantiationException if an instance cannot be created. + * @throws IllegalAccessException if access to the method or constructor is denied. + */ + public void loadProject() throws ReflectionException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { SimulationManifest manifest = configuration.getSimulationManifest(); if (manifest == null) { // For HTML, at Launcher we don't have access to resources. @@ -82,7 +106,13 @@ private void loadProject() throws ReflectionException, InvocationTargetException this.stateManager.start(manifest.getInitialState()); } + /** + * Stops the application gracefully. + */ public void stop() { + if (this.app == null) { + return; + } this.app.exit(); } @@ -147,19 +177,35 @@ public void dispose() { this.errorState.dispose(); } + /** + * Retrieves the current application width in pixel. + * + * @return The width of the application. + */ public int getAppWidth() { return Gdx.graphics.getWidth(); } + /** + * Retrieves the current application height in pixel. + * + * @return The height of the application. + */ public int getAppHeight() { return Gdx.graphics.getHeight(); } - private void handleProgramException(Exception exception) { + /** + * Handles exceptions that occur during the execution of the program. + * + * @param exception The exception to handle. + */ + public void handleProgramException(Exception exception) { if (errorState.isActive()) { stop(); + return; } errorState.enter(exception); } diff --git a/src/main/java/org/ois/core/state/ErrorState.java b/src/main/java/org/ois/core/state/ErrorState.java index da75f93..305559b 100644 --- a/src/main/java/org/ois/core/state/ErrorState.java +++ b/src/main/java/org/ois/core/state/ErrorState.java @@ -3,13 +3,33 @@ import com.badlogic.gdx.utils.ScreenUtils; import org.ois.core.utils.log.Logger; +/** + * Represents the error state in the simulation. + * This state is activated when the state manager encounters an error, + * allowing the simulation to handle exceptions gracefully. + */ public class ErrorState implements IState { private static final Logger log = Logger.get(ErrorState.class); // private final List exceptions = new ArrayList<>(); + + /** Indicates whether the error state is currently active. **/ private boolean isActive = false; + /** + * Checks if the error state is currently active. + * + * @return True if the error state is active, false otherwise. + */ + public boolean isActive() {return isActive;} + + /** + * Called when entering the error state. + * Logs any provided exceptions and sets the state to active. + * + * @param parameters Optional parameters, typically containing exceptions. + */ @Override public void enter(Object... parameters) { for (Object param : parameters) { @@ -22,42 +42,29 @@ public void enter(Object... parameters) { } @Override - public void exit() { - isActive = false; - } - - public boolean isActive() { - return isActive; - } + public void exit() {isActive = false;} @Override - public void pause() { - + public void render() { + ScreenUtils.clear(1,1,1, 1f); } @Override - public void resume() { - + public boolean update(float dt) { + // TODO: any key will make the state exit + return true; } @Override - public void resize(int width, int height) { - - } + public void resize(int width, int height) {} @Override - public void render() { - ScreenUtils.clear(1,1,1, 1f); - } + public void dispose() {isActive = false; } @Override - public boolean update(float dt) { - // TODO: any key will make the state exit - return true; - } + public void pause() {} @Override - public void dispose() { + public void resume() {} - } } diff --git a/src/main/java/org/ois/core/state/IState.java b/src/main/java/org/ois/core/state/IState.java index fa1921d..fcfd6dc 100644 --- a/src/main/java/org/ois/core/state/IState.java +++ b/src/main/java/org/ois/core/state/IState.java @@ -1,15 +1,56 @@ package org.ois.core.state; +/** + * The interface representing a state in the simulation. + * States are used to manage different phases of the simulation, and are the main class to implement by the user. + * allowing for entry, exit, and various state-specific operations. + */ public interface IState { + /** + * Called when entering the state. + * + * @param parameters Optional parameters to configure the state upon entry. + */ void enter(Object... parameters); + + /** + * Called when exiting the state. + */ void exit(); + /** + * Called to pause the state. + */ void pause(); + + /** + * Called to resume the state after it has been paused. + */ void resume(); + /** + * Called to resize the state, typically in response to window resizing. + * + * @param width The new width of the state. + * @param height The new height of the state. + */ void resize(int width, int height); + + /** + * Called to render the state. + */ void render(); + + /** + * Called to update the state. + * + * @param dt The delta time since the last update. + * @return True if the state should continue, false otherwise (will cause the state to exit). + */ boolean update(float dt); + /** + * Called to dispose of resources used by the state. + */ void dispose(); } diff --git a/src/main/java/org/ois/core/state/StateManager.java b/src/main/java/org/ois/core/state/StateManager.java index a998f33..39abff8 100644 --- a/src/main/java/org/ois/core/state/StateManager.java +++ b/src/main/java/org/ois/core/state/StateManager.java @@ -4,16 +4,32 @@ import java.util.*; +/** + * Manages a collection of states within an application, allowing registration, switching, + * and lifecycle control (enter, update, render, pause, resume, and exit) of states. + * + *

This class maintains an internal stack of active states and supports changing between states, + * updating and rendering the active state, and managing state-specific resources. It provides logging + * functionality to track state transitions and errors during state operations. + */ public class StateManager { private static final Logger log = Logger.get(StateManager.class); private static final String LOG_TOPIC = "states"; - // All known states. + + /** All known states. **/ private final Map states = new HashMap<>(); - // Active state stack by keys + /** Active state stack by keys **/ private final Stack stateStack = new Stack<>(); // Container management + /** + * Registers a state with a given key in the StateManager. + * + * @param key the unique key to identify the state + * @param state the state instance to register + * @throws IllegalArgumentException if key or state is null, or if the key already exists + */ public void registerState(String key, IState state) { if (key == null || state == null) { throw new IllegalArgumentException("Can't add null values (key: '" + key + "', state: " + state + ")"); @@ -25,11 +41,24 @@ public void registerState(String key, IState state) { this.states.put(key, state); } + /** + * Starts the StateManager with the given initial state. + * + * @param initialState the key of the state to start with + * @throws IllegalArgumentException if the state key does not exist + */ public void start(String initialState) { log.info(LOG_TOPIC, "Starting StateManager with state '" + initialState + "'"); changeState(initialState); } + /** + * Changes the current state to the one identified by the given key, exiting the current state if necessary. + * + * @param key the key of the new state + * @param params optional parameters to pass to the new state's enter method + * @throws IllegalArgumentException if the state key is not registered + */ public void changeState(String key, Object... params) { if (key == null || !this.states.containsKey(key)) { throw new IllegalArgumentException("Can't find state '" + key + "' in the registered states."); @@ -40,6 +69,15 @@ public void changeState(String key, Object... params) { enterState(key, params); } + /** + * Enters a new state by key and passes optional parameters. + * + * @param key the key of the state to enter + * @param params optional parameters to pass to the state + * @throws IllegalArgumentException if the state key is not registered + * @throws IllegalStateException if the state is already in the active state stack + * @throws RuntimeException if there is an error entering the state + */ private void enterState(String key, Object... params) { if (key == null || !this.states.containsKey(key)) { throw new IllegalArgumentException("Can't find state '" + key + "' in the registered states."); @@ -61,6 +99,11 @@ private void enterState(String key, Object... params) { } } + /** + * Exits the current active state, if there is one. + * + * @return the exited state, or null if no active state exists + */ private IState exitCurrentState() { if (!hasActiveState()) { return null; @@ -74,6 +117,13 @@ private IState exitCurrentState() { // IState interface + /** + * Updates the current active state with the given delta time. + * + * @param delta the time since the last update + * @return true if there is still an active state, false otherwise + * @throws Exception if an error occurs during the state update + */ public boolean update(float delta) throws Exception { IState current = getCurrentState(); if (current == null) { @@ -90,6 +140,11 @@ public boolean update(float delta) throws Exception { return hasActiveState(); } + /** + * Renders the current active state. + * + * @throws Exception if an error occurs during rendering + */ public void render() throws Exception { IState current = getCurrentState(); if (current == null) { @@ -103,6 +158,11 @@ public void render() throws Exception { } } + /** + * Pauses the current active state. + * + * @throws Exception if an error occurs during the pause + */ public void pause() throws Exception { IState current = getCurrentState(); if (current == null) { @@ -116,6 +176,11 @@ public void pause() throws Exception { } } + /** + * Resumes the current paused state. + * + * @throws Exception if an error occurs during the resume + */ public void resume() throws Exception { IState current = getCurrentState(); if (current == null) { @@ -129,6 +194,13 @@ public void resume() throws Exception { } } + /** + * Resizes the current active state to the given dimensions. + * + * @param width the new width + * @param height the new height + * @throws Exception if an error occurs during resizing + */ public void resize(int width, int height) throws Exception { IState current = getCurrentState(); if (current == null) { @@ -143,27 +215,38 @@ public void resize(int width, int height) throws Exception { } } + /** + * Disposes all registered states, clearing the state stack and disposing of each state's resources. + */ public void dispose() { while (hasActiveState()) { exitCurrentState(); } - for (IState state : states.values()) + for (Map.Entry state : states.entrySet()) { try { - log.info(LOG_TOPIC, "Dispose state '" + this.stateStack.peek() + "'."); - state.dispose(); + log.info(LOG_TOPIC, "Dispose state '" + state.getKey() + "'."); + state.getValue().dispose(); } catch (Exception e) { log.error("[Dispose] Caught exception from the state <" + state.getClass() + ">", e); } } } + /** + * Handles exceptions that occur during state operations, such as update, render, etc. + * + * @param topic the state operation during which the exception occurred + * @param e the exception + * @throws Exception if the exception is not handled by the state + */ private void handleCurrentStateException(String topic, Exception e) throws Exception { String outState = this.stateStack.peek(); String msg = "[" + topic + "] Caught exception from the current state '" + outState + "'"; - if (exitCurrentState() == null) { + this.stateStack.pop(); + if (!hasActiveState()) { throw new Exception(msg, e); } else { log.error(msg, e); @@ -172,6 +255,11 @@ private void handleCurrentStateException(String topic, Exception e) throws Excep // Getters + /** + * Returns the current active state. + * + * @return the current state, or null if no active state exists + */ public IState getCurrentState() { if(!hasActiveState()) { return null; @@ -179,11 +267,31 @@ public IState getCurrentState() { return this.states.get(this.stateStack.peek()); } + /** + * Checks whether there is an active state in the state stack. + * + * @return true if there is an active state, false otherwise + */ public boolean hasActiveState() { return !this.stateStack.isEmpty(); } + /** + * Returns the set of registered states. + * + * @return a set of Strings, the keys of the registered states + */ public Set states() { return this.states.keySet(); } + + /** + * Return a state given its key. + * + * @param key - the key id of the state + * @return a state registered with the key or null if not + */ + public IState getState(String key) { + return this.states.get(key); + } } diff --git a/src/main/java/org/ois/core/tools/Timer.java b/src/main/java/org/ois/core/tools/Timer.java index 1d86ac0..3423880 100644 --- a/src/main/java/org/ois/core/tools/Timer.java +++ b/src/main/java/org/ois/core/tools/Timer.java @@ -1,60 +1,198 @@ package org.ois.core.tools; +/** + * A versatile timer utility class that tracks the passage of time relative to a target duration. + * The timer supports both looping and non-looping modes, pausing, and listeners for when the timer is over. + */ public class Timer { + /** The target duration (in seconds) that the timer is counting toward. */ public float target; + + /** The amount of time (in seconds) that has elapsed since the timer started. */ public float elapsed; + + /** If true, the timer will reset and continue looping after reaching the target time. */ public boolean loop; + /** If true, the timer is paused and does not increment time. */ + private boolean paused; + + /** Listener that triggers when the timer reaches its target. */ + private Runnable onFinishListener; + + /** + * Constructs a new Timer with the default target duration of {@link Float#MAX_VALUE}. + */ public Timer() { this(Float.MAX_VALUE); } + /** + * Constructs a new Timer with the specified target duration. + * + * @param target The target time (in seconds) for the timer. + */ public Timer(float target) { this.target = target; + this.paused = false; + this.onFinishListener = null; } + /** + * Returns the amount of time remaining until the timer reaches the target time. + * + * @return The time left (in seconds) before reaching the target. + */ public float timeLeftToTarget() { return target - elapsed; } - public boolean isOver() { - return elapsed >= target; - } - + /** + * Resets the timer's elapsed time to 0. + */ public void reset() { this.elapsed = 0; } + /** + * Resets the timer's elapsed time and allows setting a new target time. + * + * @param newTarget The new target time (in seconds) for the timer. + */ + public void reset(float newTarget) { + this.target = newTarget; + reset(); + } + + /** + * Pauses the timer, preventing it from advancing. + */ + public void pause() { + this.paused = true; + } + + /** + * Checks if the timer is currently paused. + * + * @return true if the timer is paused, false otherwise. + */ + public boolean isPaused() { + return paused; + } + + /** + * Resumes the timer if it is paused. + */ + public void resume() { + this.paused = false; + } + + /** + * Gets the target time of the timer. + * + * @return The target time (in seconds). + */ public float getTarget() { return target; } + /** + * Sets a new target time for the timer. + * + * @param target The new target time (in seconds). + */ public void setTarget(float target) { this.target = target; } + /** + * Checks if the timer is set to loop after reaching the target time. + * + * @return true if the timer loops, false otherwise. + */ public boolean isLoop() { return loop; } + /** + * Enables or disables looping behavior for the timer. + * + * @param loop true to enable looping, false to disable. + */ public void setLoop(boolean loop) { this.loop = loop; } /** - * Advance the timer progress. - * @param deltaTime - the time passed since the last 'tic' - * @return true if after the tic the timer isOver + * Registers a listener that will be triggered when the timer reaches its target. + * + * @param listener A {@link Runnable} that will be executed when the timer is over. + */ + public void setOnFinishListener(Runnable listener) { + this.onFinishListener = listener; + } + + /** + * Advances the timer by the specified amount of time. + *

+ * This method increases the timer's {@code elapsed} time by the given {@code deltaTime}. + * If the timer is paused, no time will be added, and the method will return {@code false}. + *

+ *

+ * If the timer has exceeded the target time, the behavior depends on whether the timer is in looping mode: + *

+ *
    + *
  • If {@code loop} is enabled, the timer will reset the elapsed time to continue running and + * trigger any registered {@code onFinishListener}.
  • + *
  • If {@code loop} is disabled, the timer will stop and return {@code true} to indicate that it + * has finished.
  • + *
+ *

+ * If the timer reaches or exceeds the target time and an {@code onFinishListener} is registered, it will be + * executed exactly once when the target time is reached or exceeded. If looping is enabled, the listener + * will trigger at each interval where the timer surpasses the target time. + *

+ * + *

+ * Special Cases: + *

    + *
  • If the timer is paused, the elapsed time will not increase, and the method returns {@code false}.
  • + *
  • If the elapsed time surpasses the target time and looping is disabled, this method will return + * {@code true}, indicating that the timer has finished.
  • + *
+ *

+ * + * @param deltaTime The amount of time (in seconds) to add to the timer's elapsed time. This value is typically + * the time that has passed since the last call to {@code tic()}. + * @return {@code true} if the timer is over and not looping after the advancement, {@code false} otherwise. + *

+ * Returns {@code false} if the timer is paused, or if the timer continues running (either because the target + * time hasn't been reached yet, or because looping is enabled). + *

*/ public boolean tic(float deltaTime) { - if (isOver() && !loop) { + if (paused) { + // Don't advance time if paused + return false; + } + + if (!loop && elapsed >= target) { return true; } + elapsed += deltaTime; - if (isOver() && loop) { - elapsed -= target; - return true; + + if (elapsed >= target) { + if (onFinishListener != null) { + onFinishListener.run(); + } + if (loop) { + elapsed -= target; + } else { + return true; + } } + return false; } } diff --git a/src/main/java/org/ois/core/utils/ReflectionUtils.java b/src/main/java/org/ois/core/utils/ReflectionUtils.java index 260dff8..c1b699f 100644 --- a/src/main/java/org/ois/core/utils/ReflectionUtils.java +++ b/src/main/java/org/ois/core/utils/ReflectionUtils.java @@ -6,8 +6,25 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; +/** + * A utility class for reflection operations. + * This class provides methods to dynamically create instances of classes + * using their fully qualified class names. + */ public class ReflectionUtils { + /** + * Creates a new instance of the specified class. + * + * @param className the fully qualified name of the class to instantiate + * @param the type of the instance to be created + * @return a new instance of the specified class + * @throws ReflectionException if the class cannot be found or if any reflection-related error occurs + * @throws NoSuchMethodException if the default constructor is not found + * @throws InvocationTargetException if the underlying constructor throws an exception + * @throws InstantiationException if the class that declares the underlying constructor represents an abstract class + * @throws IllegalAccessException if the constructor is not accessible + */ public static T newInstance(String className) throws ReflectionException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class instanceClass = ClassReflection.forName(className); Constructor instanceConstructor = instanceClass.getDeclaredConstructor(); diff --git a/src/main/java/org/ois/core/utils/Version.java b/src/main/java/org/ois/core/utils/Version.java index 953bdea..1933e5d 100644 --- a/src/main/java/org/ois/core/utils/Version.java +++ b/src/main/java/org/ois/core/utils/Version.java @@ -3,13 +3,26 @@ import java.io.Serializable; import java.util.Objects; +/** + * Represents a version in the format of "major.minor.patch". + * Provides methods to compare versions and check their validity. + */ public class Version implements Serializable { + /** Constant representing a snapshot version. */ public static final String SNAPSHOT = "SNAPSHOT"; + /** Constant representing a version that is not found. */ public static final Version NOT_FOUND = new Version("0.0.0"); - + /** The tokens representing the individual version components. */ private final String[] tokens; + /** The full version string. */ private final String version; + /** + * Constructs a Version object with the given version string. + * + * @param version the version string to be parsed (must not be null or blank). + * @throws IllegalArgumentException if the version string is null or blank. + */ public Version(String version) { if (version == null || version.isBlank()) { throw new IllegalArgumentException("Provide a valid version"); @@ -18,10 +31,21 @@ public Version(String version) { this.tokens = version.split("\\."); } + /** + * Checks if the version is valid. + * + * @return true if the version is not equal to {@link #NOT_FOUND}, false otherwise. + */ public boolean isValid() { return !NOT_FOUND.equals(this.version); } + /** + * Compares this version to another version to check if it is at least that version. + * + * @param atLeast the version to compare against (can be null, which means always true). + * @return true if this version is at least the given version, false otherwise. + */ public boolean isAtLeast(Version atLeast) { if (atLeast == null) { return true; @@ -47,7 +71,15 @@ public boolean isAtLeast(Version atLeast) { return true; } - private int compareTokens(String toCheck, String atLeastToken) { + /** + * Compares two version tokens to determine their order. + * + * @param toCheck the token of this version to compare. + * @param atLeastToken the token of the other version to compare against. + * @return a negative integer, zero, or a positive integer as this token + * is less than, equal to, or greater than the specified token. + */ + int compareTokens(String toCheck, String atLeastToken) { boolean toCheckIsBlank = toCheck == null || toCheck.isBlank(); boolean atLeastTokenIsBlank = atLeastToken == null || atLeastToken.isBlank(); if (toCheckIsBlank && atLeastTokenIsBlank) { @@ -84,7 +116,13 @@ private int compareTokens(String toCheck, String atLeastToken) { return comparison; } - private boolean isNumeric(String str) { + /** + * Checks if the given string is numeric. + * + * @param str the string to check. + * @return true if the string is numeric, false otherwise. + */ + boolean isNumeric(String str) { if (str == null || str.isEmpty()) { return false; } @@ -98,7 +136,13 @@ private boolean isNumeric(String str) { return true; } - private boolean isAlphaNumeric(String str) { + /** + * Checks if the given string is alphanumeric. + * + * @param str the string to check. + * @return true if the string is alphanumeric, false otherwise. + */ + boolean isAlphaNumeric(String str) { if (str == null || str.isEmpty()) { return false; } @@ -112,6 +156,13 @@ private boolean isAlphaNumeric(String str) { return true; } + /** + * Compares this version's numeric token to a specified numeric token. + * + * @param toCheck the token of this version to check. + * @param atLeast the token to compare against. + * @return a negative integer, zero, or a positive integer based on the comparison. + */ private int compareToCheckToNumericAtLeast(String toCheck, String atLeast) { if (isNumeric(toCheck)) { return compareNumerals(toCheck, atLeast); @@ -122,6 +173,13 @@ private int compareToCheckToNumericAtLeast(String toCheck, String atLeast) { return 1; } + /** + * Compares an alphanumeric token to a specified numeric token. + * + * @param toCheck the token of this version to check. + * @param atLeast the token to compare against. + * @return a negative integer, zero, or a positive integer based on the comparison. + */ private int compareAlphaNumericToCheckToNumericAtLeast(String toCheck, String atLeast) { String toCheckFirstNumerals = getTokenFirstNumerals(toCheck); if (toCheckFirstNumerals.isBlank()) { @@ -130,11 +188,24 @@ private int compareAlphaNumericToCheckToNumericAtLeast(String toCheck, String at return compareNumerals(toCheckFirstNumerals, atLeast); } - private int compareNumerals(String toCheck, String atLeast) { + /** + * Compares two numeral strings. + * + * @param toCheck the numeral string to compare. + * @param atLeast the other numeral string to compare against. + * @return a negative integer, zero, or a positive integer based on the comparison. + */ + int compareNumerals(String toCheck, String atLeast) { return (Integer.valueOf(toCheck).compareTo(Integer.valueOf(atLeast))); } - private String getTokenFirstNumerals(String token) { + /** + * Extracts the leading numeric characters from a token. + * + * @param token the token to extract from. + * @return a string representing the leading numerals. + */ + String getTokenFirstNumerals(String token) { char[] chars = token.toCharArray(); StringBuilder numerals = new StringBuilder(); for (char c : chars) { diff --git a/src/main/java/org/ois/core/utils/io/FileUtils.java b/src/main/java/org/ois/core/utils/io/FileUtils.java index 64124e0..74a9a3c 100644 --- a/src/main/java/org/ois/core/utils/io/FileUtils.java +++ b/src/main/java/org/ois/core/utils/io/FileUtils.java @@ -6,7 +6,20 @@ import java.util.*; import java.util.stream.Collectors; +/** + * Utility class for file and directory operations. + * Provides methods to copy files, create directories, delete contents, and handle files more effectively. + */ public class FileUtils { + /** + * Copies a file from an InputStream to a target path. + * + * @param src the InputStream of the source file + * @param target the target path to copy the file to + * @param failIfCantCreate whether to throw an exception if the file cannot be created + * @return true if the file was copied successfully, false otherwise + * @throws IOException if an I/O error occurs + */ public static boolean copyFile(InputStream src, Path target, boolean failIfCantCreate) throws IOException { try { Files.copy(src, target); @@ -19,6 +32,15 @@ public static boolean copyFile(InputStream src, Path target, boolean failIfCantC } } + /** + * Copies a file from one path to another. + * + * @param src the path of the source file + * @param target the target path to copy the file to + * @param failIfCantCreate whether to throw an exception if the file cannot be created + * @return true if the file was copied successfully, false otherwise + * @throws IOException if an I/O error occurs + */ public static boolean copyFile(Path src, Path target, boolean failIfCantCreate) throws IOException { try { Files.copy(src, target); @@ -31,6 +53,13 @@ public static boolean copyFile(Path src, Path target, boolean failIfCantCreate) } } + /** + * Creates a directory if it does not exist. + * + * @param dirPath the path of the directory to create + * @param failIfCantCreate whether to throw an exception if the directory cannot be created + * @return true if the directory was created, false if it already exists + */ public static boolean createDirIfNotExists(Path dirPath, boolean failIfCantCreate) { File dir = dirPath.toFile(); if (dir.exists()) { @@ -43,6 +72,14 @@ public static boolean createDirIfNotExists(Path dirPath, boolean failIfCantCreat return created; } + /** + * Copies all files and subdirectories from the source path to the target path, excluding specified patterns. + * + * @param sourcePath the path to copy from + * @param targetPath the path to copy to + * @param excludePatterns the patterns of files/directories to exclude from copying + * @throws IOException if an I/O error occurs + */ public static void copyDirectoryContent(Path sourcePath, Path targetPath, String... excludePatterns) throws IOException { // Copy all files and subdirectories from source to target EnumSet options = EnumSet.of(FileVisitOption.FOLLOW_LINKS); @@ -68,6 +105,12 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO }); } + /** + * Deletes all content within the specified directory. + * + * @param directoryPath the path of the directory to clear + * @return true if the content was deleted successfully, false if the directory does not exist + */ public static boolean deleteDirectoryContent(Path directoryPath) { File directory = directoryPath.toFile(); if (!directory.exists() || !directory.isDirectory()) { @@ -76,6 +119,12 @@ public static boolean deleteDirectoryContent(Path directoryPath) { return deleteContent(directory); } + /** + * Deletes the contents of a directory recursively. + * + * @param directory the directory to delete content from + * @return true if all contents were deleted successfully, false otherwise + */ private static boolean deleteContent(File directory) { File[] files = directory.listFiles(); if (files == null) { @@ -91,6 +140,13 @@ private static boolean deleteContent(File directory) { return result; } + /** + * Creates a file if it does not already exist. + * + * @param path the path of the file to create + * @return true if the file was created, false if it already exists + * @throws IOException if an I/O error occurs + */ public static boolean createFileIfNotExists(Path path) throws IOException { File file = path.toFile(); if (file.exists()) { diff --git a/src/main/java/org/ois/core/utils/io/ZipUtils.java b/src/main/java/org/ois/core/utils/io/ZipUtils.java index 91dbf36..24337c3 100644 --- a/src/main/java/org/ois/core/utils/io/ZipUtils.java +++ b/src/main/java/org/ois/core/utils/io/ZipUtils.java @@ -7,15 +7,45 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +/** + * Utility class for creating ZIP archives. + * Provides methods to zip files and directories, with options for entry name filtering. + */ public class ZipUtils { + + /** + * Interface for converting a file or directory to a ZIP entry name. + */ public interface ZipEntryConvertor { + /** + * Gets the entry name for a given file or directory if it is not filtered. + * + * @param item the file or directory to convert + * @return the entry name, or null if the item should be filtered + */ String getEntryNameIfNotFiltered(File item); } + /** + * Zips the specified items into a ZIP archive at the target path. + * Uses the file name as the entry name. + * + * @param archiveTargetPath the path where the ZIP archive will be created + * @param itemsToZip the paths of the items to be zipped + * @throws IOException if an I/O error occurs during zipping + */ public static void zipItems(Path archiveTargetPath, Path... itemsToZip) throws IOException { zipItems(archiveTargetPath, File::getName,itemsToZip); } + /** + * Zips the specified items into a ZIP archive at the target path, allowing for custom entry name conversion. + * + * @param archiveTargetPath the path where the ZIP archive will be created + * @param itemConvertor the converter used to determine entry names for the items + * @param itemsToZip the paths of the items to be zipped + * @throws IOException if an I/O error occurs during zipping + */ public static void zipItems(Path archiveTargetPath, ZipEntryConvertor itemConvertor, Path... itemsToZip) throws IOException { try(FileOutputStream fos = new FileOutputStream(archiveTargetPath.toString()); ZipOutputStream zipOut = new ZipOutputStream(fos)) { @@ -35,6 +65,17 @@ public static void zipItems(Path archiveTargetPath, ZipEntryConvertor itemConver } } + /** + * Recursively zips a directory and its contents. + * + * @param dir the directory to zip + * @param itemConvertor the converter used to determine entry names for the items + * @param baseName the base name for entries within the directory + * @param zipOut the ZIP output stream to write to + * @param buffer a byte array used for reading file data + * @param addedEntries a set of already added entries to prevent duplicates + * @throws IOException if an I/O error occurs during zipping + */ private static void zipDirectory(File dir, ZipEntryConvertor itemConvertor, String baseName, ZipOutputStream zipOut, byte[] buffer, Set addedEntries) throws IOException { File[] files = dir.listFiles(); @@ -54,6 +95,17 @@ private static void zipDirectory(File dir, ZipEntryConvertor itemConvertor, Stri } } + /** + * Zips a single file. + * + * @param file the file to zip + * @param itemConvertor the converter used to determine the entry name for the file + * @param baseName the base name for the entry + * @param zipOut the ZIP output stream to write to + * @param buffer a byte array used for reading file data + * @param addedEntries a set of already added entries to prevent duplicates + * @throws IOException if an I/O error occurs during zipping + */ private static void zipFile(File file, ZipEntryConvertor itemConvertor, String baseName, ZipOutputStream zipOut, byte[] buffer, Set addedEntries) throws IOException { String fileConvertedName = itemConvertor.getEntryNameIfNotFiltered(file); diff --git a/src/main/java/org/ois/core/utils/io/data/DataNode.java b/src/main/java/org/ois/core/utils/io/data/DataNode.java index 648759f..67186d8 100644 --- a/src/main/java/org/ois/core/utils/io/data/DataNode.java +++ b/src/main/java/org/ois/core/utils/io/data/DataNode.java @@ -5,6 +5,20 @@ /** * Represents a node that can store various types of data, including objects, collections, and primitive values. + * + *

A DataNode can represent four types of data: + *

    + *
  • Unknown: Undefined or unclassified data
  • + *
  • Object: A map of named attributes
  • + *
  • Collection: A list of values
  • + *
  • Primitive: A single primitive value, such as String, int, float, or boolean
  • + *
+ * + *

This class provides methods to create and manipulate nodes of different types, convert between types, + * and retrieve data from nodes in various formats such as collections or maps. + * + *

It supports iterable properties for object-like nodes (maps) and iterable content for collection-like nodes. + * Nodes are mutable and support method chaining for ease of use. */ public class DataNode implements Iterable { @@ -77,6 +91,11 @@ public static DataNode Map(Map values) { return mapNode; } + /** + * Creates a new DataNode of type Collection. + * + * @return a new DataNode representing a collection + */ public static DataNode Collection() { return new DataNode(Type.Collection); } @@ -146,55 +165,132 @@ public static DataNode Collection(boolean... values) { return new DataNode(Type.Collection).add(values); } + /** + * Creates a new DataNode representing a primitive string value. + * + * @param value the string value to set as a primitive + * @return a new DataNode representing the primitive string value + */ public static DataNode Primitive(String value) { return new DataNode(Type.Primitive).setValue(value); } + + /** + * Creates a new DataNode representing a primitive integer value. + * + * @param value the integer value to set as a primitive + * @return a new DataNode representing the primitive integer value + */ public static DataNode Primitive(int value) { return new DataNode(Type.Primitive).setValue(value); } + + /** + * Creates a new DataNode representing a primitive float value. + * + * @param value the float value to set as a primitive + * @return a new DataNode representing the primitive float value + */ public static DataNode Primitive(float value) { return new DataNode(Type.Primitive).setValue(value); } + + /** + * Creates a new DataNode representing a primitive boolean value. + * + * @param value the boolean value to set as a primitive + * @return a new DataNode representing the primitive boolean value + */ public static DataNode Primitive(boolean value) { return new DataNode(Type.Primitive).setValue(value); } // User methods to retrieve data stored at nodes + /** + * Returns the node as a collection of String values. + * + * @return a collection of Strings, representing the node's content + */ public String[] toStringCollection() { List list = toStringCollection(new ArrayList<>()); return list.toArray(String[]::new); } + /** + * Fills the provided collection with the node's content as String values. + * + * @param destination the collection to fill with Strings + * @param the type of the destination collection + * @return the destination collection filled with String representations of the node's content + */ public > T toStringCollection(T destination) { destination.addAll(this.content.stream().map(DataNode::getString).collect(Collectors.toList())); return destination; } + /** + * Fills the provided collection with the node's content as int values. + * + * @param destination the collection to fill with integers + * @param the type of the destination collection + * @return the destination collection filled with integers representations of the node's content + */ public > T toIntCollection(T destination) { destination.addAll(this.content.stream().map(DataNode::getInt).collect(Collectors.toList())); return destination; } + /** + * Fills the provided collection with the node's content as float values. + * + * @param destination the collection to fill with floats + * @param the type of the destination collection + * @return the destination collection filled with float representations of the node's content + */ public > T toFloatCollection(T destination) { destination.addAll(this.content.stream().map(DataNode::getFloat).collect(Collectors.toList())); return destination; } + /** + * Fills the provided collection with the node's content as boolean values. + * + * @param destination the collection to fill with booleans + * @param the type of the destination collection + * @return the destination collection filled with boolean representations of the node's content + */ public > T toBooleanCollection(T destination) { destination.addAll(this.content.stream().map(DataNode::getBoolean).collect(Collectors.toList())); return destination; } + /** + * Fills the provided map with the node's attributes. + * + * @param destination the map to fill with the node's attributes + * @return the destination map filled with the node's attributes + */ public Map toMap(Map destination) { destination.putAll(this.attributes); return destination; } + /** + * Returns the node's attributes as a map. + * + * @return a map representing the node's attributes + */ public Map toMap() { return toMap(new Hashtable<>()); } + /** + * Fills the provided map with the node's attributes as String values. + * + * @param destination the map to fill with the node's attributes + * @return the destination map filled with the node's attributes as String values + */ public Map toStringMap(Map destination) { for (Map.Entry attribute : properties()) { destination.put(attribute.getKey(), attribute.getValue().getString()); @@ -202,10 +298,21 @@ public Map toStringMap(Map destination) { return destination; } + /** + * Returns the node's attributes as a map of String values. + * + * @return a map representing the node's attributes as String values + */ public Map toStringMap() { return toStringMap(new Hashtable<>()); } + /** + * Fills the provided map with the node's attributes as Integer values. + * + * @param destination the map to fill with the node's attributes + * @return the destination map filled with the node's attributes as Integer values + */ public Map toIntMap(Map destination) { for (Map.Entry attribute : properties()) { destination.put(attribute.getKey(), attribute.getValue().getInt()); @@ -213,10 +320,21 @@ public Map toIntMap(Map destination) { return destination; } + /** + * Returns the node's attributes as a map of Integer values. + * + * @return a map representing the node's attributes as Integer values + */ public Map toIntMap() { return toIntMap(new Hashtable<>()); } + /** + * Fills the provided map with the node's attributes as Float values. + * + * @param destination the map to fill with the node's attributes + * @return the destination map filled with the node's attributes as Float values + */ public Map toFloatMap(Map destination) { for (Map.Entry attribute : properties()) { destination.put(attribute.getKey(), attribute.getValue().getFloat()); @@ -224,10 +342,21 @@ public Map toFloatMap(Map destination) { return destination; } + /** + * Returns the node's attributes as a map of Float values. + * + * @return a map representing the node's attributes as Float values + */ public Map toFloatMap() { return toFloatMap(new Hashtable<>()); } + /** + * Fills the provided map with the node's attributes as Boolean values. + * + * @param destination the map to fill with the node's attributes + * @return the destination map filled with the node's attributes as Boolean values + */ public Map toBooleanMap(Map destination) { for (Map.Entry attribute : properties()) { destination.put(attribute.getKey(), attribute.getValue().getBoolean()); @@ -235,10 +364,20 @@ public Map toBooleanMap(Map destination) { return destination; } + /** + * Returns the node's attributes as a map of Boolean values. + * + * @return a map representing the node's attributes as Boolean values + */ public Map toBooleanMap() { return toBooleanMap(new Hashtable<>()); } + /** + * Returns the type of the DataNode based on its contents. + * + * @return the type of the DataNode, which can be Primitive, Collection, Object, or Unknown + */ public Type getType() { if (!Type.Unknown.equals(nodeType)) { return nodeType; @@ -258,27 +397,68 @@ public Type getType() { // Node as Map/Object (Object = Map of attributes) + /** + * Sets an attribute in this node. + * + * @param key the attribute name + * @param attributeValue the DataNode value of the attribute + * @return the DataNode for chaining + */ public DataNode set(String key, DataNode attributeValue) { this.attributes.put(key,attributeValue); return this; } + /** + * Sets an attribute in this node with a String value. + * + * @param key the attribute name + * @param attributeValue the String value of the attribute + * @return the DataNode for chaining + */ public DataNode set(String key, String attributeValue) { return set(key, Primitive(attributeValue)); } + /** + * Sets an attribute in this node with an int value. + * + * @param key the attribute name + * @param attributeValue the int value of the attribute + * @return the DataNode for chaining + */ public DataNode set(String key, int attributeValue) { return set(key, String.valueOf(attributeValue)); } + /** + * Sets an attribute in this node with a float value. + * + * @param key the attribute name + * @param attributeValue the float value of the attribute + * @return the DataNode for chaining + */ public DataNode set(String key, float attributeValue) { return set(key, String.valueOf(attributeValue)); } + /** + * Sets an attribute in this node with a boolean value. + * + * @param key the attribute name + * @param attributeValue the boolean value of the attribute + * @return the DataNode for chaining + */ public DataNode set(String key, boolean attributeValue) { return set(key, String.valueOf(attributeValue)); } + /** + * Retrieves the attribute value corresponding to the given key. + * + * @param attributeNodeKeys the sequence of attribute keys to traverse + * @return the DataNode corresponding to the final key, or null if not found + */ public DataNode get(String... attributeNodeKeys) { DataNode currentNode = this; for (String key : attributeNodeKeys) { @@ -290,11 +470,23 @@ public DataNode get(String... attributeNodeKeys) { return currentNode; } + /** + * Checks if the node contains the specified property. + * + * @param property the name of the property to check + * @return true if the property exists; false otherwise + */ public boolean contains(String property) { return this.attributes.containsKey(property); } - // for optional attributes + /** + * Retrieves the property value corresponding to the given key sequence. + * If the key is not found, a new DataNode is created if necessary. + * + * @param attributeNodeKeys the sequence of attribute keys to traverse + * @return the DataNode corresponding to the final key, or a new DataNode if not found + */ public DataNode getProperty(String... attributeNodeKeys) { DataNode currentNode = this; for (int i = 0; i < attributeNodeKeys.length; i++) { @@ -310,45 +502,83 @@ public DataNode getProperty(String... attributeNodeKeys) { } /** - * The amount of properties that the node contains - * @return - the number of attributes registered at the node + * Returns the number of properties that the node contains. + * + * @return the number of attributes registered at the node */ public int getPropertyCount() { return this.attributes.size(); } - // We implement Iterable on the node attributes. + /** + * Represents the attributes of a DataNode, allowing iteration over its entries. + */ public static class Attributes implements Iterable> { private final Map attributes; + + /** + * Constructs an Attributes object with the specified map of attributes. + * + * @param attributes the map of attributes to be wrapped + */ public Attributes(Map attributes) { this.attributes = attributes; } + @Override public Iterator> iterator() { return this.attributes.entrySet().iterator(); } + /** + * Returns the number of attributes contained in this Attributes object. + * + * @return the number of attributes + */ public int size() { return this.attributes.size(); } } /** - * Get the node Attributes object that implements Iterable on the node properties. - * Can be used for 'foreach' on the attributes/entries - * @return Attributes object, iterable on the node attributes + * Retrieves the Attributes object that implements Iterable on the node properties. + * This can be used for 'foreach' on the attributes/entries. + * + * @return an Attributes object, iterable on the node attributes */ public Attributes properties() { return new Attributes(this.attributes); } + + /** + * Removes an attribute from an object-type DataNode. + * + * @param key the name of the attribute to remove + * @return the removed DataNode, or null if the attribute was not present + */ + public DataNode remove(String key) { + return this.attributes.remove(key); + } + + /** + * Clears all attributes from an object-type DataNode. + * + * @return this DataNode, for method chaining + */ + public DataNode clearAttributes() { + this.attributes.clear(); + return this; + } + // Node as Collection /** - * Add a collection of primitive values to add to the collection node + * Adds a collection of primitive values to the collection node. + * * @param primitiveValues a collection of primitive values to add to the collection node * @return this node, for chaining - * @param primitive values + * @param the type of primitive values being added */ public DataNode add(Collection primitiveValues) { for(T data : primitiveValues) { @@ -357,15 +587,33 @@ public DataNode add(Collection primitiveValues) { return this; } + /** + * Adds multiple DataNode values to the collection node. + * + * @param values the DataNode values to add to the collection + * @return this node, for chaining + */ public DataNode add(DataNode... values) { this.content.addAll(List.of(values)); return this; } + /** + * Adds multiple String values to the collection node. + * + * @param values the String values to add to the collection + * @return this node, for chaining + */ public DataNode add(String... values) { return add(List.of(values)); } + /** + * Adds multiple integer values to the collection node. + * + * @param values the integer values to add to the collection + * @return this node, for chaining + */ public DataNode add(int... values) { List list = new ArrayList<>(values.length); for (int data : values) { @@ -374,6 +622,12 @@ public DataNode add(int... values) { return add(list); } + /** + * Adds multiple float values to the collection node. + * + * @param values the float values to add to the collection + * @return this node, for chaining + */ public DataNode add(float... values) { List list = new ArrayList<>(values.length); for (float data : values) { @@ -382,6 +636,12 @@ public DataNode add(float... values) { return add(list); } + /** + * Adds multiple boolean values to the collection node. + * + * @param values the boolean values to add to the collection + * @return this node, for chaining + */ public DataNode add(boolean... values) { List list = new ArrayList<>(values.length); for (boolean data : values) { @@ -390,48 +650,114 @@ public DataNode add(boolean... values) { return add(list); } + /** + * Sets the value at the specified index in the collection node to the given DataNode value. + * + * @param index the index at which to set the value + * @param value the DataNode value to set + * @return the DataNode at the specified index + */ public DataNode set(int index, DataNode value) { return this.content.set(index, value); } + /** + * Sets the value at the specified index in the collection node to the given String value. + * + * @param index the index at which to set the value + * @param value the String value to set + * @return the DataNode at the specified index + */ public DataNode set(int index, String value) { return set(index,Primitive(value)); } + /** + * Sets the value at the specified index in the collection node to the given integer value. + * + * @param index the index at which to set the value + * @param value the integer value to set + * @return the DataNode at the specified index + */ public DataNode set(int index, int value) { return set(index,Primitive(value)); } + /** + * Sets the value at the specified index in the collection node to the given float value. + * + * @param index the index at which to set the value + * @param value the float value to set + * @return the DataNode at the specified index + */ public DataNode set(int index, float value) { return set(index,Primitive(value)); } + /** + * Sets the value at the specified index in the collection node to the given boolean value. + * + * @param index the index at which to set the value + * @param value the boolean value to set + * @return the DataNode at the specified index + */ public DataNode set(int index, boolean value) { return set(index,Primitive(value)); } + /** + * Retrieves the DataNode value at the specified index in the collection node. + * + * @param index the index of the value to retrieve + * @return the DataNode at the specified index + */ public DataNode get(int index) { return this.content.get(index); } + /** + * Retrieves the String value at the specified index in the collection node. + * + * @param index the index of the value to retrieve + * @return the String value at the specified index + */ public String getString(int index) { return get(index).getString(); } + /** + * Retrieves the integer value at the specified index in the collection node. + * + * @param index the index of the value to retrieve + * @return the integer value at the specified index + */ public int getInt(int index) { return get(index).getInt(); } + /** + * Retrieves the float value at the specified index in the collection node. + * + * @param index the index of the value to retrieve + * @return the float value at the specified index + */ public float getFloat(int index) { return get(index).getFloat(); } + /** + * Retrieves the boolean value at the specified index in the collection node. + * + * @param index the index of the value to retrieve + * @return the boolean value at the specified index + */ public boolean getBoolean(int index) { return get(index).getBoolean(); } /** * Get the amount of content stored at the collection node. + * * @return - the number of values stored at the node */ public int contentCount() { @@ -444,12 +770,23 @@ public Iterator iterator() { return this.content.iterator(); } + /** + * Clears all content from a collection-type DataNode. + * + * @return this DataNode, for method chaining + */ + public DataNode clearContent() { + this.content.clear(); + return this; + } + // Node as Primitive /** - * Set the value of a node, for primitive nodes (single primitive value) - * @param primitiveValue - value of the node - * @return - the data node for chaining + * Sets the value of this node as a primitive String value. + * + * @param primitiveValue the value to set + * @return the DataNode for chaining */ public DataNode setValue(String primitiveValue) { this.value = primitiveValue; @@ -457,35 +794,39 @@ public DataNode setValue(String primitiveValue) { } /** - * Set the value of a node, for primitive nodes (single primitive value) - * @param primitiveValue - value of the node - * @return - the data node for chaining + * Sets the value of this node as a primitive int value. + * + * @param primitiveValue the value to set + * @return the DataNode for chaining */ public DataNode setValue(int primitiveValue) { return setValue(String.valueOf(primitiveValue)); } /** - * Set the value of a node, for primitive nodes (single primitive value) - * @param primitiveValue - value of the node - * @return - the data node for chaining + * Sets the value of this node as a primitive float value. + * + * @param primitiveValue the value to set + * @return the DataNode for chaining */ public DataNode setValue(float primitiveValue) { return setValue(String.valueOf(primitiveValue)); } /** - * Set the value of a node, for primitive nodes (single primitive value) - * @param primitiveValue - value of the node - * @return - the data node for chaining + * Sets the value of this node as a primitive boolean value. + * + * @param primitiveValue the value to set + * @return the DataNode for chaining */ public DataNode setValue(boolean primitiveValue) { return setValue(String.valueOf(primitiveValue)); } /** - * Get the value of the primitive node, and interpret it as a String - * @return the value of the node as a String + * Retrieves the value of this node as a String. + * + * @return the String representation of the value */ public String getString() { if (value == null) { @@ -495,8 +836,9 @@ public String getString() { } /** - * Get the value of the primitive node, and interpret it as an Integer - * @return the value of the node as an Integer + * Retrieves the value of this node as an int. + * + * @return the int representation of the value */ public int getInt() { String val = getString(); @@ -507,8 +849,9 @@ public int getInt() { } /** - * Get the value of the primitive node, and interpret it as a Float - * @return the value of the node as a Float + * Retrieves the value of this node as a float. + * + * @return the float representation of the value */ public float getFloat() { String val = getString(); @@ -519,8 +862,9 @@ public float getFloat() { } /** - * Get the value of the primitive node, and interpret it as a Boolean - * @return the value of the node as a Boolean + * Retrieves the value of this node as a boolean. + * + * @return the boolean representation of the value */ public boolean getBoolean() { String val = getString(); @@ -530,6 +874,59 @@ public boolean getBoolean() { return Boolean.parseBoolean(val); } + /** + * Provides a deep copy of the DataNode. + * + * @return a new DataNode that is a copy of this node + */ + public DataNode deepCopy() { + DataNode copy = new DataNode(this.nodeType); + if (this.nodeType == Type.Primitive) { + copy.setValue(this.value); + } else if (this.nodeType == Type.Collection) { + for (DataNode child : this.content) { + copy.add(child.deepCopy()); + } + } else if (this.nodeType == Type.Object) { + for (Map.Entry entry : this.attributes.entrySet()) { + copy.set(entry.getKey(), entry.getValue().deepCopy()); + } + } + return copy; + } + + @Override + public int hashCode() { + return Objects.hash(nodeType, value, content, attributes); + } + + /** + * Returns a string representation of the DataNode. + * + *

The representation varies depending on the type of the node: + *

    + *
  • For a primitive node, it returns the string value
  • + *
  • For a collection node, it returns the string representation of the collection
  • + *
  • For an object node, it returns the string representation of the attributes
  • + *
  • For an unknown type, it returns an empty string
  • + *
+ * + * @return a string representation of the node + */ + @Override + public String toString() { + if (nodeType == Type.Primitive) { + return value; + } + if (nodeType == Type.Collection) { + return content.toString(); + } + if (nodeType == Type.Object) { + return attributes.toString(); + } + return ""; + } + @Override public boolean equals(Object obj) { if (this == obj) { diff --git a/src/main/java/org/ois/core/utils/io/data/DataObject.java b/src/main/java/org/ois/core/utils/io/data/DataObject.java index ea2f2e9..c965139 100644 --- a/src/main/java/org/ois/core/utils/io/data/DataObject.java +++ b/src/main/java/org/ois/core/utils/io/data/DataObject.java @@ -1,7 +1,24 @@ package org.ois.core.utils.io.data; - +/** + * Interface representing a data object that can load data from a {@link DataNode} + * and convert itself to a {@link DataNode}. + * + * @param the type of the data object + */ public interface DataObject { + /** + * Loads data from the specified {@link DataNode} and populates the data object. + * + * @param data the {@link DataNode} containing the data to be loaded + * @return the populated data object + */ T loadData(DataNode data); + + /** + * Converts the data object to a {@link DataNode} representation. + * + * @return the {@link DataNode} representing the data object + */ DataNode convertToDataNode(); } diff --git a/src/main/java/org/ois/core/utils/io/data/formats/DataFormat.java b/src/main/java/org/ois/core/utils/io/data/formats/DataFormat.java index c9ce4f3..4bb9df2 100644 --- a/src/main/java/org/ois/core/utils/io/data/formats/DataFormat.java +++ b/src/main/java/org/ois/core/utils/io/data/formats/DataFormat.java @@ -5,14 +5,46 @@ import java.io.*; +/** + * Interface for defining data formats for serialization and deserialization + * of {@link DataNode} and {@link DataObject} instances. + */ public interface DataFormat { + + /** + * Deserializes a string representation of data into a {@link DataNode}. + * + * @param data the string representation of the data to be deserialized + * @return the {@link DataNode} populated with the deserialized data + */ DataNode deserialize(String data); + + /** + * Serializes a {@link DataNode} into its string representation. + * + * @param data the {@link DataNode} to be serialized + * @return the string representation of the serialized data + */ String serialize(DataNode data); + /** + * Serializes a {@link DataObject} into its string representation + * by first converting it to a {@link DataNode}. + * + * @param data the {@link DataObject} to be serialized + * @return the string representation of the serialized data + */ default String serialize(DataObject data) { return serialize(data.convertToDataNode()); } + /** + * Loads data from an {@link InputStream} and deserializes it into a {@link DataNode}. + * + * @param inputStream the input stream containing the data + * @return the {@link DataNode} populated with the loaded data + * @throws IOException if an I/O error occurs while reading the stream + */ default DataNode load(InputStream inputStream) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { StringBuilder jsonBuilder = new StringBuilder(); @@ -24,14 +56,39 @@ default DataNode load(InputStream inputStream) throws IOException { } } + /** + * Loads data from an {@link InputStream} into the specified {@link DataObject}. + * + * @param objToLoad the {@link DataObject} to populate with data + * @param inputStream the input stream containing the data + * @param the type of the data object + * @return the populated {@link DataObject} + * @throws IOException if an I/O error occurs while reading the stream + */ default > T load(T objToLoad, InputStream inputStream) throws IOException { return objToLoad.loadData(load(inputStream)); } + /** + * Loads data from a byte array into the specified {@link DataObject}. + * + * @param objToLoad the {@link DataObject} to populate with data + * @param source the byte array containing the data + * @param the type of the data object + * @return the populated {@link DataObject} + */ default > T load(T objToLoad, byte[] source) { return load(objToLoad, new String(source)); } + /** + * Loads data from a string into the specified {@link DataObject}. + * + * @param objToLoad the {@link DataObject} to populate with data + * @param source the string containing the data + * @param the type of the data object + * @return the populated {@link DataObject} + */ default > T load(T objToLoad, String source) { return objToLoad.loadData(deserialize(source)); } diff --git a/src/main/java/org/ois/core/utils/io/data/formats/JsonFormat.java b/src/main/java/org/ois/core/utils/io/data/formats/JsonFormat.java index c9ddfc1..8d02267 100644 --- a/src/main/java/org/ois/core/utils/io/data/formats/JsonFormat.java +++ b/src/main/java/org/ois/core/utils/io/data/formats/JsonFormat.java @@ -8,6 +8,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Implementation of the DataFormat interface for handling JSON data. + * Provides methods to serialize and deserialize JSON strings to and from DataNode structures. + */ public class JsonFormat implements DataFormat { private static final JsonFormat HUMAN_READABLE = new JsonFormat(new Options()); @@ -15,21 +19,36 @@ public class JsonFormat implements DataFormat { /** * Options for controlling the formatting of the output JSON string. - * If all attributes equal to "", the output will have no whitespace at all. + * If all attributes are empty strings, the output will have no whitespace at all. */ public static class Options { public final String indentSymbol; public final String newLineSymbol; + /** + * Default constructor which initializes options with a tab for indentation + * and a newline character for line breaks. + */ public Options() { this("\t", "\n"); } + /** + * Constructor that allows custom symbols for indentation and new lines. + * + * @param indentSymbol the string used for indentation + * @param newLineSymbol the string used for new lines + */ public Options(String indentSymbol, String newLineSymbol) { this.indentSymbol = indentSymbol; this.newLineSymbol = newLineSymbol; } + /** + * Checks if the options are set for compact (no whitespace) output. + * + * @return true if both indent and new line symbols are empty, false otherwise + */ public boolean isCompact() { return indentSymbol.isEmpty() && newLineSymbol.isEmpty(); } @@ -37,16 +56,29 @@ public boolean isCompact() { private final Options options; + /** + * Constructs a JsonFormat instance with the specified options. + * + * @param options the formatting options to use + */ public JsonFormat(Options options) { this.options = options; } - // Static method for human-readable JSON format + /** + * Static method for obtaining a human-readable JSON format instance. + * + * @return a JsonFormat instance configured for human-readable output + */ public static JsonFormat humanReadable() { return HUMAN_READABLE; } - // Static method for compact (no-whitespace) JSON format + /** + * Static method for obtaining a compact (no-whitespace) JSON format instance. + * + * @return a JsonFormat instance configured for compact output + */ public static JsonFormat compact() { return COMPACT; } @@ -56,6 +88,13 @@ public String serialize(DataNode data) { return buildJson(data, 0); } + /** + * Builds a JSON string from a DataNode at a specified indentation level. + * + * @param dataNode the DataNode to convert + * @param indentLevel the current level of indentation + * @return a JSON string representation of the DataNode + */ private String buildJson(DataNode dataNode, int indentLevel) { switch (dataNode.getType()) { case Object: @@ -69,6 +108,13 @@ private String buildJson(DataNode dataNode, int indentLevel) { } } + /** + * Builds a JSON string representation of a DataNode of type Object. + * + * @param dataNode the DataNode to convert + * @param indentLevel the current level of indentation + * @return a JSON string representation of the DataNode + */ private String buildJsonObject(DataNode dataNode, int indentLevel) { StringBuilder jsonBuilder = new StringBuilder(); jsonBuilder.append("{"); @@ -96,6 +142,13 @@ private String buildJsonObject(DataNode dataNode, int indentLevel) { return jsonBuilder.toString(); } + /** + * Builds a JSON string representation of a DataNode of type Collection (Array). + * + * @param dataNode the DataNode to convert + * @param indentLevel the current level of indentation + * @return a JSON string representation of the DataNode + */ private String buildJsonArray(DataNode dataNode, int indentLevel) { StringBuilder jsonBuilder = new StringBuilder(); jsonBuilder.append("["); @@ -120,6 +173,12 @@ private String buildJsonArray(DataNode dataNode, int indentLevel) { return jsonBuilder.toString(); } + /** + * Builds a JSON string representation of a DataNode of type Primitive. + * + * @param dataNode the DataNode to convert + * @return a JSON string representation of the DataNode + */ private String buildJsonPrimitive(DataNode dataNode) { String value = dataNode.getString(); try { @@ -138,6 +197,12 @@ private String buildJsonPrimitive(DataNode dataNode) { } } + /** + * Escapes special characters in a JSON string to ensure valid JSON format. + * + * @param value the string to escape + * @return the escaped string + */ private String escapeJson(String value) { if (value == null) { return "null"; @@ -173,13 +238,20 @@ private String escapeJson(String value) { return escaped.toString(); } - + /** + * A helper class to maintain the state of the JSON parsing process. + */ private static class ParseState { String json; int currentIndex; int lineNumber; int columnNumber; + /** + * Constructs a ParseState for a given JSON string. + * + * @param data the JSON string to parse + */ public ParseState(String data) { this.json = data; this.currentIndex = 0; @@ -187,14 +259,30 @@ public ParseState(String data) { this.columnNumber = 1; } + /** + * Returns the current character in the JSON string being parsed. + * + * @return the current character + */ public char current() { return json.charAt(currentIndex); } + /** + * Checks if there are more tokens to parse. + * + * @return true if there are more tokens, false otherwise + */ public boolean hasNextToken() { return currentIndex < json.length(); } + /** + * Consumes a specified number of characters and optionally removes whitespace. + * + * @param count the number of characters to consume + * @param removeWhiteSpace whether to remove whitespace after consuming + */ public void consume(int count, boolean removeWhiteSpace) { for (int i = 0; i < count; i++) { if (json.charAt(currentIndex) == '\n') { @@ -211,20 +299,38 @@ public void consume(int count, boolean removeWhiteSpace) { consumeWhiteSpace(); } + /** + * Consumes all whitespace characters in the JSON string. + */ public void consumeWhiteSpace() { while (hasNextToken() && Character.isWhitespace(json.charAt(currentIndex))) { consume(1,false); } } + /** + * Returns the remaining part of the JSON string from the current index. + * + * @return the remaining substring + */ public String remaining() { return json.substring(currentIndex); } + /** + * Returns the current line number in the JSON string being parsed. + * + * @return the line number + */ public int getLineNumber() { return lineNumber; } + /** + * Returns the current column number in the JSON string being parsed. + * + * @return the column number + */ public int getColumnNumber() { return columnNumber; } @@ -235,6 +341,15 @@ public DataNode deserialize(String data) { return parseJsonValue(new ParseState(data)); } + /** + * Parses the next JSON value from the provided parsing state. + * This method identifies the type of JSON value (object, array, string, or primitive) + * based on the current character and calls the appropriate parsing method. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed JSON value + * @throws IllegalArgumentException if the end of JSON data is reached unexpectedly + */ private DataNode parseJsonValue(ParseState state) { if (!state.hasNextToken()) { throw new IllegalArgumentException("Unexpected end of JSON data at line: " + state.getLineNumber() + ", column: " + state.getColumnNumber()); @@ -256,6 +371,14 @@ private DataNode parseJsonValue(ParseState state) { } } + /** + * Parses a JSON object from the provided parsing state. + * This method expects the object to be enclosed in curly braces and parses key-value pairs. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed JSON object + * @throws IllegalArgumentException if the expected tokens (keys, colons, etc.) are not found + */ private DataNode parseJsonObject(ParseState state) { DataNode node = DataNode.Object(); @@ -285,6 +408,14 @@ private DataNode parseJsonObject(ParseState state) { return node; } + /** + * Parses a JSON array from the provided parsing state. + * This method expects the array to be enclosed in square brackets and parses its elements. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed JSON array + * @throws IllegalArgumentException if the expected closing bracket ']' is not found + */ private DataNode parseJsonArray(ParseState state) { DataNode node = DataNode.Collection(); @@ -303,11 +434,25 @@ private DataNode parseJsonArray(ParseState state) { return node; } + /** + * Parses a JSON string from the provided parsing state. + * This method handles escape sequences and returns the corresponding DataNode. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed JSON string + */ private DataNode parseJsonString(ParseState state) { String str = parseString(state); return DataNode.Primitive(str); } + /** + * Parses a string from the provided parsing state, handling escape sequences. + * + * @param state the current parsing state containing the JSON data + * @return the parsed string + * @throws IllegalArgumentException if the string is unterminated or if an invalid escape sequence is encountered + */ private String parseString(ParseState state) { StringBuilder result = new StringBuilder(); @@ -356,10 +501,24 @@ private String parseString(ParseState state) { throw new IllegalArgumentException("Unterminated string at line: " + state.getLineNumber() + ", column: " + state.getColumnNumber()); } + /** + * Parses a JSON key from the provided parsing state. + * This method is essentially a wrapper around parseString to enforce string parsing. + * + * @param state the current parsing state containing the JSON data + * @return the parsed key as a string + */ private String parseJsonKey(ParseState state) { return parseString(state); } + /** + * Parses a primitive value (boolean, null, or number) from the provided parsing state. + * Determines the type of primitive based on the current character and calls the appropriate parsing method. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed primitive value + */ private DataNode parsePrimitiveValue(ParseState state) { if (state.current() == 't' || state.current() == 'f') { return parseJsonBoolean(state); @@ -368,33 +527,16 @@ private DataNode parsePrimitiveValue(ParseState state) { } else { return parseJsonNumber(state); } - -// StringBuilder value = new StringBuilder(); -// -// while (state.hasNextToken() && !Character.isWhitespace(state.current()) && state.current() != ',' && state.current() != '}' && state.current() != ']') { -// value.append(state.current()); -// state.consume(1); -// } -// -// String valueStr = value.toString(); -// if (valueStr.equals("null")) { -// return null; -// } else if (valueStr.equals("true") || valueStr.equals("false")) { -// return DataNode.Primitive(Boolean.parseBoolean(valueStr)); -// } else { -// try { -// // Check if it's a number (integer or float) -// if (valueStr.contains(".")) { -// return DataNode.Primitive(Float.parseFloat(valueStr)); -// } else { -// return DataNode.Primitive(Integer.parseInt(valueStr)); -// } -// } catch (NumberFormatException e) { -// throw new IllegalArgumentException("Invalid number format at line: " + state.getLineNumber() + ", column: " + state.getColumnNumber()); -// } -// } } + /** + * Parses a boolean value from the provided parsing state. + * Expects the value to be either "true" or "false" and returns the corresponding DataNode. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed boolean value + * @throws IllegalArgumentException if the value is not a valid boolean + */ private DataNode parseJsonBoolean(ParseState state) { String booleanStr = state.json.substring(state.currentIndex, state.currentIndex + 4); if (booleanStr.equals("true")) { @@ -409,6 +551,14 @@ private DataNode parseJsonBoolean(ParseState state) { throw new IllegalArgumentException("Invalid boolean value at at line: " + state.getLineNumber() + ", column: " + state.getColumnNumber()); } + /** + * Parses a null value from the provided parsing state. + * Expects the value to be "null" and returns null as a DataNode. + * + * @param state the current parsing state containing the JSON data + * @return null, representing the JSON null value + * @throws IllegalArgumentException if the value is not a valid null + */ private DataNode parseJsonNull(ParseState state) { String nullStr = state.json.substring(state.currentIndex, state.currentIndex + 4); if (nullStr.equals("null")) { @@ -418,6 +568,13 @@ private DataNode parseJsonNull(ParseState state) { throw new IllegalArgumentException("Invalid null value at line: " + state.getLineNumber() + ", column: " + state.getColumnNumber()); } + /** + * Parses a numeric value from the provided parsing state. + * This method can handle both integers and floating-point numbers. + * + * @param state the current parsing state containing the JSON data + * @return a DataNode representing the parsed numeric value + */ private DataNode parseJsonNumber(ParseState state) { int startIndex = state.currentIndex; while (state.hasNextToken() && (Character.isDigit(state.current()) || state.current() == '-' || state.current() == '.')) { @@ -432,653 +589,4 @@ private DataNode parseJsonNumber(ParseState state) { } } - - - - - - - - - -// // Look at first char -// // If { -> advance current by 1, return parseJsonObject -// // If [ -> advance current by 1, return parseJsonArray -// // If " -> advance current by 1, return parseJsonString -// // matchLongestNumber, if string not empty return new DataNode with its value, advance by matchLongestNumber size to consume -// // if equals to false or true, return new DataNode with its value, advance by str size to consume -// // if equals null return null -// private DataNode parseJsonValue(ParseState state) { -// skipWhitespace(state); -// if (!state.hasNextToken()) { -// throw new JsonParseException("Unexpected end of input at position " + state.getPosition()); -// } -// -// char currentChar = state.current(); -// if (currentChar == '{') { -// state.consume(1); -// return parseJsonObject(state); -// } else if (currentChar == '[') { -// state.consume(1); -// return parseJsonArray(state); -// } else if (currentChar == '"') { -// state.consume(1); -// return parseJsonString(state); -// } else if (currentChar == 't' || currentChar == 'f') { -// return parseJsonBoolean(state); -// } else if (currentChar == 'n') { -// return parseJsonNull(state); -// } else { -// return parseJsonNumber(state); -//// } -// } -// -// // create node -// // while next is not } -// // * check that next is " for the property key, advance 1 current index to consume -// // * get key with parseJsonKey -// // * check that next is : , advance 1 current index to consume -// // * get value with parseJsonValue -// // * put in the node the value with the key -// // * check if next is , if it is advance 1 current index to consume -// // after while check next is } advance 1 current index to consume -// // return node -// private DataNode parseJsonObject(ParseState state) { -// DataNode objectNode = DataNode.Object(); -// skipWhitespace(state); -// -// while (state.hasNextToken() && state.current() != '}') { -// if (state.current() != '"') { -// throw new JsonParseException("Expected '\"' to start a key at position " + state.getPosition()); -// } -// state.consume(1); // consume '\"' -// String key = parseJsonKey(state); -// skipWhitespace(state); -// if (state.current() != ':') { -// throw new JsonParseException("Expected ':' after key at position " + state.getPosition()); -// } -// state.consume(1); // consume ':' -// DataNode value = parseJsonValue(state); -// objectNode.set(key, value); -// -// skipWhitespace(state); -// if (state.current() == ',') { -// state.consume(1); // consume ',' -// } else if (state.current() != '}') { -// throw new JsonParseException("Expected '}' or ',' at position " + state.getPosition()); -// } -// } -// if (state.hasNextToken()) { -// state.consume(1); // consume '}' -// } else { -// throw new JsonParseException("Unexpected end of input, Expected '}' at position " + state.getPosition()); -// } -// return objectNode; -// } -// -// // create node -// // while next is not ] -// // * get value with parseJsonValue -// // * add value to node -// // * check if next is , if it is advance 1 current index to consume -// // after while check next is ] advance 1 current index to consume -// // return node -// private DataNode parseJsonArray(ParseState state) { -// DataNode arrayNode = DataNode.Collection(); -// skipWhitespace(state); -// -// while (state.hasNextToken() && state.current() != ']') { -// DataNode value = parseJsonValue(state); -// arrayNode.add(value); -// -// skipWhitespace(state); -// if (state.current() == ',') { -// state.consume(1); // consume ',' -// } else if (state.current() != ']') { -// throw new JsonParseException("Expected ']' or ',' at position " + state.getPosition()); -// } -// } -// if (state.hasNextToken()) { -// state.consume(1); // consume ']' -// } else { -// throw new JsonParseException("Expected ']' at position " + state.getPosition()); -// } -// return arrayNode; -// } -// -// // create node -// // build string until the next " not escaped, dont forget to advance for the amount consumed -// // advance 1 current index to consume the closing " -// // return node -// private DataNode parseJsonString(ParseState state) { -// String stringValue = parseString(state); -// return DataNode.Primitive(stringValue); -// } -// -// // build string until the next " not escaped, dont forget to advance for the amount consumed -// // advance 1 current index to consume the closing " -// // return string -// private String parseString(ParseState state) { -// StringBuilder sb = new StringBuilder(); -// while (state.hasNextToken()) { -// char currentChar = state.current(); -// if (currentChar == '"') { -// state.consume(1); // consume '"' -// return sb.toString(); -// } else if (currentChar == '\\') { // Handle escape sequences -// state.consume(1); -// if (!state.hasNextToken()) { -// throw new JsonParseException("Unexpected end of input after escape character at position " + state.getPosition()); -// } -// currentChar = state.current(); -// switch (currentChar) { -// case '"': -// case '\\': -// case '/': -// sb.append(currentChar); -// break; -// case 'b': -// sb.append('\b'); -// break; -// case 'f': -// sb.append('\f'); -// break; -// case 'n': -// sb.append('\n'); -// break; -// case 'r': -// sb.append('\r'); -// break; -// case 't': -// sb.append('\t'); -// break; -// default: -// throw new JsonParseException("Invalid escape character at position " + state.getPosition()); -// } -// } else { -// sb.append(currentChar); -// } -// state.consume(1); // consume the current character -// } -// throw new JsonParseException("Unexpected end of input in string at position " + state.getPosition()); -// } -// -// private String parseJsonKey(ParseState state) { -// return parseString(state); -// } -// -// private DataNode parseJsonBoolean(ParseState state) { -// String booleanStr = state.json.substring(state.getPosition(), state.getPosition() + 4); -// if (booleanStr.equals("true")) { -// state.consume(4); -// return DataNode.Primitive(true); -// } else if (booleanStr.equals("false")) { -// state.consume(5); -// return DataNode.Primitive(false); -// } -// throw new JsonParseException("Invalid boolean value at position " + state.getPosition()); -// } -// -// private DataNode parseJsonNull(ParseState state) { -// String nullStr = state.json.substring(state.getPosition(), state.getPosition() + 4); -// if (nullStr.equals("null")) { -// state.consume(4); -// return null; // JSON null corresponds to null in DataNode -// } -// throw new JsonParseException("Invalid null value at position " + state.getPosition()); -// } -// -// private DataNode parseJsonNumber(ParseState state) { -// int startIndex = state.getPosition(); -// while (state.hasNextToken() && (Character.isDigit(state.current()) || state.current() == '-' || state.current() == '.')) { -// state.consume(1); -// } -// String numberStr = state.json.substring(startIndex, state.getPosition()); -// if (numberStr.contains(".")) { -// return DataNode.Primitive(Float.parseFloat(numberStr)); -// } else { -// return DataNode.Primitive(Integer.parseInt(numberStr)); -// } -// } -// -// private void skipWhitespace(ParseState state) { -// while (state.hasNextToken() && Character.isWhitespace(state.current())) { -// state.consume(1); -// } -// } -// -// // Custom exception class for JSON parsing errors -// public static class JsonParseException extends RuntimeException { -// public JsonParseException(String message) { -// super(message); -// } -// } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// @Override -// public DataNode deserialize(String data) { -// Stack nodeStack = new Stack<>(); -// Stack keyStack = new Stack<>(); -// DataNode rootNode = null; // This will track the actual root node (obj or array) -// DataNode currentNode = null; -// StringBuilder token = new StringBuilder(); -// boolean inString = false; -// boolean isEscaped = false; -// -// for (int i = 0; i < data.length(); i++) { -// char c = data.charAt(i); -// -// // Handle escape characters in strings -// if (inString && isEscaped) { -// token.append(c); -// isEscaped = false; -// continue; -// } -// -// if (c == '\\' && inString) { -// isEscaped = true; -// continue; -// } -// -// // Handle string values -// if (c == '"') { -// if (inString) { -// inString = false; -// String tokenStr = token.toString(); -// token.setLength(0); // Clear token -// -// // End of a string, decide whether it's a key or value -// if (currentNode.getType() == DataNode.Type.Object && keyStack.isEmpty()) { -// keyStack.push(tokenStr); // Store key for object -// } else if (currentNode.getType() == DataNode.Type.Object) { -// currentNode.set(keyStack.pop(), DataNode.Primitive(tokenStr)); // Set key-value pair -// } else if (currentNode.getType() == DataNode.Type.Collection) { -// currentNode.add(DataNode.Primitive(tokenStr)); // Add to array -// } -// } else { -// inString = true; -// } -// continue; -// } -// -// // Skip whitespaces outside of strings -// if (Character.isWhitespace(c) && !inString) { -// continue; -// } -// -// // Handle object opening -// if (c == '{') { -// DataNode objNode = DataNode.Object(); -// if (currentNode == null) { -// rootNode = objNode; // Set root to the first object -// } else if (currentNode.getType() == DataNode.Type.Object) { -// // Only pop if there is a key to set -// if (!keyStack.isEmpty()) { -// currentNode.set(keyStack.pop(), objNode); -// } -// } else if (currentNode.getType() == DataNode.Type.Collection) { -// currentNode.add(objNode); -// } -// nodeStack.push(currentNode); -// currentNode = objNode; -// continue; -// } -// -// // Handle array opening -// if (c == '[') { -// DataNode arrayNode = DataNode.Collection(); -// if (currentNode == null) { -// rootNode = arrayNode; // Set root to the first array -// } else if (currentNode.getType() == DataNode.Type.Object) { -// // Only pop if there is a key to set -// if (!keyStack.isEmpty()) { -// currentNode.set(keyStack.pop(), arrayNode); -// } -// } else if (currentNode.getType() == DataNode.Type.Collection) { -// currentNode.add(arrayNode); -// } -// nodeStack.push(currentNode); -// currentNode = arrayNode; -// continue; -// } -// -// // Handle object closing -// if (c == '}') { -// if (token.length() > 0 && !keyStack.isEmpty()) { -// currentNode.set(keyStack.pop(), DataNode.Primitive(token.toString())); -// } -// token.setLength(0); // Clear token -// currentNode = nodeStack.pop(); -// continue; -// } -// -// // Handle array closing -// if (c == ']') { -// if (token.length() > 0) { -// currentNode.add(DataNode.Primitive(token.toString())); -// } -// token.setLength(0); // Clear token -// currentNode = nodeStack.pop(); -// continue; -// } -// -// // Handle key-value separator -// if (c == ':' && currentNode.getType() == DataNode.Type.Object) { -// keyStack.push(token.toString()); // Store the key in stack -// token.setLength(0); // Clear token -// continue; -// } -// -// // Handle array/object separators -// if (c == ',' && !inString) { -// if (token.length() > 0) { -// if (currentNode.getType() == DataNode.Type.Collection) { -// currentNode.add(DataNode.Primitive(token.toString())); -// } else if (currentNode.getType() == DataNode.Type.Object && !keyStack.isEmpty()) { -// currentNode.set(keyStack.pop(), DataNode.Primitive(token.toString())); -// } -// } -// token.setLength(0); // Clear token -// continue; -// } -// -// // Append character to token -// token.append(c); -// } -// -// // Handle any remaining token as a primitive value -// if (token.length() > 0 && currentNode.getType() == DataNode.Type.Primitive) { -// currentNode.setValue(token.toString()); -// } -// -// // Return the root node, which is either the first object or array parsed -// return rootNode; -// } - -// private static class ParseState { -// String json; -// int currentIndex; -// -// public ParseState(String data) { -// this.json = data; -// } -// -// public char current() { -// return json.charAt(currentIndex); -// } -// -// public boolean currentStartsWith(String str) { -// return json.substring(currentIndex).startsWith(str); -// } -// -// public void consume(int count) { -// currentIndex += count; -// } -// -// public boolean hasNextToken() { -// return currentIndex < json.length(); -// } -// } -// -// @Override -// public DataNode deserialize(String data) { -// return parseJsonValue(new ParseState(data)); -// } -// -// // Look at first char -// // If { -> advance current by 1, return parseJsonObject -// // If [ -> advance current by 1, return parseJsonArray -// // If " -> advance current by 1, return parseJsonString -// // matchLongestNumber, if string not empty return new DataNode with its value, advance by matchLongestNumber size to consume -// // if equals to false or true, return new DataNode with its value, advance by str size to consume -// // if equals null return null -// private DataNode parseJsonValue(ParseState state) { -// -// } -// -// // create node -// // while next is not } -// // * check that next is " for the property key, advance 1 current index to consume -// // * get key with parseJsonKey -// // * check that next is : , advance 1 current index to consume -// // * get value with parseJsonValue -// // * put in the node the value with the key -// // * check if next is , if it is advance 1 current index to consume -// // after while check next is } advance 1 current index to consume -// // return node -// private DataNode parseJsonObject(ParseState state) { -// -// } -// -// // create node -// // while next is not ] -// // * get value with parseJsonValue -// // * add value to node -// // * check if next is , if it is advance 1 current index to consume -// // after while check next is ] advance 1 current index to consume -// // return node -// private DataNode parseJsonArray(ParseState state) { -// -// } -// -// // create node -// // build string until the next " not escaped, dont forget to advance for the amount consumed -// // advance 1 current index to consume the closing " -// // return node -// private DataNode parseJsonString(ParseState state) { -// -// } -// -// // build string until the next " not escaped, dont forget to advance for the amount consumed -// // advance 1 current index to consume the closing " -// // return string -// private String parseJsonKey(ParseState state) { -// -// } -// -// -// -// -// -// -// private DataNode parseJsonValue(ParseState state) { -// switch (state.current()) { -// case '{': -// break; -// case '[': -// break; -// case '"': -// break; -// default: -// // Match the longest number at the start of the string -// String numberMatch = matchLongestNumber(state); -// if (!numberMatch.isEmpty()) { -// return parseJsonNumber(numberMatch); -// } else if (state.currentStartsWith("true") || state.currentStartsWith("false")) { -// return DataNode.Primitive(Boolean.parseBoolean(json)); -// } else if (state.currentStartsWith("null")) { -// return new DataNode(); // Treat null as unknown -// } -// } -// throw new IllegalArgumentException("Unknown JSON value: " + json); -// } -// -// @Override -// public DataNode deserialize(String data) { -// data = data.trim(); -// if (data.startsWith("{")) { -// return parseJsonObject(data); -// } else if (data.startsWith("[")) { -// return parseJsonArray(data); -// } else { -// throw new IllegalArgumentException("Invalid JSON data"); -// } -// } -// -// private DataNode parseJsonObject(String json) { -// DataNode node = DataNode.Object(); -// json = json.substring(1).trim(); // Start from the first character after '{' -// -// while (!json.isEmpty()) { -// int colonIndex = json.indexOf(":"); -// if (colonIndex == -1) { -// throw new IllegalArgumentException("Invalid JSON object: " + json); -// } -// -// // Use findClosingDelimiter to find the key -// String key = extractString(json); -// json = json.substring(colonIndex + 1).trim(); -// -// // Find the matching closing delimiter for the value -// char firstChar = json.charAt(0); -// int closingIndex = findClosingDelimiter(json, firstChar); -// DataNode valueNode = parseJsonValue(json.substring(0, closingIndex + 1)); -// -// node.set(key, valueNode); -// json = removeProcessedPart(json.substring(closingIndex + 1).trim()); -// } -// return node; -// } -// -// private DataNode parseJsonArray(String json) { -// DataNode node = DataNode.Collection(new ArrayList<>()); -// json = json.substring(1).trim(); // Start from the first character after '[' -// -// while (!json.isEmpty()) { -// char firstChar = json.charAt(0); -// int closingIndex = findClosingDelimiter(json, firstChar); -// DataNode valueNode = parseJsonValue(json.substring(0, closingIndex + 1)); -// node.add(valueNode); -// -// json = removeProcessedPart(json.substring(closingIndex + 1).trim()); -// } -// return node; -// } -// -// private DataNode parseJsonValue(String json) { -// json = json.trim(); -// if (json.startsWith("{")) { -// return parseJsonObject(json); -// } else if (json.startsWith("[")) { -// return parseJsonArray(json); -// } else if (json.startsWith("\"")) { -// int closingIndex = findClosingDelimiter(json, '"'); -// return DataNode.Primitive(json.substring(1, closingIndex)); -// } else { -// // Match the longest number at the start of the string -// String numberMatch = matchLongestNumber(json); -// if (!numberMatch.isEmpty()) { -// return parseJsonNumber(numberMatch); -// } else if (json.startsWith("true") || json.startsWith("false")) { -// return DataNode.Primitive(Boolean.parseBoolean(json)); -// } else if (json.startsWith("null")) { -// return new DataNode(); // Treat null as unknown -// } -// } -// throw new IllegalArgumentException("Unknown JSON value: " + json); -// } -// -// private String matchLongestNumber(String json) { -// // Regex to match numbers with optional sign and decimal point -// Pattern numberPattern = Pattern.compile("^-?\\d+(\\.\\d+)?"); -// Matcher matcher = numberPattern.matcher(json); -// if (matcher.find()) { -// return matcher.group(); // Returns the longest matching number -// } -// return ""; // Return empty string if no match -// } -// -// private DataNode parseJsonNumber(String json) { -// // Determine if it's an integer or float -// if (json.contains(".")) { -// return DataNode.Primitive(Float.parseFloat(json)); -// } else { -// return DataNode.Primitive(Integer.parseInt(json)); -// } -// } -// -// private String removeProcessedPart(String json) { -// json = json.trim(); -// -// // Check if the json is ending with closing delimiter without any comma -// if (json.startsWith("}") || json.startsWith("]")) { -// return json.substring(1).trim(); // Remove the closing bracket -// } -// -// int nextComma = json.indexOf(","); -// -// if (nextComma == -1) { -// return ""; // No more values to process -// } else { -// return json.substring(nextComma + 1).trim(); // Remove everything up to the next comma -// } -// } -// -// private String extractString(String json) { -// int endIndex = json.indexOf("\"", 1); -// if (endIndex == -1) { -// throw new IllegalArgumentException("Invalid string format: " + json); -// } -// return json.substring(1, endIndex); -// } -// -// private int findClosingDelimiter(String json, char openChar) { -// char closeChar = openChar == '{' ? '}' : (openChar == '[' ? ']' : '"'); -// int count = 1; -// boolean inString = false; -// -// for (int i = 1; i < json.length(); i++) { -// char c = json.charAt(i); -// -// // Handle string encapsulation -// if (c == '"' && json.charAt(i - 1) != '\\') { -// inString = !inString; // Toggle inString mode -// if (!inString && openChar == '"') { -// return i; // Found the closing quote -// } -// continue; -// } -// -// if (inString) continue; -// -// // Handle nested brackets or quotes -// if (c == openChar) count++; -// if (c == closeChar) count--; -// -// if (count == 0) { -// return i; // Found the matching closing delimiter -// } -// } -// -// throw new IllegalArgumentException("No matching closing delimiter found for: " + openChar); -// } - } diff --git a/src/main/java/org/ois/core/utils/log/ILogger.java b/src/main/java/org/ois/core/utils/log/ILogger.java index ef2f680..d0b8512 100644 --- a/src/main/java/org/ois/core/utils/log/ILogger.java +++ b/src/main/java/org/ois/core/utils/log/ILogger.java @@ -1,10 +1,26 @@ package org.ois.core.utils.log; +/** + * Interface for logging messages at various levels of severity. + * Implementations of this interface should provide functionality to log + * messages categorized by different log levels, including debug, info, + * warn, and error. + */ public interface ILogger { + /** + * Enumeration representing the different log levels. + */ enum Level { Debug, Info, Warn, Error } + /** + * Converts a string representation of a log level to its corresponding + * {@link Level} enum value. + * + * @param logLevel the string representation of the log level (e.g., "debug", "warn", "error") + * @return the corresponding {@link Level} enum value + */ static Level toLogLevel(String logLevel) { switch (logLevel.trim().toLowerCase()) { case "debug": @@ -18,11 +34,49 @@ static Level toLogLevel(String logLevel) { } } + /** + * Logs a debug message with a specified topic. + * + * @param topic the topic associated with the log message + * @param message the debug message to be logged + */ void debug(String topic, String message); + /** + * Logs a debug message without a specific topic. + * + * @param message the debug message to be logged + */ void debug(String message); + /** + * Logs an informational message with a specified topic. + * + * @param topic the topic associated with the log message + * @param message the informational message to be logged + */ void info(String topic, String message); + /** + * Logs an informational message without a specific topic. + * + * @param message the informational message to be logged + */ void info(String message); + /** + * Logs a warning message. + * + * @param message the warning message to be logged + */ void warn(String message); + /** + * Logs an error message. + * + * @param message the error message to be logged + */ void error(String message); + /** + * Logs an error message along with the associated exception. + * + * @param message the error message to be logged + * @param exception the throwable associated with the error + */ void error(String message, Throwable exception); } diff --git a/src/main/java/org/ois/core/utils/log/Logger.java b/src/main/java/org/ois/core/utils/log/Logger.java index ea515ee..6b19a6e 100644 --- a/src/main/java/org/ois/core/utils/log/Logger.java +++ b/src/main/java/org/ois/core/utils/log/Logger.java @@ -6,6 +6,13 @@ import java.util.*; +/** + * Logger implementation for logging messages with various severity levels. + * This class allows logging messages categorized by debug, info, warn, and error levels. + * It supports filtering logs by topics and setting the minimum log level. + * + * @param the type of the class for which logging is performed + */ public class Logger implements ILogger { private static final Map logMap = new HashMap<>(); @@ -15,10 +22,22 @@ public class Logger implements ILogger { private final Class logClass; + /** + * Private constructor for creating a logger instance. + * + * @param logClass the class for which logging is being created + */ private Logger(Class logClass) { this.logClass = logClass; } + /** + * Retrieves a logger instance for the specified class. + * + * @param c the class for which to retrieve the logger + * @param the type of the class + * @return a logger instance for the specified class + */ public static Logger get(Class c) { if(!logMap.containsKey(c)) { @@ -27,6 +46,11 @@ public static Logger get(Class c) return logMap.get(c); } + /** + * Sets the minimum log level for this logger. + * + * @param logLevel the minimum log level to be set + */ public static void setLogLevel(Level logLevel) { if (logLevel == null) { logLevel = DEFAULT_LEVEL; @@ -34,6 +58,11 @@ public static void setLogLevel(Level logLevel) { minLogLevel = logLevel.ordinal(); } + /** + * Sets the allowed topics for logging. Only messages with these topics will be logged. + * + * @param topics the topics to be allowed for logging + */ public static void setTopics(String... topics) { allowedTopics.clear(); if (topics == null) { @@ -42,6 +71,12 @@ public static void setTopics(String... topics) { allowedTopics.addAll(List.of(topics)); } + /** + * Determines if a message should be logged based on its topic. + * + * @param topic the topic of the message + * @return true if the message should be logged, false otherwise + */ private boolean shouldLog(String topic) { if (allowedTopics.isEmpty() || topic.isEmpty()) { // Allow logging for all messages if the topic is empty or no topics are set @@ -50,8 +85,16 @@ private boolean shouldLog(String topic) { return allowedTopics.contains(topic); } + /** + * Logs a message with the specified log level, topic, and optional exception. + * + * @param level the log level of the message + * @param topic the topic associated with the message + * @param message the message to be logged + * @param exception the optional exception to be logged (if any) + */ private void log(Logger.Level level, String topic, String message, Throwable exception) { - if (minLogLevel > level.ordinal() || !shouldLog(topic)) { + if (minLogLevel > level.ordinal() || !shouldLog(topic) || Gdx.app == null || OIS.engine == null) { return; } diff --git a/src/test/java/org/ois/core/project/SimulationManifestTest.java b/src/test/java/org/ois/core/project/SimulationManifestTest.java new file mode 100644 index 0000000..75c8139 --- /dev/null +++ b/src/test/java/org/ois/core/project/SimulationManifestTest.java @@ -0,0 +1,87 @@ +package org.ois.core.project; + +import org.ois.core.runner.RunnerConfiguration; + +import org.ois.core.utils.io.data.DataNode; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class SimulationManifestTest { + private SimulationManifest manifest; + + @BeforeMethod + public void setUp() { + manifest = new SimulationManifest(); + } + + @Test + public void testLoadData_withRequiredFields() { + // Arrange + DataNode dataNode = DataNode.Object(); + dataNode.set("initialState", "StartState"); + dataNode.set("states", DataNode.Map(Map.of("StartState", "StartClass"))); + + // Act + manifest.loadData(dataNode); + + // Assert + assertEquals(manifest.getInitialState(), "StartState"); + assertEquals(manifest.getStates(), Map.of("StartState", "StartClass")); + } + + @Test + public void testLoadData_withOptionalFields() { + // Arrange + DataNode dataNode = DataNode.Object(); + dataNode.set("initialState", "StartState"); + dataNode.set("states", DataNode.Map(Map.of("StartState", "StartClass"))); + dataNode.set("title", "Simulation Title"); + dataNode.getProperty("runner", "screenWidth").setValue(1920); + dataNode.getProperty("runner", "screenHeight").setValue(1080); + + Set platformSet = new HashSet<>(); + platformSet.add("Html"); + dataNode.getProperty("runner").set("platforms", DataNode.Collection(platformSet)); + + // Act + manifest.loadData(dataNode); + + // Assert + assertEquals(manifest.getInitialState(), "StartState"); + assertEquals(manifest.getStates(), Map.of("StartState", "StartClass")); + assertEquals(manifest.getTitle(), "Simulation Title"); + assertEquals(manifest.getScreenWidth(), 1920); + assertEquals(manifest.getScreenHeight(), 1080); + assertEquals(manifest.getPlatforms(), Set.of(RunnerConfiguration.RunnerType.Html)); + } + + @Test + public void testConvertToDataNode() { + // Arrange + manifest.setInitialState("StartState"); + manifest.setStates(Map.of("StartState", "StartClass")); + manifest.setTitle("Simulation Title"); + manifest.setScreenWidth(1920); + manifest.setScreenHeight(1080); + manifest.setPlatforms(Set.of(RunnerConfiguration.RunnerType.Html)); + + // Act + DataNode result = manifest.convertToDataNode(); + + // Assert + assertNotNull(result.get("title")); + assertEquals(result.get("title").getString(), "Simulation Title"); + assertNotNull(result.get("initialState")); + assertEquals(result.get("initialState").getString(), "StartState"); + assertNotNull(result.get("states")); + assertEquals(result.get("states").toStringMap(), Map.of("StartState", "StartClass")); + assertEquals(result.getProperty("runner", "screenWidth").getInt(), 1920); + assertEquals(result.getProperty("runner", "screenHeight").getInt(), 1080); + assertEquals(result.getProperty("runner", "platforms").toStringCollection(new HashSet<>()), Set.of("Html")); + } +} diff --git a/src/test/java/org/ois/core/runner/SimulationEngineTest.java b/src/test/java/org/ois/core/runner/SimulationEngineTest.java new file mode 100644 index 0000000..0c3c003 --- /dev/null +++ b/src/test/java/org/ois/core/runner/SimulationEngineTest.java @@ -0,0 +1,310 @@ +package org.ois.core.runner; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.Graphics; +import com.badlogic.gdx.graphics.*; +import com.badlogic.gdx.graphics.glutils.GLVersion; +import org.ois.core.project.SimulationManifest; +import org.ois.core.state.ErrorState; +import org.ois.core.state.IState; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.testng.Assert; + +import java.util.Map; + +public class SimulationEngineTest { + + private RunnerConfiguration configuration; + private SimulationEngine engine; + + @BeforeMethod + public void setup() { + Gdx.graphics = new MockGraphics(); + // Setup real objects + configuration = new RunnerConfiguration(); + engine = new SimulationEngine(configuration); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testLoadProjectThrowsExceptionWhenManifestNotLoaded() throws Exception { + // Test loading project without a manifest, which should throw an exception + engine.loadProject(); + } + + @Test + public void testCreateInitializesStateManagerNoStates() { + // Test the creation of the engine and initialization of state manager + engine.create(); + Assert.assertNotNull(engine.stateManager, "StateManager should be initialized."); + // No states - Error state should be active and store loadProject exception + Assert.assertNotNull(engine.errorState, "ErrorState should be initialized."); + Assert.assertTrue(engine.errorState.isActive()); + } + + @Test + public void testLoadProjectLoadsManifestAndStartsInitialState() throws Exception { + // Create a dummy manifest and configuration + SimulationManifest manifest = new SimulationManifest(); + manifest.setInitialState("state1"); + manifest.setStates(Map.of("state1", "org.ois.core.state.ErrorState")); + configuration.setSimulationManifest(manifest); + + // Initialize OIS, set up necessary variables and Call loadProject + engine.create(); + // Check if the states are loaded + Assert.assertTrue(engine.stateManager.states().contains("state1"), "State 'state1' should be loaded."); + // Make sure it was created with reflection and entered the state + Assert.assertTrue(((ErrorState)engine.stateManager.getCurrentState()).isActive()); + } + + @Test + public void testRenderWithActiveState() { + // Test rendering when there is an active state + SimulationManifest manifest = new SimulationManifest(); + manifest.setInitialState("mockState"); + manifest.setStates(Map.of("mockState", MockState.class.getName())); + configuration.setSimulationManifest(manifest); + + engine.create(); + + engine.render(); // Should not throw an exception + Assert.assertFalse(engine.errorState.isActive(), "ErrorState should not be active during rendering with a valid state."); + + // Check MockState properties + MockState currentState = (MockState) engine.stateManager.getCurrentState(); + Assert.assertTrue(currentState.isEntered(), "MockState should have been entered."); + Assert.assertFalse(currentState.isExited(), "MockState should not have exited yet."); + } + + @Test + public void testRenderWithActiveStateException() { + // Test rendering when there is an active state + SimulationManifest manifest = new SimulationManifest(); + manifest.setInitialState("mockState"); + manifest.setStates(Map.of("mockState", MockState.class.getName())); + configuration.setSimulationManifest(manifest); + + engine.create(); + + MockState currentState = (MockState) engine.stateManager.getCurrentState(); + currentState.throwErr = true; + + engine.render(); // Should not throw an exception + // Check changes + Assert.assertNull(engine.stateManager.getCurrentState()); + Assert.assertTrue(engine.errorState.isActive(), "ErrorState should be the active after rendering with a failed state."); + } + + @Test + public void testResizeWithoutErrorState() { + // Test resizing without an active error state + engine.stateManager.registerState("mockState", new MockState()); + engine.stateManager.start("mockState"); + + engine.resize(800, 600); // Resize without error state + Assert.assertFalse(engine.errorState.isActive(), "ErrorState should not be active during resize without an exception."); + + // Check if resize was called in MockState + MockState currentState = (MockState) engine.stateManager.getCurrentState(); + Assert.assertTrue(currentState.isResized(), "MockState should have processed resize."); + } + + @Test + public void testDisposeCallsDisposeOnStates() { + // Test that dispose is called on both the state manager and error state + engine.stateManager.registerState("mockState", new MockState()); + engine.stateManager.start("mockState"); + + engine.dispose(); + Assert.assertFalse(engine.errorState.isActive(), "ErrorState should be disposed and inactive."); + + // Check if dispose was called in MockState + Assert.assertNull(engine.stateManager.getCurrentState()); + MockState state = (MockState) engine.stateManager.getState("mockState"); + Assert.assertTrue(state.isExited(), "MockState should have exit."); + Assert.assertTrue(state.isDisposed(), "MockState should have been disposed."); + } + + @Test + public void testHandleProgramExceptionActivatesErrorState() { + // Test if the error state gets activated after an exception + Exception exception = new Exception("Test exception"); + engine.handleProgramException(exception); + Assert.assertTrue(engine.errorState.isActive(), "ErrorState should be active after handling an exception."); + } + + public static class MockState implements IState { + private boolean entered; + private boolean exited; + private boolean resized; + private boolean disposed; + + private boolean throwErr; + + @Override + public void enter(Object... parameters) { + entered = true; + } + + @Override + public void exit() { + exited = true; + } + + @Override + public void pause() { } + + @Override + public void resume() { } + + @Override + public void resize(int width, int height) { + resized = true; + } + + @Override + public void render() { + if (throwErr) { + throw new RuntimeException("some err"); + } + } + + @Override + public boolean update(float dt) { return true; } + + @Override + public void dispose() { + disposed = true; + } + + public boolean isEntered() { + return entered; + } + + public boolean isExited() { + return exited; + } + + public boolean isResized() { + return resized; + } + + public boolean isDisposed() { + return disposed; + } + } + + public static class MockGraphics implements Graphics { + @Override + public boolean isGL30Available() {return false;} + @Override + public boolean isGL31Available() {return false;} + @Override + public boolean isGL32Available() {return false;} + @Override + public GL20 getGL20() {return null;} + @Override + public GL30 getGL30() {return null;} + @Override + public GL31 getGL31() {return null;} + @Override + public GL32 getGL32() {return null;} + @Override + public void setGL20(GL20 gl20) {} + @Override + public void setGL30(GL30 gl30) {} + @Override + public void setGL31(GL31 gl31) {} + @Override + public void setGL32(GL32 gl32) {} + @Override + public int getWidth() {return 0;} + @Override + public int getHeight() {return 0;} + @Override + public int getBackBufferWidth() {return 0;} + @Override + public int getBackBufferHeight() {return 0;} + @Override + public float getBackBufferScale() {return 0;} + @Override + public int getSafeInsetLeft() {return 0;} + @Override + public int getSafeInsetTop() {return 0;} + @Override + public int getSafeInsetBottom() {return 0;} + @Override + public int getSafeInsetRight() {return 0;} + @Override + public long getFrameId() {return 0;} + @Override + public float getDeltaTime() {return 0;} + @Override + public float getRawDeltaTime() {return 0;} + @Override + public int getFramesPerSecond() {return 0;} + @Override + public GraphicsType getType() {return null;} + @Override + public GLVersion getGLVersion() {return null;} + @Override + public float getPpiX() {return 0;} + @Override + public float getPpiY() {return 0;} + @Override + public float getPpcX() {return 0;} + @Override + public float getPpcY() {return 0;} + @Override + public float getDensity() {return 0;} + @Override + public boolean supportsDisplayModeChange() {return false;} + @Override + public Monitor getPrimaryMonitor() {return null;} + @Override + public Monitor getMonitor() {return null;} + @Override + public Monitor[] getMonitors() {return new Monitor[0];} + @Override + public DisplayMode[] getDisplayModes() {return new DisplayMode[0];} + @Override + public DisplayMode[] getDisplayModes(Monitor monitor) {return new DisplayMode[0];} + @Override + public DisplayMode getDisplayMode() {return null;} + @Override + public DisplayMode getDisplayMode(Monitor monitor) {return null;} + @Override + public boolean setFullscreenMode(DisplayMode displayMode) {return false;} + @Override + public boolean setWindowedMode(int width, int height) {return false;} + @Override + public void setTitle(String title) {} + @Override + public void setUndecorated(boolean undecorated) {} + @Override + public void setResizable(boolean resizable) {} + @Override + public void setVSync(boolean vsync) {} + @Override + public void setForegroundFPS(int fps) {} + @Override + public BufferFormat getBufferFormat() {return null;} + @Override + public boolean supportsExtension(String extension) {return false;} + @Override + public void setContinuousRendering(boolean isContinuous) {} + @Override + public boolean isContinuousRendering() {return false;} + @Override + public void requestRendering() {} + @Override + public boolean isFullscreen() {return false;} + @Override + public Cursor newCursor(Pixmap pixmap, int xHotspot, int yHotspot) {return null;} + @Override + public void setCursor(Cursor cursor) {} + @Override + public void setSystemCursor(Cursor.SystemCursor systemCursor) {} + } +} diff --git a/src/test/java/org/ois/core/state/StateManagerTest.java b/src/test/java/org/ois/core/state/StateManagerTest.java new file mode 100644 index 0000000..ee6b842 --- /dev/null +++ b/src/test/java/org/ois/core/state/StateManagerTest.java @@ -0,0 +1,283 @@ +package org.ois.core.state; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import java.util.Stack; + +public class StateManagerTest { + + private StateManager stateManager; + private TestState initialState; + private TestState secondState; + private ExceptionState exceptionState; + + @BeforeMethod + public void setUp() { + stateManager = new StateManager(); + initialState = new TestState("initial"); + secondState = new TestState("second"); + exceptionState = new ExceptionState("exceptionState"); + } + + @Test + public void testRegisterState() { + stateManager.registerState("initial", initialState); + assertTrue(stateManager.states().contains("initial")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testRegisterStateWithDuplicateKey() { + stateManager.registerState("initial", initialState); + stateManager.registerState("initial", new TestState("duplicate")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testRegisterStateWithNullKeyOrState() { + stateManager.registerState(null, initialState); // should throw exception + } + + @Test + public void testStartState() { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + assertEquals(stateManager.getCurrentState(), initialState); + assertTrue(initialState.hasEntered); + } + + @Test + public void testChangeState() { + stateManager.registerState("initial", initialState); + stateManager.registerState("second", secondState); + + stateManager.start("initial"); + assertFalse(secondState.hasEntered); + + stateManager.changeState("second"); + assertEquals(stateManager.getCurrentState(), secondState); + assertTrue(secondState.hasEntered); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testChangeToUnknownState() { + stateManager.changeState("unknown"); // should throw an exception + } + + @Test + public void testExitCurrentStateOnChange() { + stateManager.registerState("initial", initialState); + stateManager.registerState("second", secondState); + + stateManager.start("initial"); + stateManager.changeState("second"); + + assertTrue(initialState.hasExited); + assertTrue(secondState.hasEntered); + assertTrue(stateManager.hasActiveState()); + } + + @Test + public void testUpdateCurrentState() throws Exception { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + + assertTrue(stateManager.update(0.1f)); + assertTrue(initialState.hasUpdated); + } + + @Test + public void testRenderCurrentState() throws Exception { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + + stateManager.render(); + assertTrue(initialState.hasRendered); + } + + @Test + public void testPauseResumeState() throws Exception { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + + stateManager.pause(); + assertTrue(initialState.hasPaused); + + stateManager.resume(); + assertTrue(initialState.hasResumed); + } + + @Test + public void testResizeState() throws Exception { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + + stateManager.resize(800, 600); + assertEquals(initialState.width, 800); + assertEquals(initialState.height, 600); + } + + @Test + public void testDisposeState() throws Exception { + stateManager.registerState("initial", initialState); + stateManager.start("initial"); + + stateManager.dispose(); + assertTrue(initialState.hasExited); + assertTrue(initialState.hasDisposed); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testEnterStateThrowsException() { + // Simulate exception during state enter + exceptionState.throwOnEnter = true; + + stateManager.registerState("exception", exceptionState); + + // This should throw a RuntimeException due to exception in state enter + stateManager.start("exception"); + } + + @Test(expectedExceptions = Exception.class) + public void testUpdateStateThrowsException() throws Exception { + // Simulate exception during state update + exceptionState.throwOnUpdate = true; + + stateManager.registerState("exception", exceptionState); + stateManager.start("exception"); + + // This should throw a RuntimeException due to exception in state update + stateManager.update(0.1f); + } + + @Test(expectedExceptions = Exception.class) + public void testRenderStateThrowsException() throws Exception { + // Simulate exception during state render + exceptionState.throwOnRender = true; + + stateManager.registerState("exception", exceptionState); + stateManager.start("exception"); + + // This should throw a RuntimeException due to exception in state render + stateManager.render(); + } + + // Mock test class representing IState with exception throwing behavior + private static class ExceptionState implements IState { + private final String name; + boolean throwOnEnter = false; + boolean throwOnUpdate = false; + boolean throwOnRender = false; + + public ExceptionState(String name) { + this.name = name; + } + + @Override + public void enter(Object... params) { + if (throwOnEnter) { + throw new RuntimeException("Exception in enter state"); + } + } + + @Override + public void exit() { + // Exit logic + } + + @Override + public boolean update(float delta) { + if (throwOnUpdate) { + throw new RuntimeException("Exception in update state"); + } + return true; + } + + @Override + public void render() { + if (throwOnRender) { + throw new RuntimeException("Exception in render state"); + } + } + + @Override + public void pause() { + // Pause logic + } + + @Override + public void resume() { + // Resume logic + } + + @Override + public void resize(int width, int height) { + // Resize logic + } + + @Override + public void dispose() { + // Dispose logic + } + } + + // Mock test class representing IState + private static class TestState implements IState { + private final String name; + boolean hasEntered = false; + boolean hasExited = false; + boolean hasUpdated = false; + boolean hasRendered = false; + boolean hasPaused = false; + boolean hasResumed = false; + boolean hasDisposed = false; + int width, height; + + public TestState(String name) { + this.name = name; + } + + @Override + public void enter(Object... params) { + hasEntered = true; + } + + @Override + public void exit() { + hasExited = true; + } + + @Override + public boolean update(float delta) { + hasUpdated = true; + return true; + } + + @Override + public void render() { + hasRendered = true; + } + + @Override + public void pause() { + hasPaused = true; + } + + @Override + public void resume() { + hasResumed = true; + } + + @Override + public void resize(int width, int height) { + this.width = width; + this.height = height; + } + + @Override + public void dispose() { + hasDisposed = true; + } + } + +} diff --git a/src/test/java/org/ois/core/tools/TimerTest.java b/src/test/java/org/ois/core/tools/TimerTest.java new file mode 100644 index 0000000..3cc3d74 --- /dev/null +++ b/src/test/java/org/ois/core/tools/TimerTest.java @@ -0,0 +1,116 @@ +package org.ois.core.tools; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +public class TimerTest { + private Timer timer; + + @BeforeMethod + public void setUp() { + timer = new Timer(10); + } + + @Test + public void testInitialState() { + assertEquals(timer.getTarget(), 10.0f); + assertEquals(timer.timeLeftToTarget(), 10.0f); + assertFalse(timer.isLoop()); + assertFalse(timer.isPaused()); + // Should not be over initially + assertFalse(timer.tic(1)); + } + + @Test + public void testIsOverExactTarget() { + timer.tic(10); + assertTrue(timer.tic(0)); + } + + @Test + public void testPauseAndResume() { + timer.tic(3); + assertEquals(timer.timeLeftToTarget(), 7.0f); + + timer.pause(); + timer.tic(5); + // Should not affect time because timer is paused + assertEquals(timer.timeLeftToTarget(), 7.0f); + + timer.resume(); + timer.tic(2); + // Should now affect time + assertEquals(timer.timeLeftToTarget(), 5.0f); + } + + @Test + public void testOnFinishListener() { + final boolean[] listenerTriggered = {false}; + timer.setOnFinishListener(() -> listenerTriggered[0] = true); + + // Listener should trigger at the target + assertTrue(timer.tic(10)); + assertTrue(listenerTriggered[0]); + // After the listener, it should be over + assertTrue(timer.tic(0)); + } + + @Test + public void testResetWithNewTarget() { + // Reset with a new target + timer.reset(20); + assertEquals(timer.getTarget(), 20.0f); + assertEquals(timer.timeLeftToTarget(), 20.0f); + } + + @Test + public void testTicWithLoopingAndListener() { + final boolean[] listenerTriggered = {false}; + timer.setLoop(true); + timer.setOnFinishListener(() -> listenerTriggered[0] = true); + // With loop, it's not over + assertFalse(timer.tic(10)); + // Listener triggered at 10 seconds + assertTrue(listenerTriggered[0]); + assertFalse(timer.tic(0)); + // Loop again, Listener triggered again at 20 seconds + listenerTriggered[0] = false; + timer.tic(10); + assertTrue(listenerTriggered[0]); + } + + @Test + public void testTicWithLoopingNoListener() { + timer.setLoop(true); + // No listener, should not report "over" + assertFalse(timer.tic(10)); + // Still not report + assertFalse(timer.tic(10)); + } + + @Test + public void testTimerResumesAfterPause() { + timer.tic(5); + assertEquals(timer.timeLeftToTarget(), 5.0f); + // No change while paused + timer.pause(); + timer.tic(5); + assertEquals(timer.timeLeftToTarget(), 5.0f); + // Should be over after resuming + timer.resume(); + timer.tic(5); + assertEquals(timer.timeLeftToTarget(), 0.0f); + assertTrue(timer.tic(0)); + } + + @Test + public void testOnFinishListenerWithoutLooping() { + final boolean[] listenerTriggered = {false}; + timer.setOnFinishListener(() -> listenerTriggered[0] = true); + // Listener should trigger + assertTrue(timer.tic(10)); + assertTrue(listenerTriggered[0]); + assertTrue(timer.tic(0)); + } +} diff --git a/src/test/java/org/ois/core/utils/VersionTest.java b/src/test/java/org/ois/core/utils/VersionTest.java new file mode 100644 index 0000000..b62a8f4 --- /dev/null +++ b/src/test/java/org/ois/core/utils/VersionTest.java @@ -0,0 +1,130 @@ +package org.ois.core.utils; + +import org.testng.Assert; +import org.testng.annotations.Test; + +public class VersionTest { + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testConstructorWithNullVersion() { + new Version(null); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testConstructorWithBlankVersion() { + new Version(""); + } + + @Test + public void testConstructorWithValidVersion() { + Version version = new Version("1.0.0"); + Assert.assertEquals(version.toString(), "1.0.0"); + } + + @Test + public void testIsValidWithValidVersion() { + Version version = new Version("1.0.0"); + Assert.assertTrue(version.isValid()); + } + + @Test + public void testIsValidWithNotFoundVersion() { + Assert.assertFalse(Version.NOT_FOUND.isValid()); + } + + @Test + public void testIsAtLeastWithEqualVersion() { + Version version1 = new Version("1.0.0"); + Version version2 = new Version("1.0.0"); + Assert.assertTrue(version1.isAtLeast(version2)); + } + + @Test + public void testIsAtLeastWithGreaterVersion() { + Version version1 = new Version("1.1.0"); + Version version2 = new Version("1.0.0"); + Assert.assertTrue(version1.isAtLeast(version2)); + } + + @Test + public void testIsAtLeastWithLesserVersion() { + Version version1 = new Version("1.0.0"); + Version version2 = new Version("1.1.0"); + Assert.assertFalse(version1.isAtLeast(version2)); + } + + @Test + public void testIsAtLeastWithNullVersion() { + Version version = new Version("1.0.0"); + Assert.assertTrue(version.isAtLeast(null)); + } + + @Test + public void testCompareTokensWithEqualTokens() { + int comparison = new Version("1.0.0").compareTokens("1", "1"); + Assert.assertEquals(comparison, 0); + } + + @Test + public void testCompareTokensWithLessToken() { + int comparison = new Version("1.0.0").compareTokens("1", "2"); + Assert.assertTrue(comparison < 0); + } + + @Test + public void testCompareTokensWithGreaterToken() { + int comparison = new Version("1.0.0").compareTokens("2", "1"); + Assert.assertTrue(comparison > 0); + } + + @Test + public void testIsNumericWithValidNumericString() { + Assert.assertTrue(new Version("1.0.0").isNumeric("123")); + } + + @Test + public void testIsNumericWithInvalidNumericString() { + Assert.assertFalse(new Version("1.0.0").isNumeric("abc")); + } + + @Test + public void testIsAlphaNumericWithValidAlphanumericString() { + Assert.assertTrue(new Version("1.0.0").isAlphaNumeric("abc123")); + } + + @Test + public void testIsAlphaNumericWithInvalidAlphanumericString() { + Assert.assertFalse(new Version("1.0.0").isAlphaNumeric("abc!123")); + } + + @Test + public void testCompareNumerals() { + Assert.assertTrue(new Version("1.0.0").compareNumerals("2", "1") > 0); + } + + @Test + public void testEqualsWithSameObject() { + Version version = new Version("1.0.0"); + Assert.assertTrue(version.equals(version)); + } + + @Test + public void testEqualsWithDifferentObject() { + Version version1 = new Version("1.0.0"); + Version version2 = new Version("1.0.0"); + Assert.assertTrue(version1.equals(version2)); + } + + @Test + public void testHashCode() { + Version version1 = new Version("1.0.0"); + Version version2 = new Version("1.0.0"); + Assert.assertEquals(version1.hashCode(), version2.hashCode()); + } + + @Test + public void testToString() { + Version version = new Version("1.0.0"); + Assert.assertEquals(version.toString(), "1.0.0"); + } +} diff --git a/src/test/java/org/ois/core/utils/io/data/DataNodeTest.java b/src/test/java/org/ois/core/utils/io/data/DataNodeTest.java index 8d2165f..46e5590 100644 --- a/src/test/java/org/ois/core/utils/io/data/DataNodeTest.java +++ b/src/test/java/org/ois/core/utils/io/data/DataNodeTest.java @@ -1,20 +1,187 @@ package org.ois.core.utils.io.data; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.*; + public class DataNodeTest { - public void testDataNodePrimitive() { + private DataNode objectNode; + private DataNode collectionNode; + private DataNode primitiveNode; + + @BeforeMethod + public void setUp() { + objectNode = DataNode.Object(); + collectionNode = DataNode.Collection(); + primitiveNode = DataNode.Primitive("testValue"); + } + + @Test + public void testObjectNodeCreation() { + Assert.assertEquals(objectNode.getType(), DataNode.Type.Object, "Object node type mismatch."); + Assert.assertTrue(objectNode.attributes.isEmpty(), "Object node should have empty attributes."); + } + + @Test + public void testCollectionNodeCreation() { + Assert.assertEquals(collectionNode.getType(), DataNode.Type.Collection, "Collection node type mismatch."); + Assert.assertTrue(collectionNode.content.isEmpty(), "Collection node should have empty content."); + } + + @Test + public void testPrimitiveNodeCreation() { + Assert.assertEquals(primitiveNode.getType(), DataNode.Type.Primitive, "Primitive node type mismatch."); + Assert.assertEquals(primitiveNode.getString(), "testValue", "Primitive node value mismatch."); + } + + @Test + public void testSetAndGetObjectNodeAttributes() { + DataNode valueNode = DataNode.Primitive("attributeValue"); + objectNode.set("attributeKey", valueNode); + + Assert.assertTrue(objectNode.contains("attributeKey"), "Object node should contain the attribute."); + Assert.assertEquals(objectNode.get("attributeKey"), valueNode, "Retrieved attribute value mismatch."); + } + + @Test + public void testSetAndGetCollectionContent() { + DataNode node1 = DataNode.Primitive("value1"); + DataNode node2 = DataNode.Primitive("value2"); + + collectionNode.add(node1, node2); + + Assert.assertEquals(collectionNode.contentCount(), 2, "Collection node should contain 2 values."); + Assert.assertEquals(collectionNode.get(0), node1, "First element of collection mismatch."); + Assert.assertEquals(collectionNode.get(1), node2, "Second element of collection mismatch."); + } + + @Test + public void testSetAndGetPrimitiveValue() { + primitiveNode.setValue("newValue"); + + Assert.assertEquals(primitiveNode.getString(), "newValue", "Primitive node value mismatch after update."); + } + + @Test + public void testToMap() { + DataNode attribute1 = DataNode.Primitive("value1"); + DataNode attribute2 = DataNode.Primitive("value2"); + + objectNode.set("key1", attribute1); + objectNode.set("key2", attribute2); + + Map map = objectNode.toMap(); + Assert.assertEquals(map.size(), 2, "Map size mismatch."); + Assert.assertEquals(map.get("key1"), attribute1, "Map entry mismatch for key1."); + Assert.assertEquals(map.get("key2"), attribute2, "Map entry mismatch for key2."); + } + + @Test + public void testToStringMap() { + DataNode attribute1 = DataNode.Primitive("value1"); + DataNode attribute2 = DataNode.Primitive("value2"); + + objectNode.set("key1", attribute1); + objectNode.set("key2", attribute2); + + Map stringMap = objectNode.toStringMap(); + Assert.assertEquals(stringMap.size(), 2, "String map size mismatch."); + Assert.assertEquals(stringMap.get("key1"), "value1", "String map entry mismatch for key1."); + Assert.assertEquals(stringMap.get("key2"), "value2", "String map entry mismatch for key2."); + } + + @Test + public void testEqualsForSameType() { + DataNode node1 = DataNode.Primitive("value1"); + DataNode node2 = DataNode.Primitive("value1"); + + Assert.assertEquals(node1, node2, "Nodes with same type and value should be equal."); + } + + @Test + public void testEqualsForDifferentTypes() { + DataNode node1 = DataNode.Primitive("value1"); + DataNode node2 = DataNode.Object(); + + Assert.assertNotEquals(node1, node2, "Nodes with different types should not be equal."); + } + + @Test + public void testGetPropertyWithDefaultCreation() { + DataNode defaultNode = objectNode.getProperty("nonExistentKey", "subKey"); + + Assert.assertTrue(objectNode.contains("nonExistentKey"), "Parent object node should contain new attribute."); + Assert.assertEquals(defaultNode.getType(), DataNode.Type.Unknown, "Newly created node type mismatch."); + Assert.assertEquals(objectNode.get("nonExistentKey").getType(), DataNode.Type.Object, "Newly created node type mismatch."); + } + + @Test + public void testAddPrimitiveCollection() { + List intList = Arrays.asList(1, 2, 3); + DataNode collection = DataNode.Collection(intList); + + Assert.assertEquals(collection.contentCount(), 3, "Collection node size mismatch."); + Assert.assertEquals(collection.getInt(0), 1, "First element mismatch."); + Assert.assertEquals(collection.getInt(1), 2, "Second element mismatch."); + Assert.assertEquals(collection.getInt(2), 3, "Third element mismatch."); + } + + @Test + public void testAddMultipleTypesToCollection() { + collectionNode.add(DataNode.Primitive("string"), DataNode.Primitive(123), DataNode.Primitive(3.14f)); + + Assert.assertEquals(collectionNode.contentCount(), 3, "Collection node size mismatch."); + Assert.assertEquals(collectionNode.getString(0), "string", "First element string mismatch."); + Assert.assertEquals(collectionNode.getInt(1), 123, "Second element int mismatch."); + Assert.assertEquals(collectionNode.getFloat(2), 3.14f, "Third element float mismatch."); + } + + @Test + public void testDeepCopy() { + DataNode original = DataNode.Object(); + original.set("key1", DataNode.Primitive("value1")); + DataNode deepCopy = original.deepCopy(); + Assert.assertNotSame(original, deepCopy, "Deep copy should create a new object."); + Assert.assertEquals(original, deepCopy, "Deep copy should be equal to the original."); + Assert.assertNotSame(original.get("key1"), deepCopy.get("key1"), "Deep copy's attributes should not reference the same objects as the original."); } - public void testDataNodeCollection() { + @Test + public void testRemoveAttribute() { + DataNode objectNode = DataNode.Object(); + objectNode.set("key", DataNode.Primitive("value")); + DataNode removed = objectNode.remove("key"); + Assert.assertEquals(removed.getString(), "value", "Removed value mismatch."); + Assert.assertFalse(objectNode.contains("key"), "Object node should no longer contain the removed attribute."); } - public void testDataNodeMap() { + @Test + public void testClearContent() { + DataNode collectionNode = DataNode.Collection(); + collectionNode.add(DataNode.Primitive("item1"), DataNode.Primitive("item2")); + + Assert.assertEquals(collectionNode.contentCount(), 2, "Collection node should initially contain 2 items."); + collectionNode.clearContent(); + + Assert.assertEquals(collectionNode.contentCount(), 0, "Collection node should be empty after clearContent."); } - public void testDataNodeObject() { + @Test + public void testClearAttributes() { + DataNode objectNode = DataNode.Object(); + objectNode.set("key1", DataNode.Primitive("value1")); + objectNode.set("key2", DataNode.Primitive("value2")); + + Assert.assertEquals(objectNode.toMap().size(), 2, "Object node should initially contain 2 attributes."); + + objectNode.clearAttributes(); + Assert.assertEquals(objectNode.toMap().size(), 0, "Object node should have no attributes after clearAttributes."); } } diff --git a/src/test/java/org/ois/core/utils/io/data/formats/JsonFormatTest.java b/src/test/java/org/ois/core/utils/io/data/formats/JsonFormatTest.java index a46dfd1..6e970f2 100644 --- a/src/test/java/org/ois/core/utils/io/data/formats/JsonFormatTest.java +++ b/src/test/java/org/ois/core/utils/io/data/formats/JsonFormatTest.java @@ -16,7 +16,7 @@ public class JsonFormatTest { - Path testFilesDirPath = Paths.get(".").toAbsolutePath().normalize().resolve(Paths.get("src","test","resources","dataNode")); + Path testFilesDirPath = Paths.get(".").toAbsolutePath().normalize().resolve(Paths.get("src","test","resources", "json")); DataNode node; @BeforeTest diff --git a/src/test/resources/dataNode/testNode.json b/src/test/resources/json/testNode.json similarity index 100% rename from src/test/resources/dataNode/testNode.json rename to src/test/resources/json/testNode.json diff --git a/src/test/resources/dataNode/testNodeCompact.json b/src/test/resources/json/testNodeCompact.json similarity index 100% rename from src/test/resources/dataNode/testNodeCompact.json rename to src/test/resources/json/testNodeCompact.json