diff --git a/.sdkmanrc b/.sdkmanrc
index b8a16ed8..04dd322e 100644
--- a/.sdkmanrc
+++ b/.sdkmanrc
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
-java=11.0.22-tem
+java=17.0.11-tem
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index af1b0015..e0fb1fe9 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -6,3 +6,7 @@ repositories {
mavenLocal()
gradlePluginPortal()
}
+
+dependencies {
+ api("org.testcontainers:testcontainers:1.19.7")
+}
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index b8feaa94..f459d387 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -4,7 +4,7 @@ object PlayioPlugin {
object Version {
- const val gradlePlugin = "0.2.1"
+ const val gradlePlugin = "0.3.0"
}
const val oss = "cloud.playio.gradle.oss"
diff --git a/core/src/main/java/io/github/zero88/schedulerx/AsyncJob.java b/core/src/main/java/io/github/zero88/schedulerx/AsyncJob.java
index 688dd313..73b28c1a 100644
--- a/core/src/main/java/io/github/zero88/schedulerx/AsyncJob.java
+++ b/core/src/main/java/io/github/zero88/schedulerx/AsyncJob.java
@@ -37,7 +37,7 @@ default void execute(@NotNull JobData jobData, @NotNull ExecutionContext<
/**
* Async execute job
*
- * WARNING: After execution, be aware to call a terminal operation of {@link Future} such
+ * CAUTION: After execution, be aware to call a terminal operation of {@link Future} such
* as {@link Future#onSuccess(Handler)}, {@link Future#onFailure(Handler)} or {@link Future#onComplete(Handler)}.
* The async job is already registered these handlers, if several {@code handler}s are registered, there is no
* guarantee that they will be invoked in order of registration.
diff --git a/core/src/main/java/io/github/zero88/schedulerx/JobData.java b/core/src/main/java/io/github/zero88/schedulerx/JobData.java
index ae8cd856..218b0bf2 100644
--- a/core/src/main/java/io/github/zero88/schedulerx/JobData.java
+++ b/core/src/main/java/io/github/zero88/schedulerx/JobData.java
@@ -1,5 +1,7 @@
package io.github.zero88.schedulerx;
+import java.util.Optional;
+
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -14,27 +16,6 @@
*/
public interface JobData {
- /**
- * Get an input data.
- *
- * It might be a static input value or a preloaded value from an external system
- * or a configuration to instruct how to get actual input data of the job in runtime execution.
- *
- * @return input data
- */
- @Nullable T get();
-
- /**
- * Declares a unique id in an external system that will be propagated to the job result.
- *
- * That makes the integration between the job monitoring and the external system seamless and easier.
- *
- * @return an external id
- * @see ExecutionResult#externalId()
- * @since 2.0.0
- */
- default @Nullable Object externalId() { return null; }
-
/**
* Create emtpy data with random external id in integer.
*
@@ -51,12 +32,13 @@ public interface JobData {
* @return JobData contains null data
* @since 2.0.0
*/
- static JobData empty(@NotNull Object externalId) {
+ static JobData empty(Object externalId) {
+ Object id = Optional.ofNullable(externalId).orElseGet(Utils::randomPositiveInt);
return new JobData<>() {
public @Nullable D get() { return null; }
@Override
- public Object externalId() { return externalId; }
+ public Object externalId() { return id; }
};
}
@@ -78,13 +60,35 @@ static JobData create(@NotNull D data) {
* @return JobData
* @since 2.0.0
*/
- static JobData create(@NotNull D data, @NotNull Object externalId) {
+ static JobData create(@NotNull D data, Object externalId) {
+ Object id = Optional.ofNullable(externalId).orElseGet(Utils::randomPositiveInt);
return new JobData<>() {
public D get() { return data; }
@Override
- public Object externalId() { return externalId; }
+ public Object externalId() { return id; }
};
}
+ /**
+ * Get an input data.
+ *
+ * It might be a static input value or a preloaded value from an external system
+ * or a configuration to instruct how to get actual input data of the job in runtime execution.
+ *
+ * @return input data
+ */
+ @Nullable T get();
+
+ /**
+ * Declares a unique id in an external system that will be propagated to the job result.
+ *
+ * That makes the integration between the job monitoring and the external system seamless and easier.
+ *
+ * @return an external id
+ * @see ExecutionResult#externalId()
+ * @since 2.0.0
+ */
+ @Nullable Object externalId();
+
}
diff --git a/core/src/main/java/io/github/zero88/schedulerx/JobFactory.java b/core/src/main/java/io/github/zero88/schedulerx/JobFactory.java
new file mode 100644
index 00000000..8e2d4b4c
--- /dev/null
+++ b/core/src/main/java/io/github/zero88/schedulerx/JobFactory.java
@@ -0,0 +1,88 @@
+package io.github.zero88.schedulerx;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jetbrains.annotations.NotNull;
+
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+
+/**
+ * Factory for creating Job instances from class names or service loader.
+ *
+ * @since 2.0.0
+ */
+public final class JobFactory {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(JobFactory.class);
+ private static final Map> CACHE = new ConcurrentHashMap<>();
+
+ private JobFactory() {
+ }
+
+ /**
+ * Get the singleton instance of JobFactory.
+ *
+ * @return the JobFactory instance
+ */
+ public static JobFactory getInstance() {
+ return Holder.INSTANCE;
+ }
+
+ /**
+ * Create a Job instance from a job class name.
+ *
+ * @param jobClassName the fully qualified class name of the job
+ * @param type of job input
+ * @param type of job output
+ * @return a Job instance
+ * @throws IllegalArgumentException if the job class cannot be found or instantiated
+ */
+ @SuppressWarnings("unchecked")
+ public Job create(@NotNull String jobClassName) {
+ try {
+ // First check if we have a cached instance
+ if (CACHE.containsKey(jobClassName)) {
+ LOGGER.debug("Using cached job instance for: " + jobClassName);
+ return (Job) CACHE.get(jobClassName);
+ }
+
+ // Try to load the class
+ Class> jobClass = Class.forName(jobClassName);
+
+ // Ensure it implements Job interface
+ if (!Job.class.isAssignableFrom(jobClass)) {
+ throw new IllegalArgumentException("Class " + jobClassName + " does not implement Job interface");
+ }
+
+ // Instantiate using default constructor
+ Job jobInstance = (Job) jobClass.getDeclaredConstructor().newInstance();
+ LOGGER.debug("Created job instance using default constructor: " + jobClassName);
+
+ // Cache the instance for future use
+ CACHE.put(jobClassName, jobInstance);
+ return jobInstance;
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Job class " + jobClassName + " must have a default constructor", e);
+ } catch (ClassNotFoundException e) {
+ throw new IllegalArgumentException("Job class not found: " + jobClassName, e);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Failed to instantiate job class: " + jobClassName, e);
+ }
+ }
+
+ /**
+ * Clear the job instance cache.
+ */
+ public void clearCache() {
+ CACHE.clear();
+ }
+
+ /**
+ * Holder for the singleton instance.
+ */
+ private static class Holder {
+ private static final JobFactory INSTANCE = new JobFactory();
+ }
+}
diff --git a/core/src/main/java/io/github/zero88/schedulerx/TimeClock.java b/core/src/main/java/io/github/zero88/schedulerx/TimeClock.java
index b61203ae..85754868 100644
--- a/core/src/main/java/io/github/zero88/schedulerx/TimeClock.java
+++ b/core/src/main/java/io/github/zero88/schedulerx/TimeClock.java
@@ -6,7 +6,7 @@
import org.jetbrains.annotations.NotNull;
/**
- * Represents for time clock
+ * Represents for a time clock
*
* @since 2.0.0
*/
@@ -14,7 +14,7 @@
public interface TimeClock {
/**
- * Obtains the current instant from the system clock.
+ * Gets the current instant from the system clock.
*
* @return the current instant using the system clock, not null
*/
diff --git a/core/src/main/java/io/github/zero88/schedulerx/TimeoutPolicy.java b/core/src/main/java/io/github/zero88/schedulerx/TimeoutPolicy.java
index b0016539..dbe541dc 100644
--- a/core/src/main/java/io/github/zero88/schedulerx/TimeoutPolicy.java
+++ b/core/src/main/java/io/github/zero88/schedulerx/TimeoutPolicy.java
@@ -7,6 +7,8 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import io.vertx.core.json.JsonObject;
+
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -33,16 +35,6 @@ public static TimeoutPolicy byDefault() {
return create(null, null);
}
- /**
- * Create timeout policy with execution timeout
- *
- * @param executionTimeout given execution timeout
- * @return timeout policy
- */
- public static TimeoutPolicy create(@NotNull Duration executionTimeout) {
- return create(null, executionTimeout);
- }
-
/**
* Create timeout policy with execution timeout
*
@@ -63,6 +55,20 @@ public static TimeoutPolicy create(@JsonProperty("evaluationTimeout") @Nullable
check.apply(executionTimeout, DefaultOptions.getInstance().executionMaxTimeout));
}
+ /**
+ * Create timeout policy with execution timeout
+ *
+ * @param executionTimeout given execution timeout
+ * @return timeout policy
+ */
+ public static TimeoutPolicy create(@NotNull Duration executionTimeout) {
+ return create(null, executionTimeout);
+ }
+
+ public static TimeoutPolicy create(JsonObject timeoutPolicy) {
+ return timeoutPolicy.mapTo(TimeoutPolicy.class);
+ }
+
/**
* Declares the trigger evaluation timeout. Default is {@link DefaultOptions#evaluationMaxTimeout}
*
@@ -81,6 +87,13 @@ public static TimeoutPolicy create(@JsonProperty("evaluationTimeout") @Nullable
@JsonGetter
public @NotNull Duration executionTimeout() { return executionTimeout; }
+ @Override
+ public int hashCode() {
+ int result = evaluationTimeout != null ? evaluationTimeout.hashCode() : 0;
+ result = 31 * result + (executionTimeout != null ? executionTimeout.hashCode() : 0);
+ return result;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o)
@@ -94,11 +107,4 @@ public boolean equals(Object o) {
Objects.equals(executionTimeout, that.executionTimeout);
}
- @Override
- public int hashCode() {
- int result = evaluationTimeout != null ? evaluationTimeout.hashCode() : 0;
- result = 31 * result + (executionTimeout != null ? executionTimeout.hashCode() : 0);
- return result;
- }
-
}
diff --git a/core/src/main/java/io/github/zero88/schedulerx/impl/Utils.java b/core/src/main/java/io/github/zero88/schedulerx/impl/Utils.java
index 068951f1..baf7148e 100644
--- a/core/src/main/java/io/github/zero88/schedulerx/impl/Utils.java
+++ b/core/src/main/java/io/github/zero88/schedulerx/impl/Utils.java
@@ -1,7 +1,6 @@
package io.github.zero88.schedulerx.impl;
import java.security.SecureRandom;
-import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
@@ -10,18 +9,12 @@
@Internal
public final class Utils {
- public static String brackets(Object any) { return "[" + any + "]"; }
-
- /*
- * The random number generator, in a holder class to defer initialization until needed.
- */
- private static class Holder {
-
- static final SecureRandom numberGenerator = new SecureRandom();
-
+ private Utils() {
}
- private Utils() { }
+ public static String brackets(Object any) {
+ return "[" + any + "]";
+ }
public static int randomPositiveInt() {
return Utils.Holder.numberGenerator.nextInt() & Integer.MAX_VALUE;
@@ -62,4 +55,13 @@ public static T castOrNull(Object data, boolean nullOrThrow) {
}
}
+ /*
+ * The random number generator, in a holder class to defer initialization until needed.
+ */
+ private static class Holder {
+
+ static final SecureRandom numberGenerator = new SecureRandom();
+
+ }
+
}
diff --git a/core/src/test/java/io/github/zero88/schedulerx/JobFactoryTest.java b/core/src/test/java/io/github/zero88/schedulerx/JobFactoryTest.java
new file mode 100644
index 00000000..cf506953
--- /dev/null
+++ b/core/src/test/java/io/github/zero88/schedulerx/JobFactoryTest.java
@@ -0,0 +1,84 @@
+package io.github.zero88.schedulerx;
+
+import static io.github.zero88.schedulerx.impl.Utils.brackets;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class JobFactoryTest {
+
+ @BeforeEach
+ void setUp() {
+ JobFactory.getInstance().clearCache();
+ }
+
+ @Test
+ void test_create_job_with_default_constructor() {
+ Job job = JobFactory.getInstance().create(TestJobWithDefaultConstructor.class.getName());
+
+ assertNotNull(job);
+ assertInstanceOf(TestJobWithDefaultConstructor.class, job);
+ }
+
+ @Test
+ void test_caching() {
+ Job job1 = JobFactory.getInstance().create(TestJobWithDefaultConstructor.class.getName());
+ Job job2 = JobFactory.getInstance()
+ .create("io.github.zero88.schedulerx" +
+ ".JobFactoryTest$TestJobWithDefaultConstructor");
+
+ assertNotNull(job1);
+ assertNotNull(job2);
+ assertSame(job1, job2, "Should return cached instance");
+ }
+
+ @ParameterizedTest
+ // @formatter:off
+ @CsvSource({
+ "non.existent.JobClass,Job class not found",
+ "io.github.zero88.schedulerx.TestUtils,does not implement Job interface",
+ "io.github.zero88.schedulerx.JobFactoryTest$TestJobWithWithoutDefaultConstructor,must have a default constructor"
+ })
+ // @formatter:on
+ void test_fail_to_create_job(String jobClassName, String expectedMessage) {
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> JobFactory.getInstance().create(jobClassName));
+ String exceptionMessage = exception.getMessage();
+ String causeMessage = exception.getCause() != null ? exception.getCause().getMessage() : "";
+ assertTrue(exceptionMessage.contains(expectedMessage) || causeMessage.contains(expectedMessage),
+ "Expected error message to contain: " + brackets(expectedMessage) + ", but got: " +
+ brackets(exceptionMessage) + " and cause: " + brackets(causeMessage));
+ }
+
+ static class TestJobWithDefaultConstructor implements Job {
+
+ @Override
+ public void execute(@NotNull JobData jobData, @NotNull ExecutionContext executionContext) {
+ executionContext.complete("Default job executed");
+ }
+
+ }
+
+
+ static class TestJobWithWithoutDefaultConstructor implements Job {
+
+ private final String arg;
+
+ public TestJobWithWithoutDefaultConstructor(String arg) { this.arg = arg; }
+
+ @Override
+ public void execute(@NotNull JobData jobData, @NotNull ExecutionContext executionContext) {
+ executionContext.complete("Default job executed: " + arg);
+ }
+
+ }
+
+}
diff --git a/core/src/test/java/io/github/zero88/schedulerx/TestUtils.java b/core/src/test/java/io/github/zero88/schedulerx/TestUtils.java
index eac0c644..a6ef9226 100644
--- a/core/src/test/java/io/github/zero88/schedulerx/TestUtils.java
+++ b/core/src/test/java/io/github/zero88/schedulerx/TestUtils.java
@@ -22,19 +22,6 @@ public final class TestUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(TestUtils.class);
- private TestUtils() { }
-
- @SuppressWarnings("java:S2925")
- public static void block(Duration duration, VertxTestContext testContext) {
- try {
- LOGGER.info("Doing a mock stuff in " + brackets(duration) + "...");
- TimeUnit.MILLISECONDS.sleep(duration.toMillis());
- LOGGER.info("Wake up after " + brackets(duration) + "!!!");
- } catch (InterruptedException e) {
- testContext.failNow(e);
- }
- }
-
public static List simulateRunActionInParallel(VertxTestContext testContext, Runnable action,
int nbOfThreads) {
final List store = new ArrayList<>();
@@ -47,6 +34,16 @@ public static List simulateRunActionInParallel(VertxTestContext testC
return store;
}
+ public static void block(Duration duration, VertxTestContext testContext) {
+ try {
+ LOGGER.info("Doing a mock stuff in " + brackets(duration) + "...");
+ TimeUnit.MILLISECONDS.sleep(duration.toMillis());
+ LOGGER.info("Wake up after " + brackets(duration) + "!!!");
+ } catch (InterruptedException e) {
+ testContext.failNow(e);
+ }
+ }
+
public static ObjectMapper defaultMapper() {
return DatabindCodec.mapper()
.copy()
diff --git a/core/src/test/java/io/github/zero88/schedulerx/custom/HttpClientJob.java b/core/src/test/java/io/github/zero88/schedulerx/custom/HttpClientJob.java
index d9f0362f..1df7e36b 100644
--- a/core/src/test/java/io/github/zero88/schedulerx/custom/HttpClientJob.java
+++ b/core/src/test/java/io/github/zero88/schedulerx/custom/HttpClientJob.java
@@ -18,6 +18,9 @@ public Future asyncExecute(@NotNull JobData jobData,
@NotNull ExecutionContext executionContext) {
Vertx vertx = executionContext.vertx();
JsonObject config = jobData.get();
+ if (config == null) {
+ return Future.failedFuture("Missing job data");
+ }
return vertx.createHttpClient()
.request(HttpMethod.GET, config.getString("host"), config.getString("path"))
.map(req -> req.setFollowRedirects(true))
diff --git a/core/src/test/java/io/github/zero88/schedulerx/trigger/CronTriggerTest.java b/core/src/test/java/io/github/zero88/schedulerx/trigger/CronTriggerTest.java
index d493f887..0c0e5ad8 100644
--- a/core/src/test/java/io/github/zero88/schedulerx/trigger/CronTriggerTest.java
+++ b/core/src/test/java/io/github/zero88/schedulerx/trigger/CronTriggerTest.java
@@ -65,6 +65,7 @@ void test_serialize_deserialize(CronTrigger trigger, JsonObject json) throws Jso
Assertions.assertEquals(t2, trigger);
Assertions.assertEquals(t2.toJson(), trigger.toJson());
Assertions.assertEquals(t2.toJson().encode(), mapper.writeValueAsString(trigger));
+ System.out.println(t2.toJson());
}
static Stream invalidExpression() {
diff --git a/core/src/test/java/io/github/zero88/schedulerx/trigger/EventTriggerTest.java b/core/src/test/java/io/github/zero88/schedulerx/trigger/EventTriggerTest.java
index 1fb6a4f6..572c8fce 100644
--- a/core/src/test/java/io/github/zero88/schedulerx/trigger/EventTriggerTest.java
+++ b/core/src/test/java/io/github/zero88/schedulerx/trigger/EventTriggerTest.java
@@ -53,6 +53,7 @@ void test_serialize_deserialize(EventTrigger trigger, JsonObject json) throws Js
Assertions.assertEquals(t3, trigger);
Assertions.assertEquals(t3.toJson(), trigger.toJson());
Assertions.assertEquals(t3.toJson().encode(), mapper.writeValueAsString(trigger));
+ System.out.println(t3.toJson());
}
static Stream invalidData() {
diff --git a/core/src/test/java/io/github/zero88/schedulerx/trigger/IntervalTriggerTest.java b/core/src/test/java/io/github/zero88/schedulerx/trigger/IntervalTriggerTest.java
index 8dc53603..fe65d8a0 100644
--- a/core/src/test/java/io/github/zero88/schedulerx/trigger/IntervalTriggerTest.java
+++ b/core/src/test/java/io/github/zero88/schedulerx/trigger/IntervalTriggerTest.java
@@ -37,7 +37,8 @@ class IntervalTriggerTest {
static void setup() {
mapper = DatabindCodec.mapper()
.findAndRegisterModules()
- .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
+ SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS);
}
static Stream validData() {
@@ -78,6 +79,7 @@ void test_serialize_deserialize(IntervalTrigger trigger, JsonObject json) throws
Assertions.assertEquals(t1.toJson(), trigger.toJson());
Assertions.assertEquals(t1.toJson().encode(), mapper.writeValueAsString(trigger));
Assertions.assertEquals(json.mapTo(IntervalTrigger.class), trigger);
+ System.out.println(t1.toJson());
}
static Stream invalidData() {
diff --git a/core/src/testFixtures/java/io/github/zero88/schedulerx/NoopJob.java b/core/src/testFixtures/java/io/github/zero88/schedulerx/NoopJob.java
index 0cd648e0..567eb947 100644
--- a/core/src/testFixtures/java/io/github/zero88/schedulerx/NoopJob.java
+++ b/core/src/testFixtures/java/io/github/zero88/schedulerx/NoopJob.java
@@ -1,12 +1,12 @@
package io.github.zero88.schedulerx;
/**
- * Represents for dummy job that do nothing
+ * Represents for a fake job that does nothing
*
* @param Type of job input data
* @param Type of job result data
*/
-public interface NoopJob extends Job {
+public interface NoopJob extends SyncJob {
static Job create() { return (jobData, executionContext) -> { }; }
diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts
index f92093d3..9f7cf144 100644
--- a/docs/build.gradle.kts
+++ b/docs/build.gradle.kts
@@ -1,10 +1,14 @@
import cloud.playio.gradle.antora.tasks.AntoraCopyTask
import cloud.playio.gradle.generator.docgen.AsciidocGenTask
+import cloud.playio.gradle.pandoc.FormatFrom
+import cloud.playio.gradle.pandoc.FormatTo
+import cloud.playio.gradle.pandoc.tasks.PandocTask
import cloud.playio.gradle.shared.prop
plugins {
id(PlayioPlugin.antora)
id(PlayioPlugin.docgen)
+ id(PlayioPlugin.pandoc)
}
dependencies {
@@ -32,19 +36,19 @@ documentation {
javadocProjects.set((extensions["PROJECT_POOL"] as Map<*, Array>)[mainProject]!!.map(project::project))
}
-// pandoc {
-// from.set(FormatFrom.markdown)
-// to.set(FormatTo.asciidoc)
-// inputFile.set(rootDir.resolve("CHANGELOG.md"))
-// outFile.set("pg-changelog.adoc")
-// config {
-// arguments.set(arrayOf("--trace"))
-// }
-// }
+ pandoc {
+ from.set(FormatFrom.markdown)
+ to.set(FormatTo.asciidoc)
+ inputFile.set(rootDir.resolve("CHANGELOG.md"))
+ outFile.set("pg-changelog.adoc")
+ config {
+ arguments.set(arrayOf("--trace"))
+ }
+ }
}
tasks {
-// named("antoraPages") { from(withType()) }
+ named("antoraPages") { from(withType()) }
named("antoraPartials") {
from(withType())
include("*.adoc")
diff --git a/event-trigger-jsonschema/build.gradle.kts b/manager/build.gradle.kts
similarity index 76%
rename from event-trigger-jsonschema/build.gradle.kts
rename to manager/build.gradle.kts
index 87d748df..63370e0a 100644
--- a/event-trigger-jsonschema/build.gradle.kts
+++ b/manager/build.gradle.kts
@@ -1,6 +1,8 @@
dependencies {
api(project(":schedulerx"))
api(VertxLibs.jsonSchema)
+ api(JacksonLibs.databind)
+ api(JacksonLibs.jsr310)
compileOnly(UtilLibs.jetbrainsAnnotations)
testImplementation(testFixtures(project(":schedulerx")))
diff --git a/manager/src/main/java/io/github/zero88/schedulerx/manager/JsonFileScheduleManager.java b/manager/src/main/java/io/github/zero88/schedulerx/manager/JsonFileScheduleManager.java
new file mode 100644
index 00000000..40833eff
--- /dev/null
+++ b/manager/src/main/java/io/github/zero88/schedulerx/manager/JsonFileScheduleManager.java
@@ -0,0 +1,94 @@
+package io.github.zero88.schedulerx.manager;
+
+import io.github.zero88.schedulerx.SchedulingMonitor;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.json.JsonObject;
+
+/**
+ * A concrete implementation of Manager that loads scheduler configurations from JSON files.
+ * The file path should be specified in the setup configuration under the "filePath" key.
+ */
+public class JsonFileScheduleManager implements SchedulingLoader {
+
+ private static final String FILE_PATH_KEY = "filePath";
+ private static final String DEFAULT_FILE_PATH = "schedulers.json";
+
+
+ /**
+ * Creates a new JsonFileManager instance with a default monitor.
+ *
+ * @param vertx the Vertx instance
+ * @param defaultMonitor the default scheduling monitor
+ */
+ public JsonFileScheduleManager(Vertx vertx, SchedulingMonitor