diff --git a/pom.xml b/pom.xml
index 2d8e109..300a087 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,14 +33,29 @@
org.springframework.boot
- spring-boot-starter-test
- test
+ spring-boot-starter-data-jpa
+
+
+ com.mysql
+ mysql-connector-j
+ runtime
+
+
+ net.javacrumbs.shedlock
+ shedlock-provider-jdbc
+ 5.15.1
org.projectlombok
lombok
true
+
+ org.testng
+ testng
+ RELEASE
+ compile
+
diff --git a/src/main/java/com/app/config/SchedulerConfig.java b/src/main/java/com/app/config/SchedulerConfig.java
new file mode 100644
index 0000000..9a89c1d
--- /dev/null
+++ b/src/main/java/com/app/config/SchedulerConfig.java
@@ -0,0 +1,21 @@
+package com.app.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+
+@Configuration
+@EnableScheduling
+public class SchedulerConfig {
+
+ @Bean
+ public TaskScheduler taskScheduler() {
+ ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
+ scheduler.setPoolSize(10); // Define the pool size
+ scheduler.setThreadNamePrefix("scheduled-task-");
+ scheduler.initialize();
+ return scheduler;
+ }
+}
diff --git a/src/main/java/com/app/config/ShedLockConfiguration.java b/src/main/java/com/app/config/ShedLockConfiguration.java
new file mode 100644
index 0000000..c80e269
--- /dev/null
+++ b/src/main/java/com/app/config/ShedLockConfiguration.java
@@ -0,0 +1,16 @@
+package com.app.config;
+
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.provider.jdbc.JdbcLockProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.sql.DataSource;
+@Configuration
+public class ShedLockConfiguration {
+
+ @Bean
+ public LockProvider lockProvider(DataSource dataSource) {
+ return new JdbcLockProvider(dataSource);
+ }
+}
diff --git a/src/main/java/com/app/controller/ScheduleController.java b/src/main/java/com/app/controller/ScheduleController.java
new file mode 100644
index 0000000..e391a00
--- /dev/null
+++ b/src/main/java/com/app/controller/ScheduleController.java
@@ -0,0 +1,30 @@
+package com.app.controller;
+
+import com.app.dto.Schedule;
+import com.app.service.ScheduleService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("schedule")
+public class ScheduleController {
+
+ private final ScheduleService ScheduleService;
+
+ @PostMapping("add")
+ public ResponseEntity addSchedule(@RequestBody Schedule schedule) {
+ return new ResponseEntity<>(ScheduleService.addSchedule(schedule), HttpStatus.OK);
+ }
+
+ @PutMapping("update")
+ public ResponseEntity updateSchedule(@RequestBody Schedule schedule) {
+ return new ResponseEntity<>(ScheduleService.updateSchedule(schedule), HttpStatus.OK);
+ }
+ @DeleteMapping("delete")
+ public ResponseEntity deleteSchedule(@RequestParam Long id) {
+ return new ResponseEntity<>(ScheduleService.deleteSchedule(id), HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/app/dto/Schedule.java b/src/main/java/com/app/dto/Schedule.java
new file mode 100644
index 0000000..d5fa7ca
--- /dev/null
+++ b/src/main/java/com/app/dto/Schedule.java
@@ -0,0 +1,14 @@
+package com.app.dto;
+
+import com.app.model.ScheduleType;
+import lombok.Data;
+
+@Data
+public class Schedule {
+ private Long id;
+ private String name;
+ private String parameters;
+ private Boolean isActive;
+ private Integer day;
+ private ScheduleType scheduleType;
+}
diff --git a/src/main/java/com/app/model/ScheduleTask.java b/src/main/java/com/app/model/ScheduleTask.java
new file mode 100644
index 0000000..995b32a
--- /dev/null
+++ b/src/main/java/com/app/model/ScheduleTask.java
@@ -0,0 +1,45 @@
+package com.app.model;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import org.hibernate.annotations.DynamicInsert;
+import org.hibernate.annotations.DynamicUpdate;
+
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "schedule")
+@Data
+@DynamicInsert
+@DynamicUpdate
+public class ScheduleTask {
+ @Id
+ @Column(name = "id", updatable = false, nullable = false)
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(length = 255)
+ private String name;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private ScheduleType scheduleType;
+
+ @Column
+ private LocalDateTime scheduleTime;
+
+ @Column(columnDefinition = "TEXT", nullable = false)
+ private String customScheduleDetails;
+
+ private LocalDateTime lastRun;
+
+ @Column
+ private LocalDateTime nextRun;
+
+ @Column
+ private Boolean isActive;
+
+ @Column(columnDefinition = "TEXT")
+ private String parameters;
+
+}
diff --git a/src/main/java/com/app/model/ScheduleType.java b/src/main/java/com/app/model/ScheduleType.java
new file mode 100644
index 0000000..cd495f3
--- /dev/null
+++ b/src/main/java/com/app/model/ScheduleType.java
@@ -0,0 +1,5 @@
+package com.app.model;
+
+public enum ScheduleType {
+ MINUTE, HOUR, DAILY, WEEKLY, MONTHLY;
+}
diff --git a/src/main/java/com/app/repository/ScheduleTaskRepository.java b/src/main/java/com/app/repository/ScheduleTaskRepository.java
new file mode 100644
index 0000000..d500b37
--- /dev/null
+++ b/src/main/java/com/app/repository/ScheduleTaskRepository.java
@@ -0,0 +1,10 @@
+package com.app.repository;
+
+import com.app.model.ScheduleTask;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface ScheduleTaskRepository extends JpaRepository {
+ List findAllByIsActiveTrue();
+}
diff --git a/src/main/java/com/app/service/ScheduleService.java b/src/main/java/com/app/service/ScheduleService.java
new file mode 100644
index 0000000..2ab9218
--- /dev/null
+++ b/src/main/java/com/app/service/ScheduleService.java
@@ -0,0 +1,11 @@
+package com.app.service;
+
+import com.app.dto.Schedule;
+
+public interface ScheduleService {
+ Schedule addSchedule(Schedule schedule);
+
+ Schedule deleteSchedule(Long id);
+
+ Schedule updateSchedule(Schedule schedule);
+}
diff --git a/src/main/java/com/app/service/ScheduleServiceImpl.java b/src/main/java/com/app/service/ScheduleServiceImpl.java
new file mode 100644
index 0000000..3f826ee
--- /dev/null
+++ b/src/main/java/com/app/service/ScheduleServiceImpl.java
@@ -0,0 +1,58 @@
+package com.app.service;
+
+import com.app.dto.Schedule;
+import com.app.model.ScheduleTask;
+import com.app.repository.ScheduleTaskRepository;
+import com.app.utils.CreateCronExpression;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class ScheduleServiceImpl implements ScheduleService {
+ private final CreateCronExpression createCronExpression;
+ private final TaskSchedulingService taskSchedulingService;
+ private final ScheduleTaskRepository scheduleTaskRepository;
+
+ @Override
+ public Schedule addSchedule(Schedule schedule) {
+ String cron = switch (schedule.getScheduleType()) {
+ case MINUTE -> createCronExpression.generateEveryMinuteCronExpression();
+ case HOUR -> createCronExpression.generateHourlyCronExpression();
+ case DAILY -> createCronExpression.generateDailyCronExpression();
+ case WEEKLY -> createCronExpression.generateWeeklyCronExpression(schedule.getDay());
+ case MONTHLY -> createCronExpression.generateMonthlyCronExpression(schedule.getDay());
+ };
+ ScheduleTask scheduleTask = new ScheduleTask();
+ scheduleTask.setCustomScheduleDetails(cron);
+ scheduleTask.setName(schedule.getName());
+ scheduleTask.setScheduleType(schedule.getScheduleType());
+ scheduleTask.setIsActive(schedule.getIsActive());
+ scheduleTask = scheduleTaskRepository.save(scheduleTask);
+ log.info(cron);
+ schedule.setId(scheduleTask.getId());
+ taskSchedulingService.scheduleTask(scheduleTask);
+ return schedule;
+ }
+
+ @Override
+ public Schedule deleteSchedule(Long id) {
+ Optional scheduleTaskOptional = scheduleTaskRepository.findById(id);
+ if(scheduleTaskOptional.isPresent()){
+ ScheduleTask scheduleTask = scheduleTaskOptional.get();
+ scheduleTask.setIsActive(false);
+ scheduleTask = scheduleTaskRepository.save(scheduleTask);
+ taskSchedulingService.cancelScheduledTask(scheduleTask.getId());
+ }
+ return null;
+ }
+
+ @Override
+ public Schedule updateSchedule(Schedule schedule) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/app/service/TaskSchedulingService.java b/src/main/java/com/app/service/TaskSchedulingService.java
new file mode 100644
index 0000000..a0d92e7
--- /dev/null
+++ b/src/main/java/com/app/service/TaskSchedulingService.java
@@ -0,0 +1,12 @@
+package com.app.service;
+
+import com.app.model.ScheduleTask;
+
+public interface TaskSchedulingService {
+
+ void scheduleTask(ScheduleTask scheduleTask);
+
+ void cancelScheduledTask(Long taskId);
+
+ void addOrUpdateTask(ScheduleTask scheduleTask);
+}
diff --git a/src/main/java/com/app/service/TaskSchedulingServiceImpl.java b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java
new file mode 100644
index 0000000..cfdcec0
--- /dev/null
+++ b/src/main/java/com/app/service/TaskSchedulingServiceImpl.java
@@ -0,0 +1,82 @@
+package com.app.service;
+
+
+import com.app.model.ScheduleTask;
+import com.app.repository.ScheduleTaskRepository;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import net.javacrumbs.shedlock.core.LockAssert;
+import net.javacrumbs.shedlock.core.LockConfiguration;
+import net.javacrumbs.shedlock.core.LockProvider;
+import net.javacrumbs.shedlock.core.SimpleLock;
+import org.springframework.scheduling.TaskScheduler;
+import org.springframework.scheduling.support.CronTrigger;
+import org.springframework.stereotype.Service;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class TaskSchedulingServiceImpl implements TaskSchedulingService {
+ private final TaskScheduler taskScheduler;
+ private final LockProvider lockProvider;
+ private final ScheduleTaskRepository scheduleTaskRepository;
+ private final Map> tasks = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ public void initializeScheduledTasks() {
+ scheduleTaskRepository.findAllByIsActiveTrue().forEach(this::scheduleTask);
+ }
+
+
+ @Override
+ public void scheduleTask(ScheduleTask scheduleTask) {
+ Runnable taskWrapper = () -> {
+ String lockName = scheduleTask.getName()+" - "+scheduleTask.getId();
+ Instant createdAt = Instant.now();
+ LockConfiguration config = new LockConfiguration(createdAt, lockName, Duration.ofMinutes(1), Duration.ofSeconds(10));
+ Optional lock = lockProvider.lock(config);
+ LockAssert.TestHelper.makeAllAssertsPass(true);
+ try {
+ lock.ifPresent(simpleLock -> {
+ try {
+ LockAssert.assertLocked();
+ // Execute the actual task logic here
+ log.info("Executing Task {}", lockName);
+ } catch (Exception e) {
+ log.error("Error executing task: {}", scheduleTask.getId(), e);
+ }
+ });
+ } finally {
+ lock.ifPresent(SimpleLock::unlock);
+ log.info("Release Task {}", lockName);
+ }
+ };
+
+ ScheduledFuture> future = taskScheduler.schedule(taskWrapper, new CronTrigger(scheduleTask.getCustomScheduleDetails()));
+ tasks.put(scheduleTask.getId(), future);
+ }
+
+ @Override
+ public void cancelScheduledTask(Long taskId) {
+ ScheduledFuture> future = tasks.get(taskId);
+ if (future != null) {
+ future.cancel(false);
+ tasks.remove(taskId);
+ }
+ }
+
+ @Override
+ public void addOrUpdateTask(ScheduleTask scheduleTask) {
+ cancelScheduledTask(scheduleTask.getId()); // Cancel the current task if it's already scheduled
+ scheduleTask(scheduleTask); // Reschedule it
+ }
+
+}
diff --git a/src/main/java/com/app/utils/CreateCronExpression.java b/src/main/java/com/app/utils/CreateCronExpression.java
new file mode 100644
index 0000000..ea8c980
--- /dev/null
+++ b/src/main/java/com/app/utils/CreateCronExpression.java
@@ -0,0 +1,79 @@
+package com.app.utils;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.Locale;
+
+@Component
+public class CreateCronExpression {
+
+ private Integer hour;
+ private Integer minute;
+
+ public CreateCronExpression(@Value("${app.default.hour}") Integer hour, @Value("${app.default.minute}") Integer minute) {
+ this.hour = hour;
+ this.minute = minute;
+ }
+
+ public String generateEveryMinuteCronExpression() {
+ // This will run at the start of every minute
+ return "0 * * * * ?";
+ }
+
+
+ public String generateHourlyCronExpression() {
+ // Validate the input
+ if (minute < 0 || minute > 59) {
+ throw new IllegalArgumentException("Minute must be between 0 and 59");
+ }
+
+ // Construct the cron expression
+ // This will run at the start of the specified minute past every hour
+ return String.format(Locale.US, "0 %d * * * ?", minute);
+ }
+
+ public String generateDailyCronExpression() {
+ // Validate the input
+ if (hour < 0 || hour > 23) {
+ throw new IllegalArgumentException("Hour must be between 0 and 23");
+ }
+ if (minute < 0 || minute > 59) {
+ throw new IllegalArgumentException("Minute must be between 0 and 59");
+ }
+ // Construct the cron expression
+ return String.format(Locale.US, "%d %d * * *", minute, hour);
+ }
+
+ public String generateWeeklyCronExpression(int dayOfWeek) {
+ // Validate the input
+ if (hour < 0 || hour > 23) {
+ throw new IllegalArgumentException("Hour must be between 0 and 23");
+ }
+ if (minute < 0 || minute > 59) {
+ throw new IllegalArgumentException("Minute must be between 0 and 59");
+ }
+ if (dayOfWeek < 1 || dayOfWeek > 7) {
+ throw new IllegalArgumentException("Day of week must be between 1 (Sunday) and 7 (Saturday)");
+ }
+ // Construct the cron expression with the second field included
+ return String.format(Locale.US, "0 %d %d * * %d", minute, hour, dayOfWeek - 1);
+ }
+
+
+ public String generateMonthlyCronExpression(int dayOfMonth) {
+ // Validate the input
+ if (hour < 0 || hour > 23) {
+ throw new IllegalArgumentException("Hour must be between 0 and 23");
+ }
+ if (minute < 0 || minute > 59) {
+ throw new IllegalArgumentException("Minute must be between 0 and 59");
+ }
+ if (dayOfMonth < 1 || dayOfMonth > 31) {
+ throw new IllegalArgumentException("Day of month must be between 1 and 31");
+ }
+
+ // Construct the cron expression
+ return String.format(Locale.US, "%d %d %d * *", minute, hour, dayOfMonth);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index a494062..87719d9 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,8 +1,19 @@
spring:
- profiles:
- active: local
application:
name: QuickStartSpringBoot
+ datasource:
+ url: jdbc:mysql://coofun.local:3306/schedular
+ username: root
+ password: root
+ jpa:
+ hibernate:
+ ddl-auto: update
+ show-sql: true
server:
port: 8080
+app:
+ default:
+ minute: 00
+ hour: 03
+
diff --git a/src/test/java/com/app/QuickStartSpringBootApplicationTests.java b/src/test/java/com/app/QuickStartSpringBootApplicationTests.java
deleted file mode 100644
index 0137031..0000000
--- a/src/test/java/com/app/QuickStartSpringBootApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.app;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class QuickStartSpringBootApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}