added scheduling support

added remind command
added support for parameters with spaces (they are contained by ")
fixed support for remainder parameters
added maxlength support for parameters
added ability to embed templates, to have a text as well
moved properties to a more appropriate position
added method do parse a duration
This commit is contained in:
Sheldan
2020-03-28 20:12:59 +01:00
parent e0474a4c98
commit 03e81a025b
64 changed files with 1318 additions and 125 deletions

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto</groupId>
<artifactId>abstracto-application</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<packaging>pom</packaging>
<modules>
<module>scheduling-int</module>
<module>scheduling-impl</module>
</modules>
</project>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>scheduling-impl</artifactId>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.scheduling.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "abstracto.scheduling")
public class JobConfigLoader {
private HashMap<String, SchedulerJobProperties> jobs = new HashMap<>();
}

View File

@@ -0,0 +1,42 @@
package dev.sheldan.abstracto.scheduling.config;
import dev.sheldan.abstracto.scheduling.factory.SchedulerJobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private QuartzProperties quartzProperties;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerJobFactory jobFactory = new SchedulerJobFactory();
jobFactory.setApplicationContext(applicationContext);
Properties properties = new Properties();
properties.putAll(quartzProperties.getProperties());
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setOverwriteExistingJobs(true);
factory.setDataSource(dataSource);
factory.setQuartzProperties(properties);
factory.setJobFactory(jobFactory);
return factory;
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.scheduling.config;
import lombok.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SchedulerJobProperties {
private String name;
private String group;
private String cronExpression;
private String clazz;
private Boolean active;
}

View File

@@ -0,0 +1,64 @@
package dev.sheldan.abstracto.scheduling.factory;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.TimeZone;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;
@Component
@Slf4j
public class QuartzConfigFactory {
public JobDetail createJob(Class<? extends QuartzJobBean> jobClass, boolean isDurable,
ApplicationContext context, String jobName, String jobGroup, boolean requestsRecovery) {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(jobClass);
factoryBean.setDurability(isDurable);
factoryBean.setApplicationContext(context);
factoryBean.setRequestsRecovery(requestsRecovery);
factoryBean.setName(jobName);
factoryBean.setGroup(jobGroup);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(jobName + jobGroup, jobClass.getName());
factoryBean.setJobDataMap(jobDataMap);
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
public CronTrigger createBasicCronTrigger(Date startTime, String cronExpression) {
return newTrigger()
.withSchedule(cronSchedule(cronExpression).inTimeZone(TimeZone.getTimeZone("UTC")).withMisfireHandlingInstructionIgnoreMisfires())
.startAt(startTime)
.build();
}
public Trigger createSimpleOnceOnlyTrigger(String triggerName, Date startTime) {
return newTrigger()
.startAt(startTime)
.withSchedule(simpleSchedule())
.build();
}
public Trigger createOnceOnlyTriggerForJob(String jobName, String jobGroup, Date startTime, JobDataMap jobDataMap) {
return newTrigger()
.startAt(startTime)
.forJob(jobName, jobGroup)
.withSchedule(simpleSchedule())
.usingJobData(jobDataMap)
.build();
}
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.scheduling.factory;
import dev.sheldan.abstracto.scheduling.config.SchedulerJobProperties;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import org.springframework.stereotype.Component;
@Component
public class SchedulerJobConverter {
public SchedulerJob fromJobProperties(SchedulerJobProperties properties) {
return SchedulerJob
.builder()
.name(properties.getName())
.groupName(properties.getGroup())
.active(properties.getActive())
.cronExpression(properties.getCronExpression())
.clazz(properties.getClazz())
.build();
}
}

View File

@@ -0,0 +1,24 @@
package dev.sheldan.abstracto.scheduling.factory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
public class SchedulerJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.scheduling.repository;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SchedulerJobRepository extends JpaRepository<SchedulerJob, Long> {
boolean existsByName(String name);
SchedulerJob findByName(String name);
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.scheduling.service;
import org.quartz.SchedulerException;
import org.quartz.spi.InstanceIdGenerator;
import java.util.UUID;
public class IdGenerationService implements InstanceIdGenerator {
@Override
public String generateInstanceId() throws SchedulerException {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,162 @@
package dev.sheldan.abstracto.scheduling.service;
import dev.sheldan.abstracto.scheduling.factory.QuartzConfigFactory;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.model.SchedulerService;
import dev.sheldan.abstracto.scheduling.service.management.SchedulerJobManagementServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
@Slf4j
public class SchedulerServiceBean implements SchedulerService {
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Autowired
private ApplicationContext context;
@Autowired
private QuartzConfigFactory scheduleCreator;
@Autowired
private SchedulerJobManagementServiceBean schedulerJobManagementServiceBean;
@Override
public void startScheduledJobs() {
List<SchedulerJob> jobs = schedulerJobManagementServiceBean.findAll();
Scheduler scheduler = schedulerFactoryBean.getScheduler();
jobs.forEach(schedulerJob -> {
if(schedulerJob.isActive()) {
scheduleNewJob(scheduler, schedulerJob);
}
});
}
private boolean isRecurringJob(SchedulerJob job) {
return job.getCronExpression() != null && CronExpression.isValidExpression(job.getCronExpression());
}
private void scheduleNewJob(Scheduler scheduler, SchedulerJob schedulerJob) {
if(!schedulerJob.isActive()) {
return;
}
try {
JobDetail jobDetail = JobBuilder.newJob((Class<? extends QuartzJobBean>) Class.forName(schedulerJob.getClazz()))
.withIdentity(schedulerJob.getName(), schedulerJob.getGroupName()).build();
if (!scheduler.checkExists(jobDetail.getKey())) {
// if its only started by triggers, it needs to be durable
boolean recurringJob = isRecurringJob(schedulerJob);
jobDetail = scheduleCreator.createJob((Class<? extends QuartzJobBean>) Class.forName(schedulerJob.getClazz()),
!recurringJob, context, schedulerJob.getName(), schedulerJob.getGroupName(), false);
if(recurringJob) {
Trigger trigger = scheduleCreator.createBasicCronTrigger(new Date(),
schedulerJob.getCronExpression());
scheduler.scheduleJob(jobDetail, trigger);
} else {
scheduler.addJob(jobDetail, true);
}
} else {
log.info("Not scheduling job {}, because it was already scheduled.", schedulerJob.getName());
}
} catch (ClassNotFoundException | SchedulerException e) {
log.error("Failed to schedule job", e);
}
}
@Override
public void scheduleJob(SchedulerJob job) {
log.info("Scheduling job {}", job.getName());
this.scheduleNewJob(schedulerFactoryBean.getScheduler(), job);
}
@Override
public void updateJob(SchedulerJob job, Date startDate) {
Trigger newTrigger;
if (job.getCronExpression() != null) {
newTrigger = scheduleCreator.createBasicCronTrigger(startDate, job.getCronExpression());
} else {
newTrigger = scheduleCreator.createSimpleOnceOnlyTrigger(job.getName(), startDate);
}
try {
schedulerFactoryBean.getScheduler().rescheduleJob(TriggerKey.triggerKey(job.getName()), newTrigger);
schedulerJobManagementServiceBean.save(job);
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
}
}
@Override
public boolean unScheduleJob(String jobName) {
try {
return schedulerFactoryBean.getScheduler().unscheduleJob(new TriggerKey(jobName));
} catch (SchedulerException e) {
log.error("Failed to un-schedule job - {}", jobName, e);
return false;
}
}
@Override
public boolean deleteJob(SchedulerJob job) {
try {
return schedulerFactoryBean.getScheduler().deleteJob(new JobKey(job.getName(), job.getGroupName()));
} catch (SchedulerException e) {
log.error("Failed to delete job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean pauseJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().pauseJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to pause job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean continueJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().resumeJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to resume job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean executeJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().triggerJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to start new job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean executeJobWithParametersOnce(String name, String group, JobDataMap dataMap, Date date) {
Trigger onceOnlyTriggerForJob = scheduleCreator.createOnceOnlyTriggerForJob(name, group, date, dataMap);
try {
schedulerFactoryBean.getScheduler().scheduleJob(onceOnlyTriggerForJob);
return true;
} catch (SchedulerException e) {
log.error("Failed to start new job - {}", name, e);
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.sheldan.abstracto.scheduling.service;
import dev.sheldan.abstracto.scheduling.config.JobConfigLoader;
import dev.sheldan.abstracto.scheduling.factory.SchedulerJobConverter;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.model.SchedulerService;
import dev.sheldan.abstracto.scheduling.service.management.SchedulerJobManagementServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
public class SchedulerStartupService {
@Autowired
private SchedulerService schedulerService;
@Autowired
private JobConfigLoader jobConfigLoader;
@Autowired
private SchedulerJobManagementServiceBean schedulerJobManagementServiceBean;
@Autowired
private SchedulerJobConverter schedulerJobConverter;
@EventListener
@Transactional
public void handleContextRefreshEvent(ContextRefreshedEvent ctxStartEvt) {
jobConfigLoader.getJobs().forEach((s, schedulerJob) -> {
SchedulerJob job = schedulerJobConverter.fromJobProperties(schedulerJob);
if(!schedulerJobManagementServiceBean.doesJobExist(job) || !schedulerJobManagementServiceBean.isJobDefinitionTheSame(job)) {
schedulerJobManagementServiceBean.createOrUpdate(job);
}
});
schedulerService.startScheduledJobs();
}
}

View File

@@ -0,0 +1,70 @@
package dev.sheldan.abstracto.scheduling.service.management;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.repository.SchedulerJobRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Slf4j
public class SchedulerJobManagementServiceBean {
@Autowired
private SchedulerJobRepository repository;
public SchedulerJob createOrUpdate(SchedulerJob job) {
if(repository.existsByName(job.getName())) {
SchedulerJob byName = repository.findByName(job.getName());
byName.setActive(job.isActive());
byName.setClazz(job.getClazz());
byName.setCronExpression(job.getCronExpression());
byName.setGroupName(job.getGroupName());
return repository.save(byName);
} else {
return this.createJob(job);
}
}
public SchedulerJob createJob(SchedulerJob job) {
log.info("Creating job {}", job.getName());
repository.save(job);
return job;
}
public List<SchedulerJob> findAll() {
return repository.findAll();
}
public SchedulerJob save(SchedulerJob job) {
repository.save(job);
return job;
}
public boolean doesJobExist(SchedulerJob schedulerJob) {
return repository.existsByName(schedulerJob.getName());
}
public boolean isJobDefinitionTheSame(SchedulerJob job) {
SchedulerJob old = repository.findByName(job.getName());
if(old == null) {
return false;
}
boolean cronExp;
if(old.getCronExpression() == null && job.getCronExpression() != null) {
cronExp = false;
} else if(old.getCronExpression() != null && job.getCronExpression() == null) {
cronExp = false;
} else if(old.getCronExpression() == null && job.getCronExpression() == null) {
cronExp = true;
} else {
cronExp = old.getCronExpression().equals(job.getCronExpression());
}
boolean active = old.isActive() == job.isActive();
boolean classEqual = old.getClazz().equals(job.getClazz());
boolean group = old.getGroupName().equals(job.getGroupName());
return cronExp && active && classEqual && group;
}
}

View File

@@ -0,0 +1,15 @@
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=never
spring.quartz.properties.org.quartz.scheduler.instanceName=quartz-abstracto-app
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.scheduler.instanceIdGenerator.class=dev.sheldan.abstracto.scheduling.service.IdGenerationService
spring.quartz.properties.org.quartz.threadPool.threadCount=20
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
spring.quartz.properties.org.quartz.jobStore.useProperties=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
spring.quartz.properties.org.quartz.jobStore.tablePrefix=qrtz_
spring.quartz.properties.org.quartz.jobStore.isClustered=false
spring.quartz.properties.org.quartz.plugin.shutdownHook.class=org.quartz.plugins.management.ShutdownHookPlugin
spring.quartz.properties.org.quartz.plugin.shutdownHook.cleanShutdown=TRUE

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>scheduling-int</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package dev.sheldan.abstracto.scheduling.model;
import lombok.*;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Builder
@Table(name = "scheduler_job")
@NoArgsConstructor
@AllArgsConstructor
public class SchedulerJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String groupName;
private String clazz;
private String cronExpression;
private boolean active;
}

View File

@@ -0,0 +1,25 @@
package dev.sheldan.abstracto.scheduling.model;
import org.quartz.JobDataMap;
import java.util.Date;
public interface SchedulerService {
void startScheduledJobs();
void scheduleJob(SchedulerJob job);
void updateJob(SchedulerJob job, Date startDate);
boolean unScheduleJob(String jobName);
boolean deleteJob(SchedulerJob job);
boolean pauseJob(SchedulerJob job);
boolean continueJob(SchedulerJob job);
boolean executeJob(SchedulerJob job);
boolean executeJobWithParametersOnce(String name, String group, JobDataMap dataMap, Date date);
}